Asset Lifecycle
How does a generated image / sound / sprite / user upload travel from creation to the browser — and what happens to it over time?
§1 The three topologies
Asset serving is not a single CDN. Five user-visible asset subdomains sit on three different architectures. That divergence is not documented anywhere; it's entirely inferred from Cloudflare DNS, worker route bindings, and each service's upload path.
Topology A — Cloudflare Worker + R2 + KV
images · soundsLayers: browser → CF edge cache → CF KV (URL_TO_R2) → CF R2 bucket → origin service → S3.
Up to seven cache tiers for a single image request. KV/R2 written on origin miss via ctx.waitUntil.
images.fuzzycode.dev— 7 route-specific bindings + catchall passthroughsounds.fuzzycode.dev/*— 1 blanket route, cache only whenoutput_format=file- R2 buckets:
fuzzycode-image-cache,fuzzycode-sound-cache
Topology B — CloudFront + S3 (no Worker)
aws (shared public bucket)Layers: browser → CloudFront edge → S3 replit-bucket-public.
Direct CNAME from aws.fuzzycode.dev to CloudFront. No Cloudflare Worker in front. CORS + Cache-Control come from S3 object metadata written at upload.
- Hosts: all published screenshots, sound files (secondary path), ASE sprite exports, user-uploads public path
Cache-Control: public, max-age=31536000, immutableeverywhere- CloudFront config lives outside this workspace
Topology C — Direct to Replit (no CDN)
sprites · uploads · cdnLayers: browser → Cloudflare DNS-only → Replit Autoscale IP → the service's Flask/FastAPI process.
A-records point directly at Replit 34.111.179.208. No edge cache, no Worker, no CDN. Each request hits the origin container.
sprites.fuzzycode.dev— serves container-local disk — URLs break when the Autoscale container recyclesuploads.fuzzycode.dev/asset/<hmac-token>— signed private path, streams S3 bytescdn.fuzzycode.dev— CDNBuddy/resolveredirect resolver, not an asset CDN
§2 The full cache ladder — images
An image request from a published page hits up to seven tiers before paying for a new OpenAI/FAL generation. Reading this ladder is the only way to predict which cache tier invalidation is needed after a change.
HTTP cache] --> CF[Cloudflare edge
caches.default] CF --> W[Worker JS
image-kv-r2-cache] W --> KV[(KV
URL_TO_R2)] KV -->|hit| R2[(R2 bucket
fuzzycode-image-cache)] R2 --> B KV -->|miss| O[ImageBuddy origin
images-api.fuzzycode.dev] O --> LRU[(In-process
LRU)] LRU --> RD[(Redis
URL → S3 map)] RD --> PG[(Postgres
image_cache meta)] PG --> S3[(S3
replit-bucket-public)] S3 -->|miss → generate| OAI[OpenAI / FAL] OAI --> S3 class B,CF,W near class KV,R2 near class LRU,RD,PG mid class S3 far class OAI ext
§3 Lifecycle matrix — upload / serve / cache / expire
| Asset type | Origin service | Store | Serve-time auth | Auto expiry | Delete path | Breaks on container recycle? |
|---|---|---|---|---|---|---|
| AI images | ImageBuddy | S3 + R2 + KV + Postgres + Redis + LRU | public | none | ?force_delete=true dev-only |
no |
| AI sounds | SoundBuddy | S3 + R2 + KV + Postgres | public (gen requires JWT) | none | none in code | no |
| Sprite sheets | SpriteBuddy | local disk (Autoscale container) | public | container recycle | no code path | YES · URLs 404 |
| ASE sprite exports | SpriteBuddy (branch) | S3 (aws.fuzzycode.dev) |
public | none | none | no |
| User uploads (public) | UploaderBuddy | S3 (replit-bucket-public) |
public (upload gated by JWT) | none | no /delete route |
no |
| User uploads (private) | UploaderBuddy | S3, streamed through service | HMAC token + JWT + runtime-controls | none — token never expires | none | no |
| Avatars | UploaderBuddy (profile) | S3 (fuzzycode_users/, fuzzycode_children/) + legacy folders |
public via CloudFront | none | none; write-amplifying (base + versioned + resized) | no |
| Published-page screenshots | Pages | S3 (aws.fuzzycode.dev/uploaded/<hash>.png/.webp) |
public | none; overwritten on re-publish (no cache purge) | none | no |
| CDN library bundles | CDNBuddy | S3 (cdn_buddy_365day_cache/) |
public (/resolve redirects) |
none | none | no |
§4 Gaps — the findings that surprised the verification pass
?force_delete=true on ImageBuddy, which doesn't clean up R2/KV/Postgres/Redis metadata.
UploaderBuddy's "private asset" tokens (build_private_asset_token, storage_backend.py:151-165) are an HMAC of the object key — no exp, no nonce, no revocation. A leaked token is a permanent read capability until the shared HMAC secret rotates.
/create_spritesheet returns url_for('static', …) — a path on the specific Autoscale container's local disk. When the container recycles (hourly, on deploy, on cold start), all non-ASE sprite URLs return 404. Only the ASE-export branch writes to S3. This is inconsistent with every other asset type's durability promise.
x-token-valid: false. The KV/R2 write-back is gated on that header — which works today — but it's one regression away from serving a watermarked image to legitimate downstream users until the cache is purged.
max-age=31536000). Cache-key collision space is bounded by prompt content: generate_safe_filename truncates prompt to 100 chars before hashing, so short prompts that normalize identically share an S3 key. Intentional for dedup on public content; would be risky if a private-content path ever reused the same helper.
§5 Verification pointers
- DNS topologies
docs/cloudflare/zone_fuzzycode.dev_dns_records.json - Worker route bindings
docs/cloudflare/zone_fuzzycode.dev_workers_routes.json - Image worker
docs/cloudflare/workers/uat-image-kv-r2-cache-worker.js:34-99, 184-189 - Sound worker
docs/cloudflare/workers/uat-sound-kv-r2-cache-worker.js:30-107 - UGC (usercontent) worker — NOT on asset path
docs/cloudflare/workers/usercontent-proxy.js:23-41 - ImageBuddy cache decorator
ImageBuddyRobustFastAPI/cache_db.py:1132, 675-1370 - SoundBuddy upload helper
SoundBuddyFastAPI2/s3_upload_helper.py:19-48 - SpriteBuddy local-disk spritesheet
SpriteBuddy/main.py:2095-2151 - SpriteBuddy ASE S3 branch
SpriteBuddy/main.py:2350-2380 - UploaderBuddy signed token
UploaderBuddy/storage_backend.py:151-208 - UploaderBuddy private asset serve
UploaderBuddy/main.py:1695-1770 - Avatar write-amplification
UploaderBuddy/main.py:2246-2311 - Published-HTML URL baking
FuzzyCode/static/script.js:57-73, 198-204, 4963-4977 - CDNBuddy resolve
CDNBuddy/main.py:1157-1370 - force_delete (dev-only image delete)
ImageBuddyRobustFastAPI/cache_db.py:1000-1014, 1159-1167