How Instatic's Form Submission System Creates Database Tables: A Deep Dive into the Unified Content Schema
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.
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. 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 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, 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.
// 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.
// 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 (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 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.
// 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:
- Editor Persistence – Adding a
base.formcomponent creates a row indata_rowsunder the components type. - Publish Processing – The
publishedHtmlPipelinecallsstampFormPageTokensto embed security attributes in the HTML. - Browser Execution –
formRuntimeJsextracts the page token, serializes form values, and POSTs to/_instatic/form/submit. - Server Handling – The handler in
server/forms/handler.tsvalidates the token, creates theform_submissionstable 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, maintaining schema consistency across the entire CMS.
Summary
- Instatic uses a unified content schema (
data_tablesanddata_rows) rather than dedicated form tables, established inserver/db/migrations-sqlite.ts. - Form definitions persist as component rows (
base.form) within the standard content storage. - The
form_submissionstable is created dynamically on first use viaCREATE TABLE IF NOT EXISTSinserver/forms/handler.ts. - Submissions store metadata alongside JSON payloads in the
values_jsoncolumn, parsed automatically by the SQLite adapter. - Token stamping in
server/forms/formRuntime.tsensures 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 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 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.
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 →