_id, _object, _tenant, and _archived are reserved. Every other column is domain data.
Identity
_id is an object id. It is the primary key.
An object id is a lowercase prefix, an underscore, and 21 characters from 0-9A-Za-z.
^[a-z][a-z0-9]*$. The body length is always 21. createId and createObjectRow build this value. parseObjectId accepts only that form.
Object name
_object is the object type. It starts with the API major version and continues with one or more lowercase segments.
^[a-z][a-z0-9]*$. objectTypeFromRoute(2, "/user") produces v2.user. The last segment is the default _id prefix.
Tenant
_tenant is the id of the tenant that owns the row. It is required text and must contain a non-blank value. A tenant id is usually itself an object id, such as tenant_0k3hN9mQ2pLx7aWc4sYbZ.
Archive
_archived is the soft-delete flag. It is a boolean and it is never null.
New rows set _archived to false. When other rows still reference this object, deletion sets _archived to true and leaves the row in place. A later read treats an archived row as deleted.
Domain columns
Columns after the four reserved fields belong to the object. Their names must not start with_. Their types belong to the table.
actor_id, active, and message in the example are domain columns. They are not part of the owned-object contract beyond being stored on the same row.
Table
A table inpublic other than keja_migrations uses this shape. The check functions are installed before migrations run.
keja_is_object_id accepts prefix_body with a 21-character body. keja_is_object_type accepts vN.segment and further segments. keja_is_tenant accepts text that is not blank.
Migrations
Each migration runs inside a transaction. After its SQL, Keja callskeja_owned_object_violations(). A table that misses a reserved column, uses the wrong type, leaves a reserved column nullable, uses a primary key other than _id, or omits a check fails the migration. The transaction rolls back. keja_migrations is the only table that is exempt.
Database calls
DBCall is the database function path.
An insert that passes a row object must pass assertOwnedObject. The row needs a valid _id, _object, _tenant, and boolean _archived. Extra keys must not start with _.
An insert that passes scalar values must name _id, _object, _tenant, and _archived in the SQL. Postgres then applies the same checks.
An update may change domain columns. When the update carries _id, _object, _tenant, or _archived, those values must still match this spec. A select is not an owned-object write.