# How Instatic’s DataTables and DataRows Universal Content Store Operates

> Discover how Instatic's data_tables and data_rows universal content store operates, persisting site content in dialect-agnostic tables for CMS, editor, plugins, and publishing.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-29

---

**Instatic persists every piece of site content in two generic database tables—`data_tables` for schema definitions and `data_rows` for versioned JSON payloads—creating a dialect-agnostic universal content store that powers the CMS, visual editor, plugins, and static publishing pipeline.**

Instatic is an open-source static site generator that abandons traditional content modeling in favor of a flexible, type-safe architecture. Instead of creating dedicated database tables for each content type, Instatic implements a **universal content store** using **DataTables** and **DataRows**, as defined in the CoreBunch/Instatic repository. This pair of generic tables handles everything from blog posts and pages to component definitions, enabling seamless integration across the admin interface, plugin ecosystem, and publishing engine.

## Database Architecture and Abstraction

The universal content store sits atop a database abstraction layer that normalizes interactions across PostgreSQL and SQLite backends.

### The DbClient Abstraction Layer

In [`src/core/persistence/dataTables.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/dataTables.ts) and [`src/core/persistence/dataRows.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/dataRows.ts), the system interacts with the database through a dialect-agnostic `DbClient` interface. Whether configured with `DATABASE_URL=postgres://…` for PostgreSQL or using `bun:sqlite` for local development, all SQL remains ANSI-standard. The adapters automatically map JSON columns—denoted with the `*_json` suffix—to and from plain JavaScript objects, ensuring consistent behavior regardless of the underlying engine.

### DataTables: The Schema Registry

The `data_tables` table, defined in [`src/core/persistence/dataTables.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/dataTables.ts), functions as a schema registry for logical content collections. Each record stores a UUID, a unique slug (such as "Posts" or "Components"), and a JSON definition describing available fields, including their types, tokens, and validation rules. This metadata enables the CMS to render dynamic forms and validates incoming data without requiring schema migrations for new content types.

### DataRows: Versioned Content Storage

Actual content items live in the `data_rows` table, implemented in [`src/core/persistence/dataRows.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/dataRows.ts). Each row maintains a foreign key reference (`table_id`) to its parent DataTable, along with a `data_json` column containing the serialized field values. Crucially, every write creates a new version, with the version allocator logic tested in [`src/__tests__/server/dataRowVersionAllocator.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/dataRowVersionAllocator.test.ts). The `version` column auto-increments per table, enabling safe drafts, rollback capabilities, and full audit trails without destructive updates.

## Content Lifecycle and Versioning

Understanding how content flows through Instatic requires examining the intersection of schema definition, row mutation, and version control.

### Schema Definition Process

When developers or plugins register a new content type via the CMS UI, the system inserts a record into `data_tables` containing the JSON schema definition. This schema describes each field’s type, token, and constraints. The admin UI hook in [`src/admin/pages/data/hooks/useDataWorkspace.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/data/hooks/useDataWorkspace.ts) relies on these definitions to render appropriate input components and validation logic, fetching available tables via the `listCmsDataTables()` function.

### Row Creation and Version Allocation

Creating content triggers an insertion into `data_rows` with the appropriate `table_id` and initial `data_json` payload. Subsequent edits do not overwrite existing records; instead, the mutation engine in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts) allocates a new version number and inserts a fresh row. This append-only approach ensures that the most recent version serves the editor while historical versions remain accessible for rollback or audit purposes.

## API Surface and Integration Points

The universal content store exposes a consistent API across HTTP endpoints, admin hooks, and plugin SDKs.

### Admin UI and HTTP API

CRUD operations are centralized in [`server/handlers/cms/data.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/data.ts), where handlers like `listCmsDataTables()`, `createDataRow()`, and `updateDataRow()` process requests. The admin interface consumes these through the shared `apiRequest` client from `@core/http`, as demonstrated in [`src/admin/pages/data/hooks/useDataWorkspace.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/pages/data/hooks/useDataWorkspace.ts). All responses are validated against TypeBox schemas, guaranteeing type safety across the JavaScript-to-database boundary.

### Plugin SDK and MCP Bridge

Plugins interact with the store through multiple pathways. The direct SDK in [`src/core/plugins/sdk.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/sdk.ts) allows internal plugin code to call mutation methods, while external integrations use the MCP (Model Context Protocol) bridge via [`server/ai/mcp/editorBridge.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/ai/mcp/editorBridge.ts). Regardless of entry point, plugin-initiated changes route through the same mutation engine used by the core CMS, ensuring consistent validation and undo history. The RPC method `cms.content.table.mutate` handles these cross-boundary requests.

