Docs
The repo ships Dockerfiles for both apps and a docker-compose stack for local Postgres + Valkey. This page covers the generic shape of each documented deploy target — not a specific account’s infrastructure.
Four random secrets, all required before the API will start. See Environment Variables for what each one gates.
openssl rand -base64 32 # JWT_SECRET
openssl rand -base64 32 # JWT_REFRESH_SECRET
openssl rand -base64 32 # ENCRYPTION_KEY
openssl rand -base64 32 # COOKIE_SECRETEach one needs to be at least 32 characters — the boot-time check below rejects anything shorter, along with the placeholder values shipped in .env.example.
Built-in PostgreSQL and Redis. Add the API service and the web service separately, run the database migration once from the API service shell (or the Railway CLI), then deploy.
Same shape as Railway — a Web Service per app, a managed PostgreSQL and a managed Redis instance, migrations run once from the Render shell on the API service.
Clone, configure .env, and bring up Postgres, Valkey, the API, and the web app with Docker Compose, behind whatever reverse proxy you put in front (an example Nginx config ships in the guide) for TLS termination. Point your payment provider webhooks at the proxied HTTPS URL, not the container port directly.
Four health endpoints, each meant for a different consumer — a load-balancer probe interval or several replica pods sharing a rate-limit key can otherwise push probe traffic over the default limit and get 429’d, which for a k8s liveness/readiness probe means pods get killed or pulled from rotation for being “unhealthy” when they’re actually fine — which is why these four routes are explicitly exempted from rate limiting.
| Endpoint | Use for |
|---|---|
| GET /health/live | Kubernetes-style liveness probe — process is up, no dependency checks, always 200. |
| GET /health/ready | Kubernetes-style readiness probe — 503 if the database is unreachable, so traffic is pulled from this instance. |
| GET /health | Detailed JSON status (database + Valkey connectivity) for ops dashboards. Always returns 200, even when the body reports status: "error" or "degraded" — not useful for anything that only checks the HTTP status code. |
| GET /health/ping | Trivial liveness check — returns { message: "pong" }. |
GET /metrics exposes a Prometheus-format scrape endpoint (unauthenticated, unversioned, excluded from the generated API docs).
The API validates its environment at startup and exits immediately if any of these are true — a fast, loud failure instead of a live deployment quietly running on placeholder secrets:
DATABASE_URL, JWT_SECRET, JWT_REFRESH_SECRET, ENCRYPTION_KEY, COOKIE_SECRET)DATABASE_URL isn’t a valid PostgreSQL connection stringFRONTEND_URL isn’t a valid http/https URL, or PORT/NODE_ENV are missing or invalidRecommended-but-not-critical variables (Resend, Stripe, Razorpay, the audit-log HMAC secret, API_URL) only print a startup warning when missing — the app still boots, but the feature that depends on them won’t work.
Behind a real reverse proxy (nginx-ingress, an ALB, Cloudflare), set TRUST_PROXY_ADDRESSES to that proxy’s own IP/CIDR — never leave it unset in that topology, and never set it to a hop count. Left unset behind a real proxy, a client can forge X-Forwarded-For to get a fresh, never-throttled IP-based rate-limit bucket on every request, which this template’s own security audit confirmed is exploitable against /auth/login. Per-account brute-force lockout is unaffected either way, since that’s tracked by user row, not IP.
cd packages/database
pnpm prisma migrate deploy
pnpm prisma db seed # first deployment onlyThe template ships with placeholder branding and structural-only legal pages — rebrand apps/web/src/config/brand.config.ts, replace the placeholder logo assets, and get the legal pages under apps/web/src/app/ (terms, privacy, refund, cookie policy) reviewed by a lawyer for your jurisdiction before they go live — they carry a visible “Placeholder” banner until you do.