Vibecode OneSignal
track this build5 phases, 10 steps, beginner friendly0%Web push is an open standard: a service worker, VAPID keys and a POST to the browser push service, which web-push libraries make a sitting. Mobile push means APNs and FCM credentials and a device token table, a weekend. Segments, A/B tests and the dashboard are the product.
You are building a lean indie version of OneSignal. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # OneSignal · indie build Web push notifications you own: VAPID keys, a service worker, permission asked only after a click, a subscriptions table, a send function that prunes dead endpoints, an admin to compose and schedule, simple segments by tag, and mobile adapters only if you have a native app. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | subscriptions and sends | | Protocol | The web-push library | the Web Push standard, no vendor | | Hosting | HTTPS everywhere | push requires it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A site served over HTTPS to add the service worker to** · free - Why: Push requires HTTPS and a service worker on the site's origin. - Get it: Your site, or a local HTTPS dev setup with mkcert. - [ ] **VAPID keys generated once** · free - Why: Changing them invalidates every subscription. - Get it: npx web-push generate-vapid-keys, into .env. - [ ] **A phone browser to test** · free - Why: Push on mobile behaves differently. - Get it: Android Chrome, or iOS Safari with the site installed to the home screen. - [ ] **FCM and APNs credentials (optional)** (optional) · free with the developer accounts - Why: Only for a native app in Phase 5. - Get it: Firebase console service account; Apple Developer APNs key. - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **A domain or subdomain** (optional) · roughly $10 a year, or free on an existing domain - Why: A public address you own, so links you share never break when a provider changes. - Get it: Register at Cloudflare Registrar, Porkbun or Namecheap, or use a subdomain of one you already own. You add one DNS record in the deploy phase. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir push && cd push && git init && npm init -y && npm pkg set type=module && npm install web-push@3 mkdir -p data && cp .env.example .env npx web-push generate-vapid-keys ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Journeys, A/B tests, cross-channel analytics, email and SMS. - segmentation, journeys and A/B testing - delivery analytics across channels - SDKs for every platform maintained for you - email and SMS in the same tool If one of those is essential to you, that is the reason to keep paying for OneSignal, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · OneSignal The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me web push notifications to replace OneSignal. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. Mobile push is a later phase and only if you have a native app; most side projects stop after Phase 4. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. The web-push library. A service worker on your site. HTTPS everywhere, which push requires. ### Data model (create this before Phase 1) - subscriptions: id, endpoint (unique), keys_p256dh, keys_auth, user_id (nullable), created_at, last_seen_at, failed_count - sends: id, title, body, url, created_at, targeted, delivered, failed ### Phase 1 · Keys and subscribe Build: generate VAPID keys once into .env. A service worker that handles push and notificationclick. A subscribe button that asks permission only after a click (never on load), posts the subscription to POST /api/subscribe, and an unsubscribe path. Done when: clicking subscribe stores one row, reloading does not duplicate it, and unsubscribe removes it. Do not build yet: sending. ### Phase 2 · Send Build: a send(title, body, url, filter) function that posts to every matching subscription with a TTL, deletes subscriptions that return 404 or 410, and increments failed_count on other errors. Done when: a test send arrives on a desktop and a phone browser, clicking opens the URL, and a revoked subscription is removed on the next send. ### Phase 3 · Admin Build: /admin behind basic auth from .env: compose a notification with a preview, a subscriber count, send history with delivered and failed counts. Done when: a send from the form arrives and the history row matches the counts. ### Phase 4 · Scheduling and segments Build: schedule a send for a time, and simple segments by a tag stored on the subscription (set from your app). Done when: a scheduled send fires within a minute of its time to only the tagged subscribers. ### Phase 5 · Mobile, only if needed Build: an interface with FCM and APNs adapters and a device_tokens table, sharing the send history. Done when: a test message arrives on both platforms from the same admin form. ### Out of scope (and why) - Journeys, A/B tests, cross-channel analytics, email and SMS. That is the dashboard product. ### README must contain - The permission-on-click rule and why browsers punish the alternative. - Key rotation: changing VAPID keys invalidates every subscription. ===== AGENTS.md ===== # Agent instructions · OneSignal indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, The web-push library, HTTPS everywhere. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ===== BUILD_PLAN.md ===== # Build plan · OneSignal Web push notifications you own: VAPID keys, a service worker, permission asked only after a click, a subscriptions table, a send function that prunes dead endpoints, an admin to compose and schedule, simple segments by tag, and mobile adapters only if you have a native app. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Keys and subscribe Permission after a click, one row per subscription, unsubscribe works. ### Steps 1. Generate VAPID keys; write sw.js handling push and notificationclick Files: `public/sw.js` ```sh mkdir push && cd push && git init && npm init -y && npm pkg set type=module && npm install web-push@3 mkdir -p data && cp .env.example .env npx web-push generate-vapid-keys ``` 2. A subscribe button posting to /api/subscribe; an unsubscribe path ### Done when - [ ] Subscribe stores one row; reload does not duplicate - [ ] Unsubscribe removes it ## Phase 2 · Send Deliver, prune dead endpoints, count failures. ### Steps 1. send(title, body, url, filter) with a TTL 2. Delete on 404 or 410; increment failed_count otherwise ### Done when - [ ] A test arrives on desktop and phone - [ ] Click opens the URL - [ ] A revoked subscription is removed on the next send ## Phase 3 · Admin Compose, preview, count, history. ### Steps 1. Basic-auth /admin with a compose form and preview 2. Subscriber count and send history with delivered and failed ### Done when - [ ] A send from the form arrives - [ ] History matches the counts ## Phase 4 · Scheduling and segments Send later, to a tag. ### Steps 1. Scheduled sends via a minute loop 2. Tags on subscriptions set from your app; filter by tag ### Done when - [ ] A scheduled send fires within a minute to only the tagged subscribers ## Phase 5 · Mobile, only if needed FCM and APNs adapters behind one interface. ### Steps 1. device_tokens table and adapters 2. Shared send history ### Done when - [ ] A test arrives on both platforms from the same form ## Not in this build - Journeys, A/B tests, cross-channel analytics, email and SMS. ## After v1, if you want it - A quiet-hours rule per subscriber timezone - Per-topic subscriptions ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/push.db # Required. From web-push generate-vapid-keys; also shipped to the browser. VAPID_PUBLIC_KEY=B... # Required · secret. Server only. VAPID_PRIVATE_KEY=... # Required. Contact for push services. VAPID_SUBJECT=mailto:you@yourdomain.com # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
You are building a lean indie version of OneSignal. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # OneSignal · indie build Web push notifications you own: VAPID keys, a service worker, permission asked only after a click, a subscriptions table, a send function that prunes dead endpoints, an admin to compose and schedule, simple segments by tag, and mobile adapters only if you have a native app. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | subscriptions and sends | | Protocol | The web-push library | the Web Push standard, no vendor | | Hosting | HTTPS everywhere | push requires it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A site served over HTTPS to add the service worker to** · free - Why: Push requires HTTPS and a service worker on the site's origin. - Get it: Your site, or a local HTTPS dev setup with mkcert. - [ ] **VAPID keys generated once** · free - Why: Changing them invalidates every subscription. - Get it: npx web-push generate-vapid-keys, into .env. - [ ] **A phone browser to test** · free - Why: Push on mobile behaves differently. - Get it: Android Chrome, or iOS Safari with the site installed to the home screen. - [ ] **FCM and APNs credentials (optional)** (optional) · free with the developer accounts - Why: Only for a native app in Phase 5. - Get it: Firebase console service account; Apple Developer APNs key. - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **A domain or subdomain** (optional) · roughly $10 a year, or free on an existing domain - Why: A public address you own, so links you share never break when a provider changes. - Get it: Register at Cloudflare Registrar, Porkbun or Namecheap, or use a subdomain of one you already own. You add one DNS record in the deploy phase. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir push && cd push && git init && npm init -y && npm pkg set type=module && npm install web-push@3 mkdir -p data && cp .env.example .env npx web-push generate-vapid-keys ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Journeys, A/B tests, cross-channel analytics, email and SMS. - segmentation, journeys and A/B testing - delivery analytics across channels - SDKs for every platform maintained for you - email and SMS in the same tool If one of those is essential to you, that is the reason to keep paying for OneSignal, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · OneSignal The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me web push notifications to replace OneSignal. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. Mobile push is a later phase and only if you have a native app; most side projects stop after Phase 4. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. The web-push library. A service worker on your site. HTTPS everywhere, which push requires. ### Data model (create this before Phase 1) - subscriptions: id, endpoint (unique), keys_p256dh, keys_auth, user_id (nullable), created_at, last_seen_at, failed_count - sends: id, title, body, url, created_at, targeted, delivered, failed ### Phase 1 · Keys and subscribe Build: generate VAPID keys once into .env. A service worker that handles push and notificationclick. A subscribe button that asks permission only after a click (never on load), posts the subscription to POST /api/subscribe, and an unsubscribe path. Done when: clicking subscribe stores one row, reloading does not duplicate it, and unsubscribe removes it. Do not build yet: sending. ### Phase 2 · Send Build: a send(title, body, url, filter) function that posts to every matching subscription with a TTL, deletes subscriptions that return 404 or 410, and increments failed_count on other errors. Done when: a test send arrives on a desktop and a phone browser, clicking opens the URL, and a revoked subscription is removed on the next send. ### Phase 3 · Admin Build: /admin behind basic auth from .env: compose a notification with a preview, a subscriber count, send history with delivered and failed counts. Done when: a send from the form arrives and the history row matches the counts. ### Phase 4 · Scheduling and segments Build: schedule a send for a time, and simple segments by a tag stored on the subscription (set from your app). Done when: a scheduled send fires within a minute of its time to only the tagged subscribers. ### Phase 5 · Mobile, only if needed Build: an interface with FCM and APNs adapters and a device_tokens table, sharing the send history. Done when: a test message arrives on both platforms from the same admin form. ### Out of scope (and why) - Journeys, A/B tests, cross-channel analytics, email and SMS. That is the dashboard product. ### README must contain - The permission-on-click rule and why browsers punish the alternative. - Key rotation: changing VAPID keys invalidates every subscription. ===== AGENTS.md ===== # Agent instructions · OneSignal indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, The web-push library, HTTPS everywhere. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ===== BUILD_PLAN.md ===== # Build plan · OneSignal Web push notifications you own: VAPID keys, a service worker, permission asked only after a click, a subscriptions table, a send function that prunes dead endpoints, an admin to compose and schedule, simple segments by tag, and mobile adapters only if you have a native app. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Keys and subscribe Permission after a click, one row per subscription, unsubscribe works. ### Steps 1. Generate VAPID keys; write sw.js handling push and notificationclick Files: `public/sw.js` ```sh mkdir push && cd push && git init && npm init -y && npm pkg set type=module && npm install web-push@3 mkdir -p data && cp .env.example .env npx web-push generate-vapid-keys ``` 2. A subscribe button posting to /api/subscribe; an unsubscribe path ### Done when - [ ] Subscribe stores one row; reload does not duplicate - [ ] Unsubscribe removes it ## Phase 2 · Send Deliver, prune dead endpoints, count failures. ### Steps 1. send(title, body, url, filter) with a TTL 2. Delete on 404 or 410; increment failed_count otherwise ### Done when - [ ] A test arrives on desktop and phone - [ ] Click opens the URL - [ ] A revoked subscription is removed on the next send ## Phase 3 · Admin Compose, preview, count, history. ### Steps 1. Basic-auth /admin with a compose form and preview 2. Subscriber count and send history with delivered and failed ### Done when - [ ] A send from the form arrives - [ ] History matches the counts ## Phase 4 · Scheduling and segments Send later, to a tag. ### Steps 1. Scheduled sends via a minute loop 2. Tags on subscriptions set from your app; filter by tag ### Done when - [ ] A scheduled send fires within a minute to only the tagged subscribers ## Phase 5 · Mobile, only if needed FCM and APNs adapters behind one interface. ### Steps 1. device_tokens table and adapters 2. Shared send history ### Done when - [ ] A test arrives on both platforms from the same form ## Not in this build - Journeys, A/B tests, cross-channel analytics, email and SMS. ## After v1, if you want it - A quiet-hours rule per subscriber timezone - Per-topic subscriptions ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/push.db # Required. From web-push generate-vapid-keys; also shipped to the browser. VAPID_PUBLIC_KEY=B... # Required · secret. Server only. VAPID_PRIVATE_KEY=... # Required. Contact for push services. VAPID_SUBJECT=mailto:you@yourdomain.com # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
You are building a production product version of OneSignal. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # OneSignal · product brief ## Problem Web push is an open standard: a service worker, VAPID keys and a POST to the browser push service, which web-push libraries make a sitting. Mobile push means APNs and FCM credentials and a device token table, a weekend. Segments, A/B tests and the dashboard are the product. ## Product outcome Notifications your product sends from its own table, with the marketing layer left to the vendor. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - HTTPS - VAPID keys - APNs and FCM credentials for mobile ## Explicit non-goals for v1 - Journeys, A/B tests, cross-channel analytics, email and SMS. - segmentation, journeys and A/B testing - delivery analytics across channels - SDKs for every platform maintained for you - email and SMS in the same tool ## Success criteria - Permission-on-click verified - Dead endpoint pruning verified - One restore drill performed ===== BRIEF.md ===== # Build brief · OneSignal The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me web push notifications to replace OneSignal. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. Mobile push is a later phase and only if you have a native app; most side projects stop after Phase 4. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. The web-push library. A service worker on your site. HTTPS everywhere, which push requires. ### Data model (create this before Phase 1) - subscriptions: id, endpoint (unique), keys_p256dh, keys_auth, user_id (nullable), created_at, last_seen_at, failed_count - sends: id, title, body, url, created_at, targeted, delivered, failed ### Phase 1 · Keys and subscribe Build: generate VAPID keys once into .env. A service worker that handles push and notificationclick. A subscribe button that asks permission only after a click (never on load), posts the subscription to POST /api/subscribe, and an unsubscribe path. Done when: clicking subscribe stores one row, reloading does not duplicate it, and unsubscribe removes it. Do not build yet: sending. ### Phase 2 · Send Build: a send(title, body, url, filter) function that posts to every matching subscription with a TTL, deletes subscriptions that return 404 or 410, and increments failed_count on other errors. Done when: a test send arrives on a desktop and a phone browser, clicking opens the URL, and a revoked subscription is removed on the next send. ### Phase 3 · Admin Build: /admin behind basic auth from .env: compose a notification with a preview, a subscriber count, send history with delivered and failed counts. Done when: a send from the form arrives and the history row matches the counts. ### Phase 4 · Scheduling and segments Build: schedule a send for a time, and simple segments by a tag stored on the subscription (set from your app). Done when: a scheduled send fires within a minute of its time to only the tagged subscribers. ### Phase 5 · Mobile, only if needed Build: an interface with FCM and APNs adapters and a device_tokens table, sharing the send history. Done when: a test message arrives on both platforms from the same admin form. ### Out of scope (and why) - Journeys, A/B tests, cross-channel analytics, email and SMS. That is the dashboard product. ### README must contain - The permission-on-click rule and why browsers punish the alternative. - Key rotation: changing VAPID keys invalidates every subscription. ===== ARCHITECTURE.md ===== # Architecture · OneSignal ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | subscriptions and sends | | Protocol | The web-push library | the Web Push standard, no vendor | | Hosting | HTTPS everywhere | push requires it | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Subscriptions | the table and endpoints | The core | | Sender | web-push and pruning | Add FCM/APNs adapters | | Admin | compose, schedule, segments | Any UI | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Any free port. - `DATABASE_PATH` · required · SQLite file. - `VAPID_PUBLIC_KEY` · required · From web-push generate-vapid-keys; also shipped to the browser. - `VAPID_PRIVATE_KEY` · required, secret · Server only. - `VAPID_SUBJECT` · required · Contact for push services. - `ADMIN_USER` · required · Any username for the basic-auth admin pages. - `ADMIN_PASS` · required, secret · Generate one: openssl rand -base64 24. Never reuse a real password. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · OneSignal product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22, node:http and node:sqlite, The web-push library, HTTPS everywhere. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ===== MILESTONES.md ===== # Delivery milestones · OneSignal Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Keys and subscribe Permission after a click, one row per subscription, unsubscribe works. ### Steps 1. Generate VAPID keys; write sw.js handling push and notificationclick Files: `public/sw.js` ```sh mkdir push && cd push && git init && npm init -y && npm pkg set type=module && npm install web-push@3 mkdir -p data && cp .env.example .env npx web-push generate-vapid-keys ``` 2. A subscribe button posting to /api/subscribe; an unsubscribe path ### Done when - [ ] Subscribe stores one row; reload does not duplicate - [ ] Unsubscribe removes it ## M2 · Send Deliver, prune dead endpoints, count failures. ### Steps 1. send(title, body, url, filter) with a TTL 2. Delete on 404 or 410; increment failed_count otherwise ### Done when - [ ] A test arrives on desktop and phone - [ ] Click opens the URL - [ ] A revoked subscription is removed on the next send ## M3 · Admin Compose, preview, count, history. ### Steps 1. Basic-auth /admin with a compose form and preview 2. Subscriber count and send history with delivered and failed ### Done when - [ ] A send from the form arrives - [ ] History matches the counts ## M4 · Scheduling and segments Send later, to a tag. ### Steps 1. Scheduled sends via a minute loop 2. Tags on subscriptions set from your app; filter by tag ### Done when - [ ] A scheduled send fires within a minute to only the tagged subscribers ## M5 · Mobile, only if needed FCM and APNs adapters behind one interface. ### Steps 1. device_tokens table and adapters 2. Shared send history ### Done when - [ ] A test arrives on both platforms from the same form ## M6 · Operate it like a product (production only) Only for the product-builder path: know when the push server is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443 ===== OPERATIONS.md ===== # Operations · OneSignal ## Backup SQLite nightly; losing subscriptions means re-asking permission. ## Restore Copy back. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Failed-send rate. ## Incident checklist Never rotate VAPID keys casually; it invalidates every subscription. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Permission-on-click verified - [ ] Dead endpoint pruning verified - [ ] One restore drill performed ## Launch constraint Do not market omitted OneSignal capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/push.db # Required. From web-push generate-vapid-keys; also shipped to the browser. VAPID_PUBLIC_KEY=B... # Required · secret. Server only. VAPID_PRIVATE_KEY=... # Required. Contact for push services. VAPID_SUBJECT=mailto:you@yourdomain.com # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
# OneSignal · indie build Web push notifications you own: VAPID keys, a service worker, permission asked only after a click, a subscriptions table, a send function that prunes dead endpoints, an admin to compose and schedule, simple segments by tag, and mobile adapters only if you have a native app. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | subscriptions and sends | | Protocol | The web-push library | the Web Push standard, no vendor | | Hosting | HTTPS everywhere | push requires it | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Everything in this build runs on it: the server, the scripts, the tests. - Get it: Download the LTS installer from nodejs.org, or install with your package manager (brew install node, or nvm install 22). Restart the terminal afterwards. - Verify: node --version prints v22 or higher - [ ] **A terminal and a code editor** · free - Why: Every step below is a command you type or a file you edit. - Get it: VS Code (code.visualstudio.com), Cursor or Zed. Open a folder for the project and use the editor's built-in terminal. - Verify: You can open a folder and run a command in its terminal - [ ] **Git** · free - Why: History for your code, and the way most hosts deploy. - Get it: Install from git-scm.com or with your package manager, then run git init in the project folder once it exists. - Verify: git --version prints a version - [ ] **A site served over HTTPS to add the service worker to** · free - Why: Push requires HTTPS and a service worker on the site's origin. - Get it: Your site, or a local HTTPS dev setup with mkcert. - [ ] **VAPID keys generated once** · free - Why: Changing them invalidates every subscription. - Get it: npx web-push generate-vapid-keys, into .env. - [ ] **A phone browser to test** · free - Why: Push on mobile behaves differently. - Get it: Android Chrome, or iOS Safari with the site installed to the home screen. - [ ] **FCM and APNs credentials (optional)** (optional) · free with the developer accounts - Why: Only for a native app in Phase 5. - Get it: Firebase console service account; Apple Developer APNs key. - [ ] **A small always-on server (VPS)** (optional) · about $5 a month - Why: This needs one process running all the time with a public address. - Get it: Hetzner Cloud (from about 4 EUR), DigitalOcean or Fly.io. Ubuntu 24.04, the smallest size. You need SSH access and a public IP. Only needed for the deploy phase; develop locally first. - [ ] **A domain or subdomain** (optional) · roughly $10 a year, or free on an existing domain - Why: A public address you own, so links you share never break when a provider changes. - Get it: Register at Cloudflare Registrar, Porkbun or Namecheap, or use a subdomain of one you already own. You add one DNS record in the deploy phase. - [ ] **Caddy on the server** (optional) · free - Why: Automatic HTTPS in front of the Node process. Without TLS the browser features this relies on (and your visitors' trust) do not work. - Get it: On the VPS: follow the install steps at caddyserver.com/docs/install for Ubuntu. One Caddyfile with your domain and a reverse_proxy line is the whole config. - Verify: caddy version prints a version on the server ## Quick start ```sh mkdir push && cd push && git init && npm init -y && npm pkg set type=module && npm install web-push@3 mkdir -p data && cp .env.example .env npx web-push generate-vapid-keys ``` Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Journeys, A/B tests, cross-channel analytics, email and SMS. - segmentation, journeys and A/B testing - delivery analytics across channels - SDKs for every platform maintained for you - email and SMS in the same tool If one of those is essential to you, that is the reason to keep paying for OneSignal, and the README should say so rather than pretend.
# Build brief · OneSignal The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me web push notifications to replace OneSignal. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. Mobile push is a later phase and only if you have a native app; most side projects stop after Phase 4. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. The web-push library. A service worker on your site. HTTPS everywhere, which push requires. ### Data model (create this before Phase 1) - subscriptions: id, endpoint (unique), keys_p256dh, keys_auth, user_id (nullable), created_at, last_seen_at, failed_count - sends: id, title, body, url, created_at, targeted, delivered, failed ### Phase 1 · Keys and subscribe Build: generate VAPID keys once into .env. A service worker that handles push and notificationclick. A subscribe button that asks permission only after a click (never on load), posts the subscription to POST /api/subscribe, and an unsubscribe path. Done when: clicking subscribe stores one row, reloading does not duplicate it, and unsubscribe removes it. Do not build yet: sending. ### Phase 2 · Send Build: a send(title, body, url, filter) function that posts to every matching subscription with a TTL, deletes subscriptions that return 404 or 410, and increments failed_count on other errors. Done when: a test send arrives on a desktop and a phone browser, clicking opens the URL, and a revoked subscription is removed on the next send. ### Phase 3 · Admin Build: /admin behind basic auth from .env: compose a notification with a preview, a subscriber count, send history with delivered and failed counts. Done when: a send from the form arrives and the history row matches the counts. ### Phase 4 · Scheduling and segments Build: schedule a send for a time, and simple segments by a tag stored on the subscription (set from your app). Done when: a scheduled send fires within a minute of its time to only the tagged subscribers. ### Phase 5 · Mobile, only if needed Build: an interface with FCM and APNs adapters and a device_tokens table, sharing the send history. Done when: a test message arrives on both platforms from the same admin form. ### Out of scope (and why) - Journeys, A/B tests, cross-channel analytics, email and SMS. That is the dashboard product. ### README must contain - The permission-on-click rule and why browsers punish the alternative. - Key rotation: changing VAPID keys invalidates every subscription.
# Agent instructions · OneSignal indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Node 22, node:http and node:sqlite, The web-push library, HTTPS everywhere. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs".
# Build plan · OneSignal Web push notifications you own: VAPID keys, a service worker, permission asked only after a click, a subscriptions table, a send function that prunes dead endpoints, an admin to compose and schedule, simple segments by tag, and mobile adapters only if you have a native app. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Keys and subscribe Permission after a click, one row per subscription, unsubscribe works. ### Steps 1. Generate VAPID keys; write sw.js handling push and notificationclick Files: `public/sw.js` ```sh mkdir push && cd push && git init && npm init -y && npm pkg set type=module && npm install web-push@3 mkdir -p data && cp .env.example .env npx web-push generate-vapid-keys ``` 2. A subscribe button posting to /api/subscribe; an unsubscribe path ### Done when - [ ] Subscribe stores one row; reload does not duplicate - [ ] Unsubscribe removes it ## Phase 2 · Send Deliver, prune dead endpoints, count failures. ### Steps 1. send(title, body, url, filter) with a TTL 2. Delete on 404 or 410; increment failed_count otherwise ### Done when - [ ] A test arrives on desktop and phone - [ ] Click opens the URL - [ ] A revoked subscription is removed on the next send ## Phase 3 · Admin Compose, preview, count, history. ### Steps 1. Basic-auth /admin with a compose form and preview 2. Subscriber count and send history with delivered and failed ### Done when - [ ] A send from the form arrives - [ ] History matches the counts ## Phase 4 · Scheduling and segments Send later, to a tag. ### Steps 1. Scheduled sends via a minute loop 2. Tags on subscriptions set from your app; filter by tag ### Done when - [ ] A scheduled send fires within a minute to only the tagged subscribers ## Phase 5 · Mobile, only if needed FCM and APNs adapters behind one interface. ### Steps 1. device_tokens table and adapters 2. Shared send history ### Done when - [ ] A test arrives on both platforms from the same form ## Not in this build - Journeys, A/B tests, cross-channel analytics, email and SMS. ## After v1, if you want it - A quiet-hours rule per subscriber timezone - Per-topic subscriptions
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/push.db # Required. From web-push generate-vapid-keys; also shipped to the browser. VAPID_PUBLIC_KEY=B... # Required · secret. Server only. VAPID_PRIVATE_KEY=... # Required. Contact for push services. VAPID_SUBJECT=mailto:you@yourdomain.com # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
# OneSignal · product brief ## Problem Web push is an open standard: a service worker, VAPID keys and a POST to the browser push service, which web-push libraries make a sitting. Mobile push means APNs and FCM credentials and a device token table, a weekend. Segments, A/B tests and the dashboard are the product. ## Product outcome Notifications your product sends from its own table, with the marketing layer left to the vendor. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - HTTPS - VAPID keys - APNs and FCM credentials for mobile ## Explicit non-goals for v1 - Journeys, A/B tests, cross-channel analytics, email and SMS. - segmentation, journeys and A/B testing - delivery analytics across channels - SDKs for every platform maintained for you - email and SMS in the same tool ## Success criteria - Permission-on-click verified - Dead endpoint pruning verified - One restore drill performed
# Build brief · OneSignal The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me web push notifications to replace OneSignal. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. Mobile push is a later phase and only if you have a native app; most side projects stop after Phase 4. ### Stack (fixed, do not substitute) - Node 22 with node:http and node:sqlite. The web-push library. A service worker on your site. HTTPS everywhere, which push requires. ### Data model (create this before Phase 1) - subscriptions: id, endpoint (unique), keys_p256dh, keys_auth, user_id (nullable), created_at, last_seen_at, failed_count - sends: id, title, body, url, created_at, targeted, delivered, failed ### Phase 1 · Keys and subscribe Build: generate VAPID keys once into .env. A service worker that handles push and notificationclick. A subscribe button that asks permission only after a click (never on load), posts the subscription to POST /api/subscribe, and an unsubscribe path. Done when: clicking subscribe stores one row, reloading does not duplicate it, and unsubscribe removes it. Do not build yet: sending. ### Phase 2 · Send Build: a send(title, body, url, filter) function that posts to every matching subscription with a TTL, deletes subscriptions that return 404 or 410, and increments failed_count on other errors. Done when: a test send arrives on a desktop and a phone browser, clicking opens the URL, and a revoked subscription is removed on the next send. ### Phase 3 · Admin Build: /admin behind basic auth from .env: compose a notification with a preview, a subscriber count, send history with delivered and failed counts. Done when: a send from the form arrives and the history row matches the counts. ### Phase 4 · Scheduling and segments Build: schedule a send for a time, and simple segments by a tag stored on the subscription (set from your app). Done when: a scheduled send fires within a minute of its time to only the tagged subscribers. ### Phase 5 · Mobile, only if needed Build: an interface with FCM and APNs adapters and a device_tokens table, sharing the send history. Done when: a test message arrives on both platforms from the same admin form. ### Out of scope (and why) - Journeys, A/B tests, cross-channel analytics, email and SMS. That is the dashboard product. ### README must contain - The permission-on-click rule and why browsers punish the alternative. - Key rotation: changing VAPID keys invalidates every subscription.
# Architecture · OneSignal ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Node 22, node:http and node:sqlite | subscriptions and sends | | Protocol | The web-push library | the Web Push standard, no vendor | | Hosting | HTTPS everywhere | push requires it | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Subscriptions | the table and endpoints | The core | | Sender | web-push and pruning | Add FCM/APNs adapters | | Admin | compose, schedule, segments | Any UI | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PORT` · required · Any free port. - `DATABASE_PATH` · required · SQLite file. - `VAPID_PUBLIC_KEY` · required · From web-push generate-vapid-keys; also shipped to the browser. - `VAPID_PRIVATE_KEY` · required, secret · Server only. - `VAPID_SUBJECT` · required · Contact for push services. - `ADMIN_USER` · required · Any username for the basic-auth admin pages. - `ADMIN_PASS` · required, secret · Generate one: openssl rand -base64 24. Never reuse a real password. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · OneSignal product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Node 22, node:http and node:sqlite, The web-push library, HTTPS everywhere. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone.
# Delivery milestones · OneSignal Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Keys and subscribe Permission after a click, one row per subscription, unsubscribe works. ### Steps 1. Generate VAPID keys; write sw.js handling push and notificationclick Files: `public/sw.js` ```sh mkdir push && cd push && git init && npm init -y && npm pkg set type=module && npm install web-push@3 mkdir -p data && cp .env.example .env npx web-push generate-vapid-keys ``` 2. A subscribe button posting to /api/subscribe; an unsubscribe path ### Done when - [ ] Subscribe stores one row; reload does not duplicate - [ ] Unsubscribe removes it ## M2 · Send Deliver, prune dead endpoints, count failures. ### Steps 1. send(title, body, url, filter) with a TTL 2. Delete on 404 or 410; increment failed_count otherwise ### Done when - [ ] A test arrives on desktop and phone - [ ] Click opens the URL - [ ] A revoked subscription is removed on the next send ## M3 · Admin Compose, preview, count, history. ### Steps 1. Basic-auth /admin with a compose form and preview 2. Subscriber count and send history with delivered and failed ### Done when - [ ] A send from the form arrives - [ ] History matches the counts ## M4 · Scheduling and segments Send later, to a tag. ### Steps 1. Scheduled sends via a minute loop 2. Tags on subscriptions set from your app; filter by tag ### Done when - [ ] A scheduled send fires within a minute to only the tagged subscribers ## M5 · Mobile, only if needed FCM and APNs adapters behind one interface. ### Steps 1. device_tokens table and adapters 2. Shared send history ### Done when - [ ] A test arrives on both platforms from the same form ## M6 · Operate it like a product (production only) Only for the product-builder path: know when the push server is down, never lose the database, and keep the server patched. ### Steps 1. Add a /healthz endpoint and an external uptime check against it Answer 200 with the build id and a quick database read. Point a free uptime monitor (or your own, from the Healthchecks entry on this site) at it so an outage is noticed before a user notices. 2. Write structured request logs and rotate them One JSON line per request: method, path, status, duration, no raw IPs. Rotate weekly with logrotate, keep eight. 3. Back the SQLite file up off the machine nightly and test a restore SQLite's .backup command makes a consistent copy while the app runs. Copy it to object storage or a second machine; then, once, restore it into a fresh checkout and confirm the app reads it. ```sh sqlite3 data/app.db ".backup '/tmp/app-$(date +%F).db'" rclone copy /tmp/app-$(date +%F).db remote:backups/ ``` 4. Lock the box down Firewall allowing only 22, 80 and 443; unattended security updates on; the app running as an unprivileged user under systemd with Restart=on-failure. ### Done when - [ ] Stopping the service triggers an uptime alert within a few minutes - [ ] A restore from last night's backup contains yesterday's data - [ ] A port scan from another machine shows only 22, 80 and 443
# Operations · OneSignal ## Backup SQLite nightly; losing subscriptions means re-asking permission. ## Restore Copy back. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Failed-send rate. ## Incident checklist Never rotate VAPID keys casually; it invalidates every subscription. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Permission-on-click verified - [ ] Dead endpoint pruning verified - [ ] One restore drill performed ## Launch constraint Do not market omitted OneSignal capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Any free port. PORT=3000 # Required. SQLite file. DATABASE_PATH=./data/push.db # Required. From web-push generate-vapid-keys; also shipped to the browser. VAPID_PUBLIC_KEY=B... # Required · secret. Server only. VAPID_PRIVATE_KEY=... # Required. Contact for push services. VAPID_SUBJECT=mailto:you@yourdomain.com # Required. Any username for the basic-auth admin pages. ADMIN_USER=admin # Required · secret. Generate one: openssl rand -base64 24. Never reuse a real password. ADMIN_PASS=change-me-to-a-long-random-string
$ choose a build depth, inspect the files, then open the complete pack in your agent
Marketing teams pay to target and measure without engineers; the free tier covers most side projects entirely.
xsegmentation, journeys and A/B testing
xdelivery analytics across channels
xSDKs for every platform maintained for you
xemail and SMS in the same tool
OneSignal pricing
growth$19/mo · monthly from, usage-based · $228/yr
free tierThe free plan covers unlimited mobile push for up to 1,000 monthly active users and 10,000 emails a month.
verified 2026-09-04 · source ↗
Is OneSignal free?
The free plan covers unlimited mobile push for up to 1,000 monthly active users and 10,000 emails a month. Paid is Growth at $19/mo (checked 2026-09-04).
Vibecode OneSignal
Kinda. The core of OneSignal is buildable in a weekend with the prompt on this page, but there are real gaps: segmentation, journeys and A/B testing, delivery analytics across channels. Read the honest list above before committing.
How much does OneSignal cost?
OneSignal costs about $19/month (Growth, checked 2026-09-04), which is $228 per year.
What do I lose by replacing OneSignal?
Honestly: segmentation, journeys and A/B testing; delivery analytics across channels; SDKs for every platform maintained for you; email and SMS in the same tool. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to OneSignal?
Yes: web-push (Web Push protocol library for Node), ntfy (simple pub-sub push notifications, self-hostable). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.