# How the Instatic Form Submission System Stores Data in Data Tables

> Discover how Instatic stores form submissions as JSON rows in data_rows, linked to dynamic form tables in data_tables. Learn its schema-agnostic persistence for Postgres and SQLite.

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

---

**Instatic stores every form submission as a JSON row in the generic `data_rows` table, linked to a dynamically created table named `form_<formId>` within the `data_tables` registry, enabling schema-agnostic persistence across both Postgres and SQLite.**

The CoreBunch/Instatic CMS treats structured content—including visitor-submitted form data—as rows in a unified relational model. When you configure a native form using the `base.form` module, the **Instatic form submission system** automatically routes submissions through a type-safe API endpoint and persists them using the generic `data_tables` and `data_rows` architecture shared by all content types.

## Overview of the Data Tables Architecture

Instatic avoids bespoke tables for every feature by employing a **generic two-table model** defined in [`server/db/migrations-pg.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/db/migrations-pg.ts) and [`server/db/migrations-sqlite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/db/migrations-sqlite.ts). Every piece of structured data, including form submissions, lives in `data_rows` linked to a metadata entry in `data_tables`.

- **`data_tables`** – Stores table metadata (name, schema version)
- **`data_rows`** – Stores actual content as JSON, linked via `table_id`

This design allows the form module to create new storage buckets on-the-fly without DDL migrations when fields change.

## The Form Submission Flow

The end-to-end flow spans client-side collection, server-side validation, and repository-based persistence.

### 1. Form Definition and Client Setup

In [`src/modules/base/forms/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/forms/index.ts), the form module declares a component that renders a `<form>` element with `data-instatic-form-id` and `data-instatic-form-mode` attributes based on the author’s configuration.

```typescript
// Example configuration
{
  id: 'base.form',
  props: {
    formId: 'contact',
    mode: 'cms'  // Uses native CMS storage
  }
}

```

Authors set a **Form ID** (`formId`) which determines the target table name at runtime.

### 2. Client-Side Data Collection

When a visitor submits the form, [`src/modules/base/forms/formRuntimeJs.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/forms/formRuntimeJs.ts) intercepts the event, serializes the inputs using `new FormData(event.target)`, and POSTs a JSON payload to `/api/forms/submit`.

```typescript
// Payload structure sent to server
{
  "formId": "contact",
  "values": {
    "name": "Alice",
    "email": "alice@example.com",
    "message": "Hello!"
  }
}

```

The runtime uses the `@core/http` utility `apiRequest` to ensure consistent headers and error handling.

### 3. Server-Side Routing and Validation

The entry point in [`server/router.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/router.ts) matches the `/api/forms/submit` path and forwards the request to the **form-submission handler**. Before any database operation, the handler validates the request body against `FormSubmissionSchema` using TypeBox (as demonstrated in [`src/core/utils/validation.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/validation.ts)).

If validation fails, the system returns structured errors via [`src/core/utils/errorMessage.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/errorMessage.ts) without touching the database.

### 4. Database Insertion via Repository

The actual storage logic resides in [`server/repositories/site.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/site.ts) inside the `handleFormSubmission` and `createFormSubmissionRow` functions. The repository:

1. Generates the table name by prefixing the `formId` with `form_` (e.g., `form_contact`)
2. Creates the table entry in `data_tables` if it does not exist
3. Inserts the submission as a new row in `data_rows` with the JSON payload

```sql
-- On-first-submit table creation
INSERT INTO data_tables (name) VALUES ('form_contact');

-- Submission storage
INSERT INTO data_rows (table_id, json) VALUES (
  (SELECT id FROM data_tables WHERE name = 'form_contact'),
  '{"name":"Alice","email":"alice@example.com","message":"Hello!"}'
);

```

Because the repository uses dialect-neutral SQL via the unified DB adapters, this flow works identically for both Postgres and SQLite deployments.

## Database Schema Implementation

The underlying schema is defined in [`server/db/migrations-pg.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/db/migrations-pg.ts) (Postgres) and [`server/db/migrations-sqlite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/db/migrations-sqlite.ts) (SQLite). Both define `data_tables` and `data_rows` with the following characteristics:

- **Schema-agnostic fields** – Form fields are stored as JSON, so adding or removing inputs does not require schema migrations
- **Indexed lookups** – The `table_id` foreign key enables efficient queries filtered by form ID
- **Unified types** – The same TypeScript types drive both the validation layer (TypeBox) and the repository layer, ensuring end-to-end type safety

## Practical Implementation Example

To store submissions from a contact form, configure the module and let the runtime handle persistence:

```typescript
// Visual editor configuration (src/modules/base/forms usage)
{
  id: 'base.form',
  props: {
    formId: 'newsletter_signup',
    mode: 'cms'
  }
}

```

After a visitor submits, the resulting database state includes:

```sql
-- Query to retrieve submissions
SELECT dr.json, dr.created_at
FROM data_rows dr
JOIN data_tables dt ON dr.table_id = dt.id
WHERE dt.name = 'form_newsletter_signup';

```

Each submission appends a new row to `form_newsletter_signup`, accessible via the same API used for pages, products, or any other CMS content.

## Summary

- **Instatic uses a generic relational model** where `data_tables` registers form-specific buckets and `data_rows` stores the actual JSON submissions.
- **Table names are dynamic**, following the pattern `form_<formId>`, created automatically on the first submission without manual migrations.
- **Type-safe boundaries** ensure every submission is validated via TypeBox schemas before reaching [`server/repositories/site.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/site.ts).
- **Cross-database compatibility** is achieved through dialect-neutral repository patterns that support both Postgres and SQLite.
- **Schema flexibility** allows form fields to change without database alterations, as values are stored as JSON documents.

## Frequently Asked Questions

### What database tables does Instatic create for form submissions?

Instatic creates entries in the generic `data_tables` registry with names like `form_contact` or `form_newsletter`, paired with corresponding rows in `data_rows`. It does not create dedicated physical tables for each form; instead, it uses the unified `data_tables`/`data_rows` schema defined in [`server/db/migrations-pg.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/db/migrations-pg.ts) and [`server/db/migrations-sqlite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/db/migrations-sqlite.ts).

### How does Instatic handle form validation before storage?

The system validates all incoming payloads against `FormSubmissionSchema` using TypeBox at the boundary layer in [`server/repositories/site.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/repositories/site.ts). Validation errors are processed through [`src/core/utils/errorMessage.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/errorMessage.ts) and returned to the client before any database write occurs, ensuring only well-formed data reaches the storage layer.

### Can form submissions be queried like other CMS content?

Yes. Because submissions live in the same `data_tables`/`data_rows` architecture as pages and custom content types, you can query them via the internal API, expose them through the admin UI, export CSV files, or join them with other tables in custom plugins using standard SQL against the `form_<formId>` table identifier.

### Does adding new form fields require database migrations?

No. Form fields are stored as JSON in the `data_rows` table, making the storage **schema-agnostic**. Adding or removing fields in the form configuration only changes the JSON structure written to new rows; existing rows remain untouched and valid, eliminating the need for DDL migrations when modifying forms.