GAMES_SHELF // DOCSTechnical Reference

Games Shelf - Technical Reference#


Architecture#

Games Shelf follows the same lightweight family architecture as the other local apps: one Node.js HTTP process, SQLite persistence, no browser framework, and no build step for application code.

games-app/
  server.js                 HTTP entrypoint, static serving, route dispatch
  server/
    admin.js                loopback gate, admin API, backups and maintenance
    auth.js                 scrypt passwords, sessions, account changes, throttling
    backup.js               hourly compressed SQLite snapshots and retention
    db.js                   schema, migrations, validation, scoped game queries
    pegi.js                 opt-in PEGI HTTP lookup and result parser
    covers.js               SteamGridDB client, throttling, matching, artwork selection
    version.js              validated atomic reads/writes of the VERSION file
  admin/
    index.html              localhost control-panel markup
    style.css               dense terminal-style admin theme
    js/                     dashboard, accounts, catalogue, tools and shared ES modules
  scripts/
    generate-docs.js        Markdown-to-HTML documentation generator/checker
  public/
    index.html              application and authentication markup
    app.js                  browser state, rendering, auth, forms, API calls
    js/platforms.js         grouped platform catalogue and release-name matching
    style.css               dense dark responsive theme
    manifest.webmanifest    installable-app metadata
    favicon.svg             application icon
    icon-192.png            installable-app icon
    icon-512.png            high-resolution installable-app icon
    social-preview.*        source SVG and rendered 1200x630 social card
    robots.txt              crawler policy for public and private surfaces
    sitemap.xml             canonical public URLs for gameskat.net
    docs/                   generated standalone HTML documentation
  docs/
    user-guide.md           user documentation source
    technical.md            this file
  test/
    auth.test.js            sessions, password changes, isolation
    backup.test.js          hourly ZIP creation and scheduling
    pegi.test.js            PEGI result parsing
    covers.test.js          conservative cover-title normalization
    seo.test.js             canonical metadata, crawler policy, image dimensions
    admin.test.js           localhost gate and cross-account admin summaries
    version.test.js         arbitrary release-string persistence and validation
  VERSION                   release string displayed in the application header
  games.db                  runtime SQLite database

Request flow#

