1# environment separation strategy 2 3plyr.fm uses a simple three-tier deployment strategy: development → staging → production. 4 5## environments 6 7| environment | trigger | backend URL | database | frontend | storage | 8|-------------|---------|-------------|----------|----------|---------| 9| **development** | local | localhost:8001 | plyr-dev (neon) | localhost:5173 | audio-dev, images-dev (r2) | 10| **staging** | push to main | api-stg.plyr.fm | plyr-staging (neon) | stg.plyr.fm (main branch) | audio-staging, images-staging (r2) | 11| **production** | github release | api.plyr.fm | plyr-prod (neon) | plyr.fm (production-fe branch) | audio-prod, images-prod (r2) | 12 13## workflow 14 15### local development 16 17```bash 18# start backend (hot reloads) 19just run-backend 20 21# start frontend (hot reloads) 22just frontend dev 23 24# start transcoder (hot reloads) 25just transcoder run 26``` 27 28connects to `plyr-dev` neon database and uses `fm.plyr.dev` atproto namespace. 29 30### staging deployment (automatic) 31 32**trigger**: push to `main` branch 33 34**backend**: 351. github actions runs `.github/workflows/deploy-staging.yml` 362. runs `alembic upgrade head` via `release_command` 373. backend available at `https://api-stg.plyr.fm` (custom domain) and `https://relay-api-staging.fly.dev` (fly.dev domain) 38 39**frontend**: 40- cloudflare pages project `plyr-fm-stg` tracks `main` branch 41- uses production environment with `PUBLIC_API_URL=https://api-stg.plyr.fm` 42- available at `https://stg.plyr.fm` (custom domain) 43 44**testing**: 45- frontend: `https://stg.plyr.fm` 46- backend: `https://api-stg.plyr.fm/docs` 47- database: `plyr-staging` (neon) 48- storage: `audio-staging`, `images-staging` (r2) 49 50### production deployment (manual) 51 52**trigger**: run `just release` (creates github tag, merges main → production-fe) 53 54**backend**: 551. github actions runs `.github/workflows/deploy-prod.yml` 562. runs `alembic upgrade head` via `release_command` 573. backend available at `https://api.plyr.fm` 58 59**frontend**: 601. release script merges `main``production-fe` branch 612. cloudflare pages production environment tracks `production-fe` branch 623. uses production environment with `PUBLIC_API_URL=https://api.plyr.fm` 634. available at `https://plyr.fm` 64 65**creating a release**: 66```bash 67# after validating changes in staging: 68just release 69``` 70 71this will: 721. create timestamped github tag (triggers backend deploy) 732. merge main → production-fe (triggers frontend deploy) 74 75**testing**: 76- frontend: `https://plyr.fm` 77- backend: `https://api.plyr.fm/docs` 78- database: `plyr-prod` (neon) 79- storage: `audio-prod`, `images-prod` (r2) 80 81## configuration files 82 83### backend 84 85**fly.staging.toml** / **fly.toml**: 86- release_command: `uv run alembic upgrade head` (runs migrations before deploy) 87- environment variables configured via `flyctl secrets set` 88 89### frontend 90 91**cloudflare pages** (two separate projects): 92 93**plyr-fm** (production): 94- framework: sveltekit 95- build command: `cd frontend && bun run build` 96- build output: `frontend/build` 97- production branch: `production-fe` 98- production environment: `PUBLIC_API_URL=https://api.plyr.fm` 99- custom domain: `plyr.fm` 100 101**plyr-fm-stg** (staging): 102- framework: sveltekit 103- build command: `cd frontend && bun run build` 104- build output: `frontend/build` 105- production branch: `main` 106- production environment: `PUBLIC_API_URL=https://api-stg.plyr.fm` 107- custom domain: `stg.plyr.fm` 108 109### secrets management 110 111all secrets configured via `flyctl secrets set`. key environment variables: 112- `DATABASE_URL` → neon connection string (env-specific) 113- `FRONTEND_URL` → frontend URL for CORS (production: `https://plyr.fm`, staging: `https://stg.plyr.fm`) 114- `ATPROTO_APP_NAMESPACE` → atproto namespace (environment-specific, separates records by environment) 115 - development: `fm.plyr.dev` (local `.env`) 116 - staging: `fm.plyr.stg` 117 - production: `fm.plyr` 118- `ATPROTO_CLIENT_ID`, `ATPROTO_REDIRECT_URI` → oauth config (env-specific, must use custom domains for cookie-based auth) 119 - production: `https://api.plyr.fm/oauth-client-metadata.json` and `https://api.plyr.fm/auth/callback` 120 - staging: `https://api-stg.plyr.fm/oauth-client-metadata.json` and `https://api-stg.plyr.fm/auth/callback`- `OAUTH_ENCRYPTION_KEY` → unique per environment 121- `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` → r2 credentials 122- `LOGFIRE_WRITE_TOKEN`, `LOGFIRE_ENVIRONMENT` → observability config 123 124## database migrations 125 126migrations run automatically on deploy via fly.io `release_command`. 127 128**both environments**: 129- use `alembic upgrade head` to run migrations 130- migrations run before deployment completes 131- alembic tracks applied migrations via `alembic_version` table 132 133### rollback strategy 134 135if a migration fails: 1361. **staging**: fix and push to main 1372. **production**: 138 - revert via alembic: `uv run alembic downgrade -1` 139 - or restore database from neon backup 140 141## monitoring 142 143**staging**: 144- logfire: environment filter `LOGFIRE_ENVIRONMENT=staging` 145- backend logs: `flyctl logs -a relay-api-staging` 146 147**production**: 148- logfire: environment filter `LOGFIRE_ENVIRONMENT=production` 149- backend logs: `flyctl logs -a relay-api` 150 151## costs 152 153**current**: ~$25-30/month 154- fly.io backend (production): ~$10/month (shared-cpu-1x, 256MB RAM) 155- fly.io backend (staging): ~$10/month (shared-cpu-1x, 256MB RAM) 156- fly.io transcoder: ~$0-5/month (auto-scales to zero when idle) 157- neon postgres: $5/month (starter plan) 158- cloudflare pages: free (frontend hosting) 159- cloudflare R2: ~$0.16/month (6 buckets across dev/staging/prod) 160 161## workflow summary 162 163- **merge PR to main**: deploys staging backend + staging frontend to `stg.plyr.fm` 164- **run `just release`**: deploys production backend + production frontend to `plyr.fm` 165- **database migrations**: run automatically before deploy completes 166- **rollback**: revert github release or restore database from neon backup 167 168## custom domain architecture 169 170both environments use custom domains on the same eTLD+1 (`plyr.fm`) to enable secure cookie-based authentication: 171 172**staging**: 173- frontend: `stg.plyr.fm` → cloudflare pages project `plyr-fm-stg` 174- backend: `api-stg.plyr.fm` → fly.io app `relay-api-staging` 175- same eTLD+1 allows HttpOnly cookies with `Domain=.plyr.fm` 176 177**production**: 178- frontend: `plyr.fm` → cloudflare pages project `plyr-fm` 179- backend: `api.plyr.fm` → fly.io app `relay-api` 180- same eTLD+1 allows HttpOnly cookies with `Domain=.plyr.fm` 181 182this architecture prevents XSS attacks by storing session tokens in HttpOnly cookies instead of localStorage. 183 184## cloudflare access (staging only) 185 186staging environments are protected with Cloudflare Access to prevent public access while maintaining accessibility for authorized developers. 187 188### configuration 189 190**protected domains**: 191- `stg.plyr.fm` (frontend) - requires GitHub authentication 192- `api-stg.plyr.fm` (backend API) - requires GitHub authentication 193 194**public bypass paths** (no authentication required): 195- `api-stg.plyr.fm/health` - uptime monitoring 196- `api-stg.plyr.fm/docs` - API documentation 197- `stg.plyr.fm/manifest.webmanifest` - PWA manifest 198- `stg.plyr.fm/icons/*` - PWA icons 199 200### how it works 201 2021. **DNS proxy**: both `stg.plyr.fm` and `api-stg.plyr.fm` are proxied through Cloudflare (orange cloud) 2032. **access policies**: GitHub OAuth or one-time PIN authentication required for all paths except bypassed endpoints 2043. **shared authentication**: both frontend and API share the same eTLD+1 (`plyr.fm`), allowing the `CF_Authorization` cookie to work across both domains 2054. **application ordering**: bypass applications for specific paths (`/health`, `/docs`, etc.) are ordered **above** the wildcard application to take precedence 206 207### requirements for proxied setup 208 209- **Cloudflare SSL/TLS mode**: set to "Full" (encrypts browser → Cloudflare → origin) 210- **Fly.io certificates**: both domains must have valid certificates on Fly.io (`flyctl certs list`) 211- **DNS records**: both domains must be set to "Proxied" (orange cloud) in Cloudflare DNS 212 213### debugging access issues 214 215**if staging is still publicly accessible**: 2161. verify DNS records are proxied (orange cloud) in Cloudflare DNS 2172. check application ordering in Cloudflare Access (specific paths before wildcards) 2183. verify policy action is "Allow" with authentication rules (not "Bypass" with "Everyone") 2194. clear browser cache or use incognito mode to bypass cached responses 2205. wait 1-2 minutes for Access policy changes to propagate 221 222**if legitimate requests are blocked**: 2231. check if path needs a bypass rule (e.g., `/health`, `/docs`) 2242. verify bypass applications are ordered above the main application 2253. ensure bypass policy uses "Bypass" action with "Everyone" selector