Skip to content

The data model

Everything in the store is built from one idea, so it is worth understanding before any individual table makes sense. This page assumes you know what a scope set is and can read SQL.

Two people in different organizations can learn the same fact. The sentence is identical, but who may read it, when it became true, and who asserted it are all different. Storing the sentence twice duplicates text and breaks deduplication. Storing it once with a merged permission set leaks it.

aizk splits every graph row in two.

Content is the immutable thing itself, the entity or the statement. Its primary key is a UUID5 derived from its own normalized text, so the same statement always lands on the same row no matter who writes it or how many times. Content carries no scope and no time.

A claim is somebody asserting that content inside a scope set, over a period. It carries the scopes, the two time ranges, the author, the source chunk it came from, and the access counters. Its primary key is a UUID7, so claims are ordered by creation.

Diagram

One statement, many claims. Your organization’s assertion and mine point at the same immutable sentence, and neither can see the other’s claim.

It gives four things at once.

Deduplication is free, because identical text collides on the deterministic ID rather than being compared. Corrections are append-only, because closing a claim’s time range leaves the content untouched and the history intact. Scope isolation is exact, because the scoped thing is the claim and a reader who cannot see any claim cannot see the content either. And the embedding is computed once per statement rather than once per organization that happens to believe it.

Content tables have no scopes column, so their policy is defined in terms of the claims that point at them. ClaimedContent.__rls__ in src/aizk/store/mixins/claimed.py emits exactly two policies.

rls.Policy.select("content_read", content.id.in_(select(claims.content_id)))
rls.Policy.insert("content_insert", sa.true())

Reading a piece of content requires a visible claim on it. Inserting is always allowed, because a content row on its own reveals nothing until a claim you can see exists, and because two callers racing to write the same statement must both succeed.

That race is real and expected, since the ID is deterministic. mint() wraps the insert in a SAVEPOINT and swallows only SQLSTATE 23505, so a duplicate returns False instead of poisoning the surrounding transaction. mint_all() tries the batch first and falls back to row-by-row only when the batch collides.

Tables are assembled from mixins in src/aizk/store/mixins/ rather than repeating columns.

Mixin What it adds
Id id as UUID7, ordered by creation
DeterministicId id as UUID5, derived from content
Timestamped created_at and updated_at
Scoped created_by, scopes uuid[], and the row policies
ClaimedContent the mint pattern and the content policies above
Embedded embedding as halfvec(1024) plus its vector index
ViewBase a security-invoker view rather than a table

Scoped is the one that matters most. It emits scope_read and scope_insert always, plus scope_update when the table is declared mutable and scope_delete when it is deletable, so mutability is a property of the model rather than something a policy file has to remember.

It also supports read_through, which makes a child table inherit its parent’s visibility. A chunk is visible when its document is, and the child’s scopes must match the parent’s, so a chunk cannot be quietly widened away from the document it belongs to.

Some rows are things people wrote and some are things the engine worked out. The distinction is not enforced by a column, it is a rule about who may delete what.

Sources are document, chunk, artifact, artifact_content and blob. They are the record.

Projections are community, profile, and the RAPTOR summary entities. They are rebuildable, a pass may replace them wholesale, and losing them costs compute rather than knowledge. Facts sit in between, since they are derived from sources but carry their own history and are never deleted, only closed.