# Persistence (https://form.dimah.dev/docs/persistence)



`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 [#memory]

```ts
import { dimahForm, memoryAdapter } from "@dimah-form/server";

export const form = dimahForm({
  database: memoryAdapter(),
  forms,
});
```

The data disappears when the process restarts.

## SQL with FumaDB [#sql-with-fumadb]

<CodeBlockTabs defaultValue="npm">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="npm">
      npm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="pnpm">
      pnpm
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="yarn">
      yarn
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="bun">
      bun
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="npm">
    ```bash
    npm i @dimah-form/db fumadb
    npm i -D tsx
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @dimah-form/db fumadb
    pnpm add -D tsx
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @dimah-form/db fumadb
    yarn add --dev tsx
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @dimah-form/db fumadb
    bun add --dev tsx
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Copy the schema for your ORM, or generate it with the CLI and re-add the
indexes. FumaDB's generator does not emit secondary indexes.

<Tabs items="[&#x22;Drizzle&#x22;, &#x22;Prisma&#x22;, &#x22;Kysely&#x22;]">
  <Tab value="Drizzle">
    ```ts title="lib/db/schema.ts"
    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",
          }),
        },
      }),
    );
    ```
  </Tab>

  <Tab value="Prisma">
    ```prisma title="prisma/schema.prisma"
    generator client {
      provider = "prisma-client-js"
    }

    datasource db {
      provider = "postgresql"
    }

    model Questionnaire {
      id         String   @id @default(cuid()) @map("id") @db.VarChar(255)
      slug       String?  @unique @map("slug") @db.VarChar(255)
      title      String   @map("title")
      definition Json     @map("definition")
      status     String   @map("status")
      createdAt  DateTime @default(now()) @map("created_at")
      updatedAt  DateTime @default(now()) @map("updated_at")
      responses  Response[] @relation("response_questionnaire")

      @@index([status, updatedAt], map: "questionnaire_status_updated_at_idx")
      @@map("questionnaire")
    }

    model Response {
      id              String    @id @default(cuid()) @map("id") @db.VarChar(255)
      questionnaireId String    @map("questionnaire_id") @db.VarChar(255)
      status          String    @map("status")
      definition      Json      @map("definition")
      answers         Json      @map("answers")
      respondentId    String?   @map("respondent_id") @db.VarChar(255)
      submittedAt     DateTime? @map("submitted_at")
      createdAt       DateTime  @default(now()) @map("created_at")
      updatedAt       DateTime  @default(now()) @map("updated_at")
      questionnaire   Questionnaire @relation("response_questionnaire", fields: [questionnaireId], references: [id], onUpdate: Restrict, onDelete: Restrict)

      @@index([questionnaireId, updatedAt], map: "response_questionnaire_id_updated_at_idx")
      @@index([respondentId, updatedAt], map: "response_respondent_id_updated_at_idx")
      @@index([questionnaireId, respondentId, status, updatedAt], map: "response_draft_lookup_idx")
      @@map("response")
    }

    model PrivateDimahFormSettings {
      id      String @id @map("id") @db.VarChar(255)
      version String @default("1.0.0") @map("version") @db.VarChar(255)

      @@map("private_dimah_form_settings")
    }
    ```
  </Tab>

  <Tab value="Kysely">
    ```sql title="db/dimah-form-indexes.sql"
    CREATE INDEX IF NOT EXISTS questionnaire_status_updated_at_idx
      ON questionnaire (status, updated_at);

    CREATE INDEX IF NOT EXISTS response_questionnaire_id_updated_at_idx
      ON response (questionnaire_id, updated_at);

    CREATE INDEX IF NOT EXISTS response_respondent_id_updated_at_idx
      ON response (respondent_id, updated_at);

    CREATE INDEX IF NOT EXISTS response_draft_lookup_idx
      ON response (questionnaire_id, respondent_id, status, updated_at);
    ```
  </Tab>
</Tabs>

Create a FumaDB client for your ORM, then pass it to `db()`:

```ts title="lib/form.ts"
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](https://github.com/dimah-kz/dimah-form/tree/main/examples/next)
show Drizzle, Prisma, and Kysely client setup.

## Generate the schema [#generate-the-schema]

```ts title="scripts/db-cli.mts"
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" })),
);
```

```bash
node --import tsx scripts/db-cli.mts generate latest -o ./lib/db/schema.ts
```

After 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 [#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`](https://form.dimah.dev/docs/configuration.md#responsestore).

## Next [#next]

<Cards>
  <Card title="Responses" href="/docs/responses" description="See why draft lookup and compare-and-swap matter." />

  <Card title="Security" href="/docs/security" description="Authorize access to the rows your store returns." />
</Cards>
