music on atproto
plyr.fm
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