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



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.

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

## Choose a store [#choose-a-store]

| Store                | Use it for                                 | Persistence         |
| -------------------- | ------------------------------------------ | ------------------- |
| `memoryAdapter()`    | Tests and local development                | Process only        |
| `db(client)`         | Supported SQL databases through FumaDB     | Durable             |
| Custom `SurveyStore` | Existing data layer or specialized storage | Your implementation |

Pass the same store object to both fill and editor.

## Connect the SQL store [#connect-the-sql-store]

```ts title="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 [#generate-an-application-owned-schema]

Configure the FumaDB CLI in your application:

```ts
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`:

```bash
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 [#implement-a-custom-store]

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

<AutoTypeTable path="packages/core/src/types.ts" name="SurveyStore" />

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

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

<AutoTypeTable path="packages/core/src/types.ts" name="StartResponseLifecycle" />

<AutoTypeTable path="packages/core/src/types.ts" name="SubmitResponseLifecycle" />

A conforming store preserves these invariants:

### Documents and snapshots [#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 [#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 [#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.
