Instatic Content Model Explained: How data_tables and data_rows Store All Content
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, 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:
-
postType– Created in the Content workspace (/admin/content), includes built-in fields liketitle,slug,body,featuredMedia,seoTitle, andseoDescription. Supports draft → published → unpublished → scheduled versioning for blog posts and product catalogs. -
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. -
page– Created in the Site workspace (/admin/site), includestitle,slug, andbody(stored as apageTree). Follows the same publishing workflow aspostTypefor CMS-managed pages. -
component– Created in Visual Component mode, stores reusable UI elements withname,tree(apageTree), andparams(afieldSchema). 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.
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 validates field types.
Row Storage. Content creators save entries to data_rows, where helper functions in 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 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
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
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.
Reading Typed Content
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
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– Source of truth forDataTableSchema,DataRowSchema,DataFieldTypeenum, and thekindtype definitions. -
src/core/data/cells.ts– Type-safe readers (readStringCell,readNodeTreeCell,slugForTable) that all handlers and plugins must use to extract values fromcells_json. -
server/repositories/data/tables.ts– CRUD operations fordata_tablesincluding creation, updates, and system table protection logic. -
server/repositories/data/rows/– Sub-directory containing read queries, mutations, bulk operations, filtering, and search logic fordata_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– Orchestrates the publishing workflow, version snapshotting, and static artifact generation. -
docs/features/content-storage.md– Comprehensive documentation of the unified store architecture. -
docs/architecture.md– High-level system diagrams showing the relationship betweendata_tablesanddata_rows.
Summary
- The Instatic content model uses a unified two-table architecture where
data_tablesstores schemas anddata_rowsstores 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 insrc/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_versionsbefore 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, which exports DataTableSchema, DataRowSchema, and the DataFieldType enum. Runtime validation occurs in server/repositories/data/tables.ts for collections and 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 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.
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 →