Database schema
All data lives in one DynamoDB table per stage (abstract-play-dev, abstract-play-prod). The partition key (pk) names a logical record family; the sort key (sk) scopes items within that family.
Most access patterns use Query on pk with optional begins_with on sk. See subsystem pages for how each family is read and written.
Games
-
Games — full game state
- pk:
GAME - sk:
<metaGame>#<completedbit>#<gameid>
- pk:
-
Game comments — chat for a game
- pk:
GAMECOMMENTS - sk:
<gameid>
- pk:
-
Notes — per-user notes on a game
- pk:
NOTE - sk:
<gameId>#<userid>
- pk:
Users
-
Users — profile, settings, challenge lists (dashboard via indexes below)
- pk:
USER - sk:
<userid> - fields include
publicRivalries(boolean, default false) — opt in to public rivalries table cleaned(boolean, optional) — set by dashboard cruft cleanup for inactive users; cleared onme_dashboardlogindashboardMaintAt(number, optional) — lease timestamp for per-user dashboard maintenance lock (me_dashboardprune/timeout sweep)- Retired (Phase 5):
games[],gamesUpdate— removed from all USER records in prod (Aug 2025) - Vacation (correspondence): optional
vacationQuotaYear,vacationPauseMsUsed(ms charged in current UTC quota year),vacationStartsAt,vacationEndsAt,vacationOpenEnded,vacationStintStartedAt— at most one stint; see Vacation mode
- pk:
-
Push subscriptions — web push endpoints (one record per browser/device)
- pk:
PUSH - sk:
<userid>#<subscriptionKey>(subscriptionKey= first 16 hex chars of SHA-256 ofpayload.endpoint) - fields:
payload,endpoint,updatedAt - legacy:
sk: <userid>(migrated on nextsave_pushor removed on 404/410)
- pk:
-
User list — public directory (name, country, lastSeen, stars, bggid, optional
onVacation;aboutfetched per profile viaplayer_about)- pk:
USERS - sk:
<userid> about— optional markdown bio (max 100KB UTF-8; no HTML or images; max 20 links). Not included inuser_names; use public queryplayer_about.onVacation(boolean, optional) — mirrored fromUSERwhen a live vacation stint is active (user_namesonly); cleared when stint ends.USERrecord also storesaboutSaveDay/aboutSaveCountfor rate limiting (10 saves per user per UTC day across humanaboutand botdescriptionedits)- Display name:
USER.nameandUSERS.nameare kept in sync when a user changes name (new_settingwithattribute: name). Names must be unique among current humanUSERSandBOTdisplay names (trim + case-insensitive check onnew_profileandnew_setting). The play UI resolves current names by userid via open queryuser_names. - Snapshot names: Fields such as
GAME.players[].name, challengechallenger/challengees, feedbackauthorName, and representative-gameuserNamestore the name at write time and are not updated on rename (used for archives and for deriving past names in summarize).
- pk:
-
Tags — per-user game tags
- pk:
TAG - sk:
<userid>
- pk:
-
Customizations — per-user, per-game UI settings (board colours, preferred colour, glyph map)
- pk:
CUSTOMIZATION#<userid> - sk:
<metaGame>
- pk:
Retired (no longer written; purge with bin/purge-legacy-palettes.mjs):
-
Palettes — legacy per-user named colour palettes (replaced by Customize)
- pk:
PALETTES - sk:
<userid>
- pk:
-
Playground saves — per-user saved playground positions (unlimited slots)
- pk:
PLAYGROUND#<userid> - sk:
<uuid> - fields:
id,name,metaGame,date(epoch ms),body(JSON string; gzip-compressed when large)
- pk:
Retired (no longer written; purged from prod Aug 2025):
-
pk:
PLAYGROUND, sk:<userid>— legacy single-slot sandbox -
Player relations — blocking (bidirectional)
- pk:
PLAYER#<blockingPlayerId>, sk:BLOCKED#<blockedPlayerId> - pk:
PLAYER#<blockedPlayerId>, sk:BLOCKEDBY#<blockingPlayerId> - pk:
PLAYER#<userid>, sk:REPRESENTATIVE#<metaGame>#<gameid>— per-user representative-game index (max 2 per metaGame)
- pk:
-
Watched games — spectator dashboard list (not a participant)
- pk:
WATCHED#<userid>, sk:<gameid> - summary fields mirror dashboard
Gameobjects plusaddedAt,seen,lastChat
- pk:
-
Game watchers — reverse index for fan-out on moves and chat
- pk:
GAMEWATCHERS#<gameid>, sk:<userid>
- pk:
-
Highlighted games — player page pins (participant only)
- pk:
HIGHLIGHT#<userid>, sk:<metaGame>#<gameid> - summary fields plus
addedAtfor display order
- pk:
-
Representative games — community recommendations per metaGame
- pk:
REPRESENTATIVE#<metaGame>, sk:<userid>#<gameid> - fields:
userId,userName,addedAt, game summary fields
- pk:
-
Recommendation impression events — logged-in show/click/challenge telemetry for offline tuning (no live reads)
- pk:
RECOMMENDS#<userid> - sk:
<epochMs>#<random> - fields:
event,batchId,surface,tier,expiresAt(TTL, ~90 days), plusmetaGame,position,reasonType,gameIds,reasonsas applicable - no GSI; rate limit 50 events/user/UTC day
- pk:
-
Game Move layout usage events — append-only play-page layout telemetry (CLI export only; no live reads)
- pk:
LAYOUTEVT#<userid>orLAYOUTEVT#anon#<sessionId> - sk:
<serverTsMs>#<random> - fields:
event,sessionId,layout,resolvedFrom,metaGame,ts,isLoggedIn, optionalstoredLayout,viewportWidth,userHash(authed),from/toonlayout_switch - no GSI; rate limit 100 events/user/UTC day (authed), 25/session/UTC day (anonymous)
- dump:
bin/dump-gamemove-layout-events.mjs; purge preview rows:bin/purge-layout-feedback-events.mjs(LAYOUTFB#*— retired)
- pk:
-
In-app dashboard notifications — per-user feed shown on
me_dashboard(distinct from email/push)- pk:
NOTIFICATION#<userid> - sk:
<epochMs>#<random> - fields:
body(typed JSON object),expiresAt(TTL seconds; 180 days on create, tightened to 30 days on first dashboard fetch) - no GSI
body.typevalues:gameStart,gameEnd,ratingChange,challengeIssued,challengeDeclined,challengeRevoked,eventInvitation,completedGameChateventInvitationis sent when an organizer saves the invite list on a moderatedORGEVENT(event_update_invites); not used for automated tournaments. Body includeseventId,eventName,organizerId,organizerName. Inspect rows withbin/dump-dashboard.mjs--include-notifications(read-only; does not refresh TTL)
- pk:
-
Announcements — site news (subsystem doc)
- pk:
ANNOUNCEMENT, sk:<id>— canonical row (title,body,status,publishedAt,attachmentKeys, …) - pk:
ANNOUNCEMENT_PUBLISHED, sk:<paddedPublishedAt>#<id>— published list index (newest first viaQuery+ScanIndexForward: false) - pk:
ANNOUNCEMENT, sk:REACTION#<emoji>#<userId>— per-reaction rows (Phase 4) - Attachments: S3 bucket
ap-announcements-attachments-<stage>
- pk:
Game lists
-
Current games by player — per-player active game summaries (stream-maintained; Phase 3 reads from here)
- pk:
CURRENTGAMES#<userid> - sk:
<gameid> - summary fields mirror dashboard
Gameobjects (id,metaGame,players,toMove,lastMoveTime,numMoves, etc.)
- pk:
-
Recent completed games by player — moved to Retired below
-
Completed games by metaGame — summary rows for the completed-games page
- pk:
COMPLETEDGAMES#<metaGame> - sk:
<timestamp>#<gameid>
- pk:
-
Completed games by player — one item per player per game
- pk:
COMPLETEDGAMES#<userid> - sk:
<timestamp>#<gameid>
- pk:
-
Completed games (global) — site-wide summary index for recent-completions queries (stream-maintained; no historical backfill)
- pk:
COMPLETEDGAMES - sk:
<timestamp>#<gameid>(timestamp=lastMoveTimeepoch ms) - same summary fields as
COMPLETEDGAMES#<metaGame>rows recent_completed_gamesqueries withsk >= <sinceMs>(lexicographic order matches time order for 13-digit epoch-ms prefixes)
- pk:
Retired (no longer written; purged from prod — zero rows remain):
- pk:
RECENTCOMPLETED#<userid>, sk:<gameid>— legacy completed-dashboard index; post-game chat usescompletedGameChatnotifications - pk:
COMPLETEDGAMES#<metaGame>#<userid>, sk:<timestamp>#<gameid>— legacy per-player-per-game index
Exploration
- Game exploration — move tree for a game position entered by a user
- pk:
GAMEEXPLORATION#<gameid> - sk:
<userid>#<movenumber>
- pk:
Ratings and meta games
-
Ratings (deprecated) — legacy realtime Elo leaderboard rows; no longer written after batch Glicko migration. Existing
RATINGS#<metaGame>items may remain in DynamoDB.- pk:
RATINGS#<metaGame> - sk:
<userid>
- pk:
-
Meta game counts — aggregate stats (retired monolith; admin recount writes sharded items)
- pk:
METAGAMES/ sk:COUNTS— retired (Phase 5); delete after verification
- pk:
-
Meta game counts (sharded, authoritative) — per-metaGame live counters; stream + inline app writes;
meta_gamesreads and adminupdate_meta_game_countswrite here- pk:
METAGAMES#<metaGame> - sk:
COUNTS - fields:
currentgames,completedgames,standingchallenges,stars,ratingsCount(distinct rated players per meta game; sourced from_summary-ratings.jsonplayerCountsByUidon recount and served bymeta_games)
- pk:
-
Per-user game overlay (Phase 5) — per-user
seen/lastChaton dashboard games; sole overlay store- pk:
USERGAME#<userid> - sk:
<gameid>
- pk:
Challenges
-
Standing challenges — open challenges listed by game
- pk:
STANDINGCHALLENGE#<metaGame> - sk:
<challengeid>
- pk:
-
Direct challenges — challenge details (
openSlots= seats open to anyone; named invitees onchallengees)- pk:
CHALLENGE - sk:
<challengeid>
- pk:
-
Fillable direct listing — same
skas the direct challenge;fillableDirect: truewhenopenSlots > 0(canonical row remainsCHALLENGE)- pk:
STANDINGCHALLENGE#<metaGame> - sk:
<challengeid>
- pk:
-
SDG-style standing requests — standing requests for open challenges with a limit
- pk:
REALSTANDING - sk:
<userid>
- pk:
Bots
-
Bot identity — Cognito client linkage and owner
- pk:
BOT - sk:
<clientId>
- pk:
-
Bot display name reservation
- pk:
BOTNAME - sk:
<normalizedName>
- pk:
Automated tournaments
-
Tournaments — signup or in-progress tournaments
- pk:
TOURNAMENT - sk:
<tournamentid>
- pk:
-
Tournament player — player reference
- pk:
TOURNAMENTPLAYER - sk:
<tournamentid>#<division>#<playerid>
- pk:
-
Tournament game — game reference
- pk:
TOURNAMENTGAME - sk:
<tournamentid>#<division>#<gameid>
- pk:
-
Completed tournaments
- pk:
COMPLETEDTOURNAMENT - sk:
<metaGame>#<tournamentid>
- pk:
-
Tournament counter — per metaGame + variants combination (
variantsis a sorted, pipe-delimited variant list)- pk:
TOURNAMENTSCOUNTER - sk:
<metaGame>#<variants>(single-leg) or…#2(two-legmatchLegs) - fields:
counter,over
- pk:
Organized events
-
Events — organizer-run event details
- pk:
ORGEVENT - sk:
<eventid>
- pk:
-
Event players
- pk:
ORGEVENTPLAYER - sk:
<eventid>#<playerid>
- pk:
-
Event games
- pk:
ORGEVENTGAME - sk:
<eventid>#<gameid>
- pk:
WebSockets
-
WebSocket connections — active API Gateway connection registry
- pk:
wsConnections - sk:
<connectionId> userId,invisible,endpoint— connection owner and API GW endpoint URLwatchingGames— string set ofmetaGame#gameIdkeys the client wants game events forwantsPresence— boolean, default true; receive presence snapshot/delta messageswatchVersion—1when the client uses targeted game watch (enables strict filtering)ttl— Unix epoch expiry (24h; refreshed on subscribe andwatchGames)
- pk:
-
WebSocket presence sequence — monotonic counter for presence deltas
- pk:
wsMeta - sk:
presenceSeq seq— incremented on each debounced presence delta broadcast
- pk:
Feedback (separate table)
In-app bugs, feature ideas, and game wishlist use table abstract-play-feedback-{stage} (not the main game table).
-
Post meta — full post record
- pk:
POST#{id} - sk:
META - GSI
ByKind:gsi1pk=KIND#{kind},gsi1sk=VOTES#…/RECENT#…/UPDATED#…on companionLIST#*rows - GSI
ByStatus:gsi2pk=STATUS#{kind}#{status},gsi2sk= createdAt (admin triage, Phase 3+)
- pk:
-
List index rows — sparse GSI projections for board sort orders
- pk:
POST#{id} - sk:
LIST#VOTES|LIST#RECENT|LIST#UPDATED
- pk:
-
Comments — pk:
POST#{id}, sk:COMMENT#{ts}#{commentId} -
Votes — pk:
POST#{id}, sk:VOTE#{userId} -
Subscriptions — pk:
POST#{id}, sk:SUB#{userId} -
User index — pk:
USER#{userId}, sk:POST#{kind}#{createdAt}#{id} -
Tag vocabulary — pk:
CONFIG#FEEDBACK, sk:TAGVOCAB— ordered{ id, kinds }[]for bug/feature topic tags
Post META and LIST#* rows may include tags (string slugs). META may include suggestedTags (submitter suggestions; admin-only on get).
Attachments: S3 bucket ap-feedback-attachments-{stage}, keys under staging/{userId}/ (presign upload in Phase 2).
Related docs
- Games and moves
- Challenges
- Tournaments
- Events
- Bots
- WebSockets
- Player blocking
- Recommendations — impression event storage