Objects Layer
The objects layer is a unified JSONB shadow table (cinatra.objects) that provides a single queryable surface over every persistent object in Cinatra — regardless of which package stores it or what storage strategy that package uses internally.
Why it exists
Section titled “Why it exists”Cinatra packages own their own persistence: blob-keyed metadata, row-per-entity tables, or relational tables. This works well for per-package operations but makes cross-package queries (e.g. “show all objects created by this user”, “find everything related to this campaign”) expensive without a shared index.
cinatra.objects solves this with a fire-and-forget shadow write on every mutation. The primary store remains authoritative; the objects table is derived state that is always within one write of being current.
Table schema
Section titled “Table schema”CREATE TABLE cinatra.objects ( -- Base columns id text PRIMARY KEY, type text NOT NULL, -- namespaced type id (@scope/pkg:local) parent_id text, -- self-FK; denormalised parent_type below parent_type text, data jsonb NOT NULL, -- full object payload created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), created_by text, -- better-auth user id org_id text, -- organisation scope
-- Provenance and lifecycle source text, -- where the row came from (manual UI, agent run, import, …) agent_id text, -- agent that produced this row, when applicable run_id text, -- specific run id, for agent-produced rows deleted_at timestamptz, -- soft-delete; rows with deleted_at IS NOT NULL are tombstoned classification_confidence real, -- LLM classification confidence (0..1) when type was inferred
-- Versioning and packaging package_version text, -- package version that produced this row agent_spec_version text, -- OAS spec version active at write time canonical_keys text[], -- per-type canonical identity keys (used for dedupe / upsert) external_id text, -- foreign-system identity, if any
-- Derived index sync state graphiti_sync_status text NOT NULL DEFAULT 'synced', -- 'synced' | 'pending' | 'failed' exported_to jsonb NOT NULL DEFAULT '{}' -- per-channel export receipts);ON CONFLICT (id) DO UPDATE is used on every write, so all mutations are idempotent upserts and the table can be backfilled or re-backfilled at any time without producing duplicates.
Object categories and types
Section titled “Object categories and types”Every registered type belongs to one of five categories. All type IDs are namespaced as @cinatra/<package>:<local-id>:
| Category | Types | Notes |
|---|---|---|
| profile | @cinatra/entity-accounts:account, @cinatra/entity-contacts:contact | Long-lived identity records; written by the CRM connectors and entity surfaces. |
| project | @cinatra/campaigns:campaign, @cinatra/campaigns:context, @cinatra/agent-builder:agent-template, @cinatra/lists:list | Long-lived containers that group child objects (a campaign, a list of recipients, a saved agent template). |
| idea | @cinatra/asset-blog:blog-post-idea | Intermediate creative stage produced by the blog agent. |
| content | @cinatra/asset-blog:blog-post, @cinatra/asset-blog:saved-media | Published or publishable outputs. |
| report | @cinatra/campaigns:recipients, @cinatra/objects:object | Agent-produced outputs; objects:object is the fallback type for anything not registered to a more specific one. |
Extensions can register their own object types at startup (see registerObjectType in @cinatra/object-types). The list above describes the first-party types that ship with the platform; an instance with additional installed extensions will see more.
Parent-child hierarchy
Section titled “Parent-child hierarchy”account └── contact
blog-post-idea (idea) └── blog-post (content, optional parent)
saved-media (content, no parent)campaign (project, no parent)campaigns:context (project, no parent)agent-template (project, no parent)list (project, no parent)campaigns:recipients (report, no parent in objects)parent_id / parent_type are nullable. Objects without a registered parent (reports, top-level projects) store NULL.
Dual-write hooks
Section titled “Dual-write hooks”Every package that owns a registered type calls shadowUpsertObject() from src/lib/objects-dual-write.ts on every mutating store operation:
import "server-only";import { upsertObject, type UpsertObjectInput } from "@/lib/objects-store";
export function shadowUpsertObject(input: UpsertObjectInput): void { try { upsertObject(input); // fire-and-forget — not awaited } catch (error) { console.error( `[objects:shadow-write] type=${input.type} id=${input.id ?? "(auto)"} failed:`, error, ); }}Key properties:
- Never throws — a failed shadow write is logged and swallowed; the primary store write is unaffected.
- Fire-and-forget —
upsertObjectis not awaited; latency impact on the calling operation is negligible. - Idempotent —
ON CONFLICT (id) DO UPDATE SET type = EXCLUDED.type, data = EXCLUDED.data, ...so any operation is safe to replay. - Type updates on conflict —
typeis included in the update clause, so an id that transitions between types (e.g. startup → account) is correctly re-typed on the next write.
Hook sites
Section titled “Hook sites”| Store file | Types written |
|---|---|
packages/entity-accounts/src/integration/register-object-types.ts | @cinatra/entity-accounts:account |
packages/entity-contacts/src/integration/register-object-types.ts | @cinatra/entity-contacts:contact |
packages/asset-blog/src/integration/register-object-types.ts | @cinatra/asset-blog:blog-post-idea, @cinatra/asset-blog:blog-post, @cinatra/asset-blog:saved-media |
packages/agents/src/integration/register-object-types.ts | @cinatra/agent-builder:agent-template |
packages/objects/src/integration/register-types.ts | @cinatra/objects:object, @cinatra/campaigns:campaign, @cinatra/campaigns:context, @cinatra/campaigns:recipients |
packages/lists/src/integration/register-object-types.ts | @cinatra/lists:list |
Outputs that agent runs produce — contact discovery batches, generated content drafts, miscellaneous run artifacts — flow through these registered types, typically attached to a @cinatra/lists:list or written as a generic @cinatra/objects:object, rather than each agent registering its own bespoke result type. Per-run provenance (the agent id and run id that produced the row) is stamped on the object via the AsyncLocalStorage-based propagation set up at run start.
Backfill
Section titled “Backfill”One-shot backfill scripts populate the table from existing primary-store data. Safe to re-run on any database at any time.
pnpm backfill:objects # all directly-stored first-party typespnpm backfill:objects:entities # synthesized contacts + accountspnpm verify:objects-counts # side-by-side count comparisonType registration
Section titled “Type registration”Each package registers its object types with objectTypeRegistry from @cinatra/object-types at startup. The registry holds the Zod schema, category, lifecycle rules, renderers, and relation declarations for each type.
import { objectTypeRegistry } from "@cinatra/object-types";
objectTypeRegistry.register({ type: "@cinatra/asset-blog:blog-post", category: "content", schema: blogPostSchema, lifecycle: { sources: ["agent", "user"], mutableBy: ["agent", "user"] }, renderers: { listRow: BlogPostListRow, card: BlogPostCard, detail: BlogPostDetail },});Type IDs use the @scope/package:local-id namespace format enforced by OBJECT_TYPE_NAMESPACE_RE.
Querying the objects table
Section titled “Querying the objects table”From server components
Section titled “From server components”Use readAllObjects() from src/lib/objects-store.ts to query across all or a filtered subset of types. Pair it with registerAllObjectTypes() from src/lib/register-all-object-types.ts to ensure the registry is populated before resolving renderers.
import { readAllObjects } from "@/lib/objects-store";import { registerAllObjectTypes, ASSET_TYPE_IDS } from "@/lib/register-all-object-types";import { objectTypeRegistry } from "@cinatra/object-types";import { hasReactRenderers } from "@cinatra/object-types/renderer-types";
// Ensure all registered types are loaded (idempotent)registerAllObjectTypes();
// Fetch — optionally filter by type IDsconst objects = readAllObjects({ typeIds: Array.from(ASSET_TYPE_IDS) });
// Render via each type's listRow rendererobjects.map((obj) => { const def = objectTypeRegistry.resolve(obj.type); if (!def || !hasReactRenderers(def)) return null; const ListRow = def.renderers.listRow; return <ListRow value={obj.data as Record<string, unknown>} />;});readAllObjects is limited to 200 rows per call. Use the typeIds filter to stay within relevant scope.
Family helper sets
Section titled “Family helper sets”src/lib/register-all-object-types.ts exports two pre-built sets for filtering by UI family:
| Export | Contents |
|---|---|
ASSET_TYPE_IDS | The set of asset-family type IDs used for ?family=assets filtering (blog post ideas, blog posts, saved media) |
ENTITY_TYPE_IDS | The set of registered entity-family type IDs used for ?family=entities filtering (contacts, accounts, campaigns, agent templates, lists) |
OBJECT_TYPE_NEW_URLS | Creation route per type (used by /objects/new chooser) |
The unified workspace surface lives in the sidebar’s Information → Data group.
/data— unified list page showing all objects across registered types, filterable by?family=assets,?family=entities, or?type=<typeId>. Each row is rendered via the type’s registeredlistRowrenderer./data/types— type chooser browse view./data/new— type chooser linking each registered type to its creation route./assets,/entities/accounts,/entities/contacts, etc. remain reachable as typed entry points; they read from the same underlyingobjectstable and surface only their family./administration/objects(admin-only) — operational view used for cross-org diagnostics and type-registry inspection.
All sub-routes (detail, edit, new) under typed paths are unchanged — they read and write through the same Objects layer.
Related
Section titled “Related”- Architecture — full platform architecture
src/lib/objects-store.ts—upsertObject,readAllObjects,readObjectsByType,ObjectRecordsrc/lib/objects-dual-write.ts—shadowUpsertObjecthelpersrc/lib/register-all-object-types.ts—registerAllObjectTypes, family sets, creation URL mappackages/object-types/src/— registry, type definitions, category taxonomy