Skip to content

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

  1. Create a project at Neon and copy the connection string (use the pooled one for serverless).
  2. You'll set it as FS_DB_URL on the API in the next step.

2. API: Fly.io

  1. Install flyctl, then run fly launch in backend/ (it detects the Dockerfile, but don't deploy yet).
  2. Set secrets (Configuration):

    fly secrets set \
      FS_DB_URL="<neon-connection-string>" \
      FS_ENVIRONMENT=prod \
      FS_BASE_API_URL=https://api.yourdomain.com \
      FS_BASE_WEB_URL=https://app.yourdomain.com \
      FS_JWT_SECRET_KEY=... FS_CRON_SECRET=...
    
  3. fly deploy, then run migrations against Neon: ./sqitch.sh prod deploy (prod target = FS_DB_URL).

  4. 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/, build npm run build, env VITE_API_BASE_URL=https://api.yourdomain.com; domain app.yourdomain.com.
  • landing: root directory landing/, build npm run build; domain yourdomain.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 (build/client) 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; app and the apex → Vercel.
  • Confirm FS_BASE_API_URL / FS_BASE_WEB_URL match 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.