# Instatic Universal Content Store Architecture: data_tables and data_rows Explained

> Explore Instatic s universal content store architecture Learn about data_tables and data_rows and how they enable type safe polymorphic content management with TypeBox schemas

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: architecture
- Published: 2026-08-01

---

**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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/schemas.ts), the `DataTableSchema` TypeBox definition specifies the structure:

```typescript
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:

- **`kind`** restricts values to `postType`, `data`, `page`, `component`, or `layout`, determining how the table behaves in the admin UI and routing layer.
- **`fields_json`** (stored as JSON in the database, represented as `fields` in the schema) contains an array of `DataField` objects defining columns—supporting types like `text`, `number`, `media`, `relation`, `repeater`, and `pageTree`.
- **`system`** flag protects built-in tables (`posts`, `pages`, `components`, `layouts`) from deletion or renaming.
- **`route_base`** determines 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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/schemas.ts) defines the structure:

```typescript
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:

- **`cells`** stores a free-form map (`Record<string, unknown>`) where keys correspond to field IDs defined in the parent table’s schema. A `text` field stores a string, while a `repeater` stores an ordered array of sub-items.
- **`status`** drives the publishing workflow, supporting `draft`, `published`, `unpublished`, and `scheduled` states.
- **`seq`** enables 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`](https://github.com/CoreBunch/Instatic/blob/main/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_versions`** stores historic snapshots of rows at publish time, defined by `DataRowVersionSchema` at `src/core/data/schemas.ts#L64-L71`.
- **`data_row_redirects`** maintains URL history, mapping old slugs to current rows after changes, defined by `DataRowRedirectSchema` at `src/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`](https://github.com/CoreBunch/Instatic/blob/main/server/db/migrations-sqlite.ts) creates the unified store with strict constraints:

```sql
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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/data/tables.ts) and row mutations at [`server/repositories/data/rows/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/data/rows/mutations.ts).

## Summary

- **Unified storage:** All content types share `data_tables` (schemas) and `data_rows` (instances), eliminating redundant table structures.
- **Polymorphic payloads:** The `cells` JSON column stores heterogeneous data shapes validated against parent table schemas.
- **Type safety:** TypeBox schemas in [`src/core/data/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/schemas.ts) enforce contracts across the server, admin UI, and plugins.
- **Built-in versioning:** `data_row_versions` and `data_row_redirects` provide 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.