dimah-survey
Build the backend

Persistence

Use memory locally, connect the SQL store, or implement SurveyStore.

Every server instance requires a SurveyStore as database. memoryAdapter() is the process-local reference implementation. @dimah-survey/db adapts a FumaDB client for durable SQL storage. You may also implement the contract directly.

Your application owns its tables, migrations, indexes, and database client. dimah-survey owns the behavioral contract.

Choose a store

StoreUse it forPersistence
memoryAdapter()Tests and local developmentProcess only
db(client)Supported SQL databases through FumaDBDurable
Custom SurveyStoreExisting data layer or specialized storageYour implementation

Pass the same store object to both fill and editor.

Connect the SQL store

lib/survey.ts
import { DimahSurveyDB, db } from "@dimah-survey/db";
import { dimahSurvey } from "@dimah-survey/server";
import { drizzleAdapter } from "fumadb/adapters/drizzle";

const database = db(
  DimahSurveyDB.client(
    drizzleAdapter({ db: drizzleOrm, provider: "sqlite" }),
  ),
);

export const editor = dimahSurvey({
  audience: "editor",
  database,
});

The SQL table names are dimah_survey and dimah_response. Drizzle export names remain survey and response, because those are the model keys db() queries.

draft_json, published_json, and settings are separate columns. settings is not null and has no database default. Each response stores a copy of the published document in definition. db() does not update definition after insert.

Generate an application-owned schema

Configure the FumaDB CLI in your application:

import { createCli } from "fumadb/cli";
import { DimahSurveyDB } from "@dimah-survey/db";
import { drizzleAdapter } from "fumadb/adapters/drizzle";
import { drizzle } from "drizzle-orm/node-sqlite";

await createCli({
  db: DimahSurveyDB.client(
    drizzleAdapter({ db: drizzle(":memory:"), provider: "sqlite" }),
  ),
  command: "dimah-survey",
  version: "YOUR_APP_VERSION",
}).main();

The CLI version identifies your wrapper command; the schema version to generate is currently 1.0.0:

dimah-survey generate 1.0.0 -o ./db/survey.ts

Migrate the generated file as part of your application. FumaDB does not emit secondary indexes, so also apply dimah_response_one_open_draft: one draft per survey and identified respondent. Prisma cannot express that predicate; apply the index SQL after creating the tables.

These exported files are readable references to copy or generate from. Do not import them as your runtime ORM schema:

  • @dimah-survey/db/schema/drizzle.ts
  • @dimah-survey/db/schema/tables.sql
  • @dimah-survey/db/schema/indexes.sql
  • @dimah-survey/db/schema/schema.prisma

There is no survey version table. History is response.definition.

Implement a custom store

import type { SurveyStore } from "@dimah-survey/core";

Prop

Type

Start and submit callbacks are not part of the HTTP body. The server passes them into the store write:

import type {
  StartResponseLifecycle,
  SubmitResponseLifecycle,
} from "@dimah-survey/core";

Prop

Type

Prop

Type

A conforming store preserves these invariants:

Documents and snapshots

  • Keep draftJson, publishedJson, and settings independent.
  • Insert default settings with a new survey; only saveSurveySettings changes them afterward.
  • Copy publishedJson into response.definition at start and never update that column.
  • Partial save replaces data; persist the object passed by the server.

Atomic writes

  • Check expectedUpdatedAt inside 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 and response cap.
  • Reopen enforces reopen and refuses a second identified draft.
  • Lists honor pagination, sort by updatedAt descending, and omit definition and data from summaries.

Use memoryAdapter() and the shared store contract tests as the behavioral reference when implementing an adapter.

On this page