Database
Persistence is required. A SQL database is not.
dimahForm({ database }) needs a ResponseStore. @dimah-form/server has no ORM. @dimah-form/db is optional — install it only when drafts must survive a restart.
| Adapter | Package | When |
|---|---|---|
memoryAdapter() | @dimah-form/server | Tests, local, Quickstart |
db(formDb) | @dimah-form/db | Production (FumaDB + Drizzle, Prisma, or Kysely) |
| Custom | your code | Implement ResponseStore |
1. In-memory
No extra packages. Data lives in the process and is gone on restart.
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { forms } from "@/lib/forms";
export const form = dimahForm({
database: memoryAdapter(),
forms,
});2. Production
Install @dimah-form/db and fumadb when you need SQL:
bash pnpm add @dimah-form/db fumadb Schema
Copy these tables, or generate with the CLI and re-add the indexes (FumaDB generate does not emit them).
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",
}),
},
}),
);Client
import { createClient } from "@libsql/client";
import { DimahFormDB } from "@dimah-form/db";
import { drizzle } from "drizzle-orm/libsql";
import { drizzleAdapter } from "fumadb/adapters/drizzle";
import { relations } from "./schema";
const sqlite = createClient({
url: process.env.DATABASE_URL ?? "file:local.db",
});
export const formDb = DimahFormDB.client(
drizzleAdapter({
db: drizzle({ client: sqlite, relations }),
provider: "sqlite",
}),
);Connect
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,
});
export type Form = typeof form;CLI
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/schema.tsgenerate overwrites the file. Re-add these indexes afterward:
| 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 |
Custom store
Implement ResponseStore and pass it to dimahForm({ database }) directly. Do not wrap it in db().