Skip to main content

App Works — consolidated data model

Status: Draft · Created: 2026-09-17 · Program: App Works Closes: SK-11 (no consolidated data model; CONTRACTS.md §2 misses three schema changes) Owner: programme level. Each epic owns its own tables — this document only collects them, and the epic's plan.md §"Data model" stays authoritative for columns and semantics. Companion documents: CONTRACTS.md §2 (entities and persisted state) · §3 (capability ports) · README.md §7 rule 6 (migration timestamps) · TRACKER.md "Migration timestamp blocks" · CONFIGURATION.md §5 (database-backed settings) · quickstart.md §7 (running migrations locally) · contracts/README.md


0. How to read this document​

Authority order. An epic's plan.md "Data model" section is normative for its own tables. CONTRACTS.md §2 is normative for names and owners shared across epics. This file is the single place where all of them can be seen at once, with the migration each one reserves and how it is classified for the workspace backup. Where this file and an epic's plan disagree, the plan wins and this file has a bug — fix it here.

Every table is additive. README §7 rule 1 and Resolution R-26 (additive-only, top priority): no existing table, column, index or default is removed, renamed, narrowed or marked obsolete by this programme. Every migration's down() drops exactly what its up() created — never more.

Line numbers move. The programme's epic documents are being edited while this document is written. Every citation below was resolved against the working tree at the time of writing and carries the section heading as well as the line, so a moved line is re-findable: search the heading, then re-stamp the number.

Counts. 19 new tables and 9 extensions of existing tables, across 23 reserved migrations in the 1792<epic><slot>00000 block. APW-01 and APW-13 add no schema.


1. Inventory​

TS = the reserved migration timestamp. R-25 = how the table is classified for the workspace backup (Resolution R-25): a BACKUP_DOMAIN_SPECS export file, an entry in BACKUP_DROPPED_ENTITIES, or "rides an existing export".

1.1 New tables​

#TableOwnerTSMigration fileR-25Source
1work_upstream_statesAPW-021792020000000CreateWorkUpstreamStates.tsexport data/works/upstream-states.jsonlAPW-02/plan.md §3.1 · migration §3.2
2work_app_spec_statesAPW-031792030000000CreateWorkAppSpecStates.tsexport data/works/app-spec-states.jsonl (three *Hash columns benign)APW-03/plan.md §3.1 · §3.3
3work_app_provisioningsAPW-041792040000000CreateWorkAppProvisionings.tsexport data/works/app-provisionings.jsonl (tokenCap benign)APW-04/plan.md §3.1 · §3.4
4work_buildsAPW-051792050000000CreateWorkBuilds.tsexport data/works/builds.jsonl; buildInputsHash droppedAPW-05/plan.md §3.1 · §3.4
5work_build_preparationsAPW-051792050000000 (same migration)CreateWorkBuilds.tsNOT STATED — see §9 item 1APW-05/plan.md §3.1b
6work_app_runtime_statesAPW-061792060100000CreateWorkAppRuntimeStates.tsexport data/works/app-runtime-states.jsonlAPW-06/plan.md §7.2 · §7.3
7work_app_env_valuesAPW-071792070000000CreateAppEnvAndDependencies.tsexport under data/works/; valueEncrypted → { wasSet }, valueBytes droppedAPW-07/plan.md §3.1 · §3.4
8work_app_dependenciesAPW-071792070000000 (same migration)CreateAppEnvAndDependencies.tsexport under data/works/; configEncrypted/outputsEncrypted → { wasSet }APW-07/plan.md §3.2 · §3.4
9upstream_pull_requestsAPW-091792090000000CreateUpstreamPullRequests.tsexport data/works/upstream-pull-requests.jsonlAPW-09/plan.md §3.1 · §3.2
10apps_tier_gate_runsAPW-101792100000000CreateAppsTierGate.tsdropped, with a reasonAPW-10/plan.md §4
11apps_tier_attestationsAPW-101792100000000CreateAppsTierGate.tsdropped, with a reasonAPW-10/plan.md §4
12apps_tier_state_eventsAPW-101792100000000CreateAppsTierGate.tsdropped, with a reasonAPW-10/plan.md §4
13apps_tier_quarantinesAPW-101792100100000CreateAppsTierQuarantineAndSignals.tsdropped, with a reasonAPW-10/plan.md §4
14apps_tier_abuse_signalsAPW-101792100100000CreateAppsTierQuarantineAndSignals.tsdropped, with a reasonAPW-10/plan.md §4
15apps_tier_image_allowancesAPW-101792100100000CreateAppsTierQuarantineAndSignals.tsdropped, with a reasonAPW-10/plan.md §4
16apps_tier_quota_profilesAPW-101792100200000CreateAppsTierQuotaAndMetering.tsdropped, with a reasonAPW-10/plan.md §4
17apps_tier_usage_windowsAPW-101792100200000CreateAppsTierQuotaAndMetering.tsrecord-only export data/runs/apps-tier-usage-windows.jsonl (runs domain, time trim)APW-10/plan.md §4
18app_launcher_preferencesAPW-111792110000000CreateAppLauncherPreferences.tsexport data/account/app-launcher-preferences.jsonlAPW-11/plan.md §3.2 · §3.4
19external_identitiesAPW-121792120000000CreateExternalIdentities.tsdropped (a sign-in binding must never be restored)APW-12/plan.md §3.1 · §3.6

1.2 Extensions of existing tables​

TableOwnerTSWhat is addedR-25Source
work_deploymentsAPW-0617920600000005 nullable columns: buildId, componentStatuses, smokeResult, appTarget, appRenderrides the existing deployments.jsonlAPW-06/plan.md §7.1 · §7.3
tasksAPW-0817920800000006 columns: mergeCommitSha, deliveryState, deliveryBuildId, deliveryDeploymentId, deliveryUpdatedAt, deliveryClosedByIdrides the existing data/tasks/tasks.jsonlAPW-08/plan.md §3.1
tasksAPW-0817921101000001 column: branchGuardRefusal text NULL (added 2026-09-25; stamped outside the APW-08 block, see §4)rides the existing data/tasks/tasks.jsonlAPW-08/plan.md §3.5
goalsAPW-0817920801000001 column: workId uuid NULLrides data/missions/goals.jsonlAPW-08/plan.md §3.2
missionsAPW-0817920802000003 columns: outputMode, taskOutput, taskOutputNoticeAtrides data/missions/missions.jsonlAPW-08/plan.md §3.3
organizationsAPW-0417920401000001 column: appProvisionCaps (simple-json, nullable)rides data/organizations/organization.jsonlAPW-04/tasks.md T41
worksAPW-1017921002000001 column: appsTierQuotaProfile varchar(32) NULL (NULL = starter)rides works/works.jsonlAPW-10/plan.md §4
worksAPW-1117921100000001 column: appLauncherExposed boolean NULL (NULL = kind default)rides works.jsonlAPW-11/plan.md §3.1 · §3.4
sessionAPW-1217921201000002 nullable columns: externalIdentityId, externalSid (+2 indexes)already dropped with the session tablesAPW-12/plan.md §3.2 · §3.6
works.sourceRepository (existing simple-json)APW-01noneadditive keys only — template provenance block; no column, no migrationrides works.jsonlAPW-01/plan.md §3.1

