Cross-cutting · verified · topology-reality check

Asset Lifecycle

How does a generated image / sound / sprite / user upload travel from creation to the browser — and what happens to it over time?

Last verified 2026-04-17 against code
Slug asset-lifecycle
Topologies 3 distinct architectures, not 1
Asset types 5 (images · sounds · sprites · uploads · cdn libs)

§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 · sounds

Layers: 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 passthrough
  • sounds.fuzzycode.dev/* — 1 blanket route, cache only when output_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, immutable everywhere
  • CloudFront config lives outside this workspace

Topology C — Direct to Replit (no CDN)

sprites · uploads · cdn

Layers: 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 recycles
  • uploads.fuzzycode.dev/asset/<hmac-token> — signed private path, streams S3 bytes
  • cdn.fuzzycode.dev — CDNBuddy /resolve redirect 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.

flowchart LR classDef near fill:#eaf3ff,stroke:#1d58b1,color:#111 classDef mid fill:#fff4d6,stroke:#a47a3a,color:#111 classDef far fill:#fdf6ea,stroke:#683c06,color:#111 classDef ext fill:#c7d9e8,stroke:#1d58b1,color:#111 B[Browser
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

No deletion or retention policy anywhere in code. No S3 lifecycle rules in this repo; no TTL job; no cleanup script for any asset type. Avatars write multiple versions on each change with zero garbage collection. The only delete path is a developer-only ?force_delete=true on ImageBuddy, which doesn't clean up R2/KV/Postgres/Redis metadata.
Signed URLs without expiry. 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.
SpriteBuddy's primary output is ephemeral. /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.
Watermarked / error images can poison the public cache. On invalid JWT, ImageBuddy returns a watermarked image with 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.
Cache-Control is uniformly 365-day-immutable at origin (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