Deploy: Fly.io + Neon + Vercel¶
A best-of-breed split: the API on Fly.io (global containers), PostgreSQL on Neon (serverless), and the app + landing on Vercel (static, edge CDN). Pick this when you want each piece on the platform that does it best.
Outcome: API at api.yourdomain.com, app at app.yourdomain.com, landing at yourdomain.com.
1. Database: Neon¶
- Create a project at Neon and copy the connection string (use the pooled one for serverless).
- You'll set it as
FS_DB_URLon the API in the next step.
2. API: Fly.io¶
- Install flyctl, then run
fly launchinbackend/(it detects the Dockerfile, but don't deploy yet). -
Set secrets (Configuration):
-
fly deploy, then run migrations against Neon:./sqitch.sh prod deploy(prod target =FS_DB_URL). - Add the domain:
fly certs add api.yourdomain.com, then point DNS at Fly.
3. App + landing: Vercel¶
Create two Vercel projects from the same repo:
- frontend: root directory
frontend/, buildnpm run build, envPUBLIC_API_BASE_URL=https://api.yourdomain.com; domainapp.yourdomain.com. - landing: root directory
landing/, buildnpm run build; domainyourdomain.com.
Vercel auto-deploys on push and provisions SSL. No other configuration is needed: each directory ships a vercel.json that pins the output directory and handles routing (the SPA fallback for the app, clean URLs for the landing). Environment variables set in the Vercel project are available at build time, which is when a static build needs them.
Serving the app from a sub-path¶
To serve the app at yourdomain.com/app instead of app.yourdomain.com: Vercel serves one project per domain, so the landing project (which owns yourdomain.com) forwards the prefix. In the landing's vercel.json, add a rewrite that preserves the path:
{
"rewrites": [{ "source": "/app/:path*", "destination": "https://<frontend-deployment>/app/:path*" }]
}
The frontend and backend settings that go with this are in Serving from a Sub-Path.
4. Wire it together¶
- DNS:
api→ Fly.io;appand the apex → Vercel. - Confirm
FS_BASE_API_URL/FS_BASE_WEB_URLmatch the live URLs so CORS and cookies work (Configuration). - Stripe webhook →
https://api.yourdomain.com/webhooks/stripe(Billing & Subscriptions). - Review the Security checklist before launch.
Next steps¶
Security headers¶
The kit ships frontend/vercel.json (it pins the output directory and provides the SPA fallback). Add a headers key to it:
{
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "Content-Security-Policy",
"value": "default-src 'self'; connect-src 'self' https://api.yourdomain.com; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'"
},
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Strict-Transport-Security", "value": "max-age=31536000; includeSubDomains" },
{ "key": "Permissions-Policy", "value": "camera=(), microphone=(), geolocation=()" }
]
}
]
}
Replace https://api.yourdomain.com in connect-src with your real API URL, or the browser will block the app from calling it. See Security.