APW-01 reserves slot 1792010000000 and does not use it. APW-01/plan.md §3.1 states "Migration: none. Slot 1792010000000 (APW-01 slot 00) is reserved and unused". APW-13 adds no table, column or migration (APW-13/plan.md §3).

APW-12 adds no table for the OIDC replay store — it reuses the existing verification table with identifier: 'ever-id:<kind>' rows (APW-12/plan.md §3.3).


2. New tables — owner, columns, keys, retention​

Every row below is a key column; the full column list is the cited section. Types are written exactly as the plan writes them (simple-json = TypeORM simple-json, bigint ts = TypeORM TimestampColumn).

2.1 work_upstream_states — APW-02​

Entity WorkUpstreamState, packages/agent/src/entities/work-upstream-state.entity.ts (new).

ColumnTypeDefaultNote
iduuid PK
workIduuid, unique@ManyToOne(() => Work, { onDelete: 'CASCADE' })
relationvarchar(16)link · fork · private-copy
dataOwner / dataRepovarchar(100)Work Repository coordinates (canonical)
upstreamOwner / upstreamRepovarchar(100), nullnull for link
upstreamDefaultBranchvarchar(255), nullupdated on rename (FR-43)
readinessStatevarchar(24)'preparing'preparing · ready · timed_out · failed · waiting_for_setup_pr
readinessReasonvarchar(48), nullaccess_revoked, dispatch_unavailable, …
readinessStartedAt / readinessHeartbeatAtbigint tsheartbeat = the sweeper's liveness signal
readinessDispatchesint0≤ 3 automatic (FR-23)
readinessManualRetries / readinessManualWindowAtint / bigint ts0 / null≤ 3 per rolling hour (FR-19)
setupPullRequestUrl / setupPullRequestNumbervarchar(500) / int, nullfrom APW-01's handler outcome
copyPushedShavarchar(40), nullprivate-copy idempotency (FR-21)
aheadBy / behindByint, null
upstreamHeadShavarchar(40), null
syncSchedule / nextSyncAtvarchar(64) / bigint ts, nulleffective cron; nextSyncAt null while paused
lastSyncResultvarchar(24), nullup_to_date · fast_forwarded · pull_request_opened · pull_request_updated · conflict · skipped · paused · failed
conflictTaskIduuid, nullno FK — a deleted Task must not cascade
manualSyncCount / manualSyncWindowAtint / bigint ts, null0≤ 6 per rolling hour (FR-33)
upstreamStatusvarchar(16)'unknown'available · archived · unavailable · none · unknown
dataRepositoryStatusvarchar(16)'available'available · missing
actionsStatevarchar(24)'pending'pending · clean · needs_admin · permission_missing · failed · not_applicable
actionsSeenWorkflowIds, actionsDisabledWorkflows, actionsKeptWorkflowssimple-json, nullnumber[] ≤ 500; {id,path}[] ≤ 100 each
tenantId / organizationIduuid, nullscope stamping
createdAt / updatedAtCreate/UpdateDateColumn
  • Indexes / uniqueness: uq_work_upstream_states_work (workId) UNIQUE · idx_work_upstream_states_next_sync (nextSyncAt) · idx_work_upstream_states_readiness (readinessState, readinessHeartbeatAt).
  • FK: works(id) ON DELETE CASCADE.
  • Retention (R-25): exports as data/works/upstream-states.jsonl, reached through the parent Work ids; no column is redacted.
  • Source: APW-02/plan.md §3.1 "work_upstream_states — entity WorkUpstreamState (new)" · migration §3.2.

2.2 work_app_spec_states — APW-03​

Entity WorkAppSpecState, packages/agent/src/entities/work-app-spec-state.entity.ts (new).

ColumnTypeDefaultNote
iduuid PK
workIduuid NOT NULLunique; CASCADE with works.id
trackedBranchvarchar(255) NOT NULL
requestedSeq / startedSeq / evaluatedSeqbigint NOT NULL0coalescing; pending ⇔ evaluatedSeq < requestedSeq
headCommitSha / headSpecHashvarchar(40) / varchar(64) NULLhash = sha256 of canonical JSON of spec
validationStatusvarchar(24) NOT NULL'missing'valid · valid_with_warnings · invalid · missing · unreadable
issuessimple-json NULLAppSpecIssue[] ≤ 200
errorCount / warningCount / issuesTruncatedint / boolean NOT NULL0 / false
effectiveCommitSha / effectiveSpecHashvarchar(40) / varchar(64) NULL
effectiveSpecsimple-json NULLcache only — the file at effectiveCommitSha is authoritative; holds no secret values
lastEvaluationTriggervarchar(24) NULLcreated · push · pr_merged · manual · lazy · blueprint_applied · build
blueprintId / blueprintVersion / blueprintRepo / blueprintShavarchar(64/32/128/40) NULL
blueprintMatchSourcevarchar(16) NULLmanifest · alias · fork · probe · explicit · file
blueprintApplyStatusvarchar(16) NULLapplying · applied · failed
licenseSpdx / licenseClass / licenseSourcevarchar(200/8/16) NULLgreen · amber · red · unknown
licenseEvidence / licenseObligationssimple-json NULL≤ 20 paths each; string[]
licenseRegistryHash / licenseRegistrySourcevarchar(64) / varchar(16) NULLlive · last_good · snapshot
attestationsimple-json NULL{ userId, attestedAt, spdx, class, textId, textSha256, commitSha } — the one license attestation record (C3, R-3)
sourceOfferRequiredboolean NOT NULLfalse
displayName / trademarkNoticevarchar(80) / varchar(500) NULL
protectedPathssimple-json NULLstring[] ≤ 50 — agents may not modify
  • Indexes / uniqueness: uq_work_app_spec_states_work (workId) UNIQUE · idx_work_app_spec_states_blueprint (blueprintId, blueprintVersion) · idx_work_app_spec_states_registry (licenseRegistryHash).
  • FK: works(id) ON DELETE CASCADE.
  • Retention (R-25): exports as data/works/app-spec-states.jsonl; the three *Hash columns are reviewed as benign digests.
  • Source: APW-03/plan.md §3.1 · migration §3.3.

