Deployment

This page covers building each package for production, the environment variables required, hosting options, and common deployment pitfalls.


Production build commands

Run these from the repo root. Each command triggers next build inside the respective package.

# Build backend API
pnpm --filter @repo/backend-js build
 
# Build frontend app
pnpm --filter @repo/frontend-app build
 
# Build documentation site
pnpm --filter @repo/docs build

After building, start each service with its start script (Next.js production server):

pnpm --filter @repo/backend-js start   # runs on the port next start listens to
pnpm --filter @repo/frontend-app start
pnpm --filter @repo/docs start

[!TIP] In a process manager like PM2, map each package to a start command and assign ports via PORT environment variable (e.g. PORT=3001 pnpm --filter @repo/backend-js start).


Port assignments

ServiceDevelopment portRecommended production port
backend-js30013001 (behind reverse proxy)
frontend-app30003000 (behind reverse proxy) or served via CDN
docs30023002 (behind reverse proxy)

Use a reverse proxy (nginx, Caddy, Traefik) in front of all three services to terminate TLS and route by hostname or path prefix.


Required environment variables in production

backend-js

All of these must be set via your deployment platform’s secrets manager (never committed to the repo).

VariableRequiredNotes
MYSQL_HOSTProduction DB host or socket
MYSQL_USER
MYSQL_PASSWORDSecret — never expose
MYSQL_DATABASEUse erronka (matches schema.sql)
MYSQL_PORToptionalDefaults to 3306
JWT_SECRETMinimum 32 random bytes; rotate periodically
JWT_ACCESS_EXPIRES_INoptionalDefault 15m
JWT_REFRESH_EXPIRES_DAYSoptionalDefault 7
RESEND_API_KEYRequired for all email flows
GOOGLE_DIRECTIONS_API_KEYRequired for route creation and geocoding
TRACKING_BASE_URLPublic URL to the tracking page, e.g. https://tolosaerronka.es/tracking
RESET_BASE_URLPublic URL to the password-reset page
TRACKING_EXPIRES_DAYSoptionalDefault 15
DEFAULT_USER_PASSWORDMust be overridden; the source fallback is public

[!WARNING] JWT_SECRET and DEFAULT_USER_PASSWORD have hardcoded fallbacks in src/app/config/envConfig.ts. These fallbacks are visible in the public repository. If you deploy without overriding them, any attacker can forge JWT tokens and know the initial password of every created user.

frontend-app

VariableRequiredNotes
NEXT_PUBLIC_API_BASE_URL✅ in devProduction code falls back to https://api.tolosaerronka.es/api when NODE_ENV=production. Override to change the API domain.
NEXT_PUBLIC_HERE_API_KEYHERE Maps key for map rendering
NEXT_PUBLIC_DOCS_BASE_URLoptionalLink to the docs site

MySQL in production

Connection pooling

The backend uses mysql2 with a connection pool (src/app/config/dbConfig.ts). The pool is a module-level singleton — it is created once when the module first loads and reused across all requests.

Key considerations:

  • Set connectionLimit to match your MySQL server’s max_connections capacity. A typical starting value is 10.
  • In serverless environments (Vercel, AWS Lambda), each function invocation may spawn a new pool. This can exhaust MySQL connections quickly. Consider using PlanetScale or Neon with their HTTP drivers, or a dedicated connection proxy like ProxySQL or RDS Proxy.
  • In self-hosted deployments (a single long-running Node process), the pool behaves correctly as-is.

SSL / TLS

If your production MySQL instance requires TLS (common on managed services like AWS RDS, GCP Cloud SQL, Azure Database for MySQL), add SSL configuration to the pool options in dbConfig.ts:

ssl: {
  rejectUnauthorized: true,
  ca: process.env.MYSQL_SSL_CA,
}

And add MYSQL_SSL_CA (PEM content) to your secrets.


Next.js hosting options

The frontend is a standard Next.js App Router application and deploys to Vercel with zero configuration.

  1. Connect the repository in the Vercel dashboard.
  2. Set Root Directory to frontend-app.
  3. Add all NEXT_PUBLIC_* environment variables in the Vercel project settings.
  4. Deploy.

[!NOTE] Vercel does not run the backend. Deploy backend-js separately on a VPS or container platform so it has a persistent MySQL connection.

Option B — Self-hosted (VPS / Docker)

Run each service as a long-lived process behind nginx:

# nginx snippet — route by subdomain
server {
    server_name api.tolosaerronka.es;
    location / { proxy_pass http://localhost:3001; }
}
server {
    server_name tolosaerronka.es;
    location / { proxy_pass http://localhost:3000; }
}
server {
    server_name docs.tolosaerronka.es;
    location / { proxy_pass http://localhost:3002; }
}

Use Certbot to provision TLS certificates automatically.


The authentication flow uses an HttpOnly refresh_token cookie issued by the backend and sent automatically by the browser on every /api/auth/refresh call.

For this to work cross-origin in production:

SettingDevelopmentProduction
SameSite (refresh cookie)LaxNone
Secure (refresh cookie)falsetrue (requires HTTPS)
CORS credentialstruetrue
CORS originhttp://localhost:3000frontend origin (e.g. https://tolosaerronka.es)

If the frontend and backend are on different subdomains, the browser will only send the None; Secure cookie if TLS is active end-to-end. An HTTP-only deployment will silently drop the refresh cookie after login.

[!WARNING] Never set Access-Control-Allow-Origin: * when credentials: true is required. The browser will reject such responses. Always specify the exact frontend origin.


Health check endpoints

There is no dedicated /health endpoint in the current codebase. The simplest proxy health check is:

GET /api/auth/me

This endpoint always responds with a JSON body (either 401 Unauthorized or the authenticated user). A non-5xx response confirms the backend is up and MySQL is reachable.


Common deployment pitfalls

SymptomCauseFix
Refresh token loop in productionSameSite=None cookie not sent over HTTPEnable TLS; ensure Secure flag is set
CORS error on credentialed requestAccess-Control-Allow-Origin: * with credentials: trueSet exact frontend origin in CORS config
connect ECONNREFUSED on backend startMySQL not reachable from deployment hostCheck MYSQL_HOST, firewall rules, VPC peering
Route creation always failsGOOGLE_DIRECTIONS_API_KEY not set or quota exceededVerify key in GCP console; check billing
Emails not sentRESEND_API_KEY missing or domain not verifiedAdd key to secrets; verify sending domain in Resend dashboard
Token forgery riskJWT_SECRET using source-code fallbackSet a strong unique JWT_SECRET in your secrets manager