NestledJS App Previews
NestledJS projects run two processes in the preview container — a NestJS API and a React Router (Vite) frontend — each on their own subdomain. This guide walks through the exact configuration, in the order you should do it.
Prerequisites
- Preview environments enabled for your project (see Preview Environments)
- A
DATABASE_URLthe preview can reach (a dev/staging database — never production) - Your project uses
pnpmand Nx
Step 1: Make the Vite Dev Server Reachable (one-time code change)
This is the step that's easy to miss — and without it the web preview will fail. By default the Nx/Vite dev server binds to localhost and rejects unknown hostnames, so the preview's reverse proxy can't reach it (you get a 502) and Vite blocks the request.
In apps/web/vite.config.ts, update the server block:
server: {
port: 4200,
host: process.env.VITE_HOST || 'localhost',
allowedHosts: ['.preview.flightdesk.dev'],
// ...keep any existing fs / proxy settings
},
host: process.env.VITE_HOST || 'localhost'lets the preview bind to0.0.0.0(via theVITE_HOSTsecret below) while stayinglocalhostfor local dev.allowedHosts: ['.preview.flightdesk.dev']lets Vite accept the preview subdomain. The leading dot matches all subdomains.
Commit this change on the branch you want to preview — the preview builds from the branch, so the fix has to be present there.
Step 2: Set Your Environment Variables
In Project Settings → Secrets, add the variables below. FlightDesk only auto-injects NODE_ENV, NODE_OPTIONS, and NX_DAEMON into the container — everything else your app needs, including HOST, must be set here as a secret.
| Key | Value | Why |
|---|---|---|
| DATABASE_URL | Connection string for a dev/staging DB | App can't boot without it |
| JWT_SECRET | Any random string | Auth signing |
| API_COOKIE_SECRET | Any random string | Cookie signing |
| HOST | 0.0.0.0 | NestJS must bind all interfaces or Caddy 502s |
| VITE_HOST | 0.0.0.0 | Vite dev server must bind all interfaces (pairs with Step 1) |
| API_URL | {{PREVIEW_URL:api}} | API's own public URL |
| SITE_URL | {{PREVIEW_URL}} | Frontend's public URL |
| VITE_API_URL | {{PREVIEW_URL:api}} | Where the browser calls the API |
| ALLOWED_ORIGINS | {{PREVIEW_URL}} | CORS allow-list for the frontend |
| VITE_COOKIE_NAME | __session_{{BRANCH_SLUG}} | Unique per-branch session cookie name |
| API_COOKIE_DOMAIN | .preview.flightdesk.dev | Shares the cookie across the web + api subdomains |
| EMAIL_PROVIDER | mock | Don't send real emails from previews |
The
HOST=0.0.0.0/VITE_HOST=0.0.0.0pair is the #1 cause of "the preview goes Ready but every request 502s." Inside the container, anything bound tolocalhostis unreachable from Caddy across the Docker bridge.
About the cookie name. In NestledJS, both the web app and the API read
VITE_COOKIE_NAME(the API only falls back toAPI_COOKIE_NAME), so this one secret covers both processes — you don't set it twice. Make it unique per branch with{{BRANCH_SLUG}}: previews live under*.preview.flightdesk.dev, a subdomain offlightdesk.dev, so a generic__sessioncould otherwise collide with the FlightDesk app's own cookie or with other branches' previews. Pair it withAPI_COOKIE_DOMAIN=.preview.flightdesk.devso the cookie is shared between thewebandapisubdomains but never leaks up toflightdesk.dev.
The {{...}} placeholders are replaced at spin-up with the real per-branch URLs. See Preview Environment Variables for the full list.
Step 3: Configure Setup Commands
In Project Settings → Preview → Setup Commands, enter these (one per line):
pnpm install
pnpm prisma generate
These run once before your processes start. prisma generate creates the Prisma client from your schema. (Nx builds shared libraries on demand when you run nx serve, so a separate build:libs step usually isn't needed — add one only if your project requires it.)
Do not run migrations here. Preview containers connect to a shared database — running
prisma migrate deployin a preview would alter it. Only runprisma generatein setup commands.
Step 4: Configure Processes
In Project Settings → Preview → Processes, paste this JSON:
[
{ "name": "api", "command": "pnpm nx serve api", "port": 3000 },
{ "name": "web", "command": "pnpm nx serve web", "port": 4200, "primary": true }
]
Use the ports your apps actually listen on. The NestledJS API reads PORT (default 3000); the Vite web dev server uses 4200. Mark the web process primary so it gets the clean subdomain.
What this gives you for a branch named feature-payments:
- Web (primary):
https://feature-payments.preview.flightdesk.dev - API:
https://api-feature-payments.preview.flightdesk.dev
Step 5: Health Check Path (optional)
Leave this blank to start — FlightDesk then marks the preview ready as soon as the primary process stops returning 502/503 on its root URL, which is the right behavior for most apps.
If you do set a Health Check Path, note that it is appended to the primary process's URL (the web app here), and the preview is only marked ready on a 200. So /api/health would be checked against the web subdomain, not the API. Only set a path your primary process serves.
How It Works
The preview container:
- Clones your branch
- Runs
pnpm installthenpnpm prisma generate - Starts the API and the web app via PM2, binding to
0.0.0.0on their ports - Caddy terminates TLS and proxies each subdomain to the right port
Both processes restart automatically if they crash. Logs for each are available separately on the task page.
Troubleshooting
Preview is Ready but every request returns 502
A process is bound to localhost instead of 0.0.0.0. Confirm HOST=0.0.0.0 and VITE_HOST=0.0.0.0 are set in secrets, and that Step 1's vite.config.ts change is committed on the branch. Restart the preview after fixing.
Vite shows "Blocked request. This host is not allowed."
allowedHosts: ['.preview.flightdesk.dev'] is missing from vite.config.ts on the branch (Step 1).
API crashes on startup
Usually a missing env var — most often DATABASE_URL. Check the API process logs on the task page, fix the secret, and hit Restart.
Web app can't reach the API
Make sure VITE_API_URL is {{PREVIEW_URL:api}} and ALLOWED_ORIGINS includes {{PREVIEW_URL}}. The frontend bakes VITE_API_URL at dev-server start, so a change requires a Restart.