Imported from eniklas/gamatrix (
AGENTS.md). Install upstream withnpx skills add eniklas/gamatrix. Copyright stays with the author.
Agent Instructions for Gamatrix
This file is the canonical repository guidance for AI coding agents in this repo. Keep shared guidance here, and keep CLAUDE.md and .github/copilot-instructions.md as thin wrappers that point back to this file.
Project Overview
Gamatrix is a Python web application that compares game libraries across multiple users. Users upload their GOG Galaxy SQLite database through the browser; the app parses it, stores ownership data in DynamoDB, and enriches each game with multiplayer metadata from the IGDB API. The web layer runs as a FastAPI app on AWS Lambda (via Mangum); background work (parsing uploads, enriching games) runs as separate Lambda functions triggered by S3 events and SQS messages.
Build, test, and lint commands
Prefer the root just recipes when they match the task:
uv sync --extra dev --extra cdk # host-side checks/tooling (CDK tests need the extra)
just dev # run the FastAPI app locally with reload
just env # create .env from .env-sample (then fill IGDB creds)
just up # start the full local stack with Docker Compose
just gen-fixtures db=<path> # generate sample fixtures from YOUR GOG Galaxy DB (not committed)
just init-local # create local tables/bucket and seed config/users
just seed-local # seed the generated test users + sample game libraries
just bootstrap db=<path> # one-shot: gen-fixtures + init-local + seed-local
just worker # run the local background enrichment worker
just check # CI-equivalent checks: lint + typecheck + test
just lint # black --check . && flake8 src tests
just typecheck # mypy
just test # pytest
uv run pytest tests/test_games.py
uv run pytest tests/test_games.py -k merge_cross_platform_duplicates
just format # black .
just build # build Lambda container images
just deploy # deploy CDK infrastructure
just set-igdb-secret <client_id> <client_secret> # Store IGDB API credentials in Secrets Manager (post-deploy)
Local development with sample data
README is the canonical setup doc for local development, sample-data generation, host-side tooling, and troubleshooting:
- README local development
- README running checks
- CDK README for deploy-specific prerequisites
Agent-specific reminders:
- Sample fixtures are derived from the developer's own GOG Galaxy DB and remain
git-ignored under
scripts/sample_data/. seed-localintentionally hard-resets existing local users so regenerated sample data replaces the prior local state cleanly.bootstrapusesinit_local.py --skip-default-usersso the generated sample users are the only seeded user set on that path.- For host-side pytest coverage, install the
cdkextra and ensurenodeis on PATH; the CDK tests importaws_cdk/jsii, which shells out to Node.
Architecture
Gamatrix is a FastAPI web app that runs locally under Uvicorn and in AWS Lambda via Mangum. src/gamatrix/app.py builds the app, mounts static assets, and registers the auth, games, preferences, and upload routers. src/gamatrix/lambda_handler.py is the AWS entry point.
Data flow: The main data flow spans several modules:
- Users upload a local GOG Galaxy SQLite database through the browser.
src/gamatrix/upload.pygenerates a presigned S3 POST so the file goes straight to object storage.- The parser/ingest path reads that SQLite file, extracts ownership and install data, and writes normalized records into DynamoDB through
src/gamatrix/storage/dynamo.py. src/gamatrix/games/service.pybuilds the comparison view from DynamoDB data, merges cross-platform duplicates, applies metadata overrides, and filters/sorts for the current request.- If selected games are stale or not yet enriched,
src/gamatrix/games/routes.pycreates an enrichment job. In AWS that job is processed asynchronously via SQS/Lambda; locallyscripts/local_worker.pypolls the jobs table and runs the same enricher code. src/gamatrix/igdb/enricher.pygroups releases byigdb_key, fetches IGDB metadata once per group, writes the results back to the games table, and updates job progress. The UI polls/api/jobs/{job_id}and updates via HTMX fragments.
Shared infrastructure concerns are centralized:
src/gamatrix/config.pyowns environment-driven settings, table names, and secret resolution.src/gamatrix/storage/contains the DynamoDB repository, S3 wrapper, and queue wrapper.src/gamatrix/templating.pysets up the shared Jinja environment used by the routers.
Routers:
auth/routes.py— login, logout, forgot/reset password (SES email in AWS, SMTP/MailHog locally)games/routes.py—/gamespage,/games/tableHTMX fragment,/api/jobs/{job_id}polling endpoint, admin refresh routespreferences.py— (src/gamatrix/preferences.py) user preference saves (selected users, view mode, filters)upload.py— S3 presigned POST generation for browser uploads
Background Lambdas (also run as local workers via scripts/local_worker.py):
- Parser (
gogdb/) — triggered by S3OBJECT_CREATED; reads the uploaded GOG Galaxy SQLite DB, extracts ownership and install status, writes to DynamoDBuser_librariesandgamestables. - Enricher (
igdb/enricher.py) — triggered by SQS; calls IGDB API for multiplayer metadata, writes results back togamestable, updates job status.
Storage (storage/):
dynamo.py—Repositoryclass: all DynamoDB reads/writes. Handles Decimal serialization, pagination (_scan,_query_all), and table-name prefixing.s3.py— presigned POST generation for browser uploads; file downloads.queue.py— SQSsend_messagewrapper.
IGDB client (igdb/client.py):
IGDBClientauthenticates with the Twitch/IGDB API, looks up multiplayer metadata by GOG release key or slug, and respects rate limits with exponential backoff (_RateLimiter+push_out()).
Config (config.py):
Settings(pydantic-settings): sourced from environment variables /.env. In AWS, secrets come from Secrets Manager —igdb_secret_nameandjwt_secret_namepoint at the relevant secrets;resolve_igdb_credentials()andresolve_jwt_secret()fetch and cache them.- A
model_validatorrejects startup with the default JWT secret in production (unlessLOCAL_DEV=true).
Templates (src/gamatrix/templates/): Jinja2 + HTMX. games.html.jinja renders the main game list; job_status.html.jinja is polled via HTMX to show enrichment progress.
Tests: tests/, run with pytest. tests/conftest.py provides fixtures. Coverage configured in pyproject.toml.
Key conventions
- Route handlers should depend on the storage/auth abstractions instead of constructing clients inline. Use
Repositoryviaget_repo()/get_repository(), and reuseget_s3()/get_queue()for AWS-facing operations. - Persisted game data is keyed by
release_key, but the compare view deliberately merges duplicate titles by(slug, owners)so the same game across platforms collapses into one row only when the owner set is identical. - An upload only deletes stored titles for platforms it can vouch for: connected in GOG's
PlatformConnectionsand reporting at least one title (issue #186). Titles from any other platform are carried over untouched, keeping their originaldb_updated_atas the record of when they were last seen.Repository.delete_user_library_platform()(surfaced on/upload) is the deliberate way to drop them. - Saved user preferences live on the user record in DynamoDB. Always merge stored preferences with
DEFAULT_PREFERENCES, and keep the request overlay behavior consistent withparse_options()ingames/web.pyandmerge_preferences()ingames/preferences.py. - Page routes and HTMX/API routes use different auth dependencies on purpose:
current_user()redirects unauthenticated browsers to login, whilecurrent_user_api()returns401for fragment/API calls. - Local development does not mirror AWS exactly.
/upload/completeingests inline only whenLOCAL_DEV=true, and the local worker polls the jobs table because there is no local SQS event source. - The repository layer normalizes DynamoDB types and identifiers for the rest of the app: emails are lowercased on write,
user_idvalues are treated as strings, and floats are converted toDecimalbefore writes. - Tests are built around the real
RepositoryAPI with moto-backed AWS services (tests/conftest.py). Prefer seeding test data through repository methods rather than mocking internal DynamoDB calls. - Versioning is automatic through
setuptools_scmand release tags. Do not hand-edit version constants for normal changes.
Infrastructure
CDK stack in infrastructure/cdk/gamatrix_stack.py. See infrastructure/cdk/README.md for deploy instructions and post-deploy steps (IGDB secret, SES sandbox, seeding users).
DynamoDB tables (all PAY_PER_REQUEST, PITR on, name-prefixed with TABLE_PREFIX):
games(PK:release_key)users(PK:email)user_libraries(PK:user_id, SK:release_key) + GSI onrelease_keyenrichment_jobs(PK:job_id)metadata_overrides(PK:slug)config(PK:key) — locally holds hidden/single-player lists; in AWS these come from SSMpasskeys(PK:credential_id) + GSIuser_handle-indexauth_challenges(PK:challenge_id, TTL onexpires_at)api_tokens(PK:token_id) + GSIemail-index— personal tokens for unattended (scripted) DB uploads
Version
Versioning is automatic — there's nothing to bump by hand. setuptools_scm derives the package version from the latest git tag ([tool.setuptools_scm] in pyproject.toml; __version__ in src/gamatrix/__init__.py reads it from the installed metadata).
On every merge to master, .github/workflows/version.yml computes the next semver from the latest tag and tags the merge commit. The bump is patch by default; add a label to the PR to change it:
new minor version→ bumps the minor componentnew major version→ bumps the major component
(These two labels must exist in the repo for the workflow to pick them up.)
Because the Docker/CDK build context excludes .git, image builds receive the version via the SETUPTOOLS_SCM_PRETEND_VERSION build arg — just build passes it from git describe, and the CDK stack (_project_version()) passes it at synth time.