2.3 work_app_provisionings — APW-04​

Entity WorkAppProvisioning, packages/agent/src/entities/work-app-provisioning.entity.ts (new).

ColumnTypeNote
iduuid PK
workIduuid NOT NULL@ManyToOne(() => Work, { onDelete: 'CASCADE' })
userIduuid NOT NULLstarter; question recipient
taskId / agentIduuid NULLno FK (entity-cycle rule)
triggervarchar(24)auto-create · manual · chat · upstream-smoke · auto-upstream-smoke
statusvarchar(16)queued · running · needs_input · succeeded · merged · failed · cancelled
step / stepStatesvarchar(16) / simple-jsonstep ids and per-step state
detectionSourcevarchar(24) NULLapp-spec · compose · dockerfile · helm · descriptor-hint · auto
attempts / attemptBudget / attemptsUsedsimple-json / int≤ 9; budget default 3, advanced by compare-and-set
openInboxItemId, questionAskedAt, questionRemindedAtuuid / timestamptz NULL
questionReason / questionParamsvarchar(24) / simple-json NULLparams carry names only, ≤ 1 KiB, secret-scanned
tokensUsed / tokenCapbigint NOT NULLcap default 3,000,000
runnerMinutesUsed / runnerMinuteCapint NOT NULLcap default 240
activeMsbigint NOT NULLdeadline 8 h, frozen while parked
parkedReason / parkedAtvarchar(24) / timestamptz NULLkill-switch · agent-paused · workspace-paused · scope-paused (R-17 waits)
lastRunOutputtext NULLlast provision-output block ≤ 512 KiB; cleared once the guard reads it
verificationTargetKind, verificationNamespace, verificationExpiresAtvarchar(16) / varchar(63) / timestamptz NULLsweeper input
suggestionState, suggestionUpstream, suggestedAt, suggestionBundlevarchar(16/200) / timestamptz / simple-json NULLbundle ≤ 256 KiB
lease / leaseExpiresAtvarchar(36) / timestamptz NULLstep-executor CAS; lease 5 min
  • Indexes / uniqueness: uq_work_app_provisionings_active UNIQUE (workId) partial WHERE status IN ('queued','running','needs_input') · uq_work_app_provisionings_task UNIQUE (taskId) partial WHERE taskId IS NOT NULL · idx_work_app_provisionings_user_status (userId, status) · idx_work_app_provisionings_org_status (organizationId, status) · idx_work_app_provisionings_expiry (verificationExpiresAt) · uq_work_app_provisionings_suggestion UNIQUE (suggestionUpstream) partial WHERE suggestionState = 'queued'.
  • FK: works(id) ON DELETE CASCADE.
  • Retention (R-25): exports as data/works/app-provisionings.jsonl; tokenCap reviewed as benign; Organization.appProvisionCaps rides the existing organizations file.
  • Source: APW-04/plan.md §3.1 · migration §3.4.

2.4 work_builds — APW-05​

Entity WorkBuild, packages/agent/src/entities/work-build.entity.ts (new).

ColumnTypeNote
iduuid PK
workIduuid NOT NULLFK works.id ON DELETE CASCADE
numberint NOT NULLper-App-Work sequence from 1
buildPluginIdvarchar(64) NOT NULL
status / triggervarchar(16) NOT NULLqueued…blocked / push · pull_request · manual · verification
blockedReason / blockedDetailvarchar(40) / simple-json NULLnames and numbers only, ≤ 2 KiB
branch / commitSha / pullRequestNumbervarchar(255) / varchar(40) / int NULL
providerRunId / runAttemptvarchar(64) NULL / int NOT NULL DEFAULT 1
dispatchCorrelationId / dispatchedAtuuid / timestamp NULL
appSpecHash / specValidAtCommitvarchar(64) / boolean NULLfrom AppSpecService.getEffectiveSpec(workId, sha)
buildInputsHashvarchar(64) NULLsha256 over (name, fingerprint) of the synced build values — dropped from the backup
buildSecretNames / secretsSyncedAtsimple-json / timestamp NULLstring[] ≤ 50 — the EW_ names this preparation wrote
runnerLabel / runnerClassvarchar(64) / varchar(24) NULLgithub-public · github-private · github-larger · apps-builder
imageRepository / imageDigest / imageTagsvarchar(255) / varchar(71) / simple-json NULLdigest sha256:<64>; tags ≤ 3; never latest
digestConfirmed / secretCheckboolean NOT NULL DEFAULT false / varchar(16) NULLpassed · failed · not_needed
deployable / notDeployableReasonboolean NOT NULL DEFAULT false / varchar(40) NULL
failureClass / failureDetail / failureExcerptvarchar(32) / simple-json NULLclassifier output; excerpt ≤ 20 lines × ≤ 300 chars, redacted
verificationResult / verifiesBuildIdsimple-json / uuid NULL{ componentsReady, jobs[] ≤ 10, smoke[] ≤ 50 }
billableMinutes / checksBillableMinutesint NULLR-9; the second is part of the first
usageEventId / triggeredByUserIduuid NULLthe receipt (plugin_usage_events.id), no FK
tenantId / organizationIduuid NULLTier A scope, stamped by the subscriber
  • Indexes / uniqueness: uq_work_builds_work_number (workId, number) UNIQUE · uq_work_builds_provider_run (buildPluginId, providerRunId, runAttempt) UNIQUE — deliberately not partial (providerRunId NULLs are distinct on Postgres, SQLite and MySQL/MariaDB, so an unadopted Build never collides, and upsert(conflictPaths) works on every driver) · idx_work_builds_work_created (workId, createdAt) · idx_work_builds_work_commit (workId, commitSha) · idx_work_builds_status_observed (status, lastObservedAt).
  • FK: works(id) ON DELETE CASCADE.
  • Retention (R-25): exports as data/works/builds.jsonl; secret-name and spec-hash columns reviewed as benign; buildInputsHash is dropped.
  • Source: APW-05/plan.md §3.1 "work_builds — the new table" · migrations §3.3.

2.5 work_build_preparations — APW-05 (added 2026-09-17, APW05-G03)​

Entity WorkBuildPreparation (new). Per-App-Work preparation state, one row per Work.

