# Instatic Content Model Explained: How data_tables and data_rows Store All Content

> Understand Instatic's content model using data_tables and data_rows. Discover how this unified database structure stores all content as JSON cells, simplifying your data management.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: deep-dive
- Published: 2026-07-02

---

**The Instatic content model is a unified database architecture where `data_tables` defines collection schemas and `data_rows` stores every piece of content—from blog posts to visual components—as JSON cells, eliminating the need for separate tables per content type.**

In the CoreBunch/Instatic repository, the content model departs from traditional CMS patterns that use separate database tables for posts, pages, and components. Instead, it implements a flexible, normalized store using just two tables, with the `kind` column in `data_tables` distinguishing between collection types while `data_rows` handles the actual content storage via typed JSON cells.

## The Two-Table Architecture

The Instatic content model centers on a strict separation between schema definitions and content instances.

### data_tables: Schema Definitions

The `data_tables` table holds the **blueprint** for every collection in the system. Each row defines a collection's metadata, structure, and behavior through columns including `id`, `name`, `slug`, `kind`, `fields_json`, `system`, and `route_base`.

According to the schema definition in [`src/core/data/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/schemas.ts), the `fields_json` column stores an array of `DataField` objects describing custom fields, while the `kind` column categorizes the collection as `postType`, `data`, `page`, or `component`. Four system tables—`posts`, `pages`, `components`, and `layouts`—are seeded at boot with `system: true`, protecting them from deletion or renaming.

### data_rows: Content Storage

The `data_rows` table contains the **actual content** for every collection. Each row links to its parent schema via `table_id` and stores cell values in `cells_json`, keyed by field ID.

Key columns include `id`, `table_id`, `cells_json`, `slug`, `status`, `author_user_id`, `created_at`, and `deleted_at`. The `slug` and `status` columns are denormalized specifically for indexing and routing purposes, while the flexible `cells_json` structure accommodates any field type defined in the parent table.

## Four Collection Types in the Instatic Content Model

The `kind` column in `data_tables` determines the collection's behavior, built-in fields, and workflow:

1. **`postType`** – Created in the Content workspace (`/admin/content`), includes built-in fields like `title`, `slug`, `body`, `featuredMedia`, `seoTitle`, and `seoDescription`. Supports draft → published → unpublished → scheduled versioning for blog posts and product catalogs.

2. **`data`** – Created in the Data workspace (`/admin/data`), has no built-in fields. Used for simple key-value grids, form submissions, and application settings without workflow states.

3. **`page`** – Created in the Site workspace (`/admin/site`), includes `title`, `slug`, and `body` (stored as a `pageTree`). Follows the same publishing workflow as `postType` for CMS-managed pages.

4. **`component`** – Created in Visual Component mode, stores reusable UI elements with `name`, `tree` (a `pageTree`), and `params` (a `fieldSchema`). Used for building blocks that appear across multiple pages.

Custom collections are added by inserting new rows into `data_tables` via the repository layer in [`server/repositories/data/tables.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/data/tables.ts).

## Data Flow and CRUD Operations

The Instatic content model processes content through four distinct stages:

**Schema Definition.** Administrators define collections by inserting rows into `data_tables` with appropriate `fields_json` configurations. The `DataFieldType` enum in [`src/core/data/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/schemas.ts) validates field types.

**Row Storage.** Content creators save entries to `data_rows`, where helper functions in [`src/core/data/cells.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/cells.ts)—such as `readStringCell` and `readNodeTreeCell`—safely extract typed values from `cells_json`.

**CRUD Operations.** Repositories under `server/repositories/data/` provide dialect-naïve ANSI SQL operations including `listDataRows`, `createDataRow`, `saveDataRowDraft`, and `softDeleteDataRow`. These handlers manage the denormalized `slug` and `status` columns while keeping `cells_json` synchronized.

**Publishing.** When `status` transitions to `published`, the `publishDataRow` function in [`server/publish/publishRow.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishRow.ts) creates a snapshot in `data_row_versions` and triggers the static site generator to render the final HTML using stored `pageTree` cells.

## Working with the Instatic Content Model

These examples demonstrate how to interact with the unified store using the plugin SDK.

### Creating a New Collection

```typescript
import { api } from '@core/plugin-sdk'
import { DataTableKind } from '@core/data/schemas'

async function createBlogCollection() {
  const tables = api.cms.storage.collections
  const newTable = await tables.create({
    name: 'Blog',
    slug: 'blog',
    kind: 'postType' as DataTableKind,
    fields_json: [] // Uses built-in fields only
  })
  console.log('Created table ID:', newTable.id)
}

```

This SDK call routes to `POST /admin/api/cms/data/tables`, handled by `server/handlers/cms/data/`.

### Inserting Content Rows

```typescript
import { api } from '@core/plugin-sdk'

async function addBlogPost() {
  const posts = api.cms.storage.collection('blog')
  const row = await posts.create({
    title: 'Hello World',
    slug: 'hello-world',
    body: '# Welcome\nThis is the first post.',

    // Custom fields are merged into cells_json automatically
  })
  console.log('New post ID:', row.id)
}

```

The handler validates against `DataRowSchema` and persists to `data_rows.cells_json` via [`server/repositories/data/rows/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/data/rows/mutations.ts).

