# How Instatic's Form Submission System Creates Database Tables: A Deep Dive into the Unified Content Schema

> Discover how Instatic dynamically creates database tables for form submissions by storing JSON payloads in the unified data_rows infrastructure. Learn about the CREATE TABLE IF NOT EXISTS logic detailed in handler.ts.

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

---

**Instatic dynamically creates a `form_submissions` table on first use by writing submitted data into the unified `data_rows` infrastructure, storing JSON payloads alongside metadata using `CREATE TABLE IF NOT EXISTS` logic in [`server/forms/handler.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/forms/handler.ts).**

The CoreBunch/Instatic repository implements a content management system that eliminates dedicated form tables in favor of a unified storage model. This architecture stores form definitions as standard components and dynamically instantiates the submission table only when the first POST request arrives, leveraging the same SQLite infrastructure that powers posts and pages.

## The Unified Content Schema Foundation

Instatic does not rely on a bespoke schema for forms. Instead, the system builds upon two core tables introduced in the baseline migration `001_baseline` located in [`server/db/migrations-sqlite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/db/migrations-sqlite.ts). The `data_tables` register defines content types—such as posts, pages, and components—while `data_rows` stores the actual records for each type. This unified design, visible in lines 90–110 of the migration file, ensures that all content objects share the same underlying structure regardless of their purpose.

Lines 56–70 of the same migration seed the `components` table, which becomes the storage target for form definitions when users add forms to pages.

## How Form Definitions Are Stored

Form configurations are not stored in a dedicated registry. When a user adds a form to a page in the editor, Instatic treats it as a standard component of type `base.form`. This component persists as a row in `data_rows` associated with the `components` table, sharing the same persistence layer as other CMS objects. This approach eliminates schema fragmentation by treating form blueprints as ordinary content records.

## Dynamic Table Creation for Form Submissions

When a visitor submits a form, the server-side handler in [`server/forms/handler.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/forms/handler.ts) processes the POST request to `/_instatic/form/submit`. Rather than relying on a pre-existing table, the handler dynamically initializes the storage structure on first use. This lazy initialization strategy ensures the database schema remains minimal until activity actually occurs.

### On-Demand Table Initialization

The handler executes a `CREATE TABLE IF NOT EXISTS` statement identical in convention to the baseline schema. As implemented in lines 45–78 of [`server/forms/handler.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/forms/handler.ts), the SQL creates a `form_submissions` table with columns for `id`, `form_id`, `page_id`, `values_json`, and `created_at`. The `values_json` column follows the `_json` suffix convention, triggering the SQLite adapter to automatically parse the payload as JSON.

```typescript
// server/forms/handler.ts – snippet
await db.execute(`
  CREATE TABLE IF NOT EXISTS form_submissions (
    id           TEXT PRIMARY KEY,
    form_id      TEXT NOT NULL,
    page_id      TEXT NOT NULL,
    values_json  TEXT NOT NULL,
    created_at   TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now'))
  )
`);

```

### Submission Insertion Process

After validation, the handler inserts a new row containing the submitted values. The implementation uses parameterized queries to bind the UUID, form identifier, page reference, and stringified JSON payload.

```typescript
// server/forms/handler.ts – snippet
await db.execute(`
  INSERT INTO form_submissions (id, form_id, page_id, values_json)
  VALUES ($id, $formId, $pageId, $valuesJson)
`, {
  $id: crypto.randomUUID(),
  $formId,
  $pageId,
  $valuesJson: JSON.stringify(submittedValues),
});

```

## Security Through Token Stamping

Security is enforced through HMAC-signed tokens generated at publish time. Before a page goes live, the `publishedHtmlPipeline` invokes `stampFormPageTokens` from [`server/forms/formRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/forms/formRuntime.ts) (lines 20–31). This function scans for `<form>` tags, extracts the `data-instatic-form-id` attribute, and injects a signed `data-instatic-page-token` attribute along with the page ID. The browser-side runtime in [`src/modules/base/forms/formRuntimeJs.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/modules/base/forms/formRuntimeJs.ts) reads this token and includes it in the POST request to `/_instatic/form/submit`, allowing the server to verify the submission originates from a legitimate published page.

```typescript
// server/forms/formRuntime.ts – snippet
export function stampFormPageTokens(html: string, pageId: string): string {
  return html.replace(CMS_FORM_TAG_PATTERN, (tag) => {
    const formId = attrValue(tag, 'data-instatic-form-id');
    const token  = issuePublicFormPageToken({ pageId, formId });
    return tag.replace(
      /<form\b/i,
      `<form data-instatic-page-token="${escapeAttr(token)}"
             data-instatic-page-id="${escapeAttr(pageId)}"`
    );
  });
}

```

## Complete Data Flow

The end-to-end architecture connects four distinct phases:

1. **Editor Persistence** – Adding a `base.form` component creates a row in `data_rows` under the components type.
2. **Publish Processing** – The `publishedHtmlPipeline` calls `stampFormPageTokens` to embed security attributes in the HTML.
3. **Browser Execution** – `formRuntimeJs` extracts the page token, serializes form values, and POSTs to `/_instatic/form/submit`.
4. **Server Handling** – The handler in [`server/forms/handler.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/forms/handler.ts) validates the token, creates the `form_submissions` table if absent, and inserts the payload.

All data ultimately resides within the `data_tables` and `data_rows` infrastructure defined in [`server/db/migrations-sqlite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/db/migrations-sqlite.ts), maintaining schema consistency across the entire CMS.

## Summary

- Instatic uses a **unified content schema** (`data_tables` and `data_rows`) rather than dedicated form tables, established in [`server/db/migrations-sqlite.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/db/migrations-sqlite.ts).
- Form definitions persist as **component rows** (`base.form`) within the standard content storage.
- The `form_submissions` table is created **dynamically on first use** via `CREATE TABLE IF NOT EXISTS` in [`server/forms/handler.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/forms/handler.ts).
- Submissions store metadata alongside **JSON payloads** in the `values_json` column, parsed automatically by the SQLite adapter.
- **Token stamping** in [`server/forms/formRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/forms/formRuntime.ts) ensures submissions originate from published pages via HMAC-signed attributes.

## Frequently Asked Questions

### Does Instatic create the form_submissions table during installation?

No. The table is initialized lazily when the first submission occurs. The handler in [`server/forms/handler.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/forms/handler.ts) executes `CREATE TABLE IF NOT EXISTS form_submissions` only upon receiving valid POST data, ensuring the schema remains lean until actual form activity takes place.

### Where are form definitions stored if not in a dedicated forms table?

Form definitions are treated as standard CMS components. When you add a form in the editor, Instatic saves it as a row in `data_rows` with the content type `base.form`, using the same unified storage mechanism that handles pages and posts.

### How does Instatic secure form submissions against spam or forged requests?

The system employs HMAC-signed page tokens. During the publishing phase, `stampFormPageTokens` in [`server/forms/formRuntime.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/forms/formRuntime.ts) embeds a `data-instatic-page-token` attribute into each form tag. The submission handler validates this cryptographic token before accepting data, proving the request originated from a legitimately published page.

### What database columns does the form_submissions table contain?

The dynamically created table includes `id` (primary key), `form_id` (referencing the component), `page_id` (referencing the source page), `values_json` (containing the submitted field data), and `created_at` (timestamp). This structure aligns with the `_json` suffix convention used throughout the CMS for automatic JSON parsing.