Database
Add the survey tables to the database you already use.
Add dimah_survey and dimah_response to your schema, migrate with the tool
you already use, and pass the same database to both server audiences.
dimah-survey does not run migrations.
npm i @dimah-survey/db fumadbModel keys are survey and response. Copy the tables into your application.
Do not import them from @dimah-survey/db at runtime.
provider is "sqlite", "postgresql", or "mysql".
Add these tables to your Drizzle schema. This copy is SQLite. On PostgreSQL or
MySQL, use that driver's table builder and set provider to match. Keep the
partial unique index.
import { defineRelations, sql } from "drizzle-orm";
import {
blob,
check,
foreignKey,
index,
integer,
sqliteTable,
text,
uniqueIndex,
} from "drizzle-orm/sqlite-core";
export const survey = sqliteTable(
"dimah_survey",
{
id: text("id", { length: 255 }).primaryKey().notNull(),
slug: text("slug", { length: 255 }).notNull(),
status: text("status").notNull(),
draftJson: blob("draft_json", { mode: "json" }).notNull(),
publishedJson: blob("published_json", { mode: "json" }),
publishedAt: integer("published_at", { mode: "timestamp" }),
settings: blob("settings", { mode: "json" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.defaultNow(),
updatedAt: integer("updated_at", { mode: "timestamp" })
.notNull()
.defaultNow(),
},
(table) => [
uniqueIndex("dimah_survey_slug_unique").on(table.slug),
check(
"dimah_survey_status_check",
sql`${table.status} in ('draft', 'active', 'archived')`,
),
index("dimah_survey_status_updated_at_idx").on(
table.status,
table.updatedAt,
),
],
);
export const response = sqliteTable(
"dimah_response",
{
id: text("id", { length: 255 }).primaryKey().notNull(),
surveyId: text("survey_id", { length: 255 }).notNull(),
respondentId: text("respondent_id", { length: 255 }),
status: text("status").notNull(),
definition: blob("definition", { mode: "json" }).notNull(),
data: blob("data", { mode: "json" }).notNull(),
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.surveyId],
foreignColumns: [survey.id],
name: "dimah_response_survey_fk",
})
.onUpdate("restrict")
.onDelete("restrict"),
check(
"dimah_response_status_check",
sql`${table.status} in ('draft', 'submitted', 'abandoned')`,
),
index("dimah_response_survey_id_updated_at_idx").on(
table.surveyId,
table.updatedAt,
),
index("dimah_response_respondent_lookup_idx").on(
table.surveyId,
table.respondentId,
table.status,
),
uniqueIndex("dimah_response_one_open_draft")
.on(table.surveyId, table.respondentId)
.where(
sql`${table.status} = 'draft' and ${table.respondentId} is not null`,
),
],
);
export const privateDimahSurveySettings = sqliteTable(
"private_dimah_survey_settings",
{
id: text("id", { length: 255 }).primaryKey().notNull(),
version: text("version", { length: 255 }).notNull().default("1.0.0"),
},
);
export const relations = defineRelations(
{ survey, response, privateDimahSurveySettings },
(helpers) => ({
survey: {
responses: helpers.many.response({
from: helpers.survey.id,
to: helpers.response.surveyId,
}),
},
response: {
survey: helpers.one.survey({
from: helpers.response.surveyId,
to: helpers.survey.id,
}),
},
}),
);import { DimahSurveyDB, db } from "@dimah-survey/db";
import { drizzleAdapter } from "fumadb/adapters/drizzle";
export const database = db(
DimahSurveyDB.client(
drizzleAdapter({ db: drizzleDb, provider: "sqlite" }),
),
);drizzleDb is the Drizzle client you already open. Pass relations from the
schema into that client.
The store accepts provider: "mysql", but this page does not include a MySQL
schema. Translate the tables and indexes before connecting the adapter.
Next.js
Keep the store on the Node.js runtime. The last entry is the ORM package that
opens the connection: drizzle-orm, @prisma/client, or kysely.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: ["@dimah-survey/db", "fumadb", "drizzle-orm"],
};
export default nextConfig;In memory
memoryAdapter() from @dimah-survey/server stores rows in the process. They
disappear when the process exits. Use it for tests, or when you do not want
the SQL tables. Pass it as database the same way as the SQL store.
import { memoryAdapter } from "@dimah-survey/server";
export const database = memoryAdapter();Custom store
import type { SurveyStore } from "@dimah-survey/core";Prop
Type
Start and submit callbacks are server-only. They are never accepted from the HTTP body:
import type {
StartResponseLifecycle,
SubmitResponseLifecycle,
} from "@dimah-survey/core";Prop
Type
Prop
Type
A conforming store preserves these invariants:
Documents and snapshots
- Keep
draftJson,publishedJson, andsettingsindependent. - Insert default settings with a new survey; only
saveSurveySettingschanges them afterward. - Copy
publishedJsonintoresponse.definitionat start and never update that column. - Partial save replaces
data; persist the object passed by the server.
Atomic writes
- Check
expectedUpdatedAtinside the write. - Evaluate start reuse, collection windows, response caps, and one-open-draft rules in the same lock or transaction as the mutation.
- Enforce one draft per survey and identified respondent in application logic; use the partial unique index as a database backstop.
- Run the start and submit lifecycle callbacks inside that same boundary.
Queries and transitions
- Anonymous start always inserts. Identified start follows
"one-open"or"single"settings. - Start, partial save, and submit enforce the collection window.
- New starts and submits enforce the response cap.
- Reopen enforces
reopenand refuses a second identified draft. - Lists honor pagination, sort by
updatedAtdescending, and omitdefinitionanddatafrom summaries.