ColumnTypeNote
iduuid PK
workIduuid NOT NULLFK works.id ON DELETE CASCADE, UNIQUE uq_work_build_preparations_work
buildPluginIdvarchar(64) NOT NULL
buildInputsHash / secretsSyncedAtvarchar(64) / timestamp NULLsha256 over (name, fingerprint) of the last completed secret sync; written even with 0 values
buildSecretNamessimple-json NULLstring[] ≤ 50 — EW_ names written and not removed
workflowSha256varchar(64) NULLonly after a matching read-back (FR-8)
workflowStatevarchar(24) NOT NULL DEFAULT 'none'none · committed · pullRequestOpen · editedByHand
workflowPullRequestNumber / workflowPullRequestUrlint / varchar(512) NULL
webhookId / webhookStatevarchar(64) / varchar(24) NOT NULL DEFAULT 'none'none · installed · skipped · permissionMissing
runsEtag / runsCheckedAtvarchar(128) / timestamp NULLthe run-discovery cursor
repositoryBlocksimple-json NULL{ reason, detail, at } — e.g. actionsDisabled
prepareSeq / lastPreparedAtint NOT NULL DEFAULT 0 / timestamp NULLcoalescing marker
tenantId / organizationIduuid NULL
  • Indexes / uniqueness: uq_work_build_preparations_work UNIQUE (workId) — counted among the six indexes the shared migration creates.
  • FK: works(id) ON DELETE CASCADE.
  • Retention (R-25): NOT STATED — neither the APW-05 R-25 line nor its T45 task names this table. See §9 item 1; it is reported rather than invented.
  • Source: APW-05/plan.md §3.1b "work_build_preparations — per-App-Work preparation state — (added 2026-09-17, APW05-G03)".

2.6 work_app_runtime_states — APW-06​

Entity WorkAppRuntimeState (new). One row per App Work; the deploy target and everything teardown needs.

ColumnTypeNote
iduuid PK
workIduuid, unique, FK CASCADE
targetvarchar(24)none (default; R-12) · your-cluster · ever-works-apps
targetSettingssimple-json{ namespaceOverride?, ingressClass?, tls, issuer?, storageClass?, networkIsolation: true, allowRoot: false, managedSubdomain: true, primaryDomain?, autoDeploy: true, previews: false }
namespacevarchar(63), nullfrozen at the first prepare-namespace per clusterFingerprint
clusterFingerprintvarchar(32), nullfrom parseKubeconfig; never written by a cluster-check
clusterCheck / clusterCheckedAtsimple-json / timestamptzsecret-free result incl. its own fingerprint and observed ingressAddress
currentDeploymentIduuid, null
deployLockId / deployLockedAtuuid / timestamptzatomic claim; stale after 7 260 s
cancelRequestedAt / cancelRequestedByUserIdtimestamptz / uuid, nullhonoured only while the lock still holds the same Deployment id
queuedBuildId / queuedDeploymentIduuid, nulllatest-wins queue of 1
pendingDomainRebuildBuildIduuid, nullBuild requested by a rebuild domain change
upstreamSyncJudgedToShavarchar(40), nullAPW-04 FR-51
paused / pausedAt / removedAtboolean / timestamptz
deletionRequestedAt / deletionDeleteData / deletionAttempts / deletionRequestedByUserIdtimestamptz / boolean / int / uuidR-15 deletion in progress
ingressAddress / isolationEnforcedsimple-json / boolean, null{ ip?, hostname? }
healthvarchar(16)unknown · healthy · degraded · down · unreachable
consecutiveFailures / consecutivePasses / unreachableStreakint
lastHealthNotifiedAt / lastPolledAt / certInvalidSincetimestamptz
statusSnapshot / statusObservedAtsimple-json / timestamptzAppStatusSnapshot, no log text
tenantId / organizationIduuid, nullscope columns, no relation decorators
  • Indexes / uniqueness: unique workId; (target, paused, lastPolledAt) for the poller; (deployLockId); (deletionRequestedAt). No index names are given by the plan.
  • FK: workId keeps ON DELETE CASCADE — the row disappears only after APW-06 §9.7 has removed the workloads and APW-01 deletes the Work, so nothing needed for teardown is gone before it runs.
  • Retention (R-25): exports as data/works/app-runtime-states.jsonl; nothing is redacted.
  • Source: APW-06/plan.md §7.2 "work_app_runtime_states (new) — entity WorkAppRuntimeState" · migrations §7.3.

2.7 work_app_env_values — APW-07​

Entity WorkAppEnvValue (new). Secret-bearing.

ColumnTypeNote
iduuid PK
workIduuid NOT NULLFK works.id ON DELETE CASCADE
namevarchar(128) NOT NULL^[A-Z_][A-Z0-9_]{0,127}$
originvarchar(16) NOT NULLgenerated · prompted · user · derived (keypair public halves only)
valueEncryptedtext NOT NULLenc::v1:: envelope; refused when encryption is not enabled
valueBytesint NOT NULLbyte length for the 1 MiB total (FR-31); never shown
versionint NOT NULL DEFAULT 1+1 on every change; drives change flags and build fingerprints
generatorFingerprintvarchar(160) NULLcanonical generate block, e.g. "base64:24", "keypair:ed25519"
derivedFromNamevarchar(128) NULLfor <NAME>_PUBLIC rows
generatedAt / setByUserIdtimestamp / uuid NULL
  • Indexes / uniqueness: uq_work_app_env_values_work_name UNIQUE (workId, name) · idx_work_app_env_values_work (workId).
  • FK: works(id) ON DELETE CASCADE.
  • Retention (R-25): exports under data/works/ through the parent Work ids with valueEncrypted redacted to { wasSet } and valueBytes dropped. The table does not join BACKUP_DROPPED_ENTITIES.
  • Source: APW-07/plan.md §3.1 · migration §3.4 · R-25 line in §3.

2.8 work_app_dependencies — APW-07​

Entity WorkAppDependency (new). Secret-bearing.

ColumnTypeNote
iduuid PK
workIduuid NOT NULLFK works.id ON DELETE CASCADE
kindvarchar(16) NOT NULLpostgres · redis · objectStorage · smtp
deployTargetvarchar(24) NOT NULLyour-cluster · ever-works-apps
providerPluginId / providerIdvarchar(64) NOT NULL
statusvarchar(16) NOT NULLpending · provisioning · ready · degraded · failed · kept · deleting · deleted
statusReason / statusDetailvarchar(48) / simple-json NULLnames and numbers only, ≤ 2 KiB
declaredsimple-json NOT NULLthe App spec block for this kind (non-secret)
actualVersion / sizeGiBvarchar(32) / int NULL
configEncrypted / outputsEncryptedtext NULLprompted provider config; all outputs as one JSON envelope. Always NULL for ever-works-apps rows (resolved in the zone)
outputsVersionint NOT NULL DEFAULT 0
resourceRefssimple-json NULL{ namespace?, objects: [{kind,name}] ≤ 20, databases?, buckets? } — non-secret
inSpecboolean NOT NULL DEFAULT true
backupPolicy / backupStatevarchar(16) NOT NULL / varchar(16) NULLnone · operator · provider · managed / none · not_configured · healthy · overdue · failing · external · unknown
lastBackupAt, backupCheckedAt, lastProvisionedAt, lastCheckedAt, provisionLeaseUntiltimestamp NULL
  • Indexes / uniqueness: uq_work_app_dependencies_active UNIQUE (workId, kind) partial WHERE status NOT IN ('kept','deleted') · idx_work_app_dependencies_work (workId) · idx_work_app_dependencies_status (status, lastCheckedAt).
  • FK: works(id) ON DELETE CASCADE — removes rows when an App Work row is deleted; the data in clusters and tenant servers is untouched by that (FR-45). Kept rows are the record of what remains.
  • Retention (R-25): configEncrypted and outputsEncrypted redacted to { wasSet }.
  • Source: APW-07/plan.md §3.2 · migration §3.4.

