Store form responses in Supabase with optional AES-256-GCM encryption.
Install
npm install @squaredr/fieldcraft-supabase
Quick Start
import { createClient } from "@supabase/supabase-js";
import { createSupabaseAdapter } from "@squaredr/fieldcraft-supabase";
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
const adapter = createSupabaseAdapter({
client: supabase,
});
const engine = createEngine(schema, {
adapters: adapter,
});
Configuration
| Option | Type | Default | Description |
|---|
client | SupabaseClient | — | Supabase client instance (required) |
table | string | "formengine_responses" | Table name for responses |
encryptFields | string[] | — | Field IDs to encrypt |
encryptionKey | string | — | Base64-encoded 32-byte key |
onSuccess | (response, record) => void | — | Called after insert |
onError | (error) => void | — | Called on error |
Table Setup
Create the responses table in your Supabase dashboard or via a migration:
CREATE TABLE formengine_responses (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
schema_id TEXT NOT NULL,
schema_version TEXT NOT NULL,
session_token TEXT NOT NULL,
data JSONB NOT NULL,
metadata JSONB,
completion_time_ms INTEGER,
submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Optional: index for querying by schema
CREATE INDEX idx_responses_schema ON formengine_responses (schema_id);
Row-Level Security (RLS)
Enable RLS on your table and define policies to control access. The adapter works with any RLS configuration — it uses the Supabase client's auth context.
-- Enable RLS
ALTER TABLE formengine_responses ENABLE ROW LEVEL SECURITY;
-- Allow authenticated users to insert their own responses
CREATE POLICY "Users can insert own responses"
ON formengine_responses FOR INSERT
TO authenticated
WITH CHECK (session_token = auth.uid()::text);
-- Allow users to read their own responses
CREATE POLICY "Users can read own responses"
ON formengine_responses FOR SELECT
TO authenticated
USING (session_token = auth.uid()::text);
Field-Level Encryption
Unlike the Postgres adapter which encrypts the entire payload, the Supabase adapter encrypts individual fields. Unencrypted fields remain as plain JSONB for querying.
const adapter = createSupabaseAdapter({
client: supabase,
encryptFields: ["ssn", "medical_id"],
encryptionKey: process.env.ENCRYPTION_KEY!,
});
// In the database:
// data.name = "Alice" (plain text, queryable)
// data.ssn = "base64(iv+ct+tag)" (encrypted)
Each encrypted field uses AES-256-GCM with a unique random IV. Generate a key with: openssl rand -base64 32
Draft Adapter
import {
createSupabaseAdapter,
createSupabaseDraftAdapter,
} from "@squaredr/fieldcraft-supabase";
const submitAdapter = createSupabaseAdapter({ client: supabase });
const draftAdapter = createSupabaseDraftAdapter({
client: supabase,
table: "formengine_drafts", // default
ttlHours: 72, // default
});
const engine = createEngine(schema, {
adapters: submitAdapter,
draftAdapter,
sessionToken: "user-123",
});
Drafts Table
CREATE TABLE formengine_drafts (
id UUID DEFAULT gen_random_uuid() 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(),
UNIQUE (schema_id, session_token)
);
The draft adapter upserts on (schema_id, session_token) and filters out expired drafts on load.
Draft Adapter Config
| Option | Type | Default | Description |
|---|
client | SupabaseClient | — | Supabase client instance |
table | string | "formengine_drafts" | Table name |
ttlHours | number | 72 | Draft expiry in hours |
With FormEngineRenderer
import { FormEngineRenderer } from "@squaredr/fieldcraft-react";
import { createSupabaseAdapter } from "@squaredr/fieldcraft-supabase";
const adapter = createSupabaseAdapter({ client: supabase });
<FormEngineRenderer
schema={schema}
adapters={adapter}
sessionToken="user-123"
onSubmit={(response) => console.log("Saved:", response)}
/>