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 | 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