2.9 upstream_pull_requests — APW-09​

Entity UpstreamPullRequest (new).

ColumnTypeNote
iduuid PK
userIduuid NOT NULLauthor member; CASCADE with user.id
workIduuid NOT NULLCASCADE with works.id
sourceTaskIduuid NOT NULLno FK — a Task deletion must not erase a published PR's record
preparationTaskId / followUpTaskIduuid NULL
upstreamOwner / upstreamRepo / baseBranchvarchar(100/100/255)
headOwner / headRepo / headBranch / headShavarchar(100/100/255) / varchar(64) NULLbranch upstream-pr/{slug≤40}-{4 hex}
statevarchar(24) NOT NULLUPSTREAM_PULL_REQUEST_STATES; default preparing
number / urlint / varchar(512) NULL
title / body / disclosureTextvarchar(200) / text / varchar(300) NULLpublic text by design
maintainerCanModifyboolean NOT NULL DEFAULT true
approvalProposalId / approvalExpiresAtuuid / timestamptz NULL
checksSummary / reviewSummary / signatureStatevarchar(32/24/24) NULLcla_required · cla_acknowledged · cla_pending_check
refusalCode / refusalDetailvarchar(32) / varchar(500) NULLdetail is a quoted guide sentence ≤ 300 or a provider message, never a token
diffStats / checkResults / seenReviewIds / pushTimestampssimple-json NULL≤ 10 checks; ≤ 200 review ids; ≤ 20 push times (24 h window)
lastUpstreamActivityAt / lastCheckedAt / nextCheckAttimestamptz NULL
openedAt / mergedAt / closedAttimestamptz NULL
  • Indexes / uniqueness: idx_upr_work_state (workId, state) · idx_upr_user_upstream_opened (userId, upstreamOwner, upstreamRepo, openedAt) · idx_upr_due (state, nextCheckAt) · uq_upr_active_source (sourceTaskId) UNIQUE partial WHERE state IN ('preparing','needs_signature','awaiting_approval','opening','open') (FR-5, S27).
  • FK: CASCADE with user.id and with works.id.
  • Retention (R-25): exports as data/works/upstream-pull-requests.jsonl; nothing is redacted.
  • Source: APW-09/plan.md §3.1 · migration §3.2.

2.10 The apps_tier_* tables — APW-10​

Eight tables in packages/agent/src/entities/, registered in the new packages/agent/src/apps-tier/apps-tier.module.ts. Raw uuid references, no @ManyToOne across families (the EW-654 rule, as in fleet-kill-switch.entity.ts) — so no FK and no ON DELETE is stated for any of them, deliberately.

TableColumns (as the plan lists them)Index / unique
apps_tier_gate_runsid, trigger (manual/schedule), requestedByUserId?, status (running/green/red/error), scope (verified-blueprints/any), results simple-json (≤ 25 rows {id,outcome,reasonCode,durationMs}), policyRevision?, controllerVersion?, gateVersion, startedAt, finishedAt?(status, finishedAt)
apps_tier_attestationsid, itemId varchar(8), attestedByUserId, evidenceNote text (20–2 000), evidenceRef varchar(500)?, attestedAt, expiresAt, revokedAt?, revokedByUserId?, revokeReason?, notified14dAt?, notified1dAt?(itemId, expiresAt)
apps_tier_state_eventsappend-only: id, state (closed/open-verified-blueprints/open-any), actor (user/system), actorUserId?, reason varchar(500), reasonCodes simple-json, gateRunId?, automatic bool, createdAt(createdAt)
apps_tier_quarantinesid, workId, requestId uuid unique, category, source (operator/detector/pause-all/self-check), reason varchar(500), requestedByUserId?, requestedAt, networkIsolatedAt?, scaledToZeroAt?, ingressDisabledAt?, state (requested/active/releasing/released), releasedByUserId?, releaseReason?, releasedAt?, signalId?, pauseAllBatchId?partial unique (workId) WHERE state IN ('requested','active','releasing'); the SQLite branch uses a unique expression index
apps_tier_abuse_signalsid, workId, zoneName unique, kind, severity, observedAt, summary varchar(500), ruleId, test bool, status (open/dismissed/actioned), handledByUserId?, handledReason?, autoQuarantined bool, createdAt(status, severity)
apps_tier_quota_profilesname PK varchar(32), limits simple-json (FR-47 fields), monthlyEgressGiB, buildMinutesMonthly, updatedByUserId?, updatedAt; seeded starter, standardPK name
apps_tier_image_allowancesid, workId, digest char(71), reason, createdByUserId, expiresAt (≤ 30 days), createdAtUNIQUE (workId, digest)
apps_tier_usage_windowsid, workId, windowStart, unit (cpu_core_seconds/memory_mib_hours/egress_mib/storage_gib_hours/build_minutes), quantity bigint, pluginUsageEventId, createdAtUNIQUE (workId, windowStart, unit) (FR-50)
  • Retention (R-25): the seven operator tables join BACKUP_DROPPED_ENTITIES under one comment giving the reason — operator launch-gate, moderation and quota state that is not stored on a workspace's behalf (detector rules, operator reasons and operator ids never reach a tenant). AppsTierUsageWindow exports record-only as data/runs/apps-tier-usage-windows.jsonl through the parent Work ids with its time trim. Work.appsTierQuotaProfile rides works/works.jsonl.
  • Seeding: 1792100000000 seeds exactly one closed state event (actor: system, reason: 'initial') so "no row" can only mean "migration not applied" and is read as closed — the fleet kill-switch posture. 1792100200000 seeds the quota profiles.
  • Source: APW-10/plan.md §4 "Platform data model" · migrations §4.

2.11 app_launcher_preferences — APW-11​

Entity AppLauncherPreference (new), packages/agent/src/entities/app-launcher-preference.entity.ts.

