The data model
Everything in Taisce's memory namespace is either source material, a receipt of an outcome, or a projection derived from those and rebuilt from what survives. This page walks through each area of the schema as the latest migration leaves it, and explains why each relationship is shaped the way it is.
You'll learn:
- the three kinds of row, and how erasure finds every derived row;
- how observations, messages and chunks are stored;
- how entities, facts and their two time ranges work;
- how receipts, generations and embeddings are laid out;
- how refusals, retractions, corrections and feedback are recorded;
- what the control namespace holds, and how Go types map to tables.
The generated schema reference in the site's Reference section lists every column, constraint and index; this page names only the ones an explanation needs. Read the PostgreSQL overview first for how the two namespaces fit together, and the write path for how rows arrive.
One authority, and three kinds of row
Everything in the memory namespace answers to one table: observation, with each turn's messages in
turn_message. It is the log of what people said, and every erasure starts there.
The principle, set out in 0001: other tables are derived from the log and can be derived again. That makes "rebuild, never repair" safe. After an erasure, the next rebuild reads only what survived, so nothing erased can come back through it.
The schema holds three kinds of row, and it helps to tell them apart:
| Kind of row | What it is | Tables |
|---|---|---|
| Source | What somebody said or supplied. Authoritative, and never rebuilt. | observation, turn_message, curated_claim, record_retraction, agent_artifact, memory_feedback |
| Source-owned receipt | A record of an outcome that cannot be recomputed exactly from the words: what a model admitted, when it was learned, which extractor produced it. Owned by its observation through a cascading foreign key, and deliberately not tied to the derived row it describes, so it survives losing that row. | fact_receipt, fact_receipt_history, entity_name_receipt, source_extraction, fact_generation, fact_generation_record |
| Projection | Derived from the two kinds above, registered for erasure, and rebuilt from what survives. | chunk, entity, fact, fact_evidence, fact_history, rejected_claim, community, community_member, community_report, segment, and the three embedding tables |
Receipts exist because re-deriving from the words alone is not always faithful. A model does not return the same claims twice, and the moment a fact entered memory is not in the message. 0034 therefore records admitted outcomes beside their source, and treats losing one as losing authoritative data. Recovery reads receipts. A new interpretation under a different extractor is a rebuild, which publishes a new generation rather than editing the old one.
Around these sit tables that are neither source nor derived. They hold state about the system: the watermark, backlog accounting, the audit ledger, erasure receipts, embedding generation catalogs, rebuild jobs, notification deliveries, the project row and the data subject registry. Each one's migration says why it holds nothing an erasure needs to reach, or how an erasure reaches it.
The projection registry
Every projection writes a registration row in projection_dependency, in the same transaction as the
projection itself. The row names the source observation, the projection's kind and its id. (The
diagram shows five of the twelve typed references; the other seven follow the same pattern.)
Writing the registration in the same transaction matters. A projection registered afterwards would be invisible to erasure until the gap closed, and that gap is exactly when a process might die (observationstore.go, factstore.go).
The kind must be declared in projection_kind
(0007). That is a
foreign key, so an undeclared kind cannot be registered at all. The failure lands on whoever adds the
projection, on the day they add it, instead of on a later erasure that quietly misses it.
projection_kind also names the table and id column the eraser deletes from. Those names are
interpolated into a DELETE, so a CHECK restricts them to lowercase identifiers where they are
written.
survives_sharing (0019)
answers one question per kind: when a row is registered by two people and one of them is erased, is
the row kept? An entity is a shared identity, so it is kept. A report is shared text, so it goes. The
default is false, so a kind added by someone who never read the migration deletes too much rather
than leaking.
Typed references. Since 0023
each kind has a generated, typed reference column (entity_ref, fact_ref, and so on through
feedback_ref):
dependency_one_target_chkrequires exactly one of them per row;- each carries a deferred foreign key to its real target in the same project;
- so a registration cannot name a row that does not exist, and deleting a target cascades its registrations.
Adding a kind therefore takes a relationship migration as well as a catalog row.
| Kind | Table | Registered to | survives_sharing | Migration |
|---|---|---|---|---|
chunk | chunk | the message's observation | true | 0007 |
fact | fact | the source observation | true | 0007 |
entity | entity | every observation that named it | true | 0007 |
rejected_claim | rejected_claim | the source observation | true | 0007 |
community_report | community_report | every contributing observation, at its current revision | false | 0020, 0031 |
fact_history | fact_history | the fact's sources and the observation that caused the transition | false | 0029 |
agent_artifact | agent_artifact | its owning artifact observation | false | 0037 |
message_embedding | message_embedding | the message's observation | false | 0046 |
entity_embedding | entity_embedding | every contributing observation | false | 0049 |
report_embedding | report_embedding | every observation under the report | false | 0050 |
segment | segment | every covered observation | false | 0055 |
feedback | memory_feedback | each observation supporting the record it is about | false | 0061 |
Notes on the table:
chunk,factandrejected_claimare each registered to exactly one observation, so theirsurvives_sharingsetting never comes into play.- Two source tables,
agent_artifactandmemory_feedback, are registered too. They cannot be re-derived; they are registered because the registry is how an erasure finds rows. fact_evidenceandcommunity_memberare not kinds of their own. Evidence goes with its fact by cascade, and membership goes with its community.- Erasure also deletes an erased source's evidence rows directly, even when the fact they support is kept because another registration still supports it (erasurestore.go).
Source
The observation and its messages
An observation is one source:
- a conversation turn (
kind = 'turn'); - a human-authored correction or assertion (
kind = 'curated', set by recordassertion.go); - an agent artifact (
kind = 'artifact', see Governance).
It carries:
- the project (
scope); - a
log_offsetthat is contiguous within the project (observation_scope_offset_uniq); - when the turn happened (
occurred_at), separate from when it was stored (ingested_at); - who spoke first (
source_role) and the data subject it is about (data_subject_id); - a
retention_untildeadline, stamped at append time from the project's policy. A fact carries one too, taken from the latest deadline among the observations supporting it, and null when one of them is kept indefinitely: that is what lets a read refuse expired memory with a column test (0068).
Governance travels on the row rather than in a side table, because every read filters on it and a join is one more thing to forget.
Formation bookkeeping lives on the same row (0005, 0009):
formed_atis set once, when extraction has run;formation_attempts,formation_failed_atandformation_error(at most 2,000 characters) record failures;parked_atmarks a turn formation gave up on.
The error is kept on the row, not in a log, because a provider's error can quote the message that was sent to it. On the row it is personal data that erasure takes with the observation. In a log it would be personal data no erasure can reach.
fact_revision (0031) is a
counter that a trigger advances whenever a fact or evidence row derived from this observation changes.
Reports use it to prove they were written from the current state of their sources (see
Communities and their reports).
turn_message holds a turn's messages as rows rather than as a JSON document
(0002). The role policy is
enforced per message. A role inside a JSON array is a convention every reader has to parse the same
way; as a column it is NOT NULL with a CHECK.
ordinalpreserves the caller's order.group_ordinalmarks the members of an atomic unit, such as an assistant tool call and its results, so compaction never splits one.- Since 0044 the message
owns its
chunk_id, and 0048 makes that id unique, so a saved message reference resolves by UUID alone.
Chunks: one per message, partitioned by project
A chunk is a message stored as retrievable text, exactly one per message. It is the only table
partitioned by project: PARTITION BY LIST (scope), with one partition per project, created by
ProvisionScope (provision.go).
- The default partition
chunk_unpartitionedcatches writes for a project whose partition was never created. Losing memory to a missed provisioning step would be worse than a slow query, and a row in the default partition is still visible to erasure. - A composite foreign key to
turn_message (observation_id, ordinal, chunk_id)keeps a chunk's identity bound to its message. - A chunk has no vector column. Vectors live in model generations (see Derived).
Indexing and query plans covers what the partitioning buys.
Retries: one receipt per key
observation_retry (0021)
lets a caller retry a write without creating a second observation.
- The key is a UUID. Only its SHA-256 digest is stored, scoped to the project.
request_digestfingerprints the normalized request, so a retry with a different payload under the same key is refused.- When the observation is deleted, the trigger
observation_retry_forgets_payloadclears the fingerprint and the observation reference, and leaves the key digest as a tombstone. An old queued retry therefore cannot write erased words back.
Concurrency covers how two racing retries still produce one receipt.
Backlog reservations
ingestion_budget (a single row), ingestion_project_usage and ingestion_reservation
(0025) count
unformed turns.
- The default ceilings are 4,096 for the instance and 512 per project.
- Each unformed turn holds one reservation, parked turns included.
- Deferred constraint triggers take a reservation when a turn is stored, and release it when the turn is formed or deleted, in the same transaction.
- The runtime role can read these tables but not change them (planes.go, roles and grants).
The watermark: two numbers per project
watermark holds one row per project
(0005):
log_offsetis the highest offset stored.formed_offsetis the highest offset whose turn has formed with nothing unformed below it. It isNULLuntil the first turn forms.watermark_atis the turn's own time, not the time it was stored, so a backfill of last year's conversations does not look current.
The row also serves as the lock that serializes appends to one project;
Concurrency explains why.
formation_health is a single aggregate row for the whole instance
(0027).
Segments
A segment (0055) is
a model-written summary standing in for a contiguous range of one subject's formed turns. Segments roll
up in levels: level 1 summarizes turns, level 2 summarizes level-1 segments, and so on up to level 6.
A segment is a projection, not an observation. It is registered to every observation it covers, so
erasing any one of them removes it, and the compaction pass writes a new one from what survived.
UNIQUE (scope, data_subject_id, level, from_offset) allows one account of each stretch per level.
Knowledge
Entities: two kinds of identity
An entity is a node. There are two ways to identify one:
- Named entities are identified by
(scope, normalized_name), where normalization lowercases, trims and collapses whitespace.entity_typeis an attribute, not part of the identity. If it were part of the key, one thing extracted once as aplaceand once as athingwould become two nodes, each holding half its facts. - Speaker entities (0022).
A first-person reference ("I", "me", "أنا") resolves to a speaker entity identified by
(scope, speaker_subject_id), bound to the stored observation's data subject rather than to a name.
The partial unique indexes entity_named_identity_uniq and entity_speaker_identity_uniq enforce the
two kinds. entity_identity_kind_chk pins a speaker entity's name to speaker.
Two closed lists support this. speaker_term lists the first-person terms. unresolvable_term
(0016) lists pronouns and
demonstratives that can never be a fact's subject.
Spellings. canonical_name is the spelling first seen. Every spelling a source used is kept in
entity_name_receipt (0043),
owned by the observation that used it.
- The receipt deliberately has no foreign key to the entity. If the entity row is lost, the receipts are the only faithful input for rebuilding it.
- Instead, a trigger checks under
FOR KEY SHAREthat every receipt'snormalized_nameequals a live named entity'snormalized_name. - Because every recorded spelling normalizes to the entity's one key, the canonical key alone serves exact anchoring. There is no alias index (0051).
aliasesis a bounded display cache of at most 64 variants, rebuilt from receipts during recovery (entitynames.go).
The closed vocabulary
predicate (0003) holds 39
relations. Each has:
- a
semantic_type; - a
cardinality:onefor 11 of them,manyfor the rest; - an
object_kind; - an
eventflag (0054).
fact.predicate references this table, so no path, not even a hand-written INSERT, can store a
relation outside the set. The runtime role cannot modify the table
(planes.go).
fact.cardinality is a copy of the predicate's cardinality. It exists only because an exclusion
constraint cannot read another table. The composite foreign key (predicate, cardinality)
(0010) makes it
impossible for the copy to disagree with the vocabulary.
Facts are the edges
A fact is one typed, directed edge: a subject entity, a predicate, an object entity, a statement,
and two time ranges. Either end may be NULL when it did not resolve to an entity; traversal simply
does not follow it.
Both ends carry deferred, project-consistent foreign keys to entity (scope, entity_id)
(0023). So a fact cannot
point into another project, and an erasure may delete entities and facts in any order within one
transaction.
Other columns:
source_role: the role of the message that asserted it (0014);confidence: as the extractor reported it;superseded_byandsupersession_source: what replaced it, and which observation caused that (0029);version: a compare-and-swap token that a trigger changes on every update, so a correction or retraction cannot act on a record that changed after it was inspected (0032).
A fact's id is derived, not random. sourceClaimID in
recordretraction.go hashes the project, the source
observation, the message ordinal, and a signature of the predicate and resolved ends. A rebuild hashes
the result again with its generation id. So a retried assertion finds the fact it already wrote
instead of writing a second one. It also means the same claim in two different observations is two
facts, each with its own evidence.
fact_single_cardinality_excl is the invariant the model rests on:
EXCLUDE USING gist (scope WITH =, subject_entity_id WITH =, predicate WITH =,
valid WITH &&, known WITH &&)
WHERE (cardinality = 'one' AND subject_entity_id IS NOT NULL)
A single-cardinality relation cannot hold two values for one subject when both their validity and knowledge intervals overlap. The object is deliberately not in the key: "lives in Dublin" and "lives in Amman" overlapping is exactly the state being refused.
known is in the key so that a withdrawn claim, whose knowledge interval is closed, does not conflict
with a later assertion that overlaps it in valid time.
Concurrency covers how concurrent writers meet
the constraint.
Valid time and transaction time
validis when the world was that way.knownis when this system believed it.
Both are tstzrange columns that are never empty (fact_valid_not_empty_chk,
fact_known_not_empty_chk), and an open upper bound means "still". A range with an open end needs no
sentinel value and no OR in every predicate, and it can lead an index.
The current read and the as-of read differ in one predicate:
-- what is true now, as best we know now
upper_inf(f.valid) AND upper_inf(f.known)
-- what was true at T1, as this system knew it at T2: the current row where its known range
-- contains T2, otherwise the archived interval in fact_history that does
state.valid @> $t1 -- with state drawn from f.known @> $t2 or fact_history.known @> $t2
Supersession closes the old fact's valid range at the new fact's start, so the two intervals
meet exactly and any instant has one answer. It does not rewrite what was believed earlier:
- The previous
(valid, known)pair is archived infact_history, with its knowledge interval closed at the moment of the transition. The retained row's knowledge starts again at that moment. - The exclusion constraint
EXCLUDE (fact_id WITH =, known WITH &&)onfact_historykeeps one fact's archived beliefs from overlapping. - The knowledge timestamp comes from the database clock, advanced at least one microsecond past the knowledge start it supersedes. A clock that steps backwards therefore cannot produce an empty or reversed range.
A retraction closes known and leaves valid alone. It records that the system stopped believing
the claim, without inventing when the claim stopped being true.
Events. Predicates flagged event
(0054) are
accepted in the past tense, because an event is only ever reported after it happened and stays true
afterwards.
Evidence
fact_evidence is a fact's receipt: the verbatim quote, plus the byte span (byte_start,
byte_end) in the exact message it came from.
source_ordinal is part of the key
(0006). A byte offset
taken against a different message of the same turn would not fail; it would resolve to a plausible
fragment of the wrong sentence. A foreign key to turn_message (observation_id, ordinal) makes the
message mandatory.
Recall joins evidence with an inner join, so a fact without evidence is never returned (recallstore.go).
Receipts, pins and generations
source_extraction(0040) pins the extractor identity for a source before the model is called, and a trigger makes the pin immutable. A retry after a crash therefore cannot mix claims from two extractor configurations in one source.fact_receiptandfact_receipt_history(0034) keep the admitted state of each fact and its earlier intervals. Triggers keep them in step withfactandfact_history. They have no foreign key tofact, because they exist to outlive a lost fact row.fact_generationandfact_generation_record(0041) record each committed rebuild of a source: which facts it admitted, which it retired, and the operation key that makes a replay idempotent. Rows exist only for committed cutovers.
Derived
Embeddings live in model generations
A vector only means something within one model's space, so vectors are stored per generation (0046).
The generation catalog:
- An
embedding_generationrow binds a model name, an immutable revision, a digest of the endpoint, an input contract (input_version), a dimension between 1 and 4,000, and the cosine metric to a finite target: the log up tothrough_offset. A trigger refuses any update to that identity. embedding_buildtracks the worker's progress, and keeps the target separate from the coverage actually completed (0047).embedding_activenames the one generation a project searches.- The runtime role cannot write the generation or activation catalogs. Starting and activating a generation are operator operations.
The vectors. message_embedding is partitioned by generation_id. Each generation gets its own
child table and its own HNSW index, created in the same transaction that creates the generation
(embeddinggeneration.go).
- The column type is plain
vector. A generateddimensionscolumn carries a foreign key to the generation's declared dimension, so the database refuses a vector of the wrong size. CHECK (vector_norm(embedding) > 0)refuses the zero vector, for which cosine is undefined.input_digestis the SHA-256 of the message text the vector was computed from. Search compares it with the current message and drops the vector if they differ.
A project keeps at most two generations (MaxRetainedEmbeddingGenerations in
embeddingtypes.go). A model change builds a new generation
beside the old one, activates it with a one-row update, and retires the old child by detaching and
dropping it.
Entity and report vectors (0049, 0050) repeat the same shape, with separate generation, build and active tables. Equal dimensions and model names do not make two input contracts one space. Each adds two tables:
- A
*_targettable fixes, when the generation starts, the finite set of entities or reports it covers. - A
*_sourcetable holds one row per contributing observation, because an entity or report vector combines many sources and every one of them owns it.
Triggers delete a vector when any of its inputs changes. They also mark a completed generation stale
when a new named entity or report appears after its snapshot, so search cannot silently leave the
newcomer out.
Communities and their reports
A community is a group of entities densely connected to each other and sparsely connected to
everything else (0020).
- Level 0 partitions the whole graph. A deeper level exists where a group was too large to be one subject and was split again.
community_parent_chkrequires a parent exactly when the level is above 0.- A grouping depends on the whole graph, and one new fact can move, merge or split groups. So a pass replaces every community in a project rather than editing them in place.
- Communities hold no words and are not registered.
A community_report is prose a model wrote about one community, one per community. It holds
several people's material, so it is registered to every contributing observation with
survives_sharing = false: erasing any contributor deletes it.
Reports also cannot outlive a change to their sources (0031):
- Each registration records
report_source_revision, with a foreign key toobservation (scope, observation_id, fact_revision). - A trigger on
factandfact_evidenceadvances the source's revision and deletes the reports registered to it. - A report describing facts that have since changed therefore cannot stay stored. It is written again when next needed.
written_by (0060) is a digest of
the model, the code and the prompt that wrote the report, so a changed writer is a reason to write it
again.
Governance
Claims about claims: refusals, retractions, corrections and feedback
rejected_claim (0004) keeps every
proposal that did not become a fact, in the model's own words, with a reason from a closed set. There
are eight reasons today:
| Reason | Meaning | Added by |
|---|---|---|
unmapped_relation | The vocabulary cannot represent it | 0004 |
unlocatable_quote | The message cannot cite it | 0004 |
duplicate_claim | This message already said it | 0008 |
not_asserted | The message did not assert it (a denial or a hedge) | 0011 |
unresolvable_subject | The subject names nothing the message establishes | 0016 |
not_current | The message puts a state in the past | 0018 |
conflicting_value | The message already gave the relation a different current value | 0052 |
not_spoken_by_principal | It speaks for the principal from a message the principal did not speak | 0053 |
entity_name_limit | One of its ends carries a name this store cannot keep: longer than the bound, or one spelling too many for one entity | 0066 |
predicate has no foreign key here, because this column holds exactly the relations the vocabulary
refused. A refused claim still quotes somebody, so it is a registered projection like any other.
record_retraction (0032)
is a human instruction to withdraw a fact, stored as source input rather than as an edit.
target_fact_idhas no foreign key, so the instruction survives losing the fact and keeps blocking the same claim when it is re-derived.target_versionis the version the editor saw.
curated_claim (0033)
is a human correction or an authored assertion
(0036). The extractor never
reinterprets it, and its fact_id is a receipt for recovery.
memory_feedback (0061) is a
report that a record looks wrong. It asserts nothing. It is its own table rather than a kind of fact,
so no read path can return it by accident. Promoting it produces an ordinary correction or retraction.
Subjects: three meanings of one word
The migrations use "subject" in three senses:
| Sense | Where | Tables |
|---|---|---|
| The grammatical subject of a claim | 0016 | unresolvable_term, fact.subject_entity_id |
| A theme the graph holds | 0020 | community, community_report |
| The person a record is about | 0039 | data_subject, and data_subject_id on every governed row |
data_subject is an optional registry. It maps a random subject id to an external reference and a
label, with one current mapping per project. Only the random id is ever copied onto observations.
- Triggers make the identity and its lifetime immutable.
- When a new observation names a registered subject, a trigger takes a
FOR KEY SHARElock on that subject's row, so an expiry cannot remove the mapping while a source is being accepted. subject_retrydoes for registrations whatobservation_retrydoes for writes.
Artifacts, retention, rebuild jobs and notifications
- Agent artifacts (0037).
agent_artifactholds opaque bytes, up to 512 KiB each, owned by an observation of kindartifact. That observation is marked formed and has a retention deadline, so formation never reads it, and a trigger keeps it that way.agent_storage_policyholds each project's quota. A security-definer trigger updates usage on every insert, update and delete, and refuses a write that would exceed the quota. - Retention (0038).
A deadline is stamped on each observation when it is written, and
observation_scope_expiry_idxlets a sweep read a bounded page of due sources in deadline order. Shortening a policy affects what arrives next. Removing what is already held is an erasure with a receipt. - Rebuild jobs (0042).
fact_rebuild_jobholds log offsets, an extractor identity, aggregate counts and a lease. It holds no source ids and no text, so erasing a pending source leaves no personal link behind. The partial unique indexone_active_fact_rebuildallows one active job per project. - Notifications (0058).
notification_endpointis a URL and a signing secret.notification_deliveryis a durable attempt, one per endpoint per formed watermark. It carries offsets and counts and nothing from memory, which is why it has nodata_subject_idand no registration. - Erasure receipts.
erasure_request(0001) holds the selector, the reason and the per-kindresidual. ACHECKrequires the residual to be present exactly when the request is complete.
The audit ledger
audit_entry (0013) records who performed which
operation on which project, when, how much it touched (magnitude), and whether it was allowed.
- It holds no data subject, no question, no quote and no content. That is why it can be
unconditionally append-only without conflicting with erasure. Triggers refuse
UPDATEandDELETEfrom any identity. projectis plain text rather than a foreign key, so an operation on a project that no longer exists can still be recorded.operationis checked against a closed list. Every migration that adds a route redefines thatCHECKto admit the route's operation name (0026 through 0061, including the management operations in 0056). A route that tries to record an operation the list does not name has its write refused.
audit_seal (0017) chains digests over
contiguous ranges of entries. Concurrency covers how sealing runs
beside writers.
Control
The control namespace holds one table, credential, plus its own schema_migration. Its migrations
live in internal/migrate/control and advance independently of the
memory namespace's.
- A credential stores the SHA-256 digest of its token, never the token (control 0002). A 256-bit random token leaves nothing to guess, so a slow password hash would add cost to every request and defend against nothing.
- A project credential names exactly one project
(control 0003) and
declares
read_onlyorread_writeaccess (control 0004). - An operator credential reaches the management surface and no project.
credential_project_chkmakes "is an operator credential" and "names no project" the same statement (control 0005).
project lives in the memory namespace, not in control
(0012). Its settings are read on
the memory path, and the memory role has no privilege in control. The registry says who may reach
what; the memory namespace says what a project does.
So the credential's project column has no foreign key. The operator command that issues a credential
validates the name instead, because it holds a connection that can see both namespaces.
The project row itself:
- a trigger lets
surfacesgrow and never shrink; retentionisNULLfor "keep", as a deliberate choice;suspended_atmakes a project unreachable without deleting it;ProvisionScopecreates the row and the project's chunk partition in one transaction (provision.go).
observation.scope has no foreign key to project. A write for a project with no row still lands, in
the default chunk partition with no retention stamp, because losing memory to a missed provisioning
step is the worse failure. Roles and grants covers the identities that reach
each namespace.
Where the Go types meet the tables
The shared vocabulary of the read and write paths is in internal/domain. Tables and types do not map one to one:
| Table | Go type | Note |
|---|---|---|
observation, turn_message | domain.Observation, domain.Turn, domain.Message | Turn is what is written; Observation is the stored receipt |
fact (write) | domain.Claim | carries the quote, span and cardinality to be checked |
fact + fact_evidence (read) | domain.CitedFact, domain.Evidence | a read always returns a fact with its evidence |
entity | domain.Entity, domain.Anchor | an anchor is an entity plus the term that matched it |
predicate | domain.Predicate, domain.Ontology, domain.Vocabulary | loaded from the table, never declared in Go |
rejected_claim | domain.RejectedClaim | reasons are constants beside it |
watermark | domain.Freshness | stored and formed offsets, parked count, rebuild progress |
erasure_request | domain.Erasure | deleted and residual counts per kind |
audit_entry, audit_seal | domain.AuditEntry, domain.AuditSeal, domain.AuditVerification |
Where early migration comments differ
Migration files are never rewritten, so a few early header comments describe a shape that later migrations changed. The schema today:
- One instance is one tenant, and the project is the isolation boundary. 0001 and 0003 describe a tenant as a schema. Control 0001 describes a registry of tenants and payments. Control holds credentials only.
facthas deferred, project-consistent foreign keys on both ends, added by 0023. The deferral is what lets erasure delete in any order within one transaction.- Not every table other than the log is re-derivable. The source-owned receipts added from 0033 onward are authoritative.
chunkhas no embedding column. Vectors are partitioned by generation, each with its own dimension (0046).entityhas nonormalized_aliasescolumn and no GIN alias index (0051).
Where to go next
- Concurrency: how these constraints and locks hold with several writers.
- Indexing and query plans: which indexes serve which reads, and what is measured.
- Migrations: how the two migration sets are applied.
- Roles and grants: which identity may do what to these tables.
- Governance: erasure, export and retention built on the registry.