# Where state lives: Durable Objects, with D1 as the directory gitfor's runtime cost scales with the repositories and people that are active, not with one shared database. D1 is a **directory**: the lookups that turn a name, id or credential into the object that owns the data. Everything else lives in a Durable Object keyed by the repository or account it belongs to. ## Layout | Store | Holds | Keyed by | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | D1 `DB` (directory) | `accounts`, `users`, `user_emails`, `repos` (+ `repos_fts`), redirects, retired names, `collaborators`, `org_members`, `stars`, `watches`, `follows`, `scope_locators`, `signing_key_ids`, `commit_signatures`, `lfs_blobs`, operator tables (admin audit, abuse reports, instance settings, waitlist, Stripe events, cron locks, hourly limits, Artifacts stats, integrations) | global | | D1 `ADVISORY_DB` | `published_advisory_packages`: the cross-repo index dependency scans match against | global | | `RepoState` | Issues, pull requests, reviews, comments, labels, milestones, reactions, discussions, releases, attachments, CI runs/jobs/statuses/caches, webhooks, secrets, variables, environments, deploy tokens, branch protection, security alerts and advisories, LFS objects and locks, sites and deployments, activity and push events | repo id, or `site:` for a site with no repository | | `RepoSearch` | Code index of the default branch, plus text file sizes it sums into `repos.languages` after each pass | repo id | | `AccountState` | Sessions, passkeys, tokens, OAuth grants, notifications and their settings, org invitations, owner secrets and variables, projects, runners, signing keys, AI endpoints, billing, credits and usage | account id, `scratch:`, or `_anonymous` for login challenges | | `UserInbox` | Unread badge and the **involvement index** (issues and pull requests the user authored, is assigned to or reviews) | user id | | Other objects | `Counters`, `MeterBuffer`, `RepoInsights`, `Runner*`, `MergeQueue`, `WebhookLog`, `ProjectRoom`, `EditorRoom`, `McpHub`, … | per purpose | ## How code reaches a store - `repoDb(repoId)` / `accountDb(accountId)` (`src/server/scoped-db`) give the same `first / all / run / batch` surface as the D1 helper, bound to one object. Statements for different stores can't share a batch. - The tables each object creates are listed in `scripts/gen-scoped-schema.ts` and generated into `src/server/scoped-db/schema.gen.ts` from the historical D1 migrations. New scoped DDL is appended to `schema-extra.ts`; every object applies it on its next request. - No joins across stores. Query the scoped table, collect ids, look names up in the directory (`accountsById`, `repos`), and merge in code. - **Finding the owner of a row** when only its id or credential hash is known: `scope_locators` in D1 (`src/server/scoped-db/locators.ts`). New session and token formats embed their account (or repo) id so the hot paths skip the directory entirely; older formats resolve through the locators. - **Cross-repo lists** come from indexes fed by events, not from joins: `UserInbox` holds involvement, and `ADVISORY_DB` holds published advisories, and each creator's `AccountState` indexes the deploy tokens they made (`deploy_token_index`). No request path visits every repository or account: add an index instead. - **Expiry and digests run in the object itself.** Inserting into `sessions`, `auth_tokens`, `oauth_codes`, `oauth_refresh_tokens`, `notification_deliveries` or `notification_digest_items` arms the AccountState alarm for the next expiry; the alarm deletes what is due, flushes due digests and re-arms (`src/server/account-state/housekeeping.ts`). Idle accounts cost nothing; no cron walks the accounts. RepoState does the same for scheduled workflows (at `next_run_at`), CI cache eviction, job token expiry, webhook delivery retention, orphaned attachments, hidden-ref cleanup retries and jobs on runners that stopped heartbeating (`src/server/repo-state/housekeeping.ts`); a failing task backs off for five minutes. - **Stale notifications** about repositories the recipient can no longer read are filtered on every read and pruned then (`filterReadable`), instead of fanning out over every account when a repository goes private or is deleted. - **Backfills** for new indexes walk repositories or accounts a page per cron run (`src/server/backfill.ts`); readers keep their old path until `backfillDone`. - **Access control** stays in D1 on purpose: `collaborators` and `org_members` are what "which repositories can this user see" is computed from, in SQL, for listings. - **Repo and account deletion** call `destroy()` on the owning objects and `forgetScope()` on the locators; D1's cascading deletes no longer reach the moved data. - **Global search** covers repositories and accounts only. Issues, pull requests and code are searched inside a repository, or (for code) across one owner's repositories by fanning out to their objects. ## Tests Tests that touch scoped tables mock `~/server/scoped-db` with `memoryScopes().module` from `src/server/scoped-db/testing.ts`, which backs every scope with its own in-memory bun:sqlite using the real schema.