Instatic Universal Content Store Architecture: data_tables and data_rows Explained
Instatic stores all content—posts, pages, components, and custom collections—in a unified two-table architecture consisting of data_tables (schema definitions) and data_rows (JSON cell instances), enabling type-safe polymorphic content management via TypeBox schemas.
The CoreBunch/Instatic repository implements a flexible headless CMS architecture through its universal content store. Rather than maintaining separate database tables for each content type, Instatic consolidates every piece of content into a single polymorphic structure defined by two core tables. This design eliminates schema duplication while preserving full type safety across the server, admin UI, and plugin SDK.
The Two-Table Architecture
Instatic’s universal content store separates type definitions from content instances. This separation allows the system to treat posts, pages, components, and user-defined collections identically at the storage layer while enforcing distinct schemas at the application layer.
data_tables – Content Type Definitions
The data_tables table stores the schema for every content type in the system. Each row represents a distinct collection—whether a built-in type like posts or a user-created product catalog.
According to src/core/data/schemas.ts, the DataTableSchema TypeBox definition specifies the structure:
export const DataTableSchema = Type.Object({
id: Type.String(),
name: Type.String(),
slug: Type.String(),
kind: DataTableKindSchema, // postType | data | page | component | layout
singularLabel: Type.String(),
pluralLabel: Type.String(),
routeBase: Type.String(),
primaryFieldId: Type.String(),
fields: Type.Array(DataFieldSchema), // Column definitions
system: Type.Boolean(),
createdByUserId: Type.Union([Type.String(), Type.Null()]),
updatedByUserId: Type.Union([Type.String(), Type.Null()]),
createdAt: Type.String(),
updatedAt: Type.String(),
});
Key implementation details from the source code:
kindrestricts values topostType,data,page,component, orlayout, determining how the table behaves in the admin UI and routing layer.fields_json(stored as JSON in the database, represented asfieldsin the schema) contains an array ofDataFieldobjects defining columns—supporting types liketext,number,media,relation,repeater, andpageTree.systemflag protects built-in tables (posts,pages,components,layouts) from deletion or renaming.route_basedetermines the public URL prefix for all rows in the table (e.g.,/posts).
Source: DataTableSchema definition at src/core/data/schemas.ts#L17-L44.
data_rows – Polymorphic Content Instances
The data_rows table stores actual content instances. Every row references its parent schema via table_id and stores its payload in a flexible JSON column.
The DataRowSchema in src/core/data/schemas.ts defines the structure:
export const DataRowSchema = Type.Object({
id: Type.String(),
tableId: Type.String(),
cells: DataRowCellsSchema, // { fieldId: value } JSON payload
slug: Type.String(), // Denormalized for fast routing
status: DataRowStatusSchema, // draft | published | unpublished | scheduled
seq: Type.Optional(Type.Number()), // Conflict detection sequence
authorUserId: NullableUserIdSchema,
createdByUserId: NullableUserIdSchema,
updatedByUserId: NullableUserIdSchema,
publishedByUserId: NullableUserIdSchema,
author: NullableDataUserReferenceSchema,
createdBy: NullableDataUserReferenceSchema,
updatedBy: NullableDataUserReferenceSchema,
publishedBy: NullableDataUserReferenceSchema,
createdAt: Type.String(),
updatedAt: Type.String(),
publishedAt: Type.Union([Type.String(), Type.Null()]),
scheduledPublishAt: Type.Union([Type.String(), Type.Null()]),
deletedAt: Type.Union([Type.String(), Type.Null()]),
});
Critical architectural features:
cellsstores a free-form map (Record<string, unknown>) where keys correspond to field IDs defined in the parent table’s schema. Atextfield stores a string, while arepeaterstores an ordered array of sub-items.statusdrives the publishing workflow, supportingdraft,published,unpublished, andscheduledstates.seqenables soft-delete and conflict resolution when multiple admins edit simultaneously.- User references (e.g.,
author,createdBy) are populated via database joins and not stored directly in the row.
Source: DataRowSchema definition at src/core/data/schemas.ts#L99-L108.
Type Safety with TypeBox
Instatic enforces type safety through TypeBox schemas that serve as the single source of truth. The src/core/data/schemas.ts file exports TypeScript types derived from these schemas, ensuring that server-side validation, admin UI forms, and plugin SDKs remain synchronized. When you define a table with specific fields, the cells JSON in data_rows is validated against those field definitions at runtime.
Versioning and URL Management
The universal store supports content versioning and URL redirects through companion tables:
data_row_versionsstores historic snapshots of rows at publish time, defined byDataRowVersionSchemaatsrc/core/data/schemas.ts#L64-L71.data_row_redirectsmaintains URL history, mapping old slugs to current rows after changes, defined byDataRowRedirectSchemaatsrc/core/data/schemas.ts#L18-L22.
These tables enable rollback capabilities and prevent broken links when content slugs change.
Database Implementation
The SQLite migration in server/db/migrations-sqlite.ts creates the unified store with strict constraints:
create table if not exists data_tables (
id text primary key,
name text not null,
slug text not null,
kind text not null default 'data',
route_base text not null default '',
singular_label text not null,
plural_label text not null,
primary_field_id text not null default 'title',
fields_json text not null default '[]',
system integer not null default 0,
created_by_user_id text references users(id) on delete set null,
updated_by_user_id text references users(id) on delete set null,
created_at text not null default (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
updated_at text not null default (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
deleted_at text,
constraint data_tables_kind_check check (kind in ('postType', 'data', 'page', 'component'))
);
Source: server/db/migrations-sqlite.ts#L96-L112.
Working with the Universal Store
Creating Custom Collections via API
Use the CreateDataTableInput schema to define new content types dynamically:
import { apiRequest } from '@core/http';
import type { CreateDataTableInput } from '@/core/data/schemas';
const input: CreateDataTableInput = {
name: 'Products',
slug: 'products',
kind: 'data',
singularLabel: 'Product',
pluralLabel: 'Products',
primaryFieldId: 'name',
fields: [
{ type: 'text', id: 'name', label: 'Name', required: true },
{ type: 'number', id: 'price', label: 'Price', required: true },
{ type: 'media', id: 'image', label: 'Image', mediaKind: 'image' },
],
};
await apiRequest('/admin/api/cms/data/tables', {
method: 'POST',
json: input,
schema: DataTableSchema,
});
Relevant schema: CreateDataTableInputSchema at src/core/data/schemas.ts#L59-L67.
Inserting Content Rows
Populate tables using the cells object to match your schema:
import { apiRequest } from '@core/http';
import type { CreateDataRowInput } from '@/core/data/schemas';
await apiRequest(`/admin/api/cms/data/tables/${tableId}/rows`, {
method: 'POST',
json: {
cells: {
name: 'Cozy Chair',
price: 129.99,
image: 'media-12345',
},
} satisfies CreateDataRowInput,
schema: DataRowSchema,
});
Relevant schema: CreateDataRowInputSchema at src/core/data/schemas.ts#L84-L88.
Publishing Workflow
Transition rows from draft to published status:
await apiRequest(`/admin/api/cms/data/rows/${rowId}/publish`, {
method: 'POST',
schema: PublishedDataRowSchema,
});
Published view schema: PublishedDataRowSchema at src/core/data/schemas.ts#L86-L95.
Server-Side Repository Access
For backend operations, use the repository layer directly:
import { tables } from '@/server/repositories/data';
import { rows } from '@/server/repositories/data';
// Retrieve table definition
const table = await tables.getById('products');
// Insert row transactionally
await rows.mutations.insert({
tableId: table.id,
cells: { name: 'Desk Lamp', price: 45, image: 'media-987' },
});
Repository entry points: Tables CRUD at server/repositories/data/tables.ts and row mutations at server/repositories/data/rows/mutations.ts.
Summary
- Unified storage: All content types share
data_tables(schemas) anddata_rows(instances), eliminating redundant table structures. - Polymorphic payloads: The
cellsJSON column stores heterogeneous data shapes validated against parent table schemas. - Type safety: TypeBox schemas in
src/core/data/schemas.tsenforce contracts across the server, admin UI, and plugins. - Built-in versioning:
data_row_versionsanddata_row_redirectsprovide content history and URL stability. - Flexible kinds: Five table kinds (
postType,data,page,component,layout) determine routing and UI behavior while sharing the same underlying storage model.
Frequently Asked Questions
What content types does Instatic support?
Instatic supports five table kinds defined in DataTableKindSchema: postType for blog entries, page for static pages, component for reusable UI blocks, layout for page templates, and data for arbitrary custom collections. All types utilize the same data_tables and data_rows storage mechanism but receive different treatment in the routing layer and admin interface.
How does Instatic handle schema changes?
Schema changes are managed through the fields_json column in data_tables. When you modify a table's structure, the system updates this JSON definition. Existing rows in data_rows remain untouched in storage, but the application layer validates cells against the new schema on read and write operations. The seq column facilitates conflict detection when multiple administrators modify the same row simultaneously.
What is the cells column in data_rows?
The cells column is a JSON object storing field values for a specific row. Each key corresponds to a field ID defined in the parent table's schema, with values matching the field type—strings for text fields, numbers for numeric fields, media IDs for assets, and nested arrays for repeater fields. This polymorphic approach allows data_rows to store any content shape without requiring schema migrations for new fields.
How does versioning work in the universal store?
When a row is published, Instatic creates a snapshot in the data_row_versions table using the DataRowVersionSchema. This preserves the exact state of the cells data and metadata at publish time. Administrators can view historical versions and revert to previous states. Concurrently, data_row_redirects tracks URL changes by storing old slug-to-row mappings, ensuring external links remain functional after content updates.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →