- Web App (Next.js) → Vercel →
app.trynullsec.com - Worker (BullMQ + Playwright) → Railway (Docker)
- Database → Supabase (same project:
itvznvetqclwamqjrcgc) - Redis → Upstash (job queue via ioredis, rate limiting via REST)
- Import the repo in Vercel
- Set Root Directory to
apps/web - Set Framework Preset to
Next.js - Set Install Command to
pnpm install - Set Build Command to
pnpm run build - Vercel will resolve workspace dependencies from the monorepo root automatically with these settings
NEXT_PUBLIC_SUPABASE_URL=https://itvznvetqclwamqjrcgc.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<your anon key>
SUPABASE_SERVICE_ROLE_KEY=<your service role key>
UPSTASH_REDIS_REST_URL=<from Upstash console, REST tab>
UPSTASH_REDIS_REST_TOKEN=<from Upstash console, REST tab>
NEXT_PUBLIC_APP_URL=https://app.trynullsec.com
NEXT_PUBLIC_SENTRY_DSN=<optional>
NEXT_PUBLIC_POSTHOG_KEY=<optional>
NEXT_PUBLIC_POSTHOG_HOST=<optional>
RESEND_API_KEY=<optional>
REDIS_URL=<ioredis URL for BullMQ queue — same as worker>
- In Vercel → Project → Settings → Domains → Add
app.trynullsec.com - In your DNS provider, add:
CNAME app → cname.vercel-dns.com
- Create a new Railway project
- Link the GitHub repo
- Set Root Directory to
/(monorepo root — Dockerfile needs full context) - Railway uses
packages/worker/Dockerfile(configured inrailway.toml)
NEXT_PUBLIC_SUPABASE_URL=https://itvznvetqclwamqjrcgc.supabase.co
SUPABASE_SERVICE_ROLE_KEY=<your service role key>
REDIS_URL=rediss://default:<password>@<host>:6379
ANTHROPIC_API_KEY=<your key>
NEXT_PUBLIC_APP_URL=https://app.trynullsec.com
SENTRY_DSN=<optional>
RESEND_API_KEY=<optional>
- The Dockerfile runs
playwright install --with-deps chromiumwhich installs the exact Chromium build Playwright expects plus all system libraries - The worker runs via
tsx src/index.ts(thestartscript) — no separate compile step needed - Worker concurrency is set to 1 by default for memory safety
-
Authentication → URL Configuration:
- Site URL:
https://app.trynullsec.com - Redirect URLs: add
https://app.trynullsec.com/auth/callback
- Site URL:
-
Database → Replication:
- Confirm
scan_eventstable has Realtime enabled
- Confirm
-
Migrations:
- Confirm
0001_phase3_dismissals.sqlis applied (addsdismissed_at,dismissed_by,dismiss_reasonto findings,stripe_payment_intent_idto scans)
- Confirm
The web app uses @upstash/redis (HTTP-based, serverless-safe) for rate limiting.
The worker uses ioredis (TCP, persistent connection) for BullMQ.
Both connect to the same Upstash Redis instance but via different protocols:
- Worker: uses the
rediss://connection URL (REDIS_URL) - Web app rate limiter: uses REST URL + token (
UPSTASH_REDIS_REST_URL,UPSTASH_REDIS_REST_TOKEN)
Get the REST credentials from: Upstash Console → Your Database → REST API tab.
After both services are deployed:
https://app.trynullsec.com→ should redirect to/login- Sign up via magic link → check email → click → should land on
/dashboard - Submit a scan → confirm Realtime progress streams on
/scan/[id] - After completion →
/report/[id]renders with findings - Publish report →
/r/[badgeId]and badge SVG resolve - 6th scan on same account → 403 with beta limit message
- Burst 4+ requests to
POST /api/scans→ 429 rate limit - Submit a garbage URL → honest failure, no crash