Store form responses directly in PostgreSQL with Drizzle ORM.
Install
npm install @squaredr/fieldcraft-postgres
Quick Start
import { createPostgresAdapter } from "@squaredr/fieldcraft-postgres";
const adapter = createPostgresAdapter({
connectionString: process.env.DATABASE_URL!,
});
const engine = createEngine(schema, {
adapters: adapter,
});
Configuration
| Option | Type | Default | Description |
|---|
connectionString | string | — | PostgreSQL connection URL (required) |
table | string | "formengine_responses" | Table name for responses |
encryptFields | string[] | — | Field IDs to encrypt at rest |
encryptionKey | string | — | Base64-encoded 32-byte key |
ssl | boolean | true in prod | Enable SSL connections |
onSuccess | (response) => void | — | Called after successful insert |
onError | (error) => void | — | Called on error |
Database Schema
The adapter creates two tables via Drizzle ORM. Run the migration to set them up.
Responses Table
CREATE TABLE formengine_responses (
id TEXT PRIMARY KEY,
schema_id TEXT NOT NULL,
schema_version TEXT NOT NULL,
session_token TEXT NOT NULL,
data TEXT NOT NULL,
metadata JSONB,
completion_time_ms INTEGER,
submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX fe_responses_schema_idx ON formengine_responses (schema_id);
CREATE INDEX fe_responses_session_idx ON formengine_responses (session_token);
Drafts Table
CREATE TABLE formengine_drafts (
id TEXT PRIMARY KEY,
schema_id TEXT NOT NULL,
session_token TEXT NOT NULL,
partial_data JSONB NOT NULL,
current_section_id TEXT,
visited_section_ids JSONB,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX fe_drafts_session_unique_idx
ON formengine_drafts (schema_id, session_token);
CREATE INDEX fe_drafts_expires_idx ON formengine_drafts (expires_at);
Encryption
Enable AES-256-GCM encryption for sensitive fields. The entire data column is encrypted when encryptFields is set.
const adapter = createPostgresAdapter({
connectionString: process.env.DATABASE_URL!,
encryptFields: ["ssn", "credit_card", "medical_record"],
encryptionKey: process.env.ENCRYPTION_KEY!, // 32-byte base64 string
});
The key must be a base64-encoded 32-byte string. Generate one with: openssl rand -base64 32
Draft Adapter
Use the dedicated draft adapter for server-side draft persistence:
import {
createPostgresAdapter,
createPostgresDraftAdapter,
} from "@squaredr/fieldcraft-postgres";
const submitAdapter = createPostgresAdapter({
connectionString: process.env.DATABASE_URL!,
});
const draftAdapter = createPostgresDraftAdapter({
connectionString: process.env.DATABASE_URL!,
table: "formengine_drafts", // default
ttlHours: 72, // default
});
const engine = createEngine(schema, {
adapters: submitAdapter,
draftAdapter,
sessionToken: "user-123",
});
Draft Adapter Config
| Option | Type | Default | Description |
|---|
connectionString | string | — | PostgreSQL connection URL |
table | string | "formengine_drafts" | Table name |
ttlHours | number | 72 | Draft expiry in hours |
ssl | boolean | true in prod | Enable SSL |