This guide covers:
- Single-node production self-hosting (primary path)
- Local and development self-hosting (secondary path)
- PaaS-oriented deployment flow (Vercel, Netlify, Railway, Render, Fly.io)
This guide does not cover HA, Kubernetes, or multi-region architecture.
- Node.js 20+
- pnpm 10.20.0 (see packageManager in package.json)
- PostgreSQL database
- Cloudflare R2 bucket for media storage
- HTTPS-enabled deployment target for production
Recommended baseline for single-node production:
- 2 vCPU
- 2-4 GB RAM
- 20+ GB disk for app/runtime logs
- DATABASE_URL
- STORAGE_DRIVER (currently: Cloudflare R2)
- NODE_ENV=production
- AUTH_SECRET
- NEXT_PUBLIC_VAPID_PUBLIC_KEY
- VAPID_PRIVATE_KEY
- VAPID_SUBJECT
- R2_ACCOUNT_ID
- R2_BUCKET
- R2_ACCESS_KEY_ID
- R2_SECRET_ACCESS_KEY
- R2_PUBLIC_BASE_URL
Notes:
- In self-hosted mode, Fircle can read R2 credentials from env vars.
- You can later migrate to owner-managed credentials in Settings > Integrations.
Only required when EMAIL_DRIVER=zeptomail:
- EMAIL_FROM_ADDRESS
- EMAIL_FROM_NAME
- ZEPTOMAIL_API_KEY
- ZEPTOMAIL_ACCOUNT_ID
- ZEPTOMAIL_API_BASE_URL (optional override)
- SELF_HOSTED (default true)
- DOMAIN_VERIFICATION_ENABLED (default true)
- DOMAIN_VERIFICATION_TIMEOUT_MS (default 5000)
- DOMAIN_VERIFICATION_MAX_ATTEMPTS (default 3)
- DOMAIN_VERIFICATION_RETRY_DELAY_MS (default 500)
These steps apply to Vercel, Netlify, Railway, Render, and Fly.io style deployments.
- Provision a managed PostgreSQL database.
- Provision Cloudflare R2 and create an API key with bucket access.
- Set all required environment variables in your PaaS app.
- Configure build and start commands:
- Build: pnpm build
- Start: pnpm start
- Ensure migrations run on deploy:
- Preferred release step: pnpm db:migrate
- Deploy the app.
- Open your deployment URL and complete first-time bootstrap at /auth/setup.
Important:
- SELF_HOSTED should remain true for private single-family deployments.
- AUTH_SECRET must be a strong random secret in production.
- Production requires all three VAPID variables, even if you are not actively testing push yet.
-
Install dependencies:
pnpm install
-
Set environment variables in .env.
-
Start PostgreSQL:
- Linux/macOS/WSL: ./start-database.sh
- Windows native: use your own PostgreSQL instance
-
Apply schema:
pnpm db:generate
or
pnpm db:push
-
Start app:
pnpm dev
-
Open http://localhost:3000 and complete /auth/setup for fresh instances.
Bootstrap is available only when:
- SELF_HOSTED=true
- No family exists yet
Setup readiness checks run before bootstrap:
- Database connectivity (Prisma connect and SELECT 1)
- Auth secret presence
- R2 bucket readiness via HeadBucket
- VAPID configuration format and presence rules
- ZeptoMail auth probe when EMAIL_DRIVER=zeptomail
If you changed env vars while the server is already running, restart the app before retrying readiness.
Tenant resolution is based on explicit Domain records only.
- There is no fallback based on slug parsing alone.
- Unmapped hosts resolve to tenant-not-found.
- In production, unverified domains do not resolve tenant context.
Operational guidance:
- Ensure your deployment host/domain is represented in Domain.
- Keep one primary domain per family for canonical routing.
- For custom domains, complete verification before expecting production routing.
Self-host bootstrap behavior:
- If no family exists and SELF_HOSTED=true, resolution returns bootstrap-required and routes to setup flow.
- Terminate TLS at your PaaS edge or reverse proxy.
- Never commit secrets into source control.
- Rotate AUTH_SECRET, R2 keys, and email keys on a regular schedule.
- Restrict database ingress to your app network when possible.
- Keep dependency updates and security patches current.
Minimum production policy:
- Daily PostgreSQL backup snapshots (or provider PITR if available)
- Daily verification that R2 bucket objects are reachable
- Periodic restore drill to a staging environment
Recovery checklist:
- Restore database from latest valid backup.
- Validate Domain mappings and owner account access.
- Validate media accessibility from R2 public base URL.
- Re-run smoke checks on setup-readiness dependent systems (DB, storage, push, email).
Upgrade flow:
- Deploy code update.
- Run pnpm db:migrate.
- Run smoke checks:
- Sign in and open feed
- Upload media
- Verify one notification flow
Routine maintenance:
-
Dry-run orphan media cleanup:
pnpm media:cleanup
-
Delete orphans after review:
pnpm media:cleanup:delete
Recommended cadence: schedule cleanup as a cron job (daily or weekly based on media volume).
- Confirm DATABASE_URL is reachable from the runtime environment.
- Confirm database credentials and SSL mode match provider requirements.
- Verify R2_ACCOUNT_ID, R2_BUCKET, R2_ACCESS_KEY_ID, and R2_SECRET_ACCESS_KEY.
- Confirm key permissions include bucket access.
- Confirm R2 bucket name and endpoint region configuration are correct.
- Ensure NEXT_PUBLIC_VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY, and VAPID_SUBJECT are all set together.
- Ensure VAPID_SUBJECT uses mailto: or http(s) format.
- Confirm EMAIL_DRIVER=zeptomail.
- Confirm ZEPTOMAIL_API_KEY and ZEPTOMAIL_ACCOUNT_ID are valid.
- If network is restricted, allow outbound access to ZeptoMail API base URL.
- Confirm host header matches a Domain.domain entry exactly.
- In production, confirm verifiedAt is set for the domain.
- Confirm DNS points to the correct deployment target.
- Cloud storage deployment guide: docs/cloud-storage-deployment.md
- Self-hosted storage migration guide: docs/self-hosted-storage-migration.md