Goal
Store each game's IGDB data in the database and refresh it in the background, instead of calling IGDB live on every game page. This picks up #112's design (background IGDB fetching), adapted to Cloudflare Workers (ADR 0002, #253).
Today
app/models/game.js calls getIGDBGame(game.igdb_slug) on every game page view, so each view makes 1–2 outside requests (the Twitch token, then IGDB).
app/utils/igdb.server.js caches the Twitch token with memory-cache, in process memory. On Workers that memory doesn't last between requests.
- The slug is interpolated into the Apicalypse query (
where slug = "${slug}") without escaping.
- 213 games have an
igdb_slug (production, 2026-10-04).
Design
- Columns on
game: igdb_data (JSON) and igdb_data_fetched_at.
- Read path: the game page uses the stored
igdb_data. If it's missing or older than a day, the refresh runs after the response (ctx.waitUntil on Workers; fire-and-forget on Node until then). The page never waits for IGDB. The only exception is a game whose slug was just set and has no data yet: show it without the IGDB section, then show it on the next view.
- Avoid duplicate refreshes: set an
igdb_refresh_started_at before fetching, and skip if it's under ~10 minutes old.
- Cron: a scheduled job (a Cron Trigger on Workers) refreshes the stalest N games, so data stays fresh for games nobody visits. Keep N small enough for the free plan's limits.
- Twitch token: store it with its expiry in a
kv_store-style table (or KV), and refresh it shortly before it expires.
- On slug change (create or edit): clear
igdb_data, so the old game's data never shows.
- No
pgmq, no node-cron, no igdb-api-node: call the IGDB API with fetch and an escaped query.
Acceptance criteria
Notes
Goal
Store each game's IGDB data in the database and refresh it in the background, instead of calling IGDB live on every game page. This picks up #112's design (background IGDB fetching), adapted to Cloudflare Workers (ADR 0002, #253).
Today
app/models/game.jscallsgetIGDBGame(game.igdb_slug)on every game page view, so each view makes 1–2 outside requests (the Twitch token, then IGDB).app/utils/igdb.server.jscaches the Twitch token withmemory-cache, in process memory. On Workers that memory doesn't last between requests.where slug = "${slug}") without escaping.igdb_slug(production, 2026-10-04).Design
game:igdb_data(JSON) andigdb_data_fetched_at.igdb_data. If it's missing or older than a day, the refresh runs after the response (ctx.waitUntilon Workers; fire-and-forget on Node until then). The page never waits for IGDB. The only exception is a game whose slug was just set and has no data yet: show it without the IGDB section, then show it on the next view.igdb_refresh_started_atbefore fetching, and skip if it's under ~10 minutes old.kv_store-style table (or KV), and refresh it shortly before it expires.igdb_data, so the old game's data never shows.pgmq, nonode-cron, noigdb-api-node: call the IGDB API withfetchand an escaped query.Acceptance criteria
"can't change the IGDB query.playwright.config.js).igdb-api-nodeandmemory-cacheare removed.Notes