Persistence
Store forms and responses in memory, SQL, or a custom ResponseStore.
database is required by dimahForm(). It is a first-class instance option,
not a plugin. @dimah-form/server intentionally contains no ORM.
| Adapter | Use it for |
|---|---|
memoryAdapter() | Tests, examples, and process-local development |
db(formDb) from @dimah-form/db | SQL through FumaDB |
A custom ResponseStore | Your own persistence implementation |
Memory
import { dimahForm, memoryAdapter } from "@dimah-form/server";
export const form = dimahForm({
database: memoryAdapter(),
forms,
});The data disappears when the process restarts.
SQL with FumaDB
npm i @dimah-form/db fumadb
npm i -D tsxCopy the schema for your ORM, or generate it with the CLI and re-add the indexes. FumaDB's generator does not emit secondary indexes.
import { defineRelations } from "drizzle-orm";
import {
sqliteTable,
text,
blob,
integer,
foreignKey,
index,
} from "drizzle-orm/sqlite-core";
export const questionnaire = sqliteTable(
"questionnaire",
{
id: text("id", { length: 255 })
.primaryKey()
.notNull()
.$defaultFn(() => crypto.randomUUID()),
slug: text("slug", { length: 255 }).unique(),
title: text("title").notNull(),
definition: blob("definition", { mode: "json" }).notNull(),
status: text("status").notNull(),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.defaultNow(),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.defaultNow(),
},
(table) => [
index("questionnaire_status_updated_at_idx").on(
table.status,
table.updatedAt,
),
],
);
export const response = sqliteTable(
"response",
{
id: text("id", { length: 255 })
.primaryKey()
.notNull()
.$defaultFn(() => crypto.randomUUID()),
questionnaireId: text("questionnaire_id", { length: 255 }).notNull(),
status: text("status").notNull(),
definition: blob("definition", { mode: "json" }).notNull(),
answers: blob("answers", { mode: "json" }).notNull(),
respondentId: text("respondent_id", { length: 255 }),
submittedAt: integer("submitted_at", { mode: "timestamp" }),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.defaultNow(),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.defaultNow(),
},
(table) => [
foreignKey({
columns: [table.questionnaireId],
foreignColumns: [questionnaire.id],
name: "response_questionnaire_questionnaire_fk",
})
.onUpdate("restrict")
.onDelete("restrict"),
index("response_questionnaire_id_updated_at_idx").on(
table.questionnaireId,
table.updatedAt,
),
index("response_respondent_id_updated_at_idx").on(
table.respondentId,
table.updatedAt,
),
index("response_draft_lookup_idx").on(
table.questionnaireId,
table.respondentId,
table.status,
table.updatedAt,
),
],
);
export const private_dimah_form_settings = sqliteTable(
"private_dimah_form_settings",
{
id: text("id", { length: 255 }).primaryKey().notNull(),
version: text("version", { length: 255 }).notNull().default("1.0.0"),
},
);
export const relations = defineRelations(
{ questionnaire, response, private_dimah_form_settings },
(r) => ({
questionnaire: {
responses: r.many.response({
alias: "response_questionnaire",
}),
},
response: {
questionnaire: r.one.questionnaire({
from: r.response.questionnaireId,
to: r.questionnaire.id,
alias: "response_questionnaire",
}),
},
}),
);Create a FumaDB client for your ORM, then pass it to db():
import { db } from "@dimah-form/db";
import { dimahForm } from "@dimah-form/server";
import { formDb } from "@/lib/db";
import { forms } from "@/lib/forms";
export const form = dimahForm({
database: db(formDb),
forms,
});The package README and example app show Drizzle, Prisma, and Kysely client setup.
Generate the schema
import { DimahFormDB } from "@dimah-form/db";
import { runCli } from "@dimah-form/db/cli";
import { drizzleAdapter } from "fumadb/adapters/drizzle";
void runCli(
DimahFormDB.client(drizzleAdapter({ db: {} as never, provider: "sqlite" })),
);node --import tsx scripts/db-cli.mts generate latest -o ./lib/db/schema.tsAfter generating, retain these indexes:
| Index | Columns |
|---|---|
questionnaire_status_updated_at_idx | status, updated_at |
response_questionnaire_id_updated_at_idx | questionnaire_id, updated_at |
response_respondent_id_updated_at_idx | respondent_id, updated_at |
response_draft_lookup_idx | questionnaire_id, respondent_id, status, updated_at |
Commit the generated schema and apply it with your ORM's normal migration
workflow. When upgrading @dimah-form/db, regenerate into a temporary file,
review the diff, restore the secondary indexes above, and create a migration;
do not replace a production schema blindly.
Custom stores
Pass a ResponseStore implementation directly as database; do not wrap it
in db(). The store supports form CRUD, response CRUD, list/count operations,
and draft lookup. When a write receives expectedUpdatedAt, throw
StoreConflictError on a mismatch so the HTTP layer returns STALE_UPDATE.
The full method contract is ResponseStore.