Install these CLIs before running the setup script:
# Fly.io CLI
curl -L https://fly.io/install.sh | sh
flyctl auth login
# Neon CLI (for managed Postgres)
npm i -g neonctl
neonctl auth
# GitHub CLI (for setting repo secrets)
brew install gh
gh auth loginThe setup script handles: secret generation, Neon DB provisioning, Fly app creation, Fly + GitHub secrets, migrations, and first deploy.
./scripts/deploy-init.sh --spotify-client-id <your-client-id>With optional services:
./scripts/deploy-init.sh \
--spotify-client-id <id> \
--resend-api-key re_... \
--anthropic-api-key sk-ant-...Preview what it would do:
./scripts/deploy-init.sh --spotify-client-id <id> --dry-run- Generates
IRON_SESSION_PASSWORD,POLL_SECRET,TOKEN_ENCRYPTION_KEY, VAPID keys - Creates a Neon Postgres project (
us-east-2, closest to Flyord) - Creates the Fly.io app
- Sets all secrets on Fly.io
- Creates a
FLY_API_TOKENdeploy token and sets it +DATABASE_URLas GitHub repo secrets - Runs
npm run db:migrateagainst the production DB - Deploys to Fly.io
- Verifies the health check
- Go to developer.spotify.com/dashboard
- Create a new app (or use existing) — set the app name to "Swapify"
- Under Settings > Redirect URIs, add:
https://swapify.312.dev/api/auth/callback - Under Settings > APIs used, ensure Web API and Web Playback SDK are checked
- Add beta tester emails under User Management (max 5 in dev mode)
- Set
SPOTIFY_DEV_MODE=trueon Fly (see Spotify Dev Mode below) - To go public, submit a Quota Extension Request and set
SPOTIFY_DEV_MODE=false
- Add a CNAME record:
swapify.312.dev->swapify.fly.dev - After DNS propagates, provision TLS:
flyctl certs add swapify.312.dev
- Visit
https://swapify.312.dev— landing page loads - Login with Spotify — redirects through OAuth and lands on
/dashboard - Create a Swaplist — Spotify playlist created
- Share invite link — second user can join
- Add a track — appears in Spotify playlist and in app
- Swipe to react — reaction saved
-
curl https://swapify.312.dev/api/healthreturns{"status":"ok"}
Deploys happen automatically via GitHub Actions on push to main:
- CI — lint, type-check, build
- Migrate — runs
npm run db:migrateagainst production (usesDATABASE_URLsecret) - Deploy —
flyctl deploy --remote-only(usesFLY_API_TOKENsecret)
| Secret | Set by script | Purpose |
|---|---|---|
FLY_API_TOKEN |
Yes | Fly.io deploy token |
DATABASE_URL |
Yes | Production Postgres connection string |
- .github/workflows/ci.yml — PR checks (lint, type-check, build)
- .github/workflows/deploy.yml — Push to main (CI + migrate + deploy)
- .github/workflows/security.yml — CodeQL, Gitleaks, npm audit
| Component | Service | Cost |
|---|---|---|
| App server | Fly.io shared-cpu-1x, 512MB, always-on | ~$3.32/mo |
| Database | Neon free tier (0.5GB, auto-suspend) | $0 |
| Total | ~$3.32/mo |
The machine runs 24/7 (min_machines_running = 1) because the polling loop needs to stay alive to detect Spotify listens and refresh tokens.
After modifying src/db/schema.ts:
npm run db:generate # Generate new migration SQL in drizzle/
git add drizzle/ && git commit # Commit the migration
git push # Triggers: CI → migrate → deployMigrations run automatically in the deploy pipeline before the new code is deployed.
| Secret | Required | Generated by script |
|---|---|---|
SPOTIFY_CLIENT_ID |
Yes | No (you provide it) |
SPOTIFY_REDIRECT_URI |
Yes | Yes |
IRON_SESSION_PASSWORD |
Yes | Yes |
POLL_SECRET |
Yes | Yes |
TOKEN_ENCRYPTION_KEY |
Yes | Yes |
DATABASE_URL |
Yes | Yes (from Neon) |
NEXT_PUBLIC_VAPID_PUBLIC_KEY |
Yes | Yes |
VAPID_PRIVATE_KEY |
Yes | Yes |
VAPID_SUBJECT |
Yes | Yes |
RESEND_API_KEY |
No | No (you provide it) |
ANTHROPIC_API_KEY |
No | No (you provide it) |
SPOTIFY_DEV_MODE |
No | No (default: false) |
Set SPOTIFY_DEV_MODE=true when your Spotify app is still in development mode (not yet approved for extended quota). This activates conservative rate limiting to stay within Spotify's lower API budget for dev apps.
fly secrets set SPOTIFY_DEV_MODE=true| Setting | Dev Mode (true) |
Production (false) |
|---|---|---|
| Max authenticated users | 5 (Spotify's dev mode limit) | Unlimited |
| API call budget (rolling 30s) | 50 calls | 300 calls |
| Poll interval | 60s | 30s |
| Search results per query | 5 | 10 |
| Saved-tracks check | Every 10th poll cycle | Every 4th cycle |
| Playlist audit | Every 6th poll cycle | Every 2nd cycle |
| Liked playlist sync | Every 10th poll cycle | Every 4th cycle |
| Per-user search rate limit | 10 req/min | 30 req/min |
| Per-user mutation rate limit | 8 req/min | 20 req/min |
| Per-user general API limit | 20 req/min | 60 req/min |
Set SPOTIFY_DEV_MODE=false (or remove it) after your Spotify app is approved for extended quota mode. Requirements for extended quota (as of May 2025):
- Legally registered business entity
- 250,000+ monthly active users
- Available in key Spotify markets
- Active, launched service
Until then, keep dev mode enabled to avoid hitting Spotify's rate limits.
- Global API call budget (src/lib/spotify-config.ts): Tracks all Spotify API calls in a rolling 30-second window. Every
spotifyFetchcall checks the budget before proceeding. If the budget is exhausted, calls wait up to 10 seconds for capacity. - User cap enforcement: New user signups are rejected at the OAuth callback when the user count reaches the limit. Existing users can still log in.
- Polling throttling: Background operations (audit, saved-tracks check, liked sync) run at reduced frequencies. The polling loop aborts early if the API budget is exhausted.
- Per-user rate limits (src/lib/rate-limit.ts): Token-bucket limits on user-facing routes (search, mutations) are halved in dev mode.