Skip to content
← All docs

Deployment

The website ships as two deployables plus a database:

ComponentBuildRunDomain
Frontend (Next.js)npm run buildnpm start (port 3000)leadfella.com
Backend (Express)npm run builddist/node dist/server.js (port 4000)api.leadfella.com
DatabasePostgreSQL 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):

ServiceUnitPortNginx vhost
Frontend (Next standalone)leadfella-web3000leadfella.com
Backend (Express)leadfella-api4000api.leadfella.com
  1. 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.
  2. Set secrets in /opt/leadfella-api/leadfella-api.env (JWT_SECRET, ADMIN_PASSWORD_HASH).
  3. 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:

  1. Build gates - product unit tests (test-app, with MongoDB) and the website build (test-website: typecheck + build both, migrate against a throwaway Postgres).
  2. Deploy - builds the product jar and the website (frontend output: standalone baking NEXT_PUBLIC_*, backend tscdist), 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 plaintext ADMIN_PASSWORD triggers a warning - use ADMIN_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, enables compression, and rate-limits requests. TLS is terminated at the proxy; set CORS_ORIGINS to 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 before npm run build, not just at runtime.
  • The marketing routes are statically pre-rendered; /admin is client-rendered and marked noindex.
  • The app can also be deployed to any platform with first-class Next.js support (build command npm run build, output served by next 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 (or no-verify for self-signed certificates) to DATABASE_URL.

Domains & TLS

SubdomainPoints to
leadfella.comFrontend (Next.js)
api.leadfella.comBackend (Express)
app.leadfella.comProduct 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 via trust proxy) and exempts /api/health* so internal probes still pass; the frontend (src/middleware.ts) redirects when X-Forwarded-Proto is http. Set TRUST_PROXY to match your proxy depth.
  • HSTS. Strict-Transport-Security: max-age of ~1–2 years with includeSubDomains; preload on both tiers.
  • Security headers. Frontend sends a Content-Security-Policy, plus X-Content-Type-Options, X-Frame-Options: DENY, Referrer-Policy, Permissions-Policy, and Cross-Origin-Opener-Policy. The API uses helmet and removes X-Powered-By.
  • Compression. compression on the API; compress: true on Next.js.
  • Rate limiting. A baseline limiter on all /api traffic 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

  1. Apply database migrations.
  2. Deploy the backend; confirm /api/health/db is healthy.
  3. Build and deploy the frontend with NEXT_PUBLIC_API_URL pointing at the API.
  4. 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.