## Publishing Pipeline Integration

During static site generation, the publisher resolves DataRow references to inject live content into the final HTML. In [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts), the system analyzes the page’s **NodeTree** to identify nodes referencing DataRows. For each reference found, the publisher calls `fetchDataRow()` to retrieve the current `data_json` payload, replacing placeholder nodes with actual content before writing the resulting HTML to the static artifact directory managed by [`server/publish/staticArtefact.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/publish/staticArtefact.ts).

## Code Examples: Working with the Store

The following patterns demonstrate how to interact with the universal content store across different contexts.

### Listing DataTables in the Admin UI

Admin components use the `@core/http` client to fetch available content types:

```typescript
import { listCmsDataTables } from '@core/http'

async function loadTables() {
  const tables = await listCmsDataTables()
  // tables: Array<{ id: string; slug: string; fields: Array<{ id: string; token: string }> }>
  setTables(tables)
}

```

### Creating a New DataRow

Insert content items by posting to the CMS data endpoint with TypeBox schema validation:

```typescript
import { apiRequest } from '@core/http'

async function createRow(tableId: string, payload: Record<string, unknown>) {
  const newRow = await apiRequest('/cms/data/rows', {
    method: 'POST',
    body: { tableId, data: payload },
    schema: DataRowSchema,               // TypeBox schema for validation
  })
  return newRow.id
}

```

### Plugin-Side Mutations

Plugins modify content through the unified mutation API:

```typescript
// Inside a plugin RPC handler
await cms.content.table.mutate({
  kind: 'updateRow',
  tableId,
  rowId,
  data: { title: 'New title' },
})

```

### Resolving DataRows During Publishing

The publisher injects row data during static generation:

```typescript
import { fetchDataRow } from '@core/persistence'

export async function renderNode(node) {
  if (node.type === 'dataRow') {
    const row = await fetchDataRow(node.rowId)
    return renderData(row.data_json)   // inject the stored JSON into the page
  }
  // …other node types
}

```

## Summary

- **Universal Schema**: Instatic uses two generic tables—`data_tables` for schemas and `data_rows` for content—rather than dedicated tables per content type, defined in [`src/core/persistence/dataTables.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/dataTables.ts) and [`src/core/persistence/dataRows.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/dataRows.ts).
- **Dialect Agnostic**: The `DbClient` abstraction supports both PostgreSQL and SQLite using ANSI-standard SQL with automatic JSON column mapping.
- **Immutable Versioning**: Every edit creates a new row version (auto-incremented per table), enabling drafts, rollback, and audit trails without data loss.
- **Unified API**: All mutations flow through [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts), accessible via HTTP handlers, admin hooks ([`useDataWorkspace.ts`](https://github.com/CoreBunch/Instatic/blob/main/useDataWorkspace.ts)), and the Plugin SDK/MC Bridge.
- **Publishing Integration**: The publisher resolves DataRow references at build time using [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts), injecting JSON payloads into the final static HTML.

## Frequently Asked Questions

### What database dialects does the universal content store support?

The store supports both **PostgreSQL** (via `DATABASE_URL=postgres://…`) and **SQLite** (via `bun:sqlite`), as implemented in the dialect-agnostic `DbClient` interface. All SQL remains ANSI-standard, and the abstraction layer automatically handles JSON column serialization for both backends.

### How does versioning work in the DataRows table?

Each write operation creates a new row in `data_rows` with an auto-incremented `version` number specific to that table. The version allocator logic, tested in [`src/__tests__/server/dataRowVersionAllocator.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/server/dataRowVersionAllocator.test.ts), ensures that drafts and historical versions persist alongside the latest content, enabling one-click rollback and full content auditing.

### Can plugins safely modify content through the universal store?

Yes. Plugins interact with the store through the **MCP Bridge** ([`server/ai/mcp/editorBridge.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/ai/mcp/editorBridge.ts)) or the direct SDK ([`src/core/plugins/sdk.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/sdk.ts)). All plugin-initiated mutations route through the central mutation engine in [`src/core/page-tree/mutations.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/page-tree/mutations.ts), ensuring they undergo the same validation and type-checking as core CMS operations.

### How does the publisher handle DataRow references during static generation?

During the publish run, the system walks the page’s **NodeTree** and identifies DataRow references using the dynamic detection logic in [`src/core/publisher/dynamicDetection.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/publisher/dynamicDetection.ts). For each reference, it fetches the current `data_json` payload from `data_rows` and injects the content directly into the HTML output, producing fully static pages with up-to-date dynamic content.