Browser
  -> static file request ----------------------> server.js -> public/
  -> localhost /admin/* -----------------------> admin.js -> admin/ + SQLite/VERSION
  -> POST /api/login or /api/register --------> server.js -> auth.js -> SQLite
  -> authenticated /api/* + Bearer token -----> auth.js -> user identity
                                                    |
                                                    +-> db.js (user-scoped query)
                                                    +-> pegi.js (explicit lookup only)
                                                    +-> covers.js (configured lookup/bulk scan)

All authenticated routes resolve the session before dispatching feature logic. They pass the numeric user ID into every database operation rather than trusting a client-provided owner ID.


Runtime and dependencies#

Environment variables:

VariableDefaultPurpose
PORT3005HTTP listen port
HOST0.0.0.0Listen address
DB_PATH./games.dbSQLite database path
VERSION_FILE./VERSIONRelease-string file; primarily useful for isolated tests or custom deployments
BACKUP_DIR./backupsHourly ZIP backup destination
STEAMGRIDDB_API_KEYblankOptional server-wide cover API key; per-account keys can instead be configured in the UI

Start the server with npm start. Development watch mode is available through npm run dev.


Database#

SQLite runs in WAL mode with foreign keys enabled.

users#

ColumnNotes
idInteger primary key
usernameCase-insensitively unique
emailOptional and case-insensitively unique
password_hash64-byte scrypt result encoded as hex
saltRandom 16-byte salt encoded as hex
created_at, updated_atSQLite timestamps

sessions#

ColumnNotes
tokenRandom 32-byte token encoded as 64 hex characters; primary key
user_idForeign key to users, cascading on account deletion
expires_atUnix timestamp
created_atSQLite timestamp

Expired sessions are purged when the server starts. Valid sessions receive a rolling two-week expiry on authenticated use.

games#

ColumnNotes
idInteger primary key
user_idOwner account foreign key
title, platformRequired identification
pegiNull or 3, 7, 12, 16, 18
ownershipowned, wanted, or unavailable
play_statusbacklog, playing, completed, paused, or abandoned
media_formatphysical, digital, or unknown
cartridge_numberOptional integer
publisher, release_year, notesOptional metadata
favoriteBoolean integer
pegi_urlSource search URL when PEGI-assisted
cover_url, cover_source, cover_match_titleSelected artwork and match provenance
created_at, updated_atSQLite timestamps

Indexes cover owner, platform, ownership, PEGI, and case-insensitive title.

Username comparison is case-insensitive. Renaming an account later does not alter ownership because all collection queries use the immutable numeric user ID.


Authentication and authorization#

The authentication design is a reduced version of the gamebooks app's model.

Passwords#

Usernames#

Sessions#

Login throttling#

The process keeps recent failed login/registration attempts by client IP. Eight failures within 15 minutes produce HTTP 429. Successful authentication clears that IP's failure list. The throttle resets when the Node.js process restarts.

Isolation invariant#

Every collection query includes user_id = authenticatedUserId. Updates and deletes use both game ID and user ID. A game belonging to another account therefore behaves as nonexistent and returns HTTP 404.

The client never sends or selects user_id.


HTTP API#

All JSON responses use Cache-Control: no-store. Registration, login, public configuration, and the cover-only showcase route are public. Collection routes require a valid bearer token. Admin routes use a separate loopback-only boundary and do not accept normal account sessions as a substitute.

Authentication#

MethodRoutePurpose
POST/api/registerCreate an isolated account with password confirmation and optional email
POST/api/loginVerify credentials and create session
GET/api/configReturn the current public version string
GET/api/showcase/coversReturn randomized cover URLs for the public authentication-page artwork
POST/api/logoutDelete current session
GET/api/auth/meResolve current user
PUT/api/accountChange username and/or password after current-password verification
POST/api/account/avatarUpload a browser-cropped JPEG avatar, maximum 256 KB
DELETE/api/account/avatarRemove the current avatar and restore the initial fallback

Collection#

MethodRoutePurpose
GET/api/gamesList current user's games with query filters
POST/api/gamesCreate a game for current user
GET/api/games/:idRead owned game
PUT/api/games/:idReplace editable metadata on owned game
DELETE/api/games/:idDelete owned game
GET/api/statsAccount-scoped aggregates
GET/api/metaPlatforms, version, PEGI capability, current user
GET/api/pegi/search?q=...Explicit server-side PEGI search
GET/api/covers/statusProvider configuration, missing count, and bulk progress
PUT/api/covers/configValidate and store the account's SteamGridDB key
DELETE/api/covers/configRemove the account-specific provider key
GET/api/covers/search?q=...Search portrait covers for manual selection
POST/api/covers/bulkStart an account-scoped exact-title scan for missing covers

List query parameters are q, platform, ownership, playStatus, pegi, favorite, and sort.

Avatar filenames contain only the authenticated numeric user ID, timestamp, and random suffix. They are stored beneath public/avatars/; replacement and removal delete only the filename recorded for that account after a basename traversal check. Avatar binaries are excluded from Git.

Local administrator API#

The admin interface is available at http://127.0.0.1:3005/admin/. It is intentionally not an account role. A request is accepted only when the TCP peer is loopback. If nginx-style X-Real-IP or X-Forwarded-For headers are present, the first reported client must also be loopback. This prevents the public reverse proxy from exposing admin merely because it connects to Node locally.

MethodRoutePurpose
GET/api/admin/statsRuntime and whole-database counts
GET/api/admin/accountsAccount, collection, cover, and session counts
DELETE/api/admin/accounts/:id/sessionsRevoke every active session for one account
DELETE/api/admin/accounts/:idDelete an account, its avatar, and cascaded games, sessions, and integration settings
GET/api/admin/games?q=...Search up to 250 games across accounts
DELETE/api/admin/games/:idPermanently remove one explicitly selected game
GET, PUT/api/admin/versionRead or atomically replace the release string
POST/api/admin/database/checkpointTruncate-checkpoint the SQLite WAL
POST/api/admin/database/optimizeRun SQLite planner optimization
POST/api/admin/database/vacuumRebuild the SQLite database file
GET, POST/api/admin/backupsList or trigger the current hour's compressed SQLite backup
DELETE/api/admin/backups/:nameDelete one validated backup filename

Admin static files and API responses use restrictive security headers. Backup names are server-generated and deletion accepts only that exact filename shape. Backups are stored in backups/, which is excluded from Git.

server/backup.js creates one consistent SQLite snapshot at process startup and then exactly on each hour. The snapshot is compressed with the host zip command, published by atomic rename, and its temporary raw SQLite file is always removed. A second attempt in the same hour is a no-op. Archives older than 15 days are pruned during each run.

Version file#

VERSION contains one nonempty, arbitrary single-line string of at most 80 characters. It is not limited to semantic versions. The admin writes a temporary sibling and atomically renames it over the target; the main header fetches the value through /api/config on page load. Changing the version does not require restarting Node, though already-open app tabs refresh it on their next reload.


PEGI integration#

PEGI exposes a public catalogue search but no documented public developer API. server/pegi.js therefore performs an opt-in HTTPS GET only after the user selects Look up title.

The parser extracts displayed title, publisher, rating, descriptors, releases, platforms, and year. Results are cached in process memory for one hour per normalized query. The request has a 12-second timeout and a 4 MB response limit.

This integration is deliberately nonessential. Parsing or network failure returns HTTP 502 with a PEGI fallback URL; manual game creation remains available.


Cover-art integration#

SteamGridDB was selected because its API is dedicated to game artwork and exposes portrait grid images suitable for box-art cards. It requires a personal bearer API key. Account keys are stored in user_integrations; an optional STEAMGRIDDB_API_KEY environment value acts as a server-wide fallback. Keys are never returned to the browser after configuration.

Manual lookup searches up to four title candidates and returns portrait static grids. Results are cached in memory for 30 minutes. Provider calls are serialized below four requests per second, have a 15-second timeout, and retry HTTP 429 once.

Bulk lookup considers only games without a cover. Title comparison is Unicode-normalized, case-insensitive, punctuation-insensitive, and conservative: auto-selection requires exactly one exact normalized title candidate. The highest-scoring portrait grid is stored; ambiguous titles remain unmatched. Five consecutive provider errors trip a circuit breaker and mark the job failed instead of hammering the remaining catalogue. Job state is in memory and therefore does not survive a server restart, while already matched covers remain in SQLite.

Cards use a centred, full-card image with a dark left-to-right gradient, mirroring Gamebooks' cover-background treatment. Images use native lazy loading so only the visible portion of a large collection is requested.


Browser application#

public/app.js is a zero-dependency ES-module browser application. Its state contains the authenticated user, games, account statistics, platform list, result render limit, selected view, and loading state. Static platform taxonomy and release-text matching live separately in public/js/platforms.js.

Startup#

  1. Read the bearer token from local storage.
  2. Call /api/auth/me when a token exists.
  3. Show authentication on absence/failure, or mount the library on success.
  4. Load games, statistics, and metadata in parallel.

An HTTP 401 on a protected request removes the token and returns to login.

Rendering#

Game cards are generated from escaped values. Filters are sent to the server rather than applied to a global cross-user data set. Search uses a 220 ms debounce. Rendering is batched in groups of 120.

Dialog pointer safety#

Backdrop dismissal tracks pointerdown and pointerup. It closes only when both events target the dialog backdrop. This prevents a text-selection drag that begins inside the form and ends outside from dismissing the dialog.

Destructive actions use themed HTML dialogs in both the public application and localhost admin. Native browser alert, confirm, and prompt APIs are not used.


Static serving#

server.js resolves requested paths beneath public/, rejects traversal outside that directory, assigns MIME types, and serves static content with Cache-Control: no-cache. API content uses no-store.

Admin assets are not beneath the public directory. server/admin.js serves an explicit file allowlist only after the request passes the loopback check.

Generated documentation is available at:

Search and social metadata#

The public landing page uses https://gameskat.net/ as its canonical URL. It includes a focused title and description, Open Graph and Twitter large-image metadata, and WebApplication JSON-LD. The social image is authored as public/social-preview.svg and rendered to the crawler-compatible public/social-preview.png at 1200×630.

robots.txt permits the landing page and public guide while excluding /api/, /admin/, and account avatars. sitemap.xml lists only the canonical landing page and user guide. The manifest includes 192×192 and 512×512 PNG icons in addition to the scalable favicon.

The authentication landing markup contains six visible, descriptive feature cards. This gives non-JavaScript crawlers useful product content without exposing any private collection data.


Documentation workflow#

Markdown files in docs/ are the source of truth.

npm run docs:build   # regenerate public/docs/*.html
npm run docs:check   # fail if generated HTML is stale

npm test runs both the Node test suite and the documentation consistency check.


Testing#

Test fileContract
test/auth.test.jsAccount isolation, sessions, password invalidation
test/pegi.test.jsPEGI HTML parsing
test/covers.test.jsConservative cover-title normalization
test/seo.test.jsCanonical/social metadata, crawler policy, and asset dimensions
test/admin.test.jsLoopback/proxy boundary and whole-database admin summaries
test/backup.test.jsHourly ZIP creation, deduplication, cleanup, and scheduler timing
test/version.test.jsVersion-file persistence and input validation

Run all checks with npm test.

The authentication test uses a disposable SQLite database under /tmp and removes its main, WAL, and shared-memory files afterward.


Operations and backups#

Stop with Ctrl+C or send SIGTERM. The server closes SQLite before exiting.

The server automatically creates compressed, consistent live backups at startup and hourly, retaining 15 days. The admin Tools tab lists, triggers, and removes those archives and can checkpoint the WAL. For a simple offline backup:

  1. Stop the server.
  2. Copy games.db to a dated backup location.
  3. Restart the server.

When backing up a live WAL database, use SQLite's backup API or include a proper checkpoint procedure. Copying only games.db during an active write can omit transactions still present in games.db-wal.

The database is excluded from Git. Source code, generated documentation, and tests can be versioned normally.


Known boundaries#

Generated from docs/technical.md. Do not edit this HTML directly.