Self-hosting
Nesh is MIT-licensed and runs on any Node-compatible host with a Postgres database. The reference deploy targets Vercel + Supabase, but neither is required.
Requirements
- Node.js 20+ (the Vercel build image works as-is).
- Postgres 15+ — Supabase, Neon, RDS, or self-hosted all work.
- Any Node-compatible host that runs the Next.js build output (Vercel, Cloudflare, Fly.io, a VM, Docker).
- HTTPS on the production origin — browsers refuse to register Service Workers over plain HTTP.
1. Clone and install
Clone the repository and install dependencies.
git clone https://github.com/piro0919/nesh.git
cd nesh
pnpm install2. Provision Postgres + apply migrations
All schema is in supabase/migrations/. If you're using Supabase, the project ships with the CLI — start the local stack with
pnpm db:startand apply the same migrations to your remote project. For non-Supabase Postgres, run the SQL files in supabase/migrations/ in filename order.
Row-Level Security is enabled on every table. The dashboard talks to Postgres as the signed-in user via Supabase auth; server routes that bypass RLS (cron, REST send) use the service role key.
3. Environment variables
Copy to and fill in: .env.example → .env.local.
NEXT_PUBLIC_SUPABASE_URL | Postgres URL exposed by Supabase (or any PostgREST endpoint). |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Anonymous (publishable) key — safe to expose to the browser. |
SUPABASE_SERVICE_ROLE_KEY | Service-role key — server-only, bypasses RLS. Treat like a database password. |
NEXT_PUBLIC_SITE_URL | Public origin of your Nesh deployment. Used to build the apiBase shown to SDK users and the public event tracking URL. |
CRON_SECRET | Shared secret the cron job uses to authenticate. Any sufficiently random string. |
VAPID_KEY_ENCRYPTION_KEY | 32-byte AES-256-GCM key for encrypting per-project VAPID private keys at rest. Generate with the command below — losing this key means losing access to every existing project's VAPID identity. |
ADMIN_USER_IDS | Optional. Comma-separated list of Supabase user UUIDs that can access the /admin dashboard. |
# Generate VAPID_KEY_ENCRYPTION_KEY
node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"
# Generate CRON_SECRET
node -e "console.log(require('node:crypto').randomBytes(24).toString('base64url'))"4. Deploy
Vercel: connect the repo, set the env vars from step 3, and deploy. The included vercel.ts wires up the cron schedule automatically.
Self-hosted: build and run with
pnpm build
pnpm start # default :3000On non-Vercel hosts you'll need to schedule /api/cron/dispatch yourself — see step 5.
5. Schedule the cron worker
The cron worker dispatches scheduled notifications, retries failed webhook deliveries, and prunes rate-limit rows. It should run roughly every minute. Authenticate it with the CRON_SECRET as a Bearer token.
On Vercel: the bundled vercel.ts already declares this schedule.
Outside Vercel: any scheduler will do — GitHub Actions, fly.io machines, cron + curl, Kubernetes CronJob. Example crontab:
* * * * * curl -fsS -H "Authorization: Bearer $CRON_SECRET" \
https://your-nesh.example.com/api/cron/dispatch6. First user and admin
Sign up via the UI to create your account. Look up your user UUID in Supabase Studio (or via SQL) and add it to ADMIN_USER_IDS to unlock the /admin dashboard.
Operational notes
VAPID_KEY_ENCRYPTION_KEYmust not change after projects exist. Back it up alongside your database. Rotation requires re-encrypting every projects.vapid_private_key.- Web Push fan-out runs serially per project inside the send route (max ~5000 subs per project on the bundled tier). If you raise that ceiling, consider moving the fan-out to a queue.
- Event tracking (shown / clicked) hits a public endpoint by design — beacons send no credentials. Rate limited per IP.
- Server errors are persisted to
error_logsand surfaced on /admin. Useful when debugging a deploy.