ColumnTypeNote
iduuid PK
userIduuid NOT NULLFK user.id ON DELETE CASCADE
scopeKeyvarchar(40) NOT NULL'global' · 'personal' · <organizationId>
itemKeyvarchar(64) NOT NULL'platform:<catalogId>' · 'work:<uuid>'
visible / pinnedboolean NOT NULLdefaults true / false
pinOrder / sortOrdersmallint / integer NULL0..5 when pinned; 0..9999
createdAt / updatedAttimestamptz NOT NULL
  • Indexes / uniqueness: uq_app_launcher_prefs_user_scope_item UNIQUE (userId, scopeKey, itemKey) · idx_app_launcher_prefs_user_scope (userId, scopeKey).
  • Deliberate omissions, all three normative: scopeKey instead of a nullable organizationId — uniqueness over a nullable column is not portable (Postgres treats NULLs as distinct and so does SQLite); no tenantId/organizationId stamp columns — the scope subscriber would stamp the active Organization onto global rows, which would be wrong; no FK to works — item keys are polymorphic and rows for deleted or inaccessible Works are ignored on read (FR-28).
  • Retention (R-25): exports in the account section as data/account/app-launcher-preferences.jsonl, scoped by user.
  • Source: APW-11/plan.md §3.2 · migration §3.4.

2.12 external_identities — APW-12​

Entity ExternalIdentity (new), Tier B. Dropped from the workspace backup.

ColumnTypeNote
iduuid PK
userIduuid NOT NULL@ManyToOne(() => User, { onDelete: 'CASCADE' }) (FR-30)
issuervarchar(512) NOT NULLexact iss string
subjectvarchar(255) NOT NULLexact sub
emailAtLinkvarchar(320) NOT NULLdisplay only; never used to resolve an account
emailVerifiedAtLinkboolean NOT NULLalways true for rows this epic writes
linkedViavarchar(16) NOT NULLsign-up · settings
linkedAt / lastLoginAtPortableDateColumn / NULL
delegatedClientssimple-json NULLArray<{ clientId, lastSeenAt }> ≤ 10, oldest evicted (FR-48)
tenantIduuid NULLTier B scope stamp; no organizationId
  • Indexes / uniqueness: uq_external_identities_issuer_subject UNIQUE (issuer, subject) — the S24 race is decided by this index · uq_external_identities_user_issuer UNIQUE (userId, issuer) · idx_external_identities_user (userId).
  • FK: user.id ON DELETE CASCADE.
  • Retention (R-25): joins BACKUP_DROPPED_ENTITIES beside the session tables — an issuer + subject link is a sign-in binding, and a restore must never re-link an account to an identity.
  • Source: APW-12/plan.md §3.1 · migrations §3.6.

3. Extensions of existing tables​

3.1 work_deployments — APW-06​

ColumnTypeMeaning
buildIduuid, null, indexedWorkBuild.id deployed — no FK, to keep APW-05's merge order free (validated in code). Null for build.strategy: image
componentStatusessimple-json, null[{ name, role, desired, ready, restarts, lastTerminationReason?, oomKilledAt? }] at terminal state
smokeResultsimple-json, null{ inCluster: CheckResult[], public: CheckResult[], hairpin?, classification?, observedAt }
appTargetvarchar(24), nullyour-cluster · ever-works-apps
appRendersimple-json, nullphase, namespace, spec commit, env checksum, image ref/digest, job results, warnings, preconditions, rollback facts, cancelled/superseded markers — never values or log text

New WorkDeployment states: DEPLOYING, VERIFYING, ROLLED_BACK, SUPERSEDED; isTerminal() adds ROLLED_BACK and SUPERSEDED. Migration 1792060000000-ExtendWorkDeploymentsForApps.ts adds the five nullable columns and the index on buildId; down() drops only them.

Source: APW-06/plan.md §7.1 · §7.3.

3.2 tasks — APW-08​

ColumnTypeDefaultWhy
mergeCommitShavarchar(64) NULLNULLthe commit a Build/Deployment must contain
deliveryStatevarchar(24) NULLNULLone of TASK_DELIVERY_STATES; NULL = not tracked (every existing Task)
deliveryBuildIduuid NULLNULLwork_builds.id that decided the state — no FK
deliveryDeploymentIduuid NULLNULLwork_deployments.id that decided it — no FK
deliveryUpdatedAttimestamptz NULLNULLreconciler ordering and stale detection
deliveryClosedByIduuid NULLNULLwho chose Close anyway
branchGuardRefusaltext NULLNULLwhy the change rules refused a change that reached the remote (≤ 4,000 chars)

Indexes: idx_tasks_delivery_due (deliveryState, deliveryUpdatedAt) and uq_tasks_work_merge_commit (workId, mergeCommitSha) UNIQUE partial WHERE "mergeCommitSha" IS NOT NULL — makes "merge recorded once" (S29) a database guarantee. Migrations 1792080000000-AddTaskDeliveryState.ts, 1792080100000-AddGoalWorkScope.ts, 1792080200000-AddMissionTaskOutput.ts; branchGuardRefusal is added by 1792110100000-AddTaskBranchGuardRefusal.ts (added 2026-09-25, see §4).

3.3 goals — APW-08​

