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