Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Design the persistent-data layer — datastore choice, ER diagram, schema per table/collection, indexes, access-pattern-to-index mapping, migrations, retention, sensitive-data inventory, consistency and concurrency strategy, and non-relational persistent assets. Use when asked to design the data model, create the schema, define tables indexes and access patterns, map queries to indexes, or produce a DATA.md.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-07 | ✗→✓ | ▲ Improved | 594% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 484% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 327% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 559% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 870% | 0% |
Produce a DATA.md that serves as the single source of truth for the system's persistent-data layer: which datastore(s) the system uses and why, the ER (or collection) diagram, the schema for every table or collection with column-by-column typing and constraints, every index with its rationale, the ordered list of migrations, the retention and deletion policy per table, the sensitive-data inventory per column, and the consistency and concurrency strategies applied across entities. Crucially, DATA.md extends traditional database documentation with an access-patterns section that maps every query the system actually runs to the index that serves it — schemas without access patterns are under-specified. An agent reading this document alone can explain, for any query the system issues, which index serves it and at what cost class; for any table, which aggregate it persists and which invariants its constraints enforce; for any column holding sensitive data, its classification, masking rule, and encryption posture.
DATA.md is read by /operations (for migration sequencing, retention runbooks, and backup posture), /security (for the sensitive-data inventory), every per-unit /SPEC.md that touches persistence (to draw typing and constraints from a single source), /code-review for conformance against the registered schema, and /system-verification during bootstrap. The defining discipline — and the commonest violation — is every query is indexed or explicitly accepted as a scan: a schema that lists columns and indexes without the access-pattern map is half a design, because it does not prove the indexes are the right ones. Co-locating schema, indexes, and access patterns in one artifact is the deliberate read-pattern optimisation that makes this skill exist.
INV-NN references for invariants enforced at the database level (uniqueness, foreign-key integrity, check constraints).ADR-NN that chose the storage engine is cited verbatim in § 1 Datastore Overview. Multi-store architectures (a relational primary plus a blob store plus a cache) produce multiple datastore sub-blocks.migrations/, db/migrations/, priv/repo/migrations/, Flyway, Alembic, ActiveRecord, Knex) are present, read them fully. Every shipped migration appears in § 6 with its original filename preserved. Do not re-describe shipped migrations from memory — quote their actual effect..sql files, query-builder DSL, EXPLAIN artefacts), ORM model code (repositories, DAOs, Active Record models), or data-access layers, scan them to harvest the queries the system actually runs. Each harvested query maps to a row in § 5 with its origin file and function cited in the Uses index column's footnote.Read set size: 2 required + up to 3 auto-discovered. Read both required inputs end-to-end; for refinement, read every migration file and every data-access source file before drafting. Omitted reading causes two specific failure modes: tables with no owning aggregate (DOMAIN drift) and indexes with no serving query (orphan indexes — the schema is lying about which queries matter).
Data-layer design proceeds in nine phases: datastore anchoring, aggregate-to-schema mapping, index inventory, access-pattern mapping (the critical phase), migration enumeration, retention and deletion, sensitive-data inventory, consistency and concurrency, and validation. Phases are sequential but revisit earlier phases when a later one reveals a missing index, a missing migration, or a mis-sized constraint. Do not skip the access-patterns phase — it is the phase that proves the schema is the right schema.
Read ARCHITECTURE.md § 2 and § 5 to fix the datastore technology. For each distinct datastore the system uses (typically one primary relational or document store, optionally a blob store, a cache, a message queue's durable state, a search index), record: kind (postgres, mysql, dynamodb, mongo, sqlite, redis, s3, elasticsearch, cassandra, …), version pinned where load-bearing, connection model (pooled / per-request / serverless), transactional semantics (single-row atomic / single-shard / multi-shard with 2PC / none), and consistency guarantees this datastore provides out of the box (read-after-write, snapshot isolation, eventual, tunable quorum). Cite the originating ADR-NN from ARCHITECTURE.md § 5.
The primary datastore kind lands in frontmatter datastore:. If multiple datastores exist, datastore: carries the primary (the one that owns aggregate persistence); secondary stores (cache, blob) are declared in § 10 Non-Relational Side and do not contribute to datastore:.
For each aggregate in DOMAIN.md § 5 (and for entities in § 4 that persist independently), choose the schema representation:
accounts table holding both IndividualAccount and OrganisationAccount). Record the discriminator column and the per-discriminator invariants.For each table or collection write the full block: name (snake_case for relational, typically plural_noun; document stores follow their idiomatic convention), Represents (aggregate or entity from DOMAIN.md §§ 4–5), Owned by (bounded context from DOMAIN.md § 2), Invariants enforced at the DB level (every INV-NN from DOMAIN.md § 9 that is enforceable via uniqueness / FK / check constraints at the storage layer), columns, constraints, and triggers. Column table columns: name, type, nullable (yes / no), default, one-sentence description, and the INV-NN reference when the column carries an invariant (primary keys, uniqueness keys, foreign keys, monotonic counters).
Orphan tables — tables that do not map to any DOMAIN aggregate or entity — reveal unmodeled domain concepts. Surface them as an open question rather than inventing a bounded-context owner.
Table / collection count lands in frontmatter tables_or_collections:.
Every index the schema creates lands in § 4 with: Name (convention idx_<table>_<cols> for secondary indexes, pk_<table> for primary keys, uq_<table>_<cols> for unique constraints — match the real naming convention of the project if one is already in use), Table, Columns (in order — order matters for composite indexes because the prefix rule governs which queries use the index), Type (btree, hash, gin, gist, brin, composite, covering, partial, expression — name the flavour that matters; for document stores use the driver's terminology), Purpose / rationale (one sentence naming which access pattern from § 5 this serves, cited by row number or query summary — the linkage is mandatory), Expected cardinality / selectivity (back-of-envelope — "high selectivity: ~10³ distinct values across ~10⁶ rows", or "low cardinality: 3 distinct states; used only as secondary predicate").
The single unbreakable rule for this phase: every index must appear as the serving index for at least one row in § 5. Inversely, every index in § 4 must be cited from at least one § 5 row — orphan indexes waste write-budget and lie about which queries matter. Resolve the harvesting in Phase 4 before finalising; if an index has no serving query, either add the query (if the code runs it and Phase 4 missed it) or drop the index (if no code runs it). Do not carry orphan indexes forward.
Index count lands in frontmatter indexes:.
This is the phase that makes DATA.md different from traditional database docs. Every query the system runs — every SELECT, UPDATE predicate, DELETE predicate, document find, secondary-index lookup, range scan, aggregation — lands as a row in § 5.
Harvest queries from four sources:
EP-name has an intent; the intent implies a query. GET /repos/:owner/:name ⇒ "find repo by owner+name" ⇒ one row.Repo.find_by, every parameterised SQL literal, every query-builder chain maps to a row.For each query write a row in the § 5 table: Query (prose one-liner or SQL sketch — e.g., "Find repo by owner+name" or SELECT * FROM repos WHERE owner_id = $1 AND name = $2; sketches are acceptable and often clearer than prose — include when the shape matters), Frequency (one of per-request / frequent / periodic / rare — four-step enum, not prose; per-request means every hot-path request executes it, frequent means ≥ 1 per operator action, periodic means scheduled or background, rare means admin-or-support only), Expected rows (result-set cardinality — 1, ≤ 10, ≤ 100, ≤ 10³, ≤ 10⁶, or full-scan for administrative scans), Uses index (the index name from § 4 that serves this query — or (table scan) with justification in the row's trailing note), Cost class (one of O(1) / O(log n) / O(n) / O(n log n) — back-of-envelope complexity against the expected result-set; a "frequent" query with cost class O(n) on a n = 10⁶ table is a design bug, surface immediately).
Rules enforced in this phase:
(table scan) is a valid answer only with justification — "small table, ≤ 100 rows, indexing would cost more than it saves"; "admin-only periodic scan; acceptable under 1 s on production data volumes". Scan without justification is a design bug.repos_repository.go::FindByOwnerName, src/repositories/RepoRepository.ts::findByOwnerName). Speculative queries — queries invented to justify an index — are forbidden. If a query does not yet have a code path, it belongs in an open question or is deleted.per-request row with an Uses index that has low selectivity (< 10² distinct values) is a design smell; surface as a row note or open question.Access-pattern count lands in frontmatter access_patterns:.
List every migration in § 6 in the order it applies, first to last. For each: Filename (exact on-disk filename — 20240815120000_create_repos.sql, V001__create_repos.sql, 0001_create_repos.py, whichever convention the project uses; match the existing migration tool's convention when refining), What it changes (schema delta in prose — "creates repos with primary key id, unique index on (owner_id, name), FK to users(id)"), Safety (reversible? zero-downtime? requires lock? — state each explicitly; reversible: yes/no, zero-downtime: yes/no, lock: none/access-exclusive/row), Backfill (needed? strategy? — no backfill required / backfill via app-level script after migration / backfill inline in migration SQL; transactional; backfills over large tables deserve a one-line note on estimated runtime), Ordering constraints (must run after migration X / before migration Y — typically trivial once migrations are ordered on filename, but surface explicitly when feature flags or deploy coordination matters).
Explicitly identify the initial schema migration (V001 or equivalent) as "initial schema". State the convention future migrations follow in the § 6 preamble so contributors know how to add one: filename format, location, tool command to generate.
Migrations are append-only. Never edit a shipped migration; always add a new one. This is rule 6 in the Rules section and is a hard constraint.
Migration count lands in frontmatter migrations_listed:.
§ 7 Retention & Deletion. For each table (and for each column where per-column retention differs from the table's default): Retention period (calendar duration — 180 days, 7 years, indefinite, until aggregate deletion; never "as needed"), Deletion strategy (hard delete — row removed; soft delete — tombstone column marks the row inactive; archive-then-delete — row copied to an archive table before removal), GDPR / regulatory requirements (right-to-erasure: hard delete within 30 days of request; no PII; not in GDPR scope; cite the regulation by name — GDPR, HIPAA, PCI-DSS, SOX — when applicable), Backup retention (separate from live retention — typically "daily backups retained 30 days; point-in-time-recovery window 7 days"; cross-link to OPERATIONS.md for backup automation).
§ 8 Sensitive Data Inventory. For every column holding PII, secrets, financial data, health data, or other regulated data: Table.column, Classification (one of PII / secret / financial / health / other-regulated), Masking in logs (the redaction rule — typically [REDACTED] for the entire value, or last-4-only for cards, or hashed for identifiers needing correlation without reversibility), Encryption at rest (native — the datastore's native encryption; column-level — the column is encrypted with a dedicated key; application-layer — the application encrypts before storing; none — justify), Who can read (the role or principal from SECURITY.md that is authorised — "operator-with-support-role", "service-self-only", "audit-reader"; SECURITY.md owns the full role matrix, this document cites it).
A column holding sensitive data that is absent from § 8 is a modelling bug — the validation phase greps for common PII column names (email, phone, ssn, tax_id, card, dob, password, token, api_key, secret) and flags any hit not covered in § 8.
Sensitive-column count lands in frontmatter pii_columns:.
§ 9 Consistency & Concurrency. State: the default isolation level and any per-transaction overrides (default: READ COMMITTED; REPEATABLE READ for the Push acceptance transaction); optimistic-vs-pessimistic locking per aggregate (name the version / serial / parent-hash column that drives optimistic concurrency — "the repos.version column is bumped by every update; writes fail with a conflict when the observed version differs"); serial number or version columns and their semantics; tombstone / soft-delete handling (how readers filter them — a mandatory WHERE deleted_at IS NULL predicate lives in a view, a base repository method, or explicit in every query — state the enforcement mechanism).
§ 10 Non-Relational Side. Inventory every persistent asset that lives outside the primary relational schema: cache key patterns (cache:repo:{owner}/{name} — include the TTL and the invalidation trigger), blob storage paths (s3://bucket/repo/{id}/packs/{pack-sha}.pack — include the retention policy and the access control stance), queue names (q.sync_jobs — include the durability posture and the redrive target), file-system layout (when applicable — the on-disk directory shape for single-tenant persistence). For each non-relational asset, the same three questions apply: who writes it, who reads it, and under what retention policy. Cache and queue state that is replayable from durable state (relational store, event log) is marked ephemeral; state whose loss causes data loss is marked durable and must appear in § 7 Retention.
§ 11 names the explicit cross-references — one bullet per relationship, in this fixed order:
INV-NN enforced via DB constraint is cited in the table's Invariants enforced at the DB level field and appears in DOMAIN.md § 9.Verify the document holds together before finalising:
INV-NN from DOMAIN.md § 9 that is enforceable at the DB level is enforced via a constraint in § 3 and cited in the table's Invariants enforced at the DB level field.(table scan) with inline justification.O(1) or O(log n) — higher-order costs on hot paths surface as open questions.owner_context value matches a bounded context name in DOMAIN.md § 2 exactly.JSON body, protobuf field, HTTP status, endpoint path. Columns may be named (body_json text) without the document narrating wire-protocol specifics.page, button, screen, tab, modal, click. Tables persist aggregates, not UI state (except legitimate UI-state tables — if any, they still persist an aggregate named in DOMAIN).tables_or_collections, indexes, access_patterns, migrations_listed, pii_columns, open_questions.status is complete if § 12 Open Questions is "All questions resolved." and has_open_questions otherwise.Update frontmatter counts. Do not finalise the document with any section missing its heading.
These rules govern the output document. Violations are detected by the quality checklist.
Every table in § 3 has a Represents field naming an aggregate from DOMAIN.md § 5 or an entity from § 4. Orphan tables — tables with no DOMAIN concept — reveal unmodeled domain. Surface as an open question rather than inventing a mapping. Purely-infrastructural tables (session stores managed by a framework, Sidekiq's internal queues) are out of scope for this document and live in § 10 Non-Relational Side if we own them, or not at all if the framework owns them.
Every INV-NN from DOMAIN.md § 9 whose category is uniqueness / reference-integrity / value-constraint is enforceable at the database level and must be enforced via a constraint — a UNIQUE index, a FOREIGN KEY, a CHECK constraint, or the document-store equivalent. The table's Invariants enforced at the DB level field cites the INV-NN; absence is a modelling bug. Invariants of category lifecycle / temporal / authorisation typically live in application logic and are not cited here.
Every row in § 5 has Uses index = a named index from § 4, or (table scan) with an inline justification ("small table, bounded by N-of-organisations, maintenance cost of an index exceeds scan cost"; "admin-only periodic audit scan, acceptable under 5 s at production volume"). A row with Uses index left blank is an incomplete row — not an acceptable output state.
Every index in § 4 must appear as Uses index in ≥ 1 row of § 5. Orphan indexes (indexes that no query uses) waste write-budget and mislead every reader about what the schema is optimised for. Either add the serving query (if the code runs it and the phase missed it) or drop the index — never ship an index that no query uses.
Any column that holds PII, secrets, financial data, health data, or other regulated data appears in § 8 with classification, masking rule, encryption posture, and authorised readers. A schema that ships users.email without a § 8 row is a privacy bug. The validation phase greps for common sensitive column names (email, phone, ssn, tax_id, card, dob, password, token, api_key, secret, passport, iban, access_key) and any hit without a § 8 row fails the checklist.
Once a migration is shipped (merged and applied to any long-lived environment), it is never edited, never renamed, never reordered. A schema change after ship is always a new migration. This rule is a hard constraint — violating it corrupts every environment that already ran the original. State the convention explicitly in § 6.
§ 2 carries a Mermaid erDiagram for relational stores or a collection-reference diagram for document stores. Every entity in the diagram appears as a table in § 3; every table in § 3 appears in the diagram. Silent divergence between diagram and schema is a bug — regenerating the diagram is cheap; keep it in sync.
Citations use stable IDs (UC-NN, INV-NN, EVT-name, ADR-NN, THREAT-NN) or exact component / context names — never line numbers or quoted prose. Table names, column names, and index names are cited verbatim (match the casing and underscoring in § 3).
This document owns schema and access patterns, not wire formats. A column named payload_json is a schema fact; a paragraph describing how payload_json serialises over HTTP and which JSON fields it contains is a wire-format design — forbidden, owned by INTERFACES.md. When transformation happens between DB columns and wire fields, the table's description may note "serialised to INTERFACES.md §N shape" without restating the shape.
Forbidden vocabulary: page, button, screen, tab, modal, click, tap, dashboard, navigate. Those belong in IA skills. Tables persist aggregates, not UI surfaces. If the schema has a legitimate UI-state table (user preferences, saved layouts), it still persists an aggregate named in DOMAIN and is described in aggregate terms here.
This document names retention windows, sensitive-data masking rules, and safety attributes on migrations. It does not contain backup scripts, restore drills, migration deploy procedures, on-call paging, or capacity planning. Those live in OPERATIONS.md. When drafting creeps into runbook territory ("operator logs into the bastion, runs pg_dump…"), stop and replace with "detail in OPERATIONS.md".
In § 5 Access Patterns, the columns Frequency, Expected rows, Cost class take exactly one value each from their allowed enum. No prose, no qualifications, no footnotes inline. Nuance lives in a row-trailing note. Enum discipline makes the table readable at scale.
One YAML frontmatter block at the top containing common fields (skill, date, status) and data-specific fields (datastore, tables_or_collections, indexes, access_patterns, migrations_listed, pii_columns, open_questions). Never emit a second YAML block. Counts match the body exactly.
No "appropriate", "relevant", "as needed", "etc.", "various", "and so on", "many", "some", "a few". Use exact table names, exact column names, exact index names, exact invariant IDs, exact retention durations. Unresolvable ambiguity (normalised vs embedded JSON for a particular aggregate, single-table-inheritance vs class-per-table) surfaces in § 12 Open Questions with options, tradeoffs, and a recommendation.
Target 500–1500 lines. Hard cap 2000. When schema sprawls past 2000 lines, split by schema area: DATA.md becomes an index file referencing DATA_identity.md, DATA_repos.md, DATA_billing.md — each a self-contained sub-document following the same structure, each scoped to one schema area. Cross-schema-area foreign keys are called out in the index file. Do not split by artifact type (one file for schema, one for indexes, one for migrations) — read patterns stay by schema area.
markdown--- skill: DATA.md date: {YYYY-MM-DD} status: {complete | has_open_questions | blocked} datastore: {postgres | mysql | dynamodb | mongo | sqlite | redis | s3 | elasticsearch | cassandra | other} tables_or_collections: {N} indexes: {N} access_patterns: {N} migrations_listed: {N} pii_columns: {N} open_questions: {N} --- # DATA — {ProductName} > Persistent-data layer. Schema, indexes, access patterns, migrations, > retention, sensitive-data inventory, consistency and concurrency. Every > table maps to a DOMAIN aggregate; every index is justified by ≥ 1 query > in § 5 Access Patterns. Wire formats live in INTERFACES.md; backup and > restore automation in OPERATIONS.md; the role-who-can-read matrix in > SECURITY.md. ## § 1. Datastore Overview ### `{datastore-kind}` (primary) - **Kind / version:** `{kind}` `{version-if-load-bearing}` - **Chosen in:** `ADR-{NN}` — {one-line rationale from ARCHITECTURE.md} - **Connection model:** {pooled / per-request / serverless} - **Transactional semantics:** {single-row atomic / single-shard / multi-shard with 2PC / none} - **Consistency guarantees provided:** {read-after-write / snapshot isolation / eventual / tunable-quorum — one line naming the default and any override pattern} (Repeat sub-block for every distinct datastore the system uses. Secondary stores — cache, blob, search index — are briefly declared here with the detail deferred to § 10 Non-Relational Side.) --- ## § 2. ER Diagram
erDiagram REPOS ||--o{ BRANCHES : contains REPOS }o--|| USERS : "owned by" USERS ||--o{ SESSIONS : holds REPOS { bigint id PK bigint owner_id FK string name timestamp created_at }
{For document stores, replace with a collection-reference diagram showing
collections as nodes and references as edges. For mixed stores, one
diagram per store with a preamble naming which store the diagram covers.}
---
## § 3. Schema
### `{table_name}`
**Represents:** `{Aggregate or Entity from DOMAIN.md}`
**Owned by:** `{BoundedContext from DOMAIN.md § 2}`
**Invariants enforced at the DB level:** `INV-{NN}`, `INV-{NN}`
| Column | Type | Nullable | Default | Description | Invariant ref |
|--------|------|----------|---------|-------------|---------------|
| `id` | `bigint` | `no` | auto | internal identity | `INV-{NN}` |
| `owner_id` | `bigint` | `no` | — | owning user | `INV-{NN}` |
| `name` | `text` | `no` | — | repository name, case-preserved | `INV-{NN}` |
| `created_at` | `timestamptz` | `no` | `now()` | creation instant | — |
**Constraints:**
- **Primary key:** `(id)`
- **Unique:** `(owner_id, name)` → `INV-{NN}`
- **Check:** `length(name) BETWEEN 1 AND 100` → `INV-{NN}`
- **Foreign keys:** `owner_id → users(id) ON DELETE CASCADE`
**Triggers:** (if any — name, purpose, and effect. Otherwise: "No triggers.")
(Repeat for every table / collection. Group by bounded context when that
improves readability; otherwise alphabetical.)
---
## § 4. Indexes
| Name | Table | Columns | Type | Purpose / rationale | Cardinality / selectivity |
|------|-------|---------|------|---------------------|---------------------------|
| `pk_repos` | `repos` | `(id)` | btree | primary key; serves § 5 row "Find repo by id" | unique per row |
| `uq_repos_owner_name` | `repos` | `(owner_id, name)` | btree unique | enforces `INV-{NN}` + serves § 5 row "Find repo by owner+name" | unique per row |
| `idx_repos_owner_created` | `repos` | `(owner_id, created_at DESC)` | btree composite | serves § 5 row "List repos by owner ordered by created_at DESC" | high selectivity on owner_id; low on created_at |
(Repeat for every index. Every row must cite ≥ 1 § 5 access pattern in the
Purpose column — no orphan indexes.)
---
## § 5. Access Patterns
**Critical section.** Every query the system runs maps to the index that serves it.
| Query (prose or SQL sketch) | Frequency | Expected rows | Uses index | Cost class |
|-----------------------------|-----------|---------------|------------|------------|
| "Find repo by owner+name" — `SELECT * FROM repos WHERE owner_id = $1 AND name = $2` | per-request | 1 | `uq_repos_owner_name` | O(1) |
| "List repos by owner ordered by created_at DESC" | frequent | ≤ 100 | `idx_repos_owner_created` | O(log n) |
| "Admin: count orphaned repos (owner deleted)" | rare | full-scan | (table scan) | O(n) |
**Query origin footnotes** (when refining an existing system):
- "Find repo by owner+name" — `src/repositories/repos_repository.go::FindByOwnerName`
- "List repos by owner ordered by created_at DESC" — `src/repositories/repos_repository.go::ListByOwner`
- "Admin: count orphaned repos" — `scripts/audit/orphan_repos.sql` (ad hoc; admin-only)
(Every query the system actually runs appears here. No speculative queries.
Every row has a non-empty **Uses index** or `(table scan)` with a trailing
justification. Every index in § 4 appears as **Uses index** in ≥ 1 row.)
---
## § 6. Migrations
**Convention:** `{YYYYMMDDHHMMSS_description.sql}` applied by `{tool — e.g., golang-migrate, Flyway, Alembic, ActiveRecord, Knex}` against `{env-ordering — dev → staging → prod with 24 h soak between staging and prod}`. Append-only: shipped migrations are never edited or reordered.
| # | Filename | What it changes | Safety | Backfill | Ordering |
|---|----------|-----------------|--------|----------|----------|
| 1 | `20240815120000_initial_schema.sql` | creates `users`, `repos`, `branches`; initial indexes | reversible: yes; zero-downtime: n/a (initial); lock: none | none | — |
| 2 | `20240820140000_add_repos_visibility.sql` | adds `repos.visibility` column, default `'private'`; backfills via column default | reversible: yes; zero-downtime: yes; lock: access-exclusive (~50 ms for `ADD COLUMN` with default); | inline, transactional | after #1 |
| 3 | `20240901090000_index_repos_owner_created.sql` | adds `idx_repos_owner_created` | reversible: yes; zero-downtime: yes (CONCURRENTLY); lock: row-share | none | after #1 |
(Repeat for every migration in order. Initial schema migration is always #1.)
---
## § 7. Retention & Deletion
### `{table_name}`
- **Retention period:** {calendar duration — `180 days`, `7 years`, `indefinite`, `until aggregate deletion`}
- **Deletion strategy:** {hard delete / soft delete / archive-then-delete}
- **GDPR / regulatory:** {`right-to-erasure: hard delete within 30 days of request` / `no PII; not in GDPR scope` / cite regulation by name}
- **Backup retention:** {`daily backups retained 30 days; PITR window 7 days` — detail in OPERATIONS.md}
(Repeat for every table. Per-column retention overrides are noted inline
where they differ from the table default.)
---
## § 8. Sensitive Data Inventory
| Table.column | Classification | Masking in logs | Encryption at rest | Who can read |
|--------------|---------------|-----------------|--------------------|--------------|
| `users.email` | PII | `[REDACTED]` | native | service-self-only (per SECURITY.md role matrix) |
| `users.password_hash` | secret | `[REDACTED]` | application-layer (Argon2id) | service-self-only |
| `payments.card_last4` | financial | `last-4-only` | column-level | operator-with-support-role |
| `audit_log.ip_address` | PII | `truncated-to-/24` | native | auditor-role |
(Repeat for every sensitive column. If none: "No sensitive columns in this
schema." — and the validation grep must pass.)
---
## § 9. Consistency & Concurrency
- **Default isolation level:** `{READ COMMITTED | REPEATABLE READ | SERIALIZABLE}`
- **Per-transaction overrides:** {list each override with the operation it applies to — "`REPEATABLE READ` for Push acceptance; read skew on the repo state between check and commit would violate `INV-{NN}`"}
- **Optimistic vs pessimistic locking per aggregate:**
- `{AggregateName}`: optimistic via `{column-name}` (e.g., `version`, `parent_sha`); writes conflict when observed value differs — failure surface: `{ErrorCode from ERRORS.md}`
- `{AggregateName}`: pessimistic via `SELECT FOR UPDATE` on the root row — rationale: {why optimistic would not work here}
- **Serial / version columns:** list each with its monotonicity guarantee and its invariant ref
- **Tombstones / soft deletes:** mechanism (`deleted_at timestamptz`), filter enforcement ("every query through the repository layer applies `WHERE deleted_at IS NULL`; raw SQL outside the repository layer is an anti-pattern guarded by review")
---
## § 10. Non-Relational Side
### Cache (`{kind — redis | memcached | in-memory}`)
- **Key patterns:** `cache:repo:{owner}/{name}` (TTL `5 min`; invalidated on any write to `repos` where `(owner_id, name)` matches)
- **Durability:** ephemeral — full rebuild from relational store is acceptable
### Blob storage (`{kind — s3 | gcs | fs}`)
- **Paths:** `s3://{bucket}/repo/{id}/packs/{pack-sha}.pack` — content-addressed; immutable once written
- **Retention:** packs retained indefinitely; compaction merges old packs into new ones; old packs garbage-collected 7 days after dereference
- **Access:** service-role-only; signed URLs for Web Client downloads (signed by API Server)
### Queues / durable state (`{kind — sqs | kafka | rabbit | postgres-pg-boss}`)
- **Queue names:** `q.sync_jobs`, `q.sync_jobs.dlq`
- **Durability:** durable — message loss results in deferred work or retry-exhausted; dead-letter after 3 failed redelivers
(Include every persistent asset outside the primary schema. If none:
"No non-relational persistent state.")
---
## § 11. Relationship to Other Artifacts
- **DOMAIN.md** — every table in § 3 has **Represents** pointing to an aggregate (§ 5) or entity (§ 4); every DB-enforceable `INV-NN` is cited in § 3 and appears in DOMAIN.md § 9.
- **ARCHITECTURE.md** — the datastore in § 1 is the `ADR-{NN}` choice; component responsibilities that mention "persists to X" resolve to tables here.
- **INTERFACES.md** — wire shapes diverge from DB columns when transformation happens; this document stops at schema. Cross-reference: "serialised to INTERFACES.md §N shape for `EP-{name}`".
- **OPERATIONS.md** — backup automation, restore drills, migration deploy runbooks, capacity planning; this document names the retention window, OPERATIONS owns the execution.
- **SECURITY.md** — role-who-can-read matrix and threat model; this document records which columns are sensitive, SECURITY owns the authorisation rules.
- **QUALITY.md** — query-latency SLOs, observability dashboards; this document stops at cost class.
- **ERRORS.md** — optimistic-concurrency conflicts surface as codes named in § 9; cite `ERROR_CODE` strings from ERRORS.md.
- **SPECs** — draw typing, constraints, and indexes from § 3 and § 4; never invent columns or indexes in a unit spec.
---
## § 12. Open Questions
- [ ] {Question — e.g., "Should `repos.visibility` live as a `text CHECK` or as a separate `visibilities` lookup table? `text CHECK` is simpler and covers the current three values; lookup table adds referential integrity at the cost of one join per read."}
- **Option A:** `text CHECK (visibility IN ('public', 'private', 'internal'))` — simpler; requires a migration to add a new visibility level; application reads never join
- **Option B:** `visibility_id → visibilities(id)` — integrity enforced at the reference; adding a level is a data-migration; reads carry a join
- **Recommendation:** Option A for v1 — the value set is small and stable; reconsider if the visibility taxonomy grows beyond five levels
(If none: "All questions resolved.")/operations/security (this document records which columns are sensitive; SECURITY owns who-can-read rules)/architecture and /spec/interfaces/quality and /operations/behavior/errors (this document cites codes by string where concurrency failures surface)/domain/use-cases/SPEC.md through /RECONCILIATION.md) under units/<area>/u<NN>/skill, date, status, datastore, tables_or_collections, indexes, access_patterns, migrations_listed, pii_columns, open_questions)datastore: frontmatter and cites the originating ADR-NN from ARCHITECTURE.mderDiagram (relational) or collection-reference diagram (document store) with every persisted aggregate representedNullable, Default, and Description for every columnINV-NN from DOMAIN.md § 9 is enforced via a constraint in § 3 and cited in the table's Invariants enforced at the DB level field(table scan) with inline justificationper-request / frequent / periodic / rare), Expected rows (enum from 1 / ≤ 10 / ≤ 100 / ≤ 10³ / ≤ 10⁶ / full-scan), and Cost class (O(1) / O(log n) / O(n) / O(n log n))per-request or frequent row in § 5 has cost class O(1) or O(log n) (otherwise flagged as an open question)(append-only) convention is stated in the § 6 preambleemail, phone, ssn, tax_id, card, dob, password, token, api_key, secret) — every hit in the schema appears in § 8INV-NN, UC-NN, EVT-name, ADR-NN, THREAT-NN) or exact table / column / index names — never line numbers or quoted proseJSON body, protobuf field, HTTP status, endpoint path, WebSocket frame) — columns may be named (payload_json text) without the document narrating wire specificspage, button, screen, tab, modal, click, tap)pg_dump, restore procedure, on-call, paging, capacity plan) — retention and safety attributes are in scope; scripts and runbooks are nottables_or_collections, indexes, access_patterns, migrations_listed, pii_columns, open_questions) match the body exactlystatus is complete if § 12 is "All questions resolved." and has_open_questions otherwiseDATA_<area>.md files with an indexOther measured skills in the registry, with their headline benchmark lift.