_id, _object, _tenant, _archived, _createdAt, and _updatedAt are reserved. Every other column is domain data.
Identity
_id is a TypeID. It is the primary key. A TypeID starts with a lowercase prefix and continues with a time-ordered suffix, so new rows append in a Postgres B-tree instead of landing at random leaves.
createTypeId and createObjectRow build this value. parseTypeId 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.
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 a TypeID.
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.
Timestamps
_createdAt and _updatedAt are timestamptz values and they are never null. createObjectRow sets both to the current time. A later write sets _updatedAt to the new time and leaves _createdAt unchanged. _updatedAt is at or after _createdAt.
Quote these names in SQL. Postgres folds unquoted mixed-case identifiers, so the columns are "_createdAt" and "_updatedAt".
Domain columns
Columns after the 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 OwnedObjects 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_typeid accepts prefix_suffix. The suffix is 26 Crockford base32 characters encoding a UUIDv7. 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 TypeID _id, _object, _tenant, boolean _archived, and both timestamps. Extra keys must not start with _.
An insert that passes scalar values must name _id, _object, _tenant, _archived, _createdAt, and _updatedAt in the SQL. Postgres then applies the same checks.
An update may change domain columns. When the update carries a reserved field, that value must still match this spec. A select is not an OwnedObjects write.