workId uuid NULL + idx_goals_work (workId). No FK and no @ManyToOne (the entity's cycle-avoidance rule); deletion is detected by the orchestrator (S26).

3.4 missions — APW-08​

outputMode varchar(8) NOT NULL DEFAULT 'ideas', taskOutput simple-json NULL, taskOutputNoticeAt timestamptz NULL. taskOutput is { tasksPerTick: 1..3 (1), openTasksCap: 1..10 (3), agentId?: uuid }, read through normalizeMissionTaskOutput on every use (fails toward the defaults).

3.5 organizations.appProvisionCaps — APW-04​

Nullable simple-json { tokenCap?, runnerMinuteCap? } on packages/agent/src/entities/organization.entity.ts; accepted by apps/api/src/organizations/dto/update-organization.dto.ts within APW-04 plan §3.2 bounds; resolved Organization → instance env → default. Migration 1792040100000-AddOrganizationAppProvisionCaps.ts adds the one nullable column. Source: APW-04/tasks.md T41.

3.6 works.appsTierQuotaProfile and works.appLauncherExposed — APW-10 · APW-11​

ColumnTypeDefaultWhy
appsTierQuotaProfilevarchar(32) NULLNULL = starterAPW-10's per-Work quota profile
appLauncherExposedboolean NULLNULL = kind default (true for app, false otherwise)APW-11 FR-19; an explicit value always wins. Declared with an explicit type: 'boolean' because nullable union types reflect as Object

3.7 session — APW-12​

ColumnTypeWhy
externalIdentityIduuid NULL, no FKsessions opened by an identity (disconnect, sub-only notices)
externalSidvarchar(255) NULLEver ID sid (notices with sid)

Indexes idx_session_external_identity and idx_session_external_sid. No FK on purpose: a session row must never block deleting an identity, and disconnect deletes the sessions explicitly first. Better Auth's own adapter never writes these columns; NULL is the default for every other sign-in method (FR-35).


4. Migrations — the block, the sequence and the re-stamp rule​

README §7 rule 6 fixes the timestamp shape: 1792 + two-digit epic + two-digit slot + 00000. The newest migration on develop when the programme was authored was 1791200100000-CreateOnboardingChecklists.ts; re-verified on ee45946e5 it is 1791240000000-AddSafetyRailsCore.ts. Every reserved 1792… timestamp is above it.

OrderFileEpicSlot
—(reserved, unused)APW-011792010000000
11792020000000-CreateWorkUpstreamStates.tsAPW-0200
21792030000000-CreateWorkAppSpecStates.tsAPW-0300
31792040000000-CreateWorkAppProvisionings.tsAPW-0400
41792040100000-AddOrganizationAppProvisionCaps.tsAPW-0401
51792050000000-CreateWorkBuilds.ts (two tables: work_builds, work_build_preparations)APW-0500
61792050100000-AddWorkBuildSupplyChain.ts (scanSummary, signatureState, blockedEgressHosts)APW-0501
71792060000000-ExtendWorkDeploymentsForApps.tsAPW-0600
81792060100000-CreateWorkAppRuntimeStates.tsAPW-0601
91792070000000-CreateAppEnvAndDependencies.ts (two tables)APW-0700
101792080000000-AddTaskDeliveryState.tsAPW-0800
111792080100000-AddGoalWorkScope.tsAPW-0801
121792080200000-AddMissionTaskOutput.tsAPW-0802
131792090000000-CreateUpstreamPullRequests.tsAPW-0900
141792100000000-CreateAppsTierGate.tsAPW-1000
151792100100000-CreateAppsTierQuarantineAndSignals.tsAPW-1001
161792100200000-CreateAppsTierQuotaAndMetering.tsAPW-1002
171792110000000-CreateAppLauncherPreferences.tsAPW-1100
17a1792110100000-AddTaskBranchGuardRefusal.ts (1 column on tasks: branchGuardRefusal text NULL)APW-0811/01
181792120000000-CreateExternalIdentities.tsAPW-1200
191792120100000-AddExternalIdentityToSessions.tsAPW-1201
—(none)APW-13—

Row 17a (added 2026-09-25). 1792110100000-AddTaskBranchGuardRefusal.ts belongs to APW-08 but is stamped after APW-11's 1792110000000 by coordinator direction, because APW-08's slots 1792080000000–1792080300000 stay reserved for their planned migrations. It is one nullable ADD COLUMN with no dependency on anything APW-11 did, and it rides the existing data/tasks/tasks.jsonl export.

The re-stamp procedure (binding). Every plan repeats it and every task carries it:

  1. Generate the skeleton with the repository's own generator: pnpm --filter ever-works-api migration:generate -- src/migrations/<name> (see quickstart.md §7).
  2. Find the newest migration actually on develop at merge time.
  3. If develop has moved past this epic's block, re-stamp the class name and the file name to a timestamp above it, keeping the slot ordering inside the epic. Re-stamp again if develop moves between review and merge (README §7 rule 6; TRACKER.md "Migration timestamp blocks").
  4. down() must drop only what up() created. A migration whose down() is not symmetric fails review — several epics state this explicitly ("down() drops only these two tables", "down() drops only them", "down() of each drops only what its up() created").
  5. Forward-only: no migration edits a previously-shipped migration.

Cross-epic ordering constraints worth knowing. work_builds is referenced by APW-06's work_deployments.buildId without a FK, deliberately, "to keep APW-05 merge order free"; work_app_provisionings.deliveryBuildId/deliveryDeploymentId (APW-08) are likewise FK-free; and work_upstream_states.conflictTaskId is FK-free so a deleted Task cannot cascade. work_app_runtime_states keeps its CASCADE because its row must outlive the teardown (R-15).


5. Portability — SQLite, Postgres, MySQL/MariaDB​

The PR lane and the demo run SQLite; dev, stage and production run Postgres; the platform also supports MySQL/MariaDB. Portability is therefore a correctness requirement, not a nicety.

ConcernRule stated by the programmeSource
Timestamps compared in SQL (nextSyncAt <= :now)use TimestampColumn (bigint epoch ms), as WorkDeployment does — not a driver date typeAPW-02 §3.1
Dates generallyPortableDateColumnAPW-03 §3.1 · APW-12 §3.6
Partial unique indexessupported on Postgres and SQLite; APW-10's quarantine index needs a unique expression index on SQLite and the partial form on Postgres, branching on queryRunner.connection.options.typeAPW-07 §3.4 · APW-10 §4
Unique index over a nullable columnavoid — Postgres and SQLite both treat NULLs as distinct, so uniqueness does not hold; APW-11's scopeKey exists for exactly this reasonAPW-11 §3.2
work_builds indexesall six declared through TypeORM TableIndex — no raw double-quoted SQL and no partial index — so up()/down() behave identically on Postgres, SQLite, MySQL and MariaDBAPW-05 §3.3
Race-safe single-row creationINSERT … ON CONFLICT (…) DO NOTHING then read back (SQLite: INSERT OR IGNORE) — "first writer wins, losers re-read"APW-07 §3.1
Atomic sequence allocationSELECT COALESCE(MAX(number),0)+1 … FOR UPDATE on Postgres; SQLite takes the same path without the lock and retries up to 3 times on unique violationAPW-05 §3.1
Migration verificationa DATABASE_AUTOMIGRATE boot must be exercised on both a SQLite database and a Postgres service container, up and downAPW-12 tasks.md

Not stated anywhere: an index-name convention for work_app_runtime_states and for the eight apps_tier_* tables (only the column lists are given), and any FK/ON DELETE rule for the apps_tier_* tables (deliberate — raw uuid references, EW-654 rule).


6. Entity registration — the four places the drift specs check​

A new entity is not "added" until it is registered in all four places, or the database drift specs fail. Every epic repeats the same list; APW-12 adds a fifth for its repository.

  1. packages/agent/src/entities/index.ts — the export.
  2. packages/agent/src/database/_entity-names.ts — the name in AGENT_ENTITY_NAMES.
  3. packages/agent/src/database/_entities-inventory.ts — the import and the ENTITIES entry.
  4. The owning module's TypeOrmModule.forFeature([...]).
  5. (APW-12 only) packages/agent/src/database/_repository-inventory.ts — the repository entry.

The drift specs that enforce it are packages/agent/src/database/database.module.spec.ts and database.config.spec.ts. The most explicit statement of the list is APW-05/plan.md §3.4 "Entity registration", which names both new entities ('WorkBuild', 'WorkBuildPreparation') and all four targets; the same four are listed in APW-02 §3.1, APW-03 §3.1, APW-04 §3.1, APW-06 tasks, APW-07 §3.5, APW-09 §3.1, APW-10 §4, APW-11 §3.2 and APW-12 §3.1.

Scope columns. Tier A tables declare tenantId/organizationId without relation decorators so apps/api/src/scope/scope-stamping.subscriber.ts stamps them. Two tables deliberately do not: app_launcher_preferences (stamping global rows with the active Organization would be wrong) and external_identities (Tier B: tenantId only, no organizationId).


7. Retention and the workspace backup (R-25)​

Resolution R-25 requires every table an App Works epic adds to be classified in the same PR: either a file in a BACKUP_DOMAIN_SPECS domain, or an entry in BACKUP_DROPPED_ENTITIES with its reason. Each epic also extends packages/agent/src/account-transfer/backup/collectors/collectors.spec.ts so the classification is enforced by a test. The owning tasks are APW-02 T45 · APW-03 T54 · APW-04 T47 · APW-05 T45 · APW-06 T65 · APW-07 T45 · APW-09 T35 · APW-10 T42 · APW-11 T30 · APW-12 T46.

Domain / dispositionTables
works domain, scoped through the parent Work idswork_upstream_states · work_app_spec_states · work_app_provisionings · work_builds · work_app_runtime_states · work_app_env_values · work_app_dependencies · upstream_pull_requests
account domainapp_launcher_preferences
runs domain (record-only, time trim)apps_tier_usage_windows
rides an existing exportwork_deployments (deployments) · tasks · goals · missions · organizations · works
dropped, with a recorded reasonthe seven APW-10 operator tables · external_identities (with sessions and auth tokens)
redacted to { wasSet } / dropped columnwork_app_env_values.valueEncrypted → { wasSet }; work_app_env_values.valueBytes dropped; work_app_dependencies.configEncrypted/outputsEncrypted → { wasSet }; work_builds.buildInputsHash dropped
unclassifiedwork_build_preparations — see §9 item 1

Every secret-bearing column is named. README §7 rule 8 and Constitution VII: App env values, kubeconfigs, registry and Git tokens are encrypted, never logged and never returned. The two credential-bearing tables are work_app_env_values and work_app_dependencies; tier credential fingerprints are dropped.


8. Delta vs CONTRACTS.md §2 — the three missing schema changes​

CONTRACTS.md §2 states that it lists every persisted name the epics share. Three schema changes the programme actually makes are not in it. Each is real, each is grounded, and each belongs in §2 verbatim (the exact markdown to paste is in the hand-off below and in this file's companion request). Nothing is removed from §2 — these are three additions.

#Missing from CONTRACTS.md §2OwnerGrounded atWhat §2 should say
1organizations.appProvisionCaps + migration 1792040100000-AddOrganizationAppProvisionCapsAPW-04APW-04/tasks.md T41 — "Create apps/api/src/migrations/1792040100000-AddOrganizationAppProvisionCaps.ts (new) — adds the one nullable column."the column, its shape { tokenCap?, runnerMinuteCap? }, that it is nullable, and the resolution order Organization → instance env → default
2session.externalIdentityId / session.externalSid + migration 1792120100000-AddExternalIdentityToSessionsAPW-12APW-12/plan.md §3.2 and its §3.6 migration table row 1792120100000-AddExternalIdentityToSessions.ts → "add externalIdentityId, externalSid + 2 indexes to session"the two nullable columns, the two indexes, and that there is no FK on purpose
3work_builds.scanSummary / signatureState / blockedEgressHosts + migration 1792050100000-AddWorkBuildSupplyChainAPW-05APW-05/plan.md §3.3 second bullet — "P3 apps/api/src/migrations/1792050100000-AddWorkBuildSupplyChain.ts — adds scanSummary simple-json NULL ({ critical, high, medium, low, fixableCritical }), signatureState varchar(16) NULL (signed|unsigned|foreign), blockedEgressHosts simple-json NULL (≤ 10). Additive only."the three nullable columns and the fact that they are P3 (Wave 3), so §2's WorkBuild row does not read as the final shape

Stale citations in the gap register, reported rather than copied. The SK-11 row cites tasks.md:524 for the APW-04 migration (it is now APW-04/tasks.md T41 — the file grew during the programme's own editing), plan.md:450 for the APW-05 supply-chain migration (now §3.3), and plan.md:310 for APW-12 (still correct). The three facts are nevertheless real and were re-read at source; only the line numbers moved.

Two further §2 gaps found while building this document (not in the gap register, reported for the lead):

  1. work_build_preparations is a whole table missing from §2's inventory. It is owned by APW-05 and created by the same 1792050000000-CreateWorkBuilds.ts migration; the APW-05 gap note APW05-G03 added it on 2026-09-17 (APW-05/plan.md §3.1b).
  2. CONTRACTS.md §2's APW-06 WorkDeployment row lists four columns, not five — it names buildId, componentStatuses, smokeResult in the "extended" row and appTarget, appRender in the "extended (added by APW-06)" row, which together read as five, but the second row also repeats the states. It is correct in sum; it is noted here so the lead can confirm rather than assume. Also, §2 mentions the migration block only as a one-line pointer ("Migration blocks: README §7 rule 6") rather than listing the 19 files — §4 of this document supplies that list if the lead wants it folded in.

9. Open items (honest list)​

  1. work_build_preparations has no R-25 classification. Neither APW-05/plan.md §3 or §3.3 nor its T45 names the table, and ACCEPTANCE.md's ACC-REG-15 per-table assertion list omits it too. R-25's own wording ("every table an App Works epic adds") therefore has a hole. The safe reading is the works domain, giving it data/works/build-preparations.jsonl, but that is a decision for APW-05 and the lead — not something to invent here.
  2. No index names for work_app_runtime_states and no index names or FK rules for the eight apps_tier_* tables. The latter is deliberate (raw uuid references, EW-654 rule); the former looks like an omission in APW-06 §7.2 rather than a decision.
  3. No raw CREATE TABLE DDL exists anywhere except two partial prose forms (APW-03 §3.3 and APW-11 §3.4). Every other table is specified as a markdown column table or a fenced column tree, and the migration is generated from the TypeORM entity. This is the house style and is fine — it does mean the entity file, not this document, is the last word on a column's exact driver type.
  4. work_app_provisionings gained lastRunOutput, questionReason and questionParams during the programme's own editing pass. CONTRACTS.md §2's row describes the entity's purpose, not its columns, so the row is not wrong — but anyone reading §2 alone will not know the table holds a 512 KiB text column.