{"id":51132694,"url":"https://github.com/stormwarning/respawn-social-api","last_synced_at":"2026-06-25T14:30:33.991Z","repository":{"id":366195322,"uuid":"1274529044","full_name":"stormwarning/respawn-social-api","owner":"stormwarning","description":null,"archived":false,"fork":false,"pushed_at":"2026-06-25T03:35:44.000Z","size":52,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-25T05:13:59.902Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stormwarning.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-19T15:56:29.000Z","updated_at":"2026-06-25T03:35:47.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/stormwarning/respawn-social-api","commit_stats":null,"previous_names":["stormwarning/respawn-social-backend","stormwarning/respawn-social-api"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/stormwarning/respawn-social-api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stormwarning%2Frespawn-social-api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stormwarning%2Frespawn-social-api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stormwarning%2Frespawn-social-api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stormwarning%2Frespawn-social-api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stormwarning","download_url":"https://codeload.github.com/stormwarning/respawn-social-api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stormwarning%2Frespawn-social-api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34780124,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-25T02:00:05.521Z","response_time":101,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-25T14:30:33.200Z","updated_at":"2026-06-25T14:30:33.978Z","avatar_url":"https://github.com/stormwarning.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# respawn-social-backend\n\nOne job: **proxy + cache the [IGDB](https://www.igdb.com) game API** so we never\nexceed IGDB's strict rate limit (4 requests/second, shared across _all_ users).\n\nThe front-end (the [Respawn Svelte app](https://tangled.org/tidaltheory.io/respawn-social-web),\nwhich also handles all AT Protocol / Bluesky login) talks to this service for\ngame data — never to IGDB directly (IGDB blocks browser requests, and the API\ncredentials must stay server-side).\n\n---\n\n## Why this exists (the short version)\n\nA browser **cannot** call IGDB: IGDB rejects cross-origin browser requests and\nrequires a secret token. And IGDB's rate limit is _global_ to our credentials —\nif 50 users searched at once, 50 browser calls would instantly blow the limit.\n\nSo this backend is the **single choke point**: it holds the token, paces all\noutgoing calls under the limit, and caches everything in Postgres so popular\ngames are fetched from IGDB essentially **once, ever**.\n\n```\n[ Front-end ] --HTTP/JSON--\u003e [ THIS SERVICE ] --rate-limited--\u003e [ IGDB ]\n                                   |\n                              Postgres (game cache + token)\n```\n\n---\n\n## Project layout\n\n```\nsrc/\n  index.ts            App entry: HTTP server, CORS, route wiring, error handlers.\n  config.ts           Loads + validates env vars at startup (fails fast if wrong).\n  logger.ts           Structured logging (pretty in dev, JSON in prod).\n\n  db/\n    schema.ts         All database tables (the source of truth).\n    client.ts         The Postgres connection pool + Drizzle client.\n    migrate.ts        Applies SQL migrations (run on deploy).\n\n  igdb/               === The \"don't hammer IGDB\" core ===\n    token.ts          Fetches/caches/refreshes the Twitch (IGDB) access token.\n    client.ts         The rate-limited request queue (the 4 req/s gate) + retries.\n    data.ts           Read-through cache: getGame() / searchGames() with\n                      stale-while-revalidate + request dedup.\n\n  lib/\n    single-flight.ts  Helper: dedupe identical concurrent requests.\n\n  routes/\n    games.ts          GET /games/:id, GET /games/search\n```\n\nMost files have inline comments explaining the backend concept they implement.\n\n---\n\n## Prerequisites\n\n- **[Deno](https://deno.com) 2+** (runs the TypeScript directly — no build step)\n- A **PostgreSQL** database (local or hosted)\n- **Twitch app credentials** for IGDB:\n  create an app at \u003chttps://dev.twitch.tv/console/apps\u003e (Client Type:\n  _Confidential_) to get a Client ID + Secret. IGDB authenticates via Twitch.\n\n---\n\n## Setup\n\n```bash\ndeno install                  # installs npm deps into node_modules\ncp .env.example .env          # then fill in your Twitch client id + secret\n```\n\n---\n\n## Local development\n\nPostgres runs in Docker; the app runs natively for fast watch-reload.\n\n```bash\ndocker compose up -d db       # start Postgres (creates the `respawn` database)\ndeno task db:migrate          # apply the SQL in ./drizzle (creates the tables)\ndeno task dev                 # run the API in watch mode\n```\n\nThe default `DATABASE_URL` in `.env.example`\n(`postgres://postgres:postgres@localhost:5433/respawn`) already matches the\ncompose service, so no extra config is needed. (Host port is **5433** to avoid\nclashing with any native Postgres already running on 5432.)\n\n\u003e If you change `src/db/schema.ts`, regenerate the migration with\n\u003e `deno task db:generate`, then run `deno task db:migrate` again.\n\nTo stop the database: `docker compose down` (add `-v` to also wipe the data).\n\n---\n\n## Running (production)\n\n```bash\ndeno task start               # run once (no watch)\n```\n\nThe server listens on `PORT` (default 3000).\n\n\u003e **Deno permissions:** the tasks grant explicit access flags\n\u003e (`--allow-net`, `--allow-env`, `--allow-read`, `--allow-sys`). Deno denies\n\u003e network/env/filesystem access unless granted — this is its security model.\n\n---\n\n## Lint \u0026 format\n\nLinting is [oxlint](https://oxc.rs) and formatting is oxfmt (configured in\n`oxlint.config.ts` and `oxfmt.config.ts`), run via Deno's npm support.\n\n```bash\ndeno task lint                # check with oxlint\ndeno task format              # format with oxfmt\n```\n\n---\n\n## Endpoints\n\n| Method | Path                          | Description                                  |\n| ------ | ----------------------------- | -------------------------------------------- |\n| GET    | `/health`                     | Liveness check.                              |\n| GET    | `/games/:id`                  | A single game by IGDB id (cached).           |\n| GET    | `/games/search?q=zelda`       | Search games by title (cached).              |\n\nQuick check:\n\n```bash\ncurl localhost:3000/health\ncurl \"localhost:3000/games/search?q=hollow%20knight\"\n```\n\n---\n\n## How the caching works (in plain terms)\n\n- **Single game** (`getGame`): look in Postgres first. Fresh? return it (no IGDB\n  call). Stale? return the old copy _immediately_ and refresh in the background\n  (\"stale-while-revalidate\"). Missing? fetch from IGDB once and store it.\n- **Search** (`searchGames`): cached by normalized query for a few hours.\n- **Rate limit**: every IGDB call goes through one queue capped below 4 req/s, so\n  even a traffic spike just queues up instead of getting rejected (HTTP 429).\n- **Dedup**: if many users request the same uncached thing at once, only one\n  IGDB call is made; the rest await it.\n\n---\n\n## Deployment notes\n\n- Built for a **long-lived container** (Fly.io / Railway), _not_ pure serverless\n  — the in-memory rate limiter and token cache need a persistent process.\n- A `Dockerfile` is included (based on the official `denoland/deno` image; Deno\n  runs the TypeScript directly, so there's no compile step).\n- Set all `.env` values as platform secrets. Set `ALLOWED_ORIGINS` to your\n  front-end's real origin(s).\n- Single instance is assumed. The in-memory rate limiter and token cache\n  (`src/igdb/client.ts`, `src/igdb/token.ts`) live in the process; to run\n  **multiple** instances you'd want a shared cache/lock (e.g. Redis).\n\n## Note on IGDB usage terms\n\nIGDB is free for **non-commercial** use under the Twitch Developer Agreement.\nA commercial product needs a partner agreement (which also unlocks webhooks /\ndata dumps — a great future upgrade to keep the cache warm with near-zero calls).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstormwarning%2Frespawn-social-api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstormwarning%2Frespawn-social-api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstormwarning%2Frespawn-social-api/lists"}