Deployment
The website ships as two deployables plus a database:
| Component | Build | Run | Domain |
|---|---|---|---|
| Frontend (Next.js) | npm run build | npm start (port 3000) | leadfella.com |
| Backend (Express) | npm run build → dist/ | node dist/server.js (port 4000) | api.leadfella.com |
| Database | – | PostgreSQL 14+ | private |
Both are standard Node services behind a TLS-terminating reverse proxy (or a platform that provides one). They can be deployed and scaled independently.
Run it locally (one command)
From the repo root on Windows:
.\run.ps1 # product (8080 + 5173) AND website (4000 + 3000)
.\run.ps1 -WebOnly # just the website
It checks MongoDB + PostgreSQL, seeds website/backend/.env if missing, installs
deps, applies website migrations, and starts every service in its own window. See
installation.md for the manual steps.
Production model
Production mirrors the product app: the website runs on the same box as two
Node systemd services behind Nginx, with PostgreSQL local. No Docker.
Everything is set up by the single infra/provision.sh
(which provisions the product and the website):
| Service | Unit | Port | Nginx vhost |
|---|---|---|---|
| Frontend (Next standalone) | leadfella-web | 3000 | leadfella.com |
| Backend (Express) | leadfella-api | 4000 | api.leadfella.com |
- Provision once (as root):
sudo bash infra/provision.sh- installs the product stack plus Node + PostgreSQL, the website db/role, both services, both Nginx sites, and TLS via certbot. - Set secrets in
/opt/leadfella-api/leadfella-api.env(JWT_SECRET,ADMIN_PASSWORD_HASH). - Point DNS for
leadfella.com,www.leadfella.com,api.leadfella.com.
Automated deploy (CI/CD)
The single CI / Deploy to Linode workflow (.github/workflows/deploy.yml)
runs on pushes to main:
- Build gates - product unit tests (
test-app, with MongoDB) and the website build (test-website: typecheck + build both, migrate against a throwaway Postgres). - Deploy - builds the product jar and the website (frontend
output: standalonebakingNEXT_PUBLIC_*, backendtsc→dist),scps all three bundles to the server, then over SSH activates each: restart the product,npm ci --omit=dev+ migrate + restart the API, extract + restart the frontend, health-checking:8080,:4000/api/health, and:3000.
It reuses the product's SSH secrets (DEPLOY_HOST, DEPLOY_USER=leadfella,
DEPLOY_SSH_KEY, DEPLOY_PORT). No build artifacts are uploaded to GitHub.
Manual build/run (reference)
Backend
cd website/backend
npm ci
npm run build # tsc -> dist/
# Provide env (DATABASE_URL, JWT_SECRET, ADMIN_PASSWORD_HASH, CORS_ORIGINS, ...)
npm run migrate # apply pending migrations (also runs on startup)
node dist/server.js # or: npm start
- Set
NODE_ENV=production. In production the logger emits JSON (no pretty printing) and plaintextADMIN_PASSWORDtriggers a warning - useADMIN_PASSWORD_HASH. - Migrations are forward-only and idempotent: running them on each deploy is safe, and any pending ones also run when the server boots.
- Health endpoints for your load balancer:
/api/health(liveness) and/api/health/db(readiness - pings PostgreSQL). - The API already sets security headers via
helmet, enablescompression, and rate-limits requests. TLS is terminated at the proxy; setCORS_ORIGINSto the exact production frontend origin.
Frontend
cd website/frontend
npm ci
# Provide build-time env: NEXT_PUBLIC_API_URL=https://api.leadfella.com, etc.
npm run build # next build
npm start # next start (port 3000)
NEXT_PUBLIC_*values are inlined at build time, so set them beforenpm run build, not just at runtime.- The marketing routes are statically pre-rendered;
/adminis client-rendered and markednoindex. - The app can also be deployed to any platform with first-class Next.js support
(build command
npm run build, output served bynext start).
Database
- Provision PostgreSQL and create the database/user (see installation.md).
- Apply migrations from the backend (
npm run migrate) as part of the release. - For managed Postgres with TLS, append
?sslmode=require(orno-verifyfor self-signed certificates) toDATABASE_URL.
Domains & TLS
| Subdomain | Points to |
|---|---|
leadfella.com | Frontend (Next.js) |
api.leadfella.com | Backend (Express) |
app.leadfella.com | Product app (separate deployment) |
Serve everything over HTTPS only and redirect HTTP → HTTPS at the proxy.
Security hardening
The app enforces these at the application level (in addition to anything your proxy/CDN provides):
- HTTPS only / forced redirect. Both tiers 308-redirect HTTP → HTTPS in
production. The backend uses
req.secure(proxy-aware viatrust proxy) and exempts/api/health*so internal probes still pass; the frontend (src/middleware.ts) redirects whenX-Forwarded-Protoishttp. SetTRUST_PROXYto match your proxy depth. - HSTS.
Strict-Transport-Security: max-ageof ~1–2 years withincludeSubDomains; preloadon both tiers. - Security headers. Frontend sends a Content-Security-Policy, plus
X-Content-Type-Options,X-Frame-Options: DENY,Referrer-Policy,Permissions-Policy, andCross-Origin-Opener-Policy. The API useshelmetand removesX-Powered-By. - Compression.
compressionon the API;compress: trueon Next.js. - Rate limiting. A baseline limiter on all
/apitraffic plus a tight 5/minute limiter on the public submit endpoints. - Bot protection. A hidden honeypot field on the public forms (silently accepted, never stored) on top of rate limiting. Front the deployment with a WAF/CDN (e.g. Cloudflare) for network-level bot mitigation.
To enable the forced redirect outside production (e.g. staging over a proxy that
terminates TLS), set FORCE_HTTPS=true on the backend.
Release order
- Apply database migrations.
- Deploy the backend; confirm
/api/health/dbis healthy. - Build and deploy the frontend with
NEXT_PUBLIC_API_URLpointing at the API. - Smoke-test: load the site, submit the early-access form, and confirm the
signup shows in
/admin.
See configuration.md for the full env reference and troubleshooting.md for common failures.