### Reading Typed Content

```typescript
import { readStringCell, readNodeTreeCell } from '@core/data/cells'

async function fetchPost(slug: string) {
  const posts = api.cms.storage.collection('blog')
  const row = await posts.getBySlug(slug)
  
  const title = readStringCell(row.cells, 'title')
  const bodyTree = readNodeTreeCell(row.cells, 'body')
  
  return { title, bodyTree }
}

```

The `bodyTree` can be rendered by the visual editor or the static publisher.

### Publishing Draft Content

```typescript
import { api } from '@core/plugin-sdk'

async function publishPost(rowId: string) {
  const posts = api.cms.storage.collection('blog')
  await posts.update(rowId, { status: 'published' })
  // Triggers publishDataRow automatically, creating version snapshot
  // and writing static artifact to uploads/published/
}

```

## Key Implementation Files

These source files define the Instatic content model implementation:

- **[`src/core/data/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/schemas.ts)** – Source of truth for `DataTableSchema`, `DataRowSchema`, `DataFieldType` enum, and the `kind` type definitions.

- **[`src/core/data/cells.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/cells.ts)** – Type-safe readers (`readStringCell`, `readNodeTreeCell`, `slugForTable`) that all handlers and plugins must use to extract values from `cells_json`.

- **[`server/repositories/data/tables.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/data/tables.ts)** – CRUD operations for `data_tables` including creation, updates, and system table protection logic.

- **`server/repositories/data/rows/`** – Sub-directory containing read queries, mutations, bulk operations, filtering, and search logic for `data_rows`.

- **`server/handlers/cms/data/`** – HTTP endpoints (`/admin/api/cms/data/...`) exposing the store to the admin UI and external plugins.

- **[`server/publish/publishRow.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishRow.ts)** – Orchestrates the publishing workflow, version snapshotting, and static artifact generation.

- **[`docs/features/content-storage.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/features/content-storage.md)** – Comprehensive documentation of the unified store architecture.

- **[`docs/architecture.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/architecture.md)** – High-level system diagrams showing the relationship between `data_tables` and `data_rows`.

## Summary

- The Instatic content model uses a **unified two-table architecture** where `data_tables` stores schemas and `data_rows` stores all content instances.
- **Four collection kinds** (`postType`, `data`, `page`, `component`) determine behavior without requiring separate database tables.
- Content values are stored as **JSON in `cells_json`**, accessed through type-safe helpers in [`src/core/data/cells.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/cells.ts).
- **Denormalized columns** (`slug`, `status`) exist only for indexing and routing, while the source of truth remains in the JSON cells.
- The **publishing workflow** creates version snapshots in `data_row_versions` before generating static artifacts.

## Frequently Asked Questions

### What is the difference between data_tables and data_rows?

`data_tables` stores the **schema** for a collection—including its name, slug, kind, and field definitions in `fields_json`—while `data_rows` stores the **actual content instances** with values in `cells_json`. Every row in `data_rows` must reference a `data_tables` entry via its `table_id` foreign key.

### How does Instatic handle different content types without separate tables?

The `kind` column in `data_tables` distinguishes between content types (`postType`, `page`, `component`, `data`), and the `fields_json` schema defines type-specific fields. Built-in fields vary by kind—for example, `postType` includes SEO fields while `component` stores `params`—but all content physically resides in the same `data_rows` table structure.

### Where is the content schema defined in the Instatic codebase?

The schema definitions live in [`src/core/data/schemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/data/schemas.ts), which exports `DataTableSchema`, `DataRowSchema`, and the `DataFieldType` enum. Runtime validation occurs in [`server/repositories/data/tables.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/data/tables.ts) for collections and [`server/repositories/data/rows/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/data/rows/mutations.ts) for content rows.

### How does the publishing workflow interact with data_rows?

When a row's `status` column changes to `published`, the `publishDataRow` function in [`server/publish/publishRow.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/publishRow.ts) creates an immutable snapshot in `data_row_versions`, then renders the final HTML using the `pageTree` stored in `cells_json`. The published artifact is written to the `uploads/published/` directory while the original row remains editable for future drafts.