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 install

2. 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:start

and 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_URLPostgres URL exposed by Supabase (or any PostgREST endpoint).
NEXT_PUBLIC_SUPABASE_ANON_KEYAnonymous (publishable) key — safe to expose to the browser.
SUPABASE_SERVICE_ROLE_KEYService-role key — server-only, bypasses RLS. Treat like a database password.
NEXT_PUBLIC_SITE_URLPublic origin of your Nesh deployment. Used to build the apiBase shown to SDK users and the public event tracking URL.
CRON_SECRETShared secret the cron job uses to authenticate. Any sufficiently random string.
VAPID_KEY_ENCRYPTION_KEY32-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_IDSOptional. 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 :3000

On 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/dispatch

6. 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_KEY must 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_logs and surfaced on /admin. Useful when debugging a deploy.