infra: self-hosted redis to replace upstash (~$75/mo โ†’ ~$2/mo) (#674)

* infra: add self-hosted redis config, remove upstash

Upstash was costing ~$75/month at 37M commands. Self-hosted Redis on
Fly will cost ~$2/month.

Changes:
- redis/fly.toml: Redis 7 Alpine with AOF persistence, 256MB VM
- redis/README.md: deployment and switchover instructions
- Remove upstash from costs script and frontend (redis cost is
included in fly_io since it's a Fly VM)

Deployment steps:
1. fly apps create plyr-redis
2. fly volumes create redis_data --region iad --size 1 -a plyr-redis
3. fly deploy -a plyr-redis (from redis/ dir)
4. fly secrets set DOCKET_URL=redis://plyr-redis.internal:6379 -a relay-api
5. Verify docket tasks work
6. fly redis destroy plyr-redis-prd

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: update README costs and project structure for redis

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

authored by zzstoatzz.io Claude Opus 4.5 and committed by GitHub acf4b14f 253e0565

Changed files
+77 -36
frontend
src
routes
costs
redis
scripts
+7 -5
README.md
··· 119 119 โ”‚ โ””โ”€โ”€ src/routes/ # pages 120 120 โ”œโ”€โ”€ moderation/ # Rust labeler service 121 121 โ”œโ”€โ”€ transcoder/ # Rust audio service 122 + โ”œโ”€โ”€ redis/ # self-hosted Redis config 122 123 โ”œโ”€โ”€ docs/ # documentation 123 124 โ””โ”€โ”€ justfile # task runner 124 125 ``` ··· 128 129 <details> 129 130 <summary>costs</summary> 130 131 131 - ~$35-40/month: 132 - - fly.io backend (prod + staging): ~$10/month 133 - - fly.io transcoder: ~$0-5/month (auto-scales to zero) 132 + ~$20/month: 133 + - fly.io (backend + redis + moderation): ~$14/month 134 134 - neon postgres: $5/month 135 - - audd audio fingerprinting: ~$10/month 136 - - cloudflare (pages + r2): ~$0.16/month 135 + - cloudflare (pages + r2): ~$1/month 136 + - audd audio fingerprinting: $5-10/month (usage-based) 137 + 138 + live dashboard: https://plyr.fm/costs 137 139 138 140 </details> 139 141
-21
frontend/src/routes/costs/+page.svelte
··· 35 35 breakdown: CostBreakdown; 36 36 note: string; 37 37 }; 38 - upstash?: { 39 - amount: number; 40 - note: string; 41 - }; 42 38 audd: { 43 39 amount: number; 44 40 base_cost: number; ··· 97 93 data.costs.fly_io.amount, 98 94 data.costs.neon.amount, 99 95 data.costs.cloudflare.amount, 100 - data.costs.upstash?.amount ?? 0, 101 96 data.costs.audd.amount 102 97 ) 103 98 : 1 ··· 226 221 </div> 227 222 <span class="cost-note">{data.costs.cloudflare.note}</span> 228 223 </div> 229 - 230 - {#if data.costs.upstash} 231 - <div class="cost-item"> 232 - <div class="cost-header"> 233 - <span class="cost-name">upstash</span> 234 - <span class="cost-amount">{formatCurrency(data.costs.upstash.amount)}</span> 235 - </div> 236 - <div class="cost-bar-bg"> 237 - <div 238 - class="cost-bar" 239 - style="width: {barWidth(data.costs.upstash.amount, maxCost)}%" 240 - ></div> 241 - </div> 242 - <span class="cost-note">{data.costs.upstash.note}</span> 243 - </div> 244 - {/if} 245 224 246 225 <div class="cost-item"> 247 226 <div class="cost-header">
+41
redis/README.md
··· 1 + # plyr-redis 2 + 3 + self-hosted Redis on Fly.io for docket background tasks. 4 + 5 + ## deployment 6 + 7 + ```bash 8 + # first time: create app and volume 9 + fly apps create plyr-redis 10 + fly volumes create redis_data --region iad --size 1 -a plyr-redis 11 + 12 + # deploy 13 + fly deploy -a plyr-redis 14 + ``` 15 + 16 + ## connecting from other fly apps 17 + 18 + Redis is accessible via Fly's private network: 19 + 20 + ``` 21 + redis://plyr-redis.internal:6379 22 + ``` 23 + 24 + Update `DOCKET_URL` secret on backend apps: 25 + 26 + ```bash 27 + fly secrets set DOCKET_URL=redis://plyr-redis.internal:6379 -a relay-api 28 + fly secrets set DOCKET_URL=redis://plyr-redis.internal:6379 -a relay-api-staging 29 + ``` 30 + 31 + ## configuration 32 + 33 + - **persistence**: AOF (append-only file) enabled for durability 34 + - **memory**: 200MB max with LRU eviction 35 + - **storage**: 1GB volume mounted at /data 36 + 37 + ## cost 38 + 39 + ~$1.94/month (256MB shared-cpu VM) + $0.15/month (1GB volume) = ~$2.09/month 40 + 41 + vs. Upstash pay-as-you-go which was costing ~$75/month at 37M commands.
+28
redis/fly.toml
··· 1 + app = "plyr-redis" 2 + primary_region = "iad" 3 + 4 + [build] 5 + image = "redis:7-alpine" 6 + 7 + [mounts] 8 + source = "redis_data" 9 + destination = "/data" 10 + 11 + [env] 12 + # redis config via command line args in [processes] 13 + 14 + [processes] 15 + app = "--appendonly yes --maxmemory 200mb --maxmemory-policy allkeys-lru" 16 + 17 + [[services]] 18 + protocol = "tcp" 19 + internal_port = 6379 20 + 21 + # only accessible within private network 22 + [[services.ports]] 23 + port = 6379 24 + 25 + [[vm]] 26 + memory = "256mb" 27 + cpu_kind = "shared" 28 + cpus = 1
+1 -10
scripts/costs/export_costs.py
··· 39 39 # fly.io: manually updated from cost explorer (TODO: use fly billing API) 40 40 # neon: fixed $5/month 41 41 # cloudflare: mostly free tier 42 - # upstash: free tier (256MB, 500K commands/month) 42 + # redis: self-hosted on fly (included in fly_io costs) 43 43 FIXED_COSTS = { 44 44 "fly_io": { 45 45 "breakdown": { ··· 60 60 "domain": 1.00, 61 61 "total": 1.16, 62 62 "note": "r2 egress is free, pages free tier", 63 - }, 64 - "upstash": { 65 - "total": 0.00, 66 - "note": "redis for docket + caching (free tier: 256MB, 500K commands/month)", 67 63 }, 68 64 } 69 65 ··· 206 202 plyr_fly 207 203 + FIXED_COSTS["neon"]["total"] 208 204 + FIXED_COSTS["cloudflare"]["total"] 209 - + FIXED_COSTS["upstash"]["total"] 210 205 + audd_stats["estimated_cost"] 211 206 ) 212 207 ··· 231 226 "domain": FIXED_COSTS["cloudflare"]["domain"], 232 227 }, 233 228 "note": FIXED_COSTS["cloudflare"]["note"], 234 - }, 235 - "upstash": { 236 - "amount": FIXED_COSTS["upstash"]["total"], 237 - "note": FIXED_COSTS["upstash"]["note"], 238 229 }, 239 230 "audd": { 240 231 "amount": audd_stats["estimated_cost"],