FieldCraftDocsServicesBlogWork With Me →

Supabase Adapter

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

OptionTypeDefaultDescription
clientSupabaseClientSupabase client instance (required)
tablestring"formengine_responses"Table name for responses
encryptFieldsstring[]Field IDs to encrypt
encryptionKeystringBase64-encoded 32-byte key
onSuccess(response, record) => voidCalled after insert
onError(error) => voidCalled 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

OptionTypeDefaultDescription
clientSupabaseClientSupabase client instance
tablestring"formengine_drafts"Table name
ttlHoursnumber72Draft 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)}
/>