# dimah-survey

> Server-owned publishing and response lifecycles for SurveyJS JSON. Publish a document, freeze the definition each response starts with, and validate submit against that same snapshot.

SurveyJS owns the schema, Creator, question behavior, and renderer. Your application owns authentication, database migrations, files, and UI. dimah-survey owns publish, response snapshots, drafts, collection policy, and submit validation.

TypeScript packages: `@dimah-survey/core` (protocol, browser clients, schemas, errors, store types), `@dimah-survey/server` (`dimahSurvey()`, guards, validation, adapters, `memoryAdapter()`), `@dimah-survey/react` (SurveyJS Model and Creator bindings), `@dimah-survey/db` (SQL store). HTTP adapters: Next.js App Router, Express, Hono, Fastify, Elysia, SvelteKit, and Node.

Use it when a SurveyJS app needs explicit publish and reproducible response history. Skip it for a visual form builder, hosted survey product, SurveyJS renderer, or dimah-form integration.

Install: `npm i @dimah-survey/server @dimah-survey/react survey-core survey-react-ui`. Add `@dimah-survey/db` for the SQL store. Published and still before `1.0.0`. A release may change the API.

- Auth stays in the consumer `guard`. Do not look for library auth.
- `draftJson` is the editor copy. `publishedJson` is what new responses clone. A later publish does not change `response.definition`.
- Submit validation runs on the stored response definition, not the live draft or the latest publish.
- Partial save replaces `survey.data`. It is not a key patch.
- Fill and editor are separate HTTP audiences over one shared store.
- Do not wrap the SurveyJS renderer, translate SurveyJS JSON into another field model, or import `@dimah-form/*`.


# Overview (https://survey.dimah.dev/docs)



dimah-survey adds a server-owned lifecycle around
[SurveyJS](https://surveyjs.io/) JSON. It publishes survey definitions, creates
durable response drafts, and validates each submission against the exact
definition that response started with.

It does not replace SurveyJS. Your application still owns the renderer,
Creator, authentication, database migrations, and UI.

<Callout>
  The central guarantee is simple: publishing a new survey never changes an
  existing `response.definition`.
</Callout>

## The lifecycle [#the-lifecycle]

<Flow
  label="One response"
  steps="[
  { name: &#x22;Author&#x22;, kind: &#x22;data&#x22;, note: &#x22;Creator writes draftJson&#x22; },
  { name: &#x22;Publish&#x22;, kind: &#x22;server&#x22;, note: &#x22;Promote publishedJson&#x22; },
  { name: &#x22;Start&#x22;, kind: &#x22;server&#x22;, note: &#x22;Freeze response.definition&#x22; },
  { name: &#x22;Fill&#x22;, kind: &#x22;client&#x22;, note: &#x22;SurveyJS renders the snapshot&#x22; },
  { name: &#x22;Submit&#x22;, kind: &#x22;server&#x22;, note: &#x22;Validate the same snapshot&#x22; },
]"
/>

`draftJson` is the editable document. `publishedJson` is the document new
responses may start. At start, the server copies `publishedJson` into
`response.definition`; every save and submit stays attached to that copy.

## Clear ownership [#clear-ownership]

| Owner            | Responsibilities                                                              |
| ---------------- | ----------------------------------------------------------------------------- |
| SurveyJS         | Survey schema, `Model`, `<Survey>`, Creator, and question behavior            |
| Your application | Authentication, authorization, migrations, file storage, and UI               |
| dimah-survey     | Publish, collection policy, response snapshots, drafts, and submit validation |

The backend exposes two isolated audiences over one shared `SurveyStore`:

| Audience | Intended caller          | Capabilities                                                                |
| -------- | ------------------------ | --------------------------------------------------------------------------- |
| Fill     | Respondents              | Read published JSON; start, save, submit, abandon, and reopen responses     |
| Editor   | Trusted application code | Edit and publish surveys, change settings, and read full response snapshots |

The fill audience always requires a guard. The editor audience has a separate
guard and route, so respondent traffic never gains authoring access.

## Start here [#start-here]

<Cards>
  <Card title="Quickstart" href="/docs/quickstart" description="Build the smallest working Next.js integration." />

  <Card title="Response lifecycle" href="/docs/responses" description="Follow snapshot, partial save, submit, and reopen." />

  <Card title="Server integration" href="/docs/integration" description="Mount fill and editor handlers in your runtime." />

  <Card title="Is it the right fit?" href="/docs/comparison" description="Compare ownership with SurveyJS alone and hosted products." />
</Cards>

Machine-readable entry points are available at [llms.txt](https://survey.dimah.dev/llms.txt) and
[llms-full.txt](https://survey.dimah.dev/llms-full.txt). Every docs URL also has a Markdown twin by
adding `.md`.


# Quickstart (https://survey.dimah.dev/docs/quickstart)



This guide creates the smallest complete flow: publish a SurveyJS document,
start a response, render its frozen definition, save page progress, and submit
it.

<Callout type="warn">
  This quickstart uses process-local memory and a fixed respondent. Before
  production, use a durable store, derive identity from the session, and guard
  the editor route.
</Callout>

<Steps>
  <Step>
    ### Install [#install]

    <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-survey/server @dimah-survey/react survey-core survey-react-ui
        ```
      </CodeBlockTab>

      <CodeBlockTab value="pnpm">
        ```bash
        pnpm add @dimah-survey/server @dimah-survey/react survey-core survey-react-ui
        ```
      </CodeBlockTab>

      <CodeBlockTab value="yarn">
        ```bash
        yarn add @dimah-survey/server @dimah-survey/react survey-core survey-react-ui
        ```
      </CodeBlockTab>

      <CodeBlockTab value="bun">
        ```bash
        bun add @dimah-survey/server @dimah-survey/react survey-core survey-react-ui
        ```
      </CodeBlockTab>
    </CodeBlockTabs>

    `survey-react-ui` renders questions. dimah-survey owns only the lifecycle
    around the SurveyJS document.
  </Step>

  <Step>
    ### Create the server [#create-the-server]

    Create one instance for respondents and one for trusted editor operations. They
    must share the same store.

    ```ts title="lib/survey.ts"
    import {
      SURVEY_API_BASE_PATH,
      SURVEY_EDITOR_API_BASE_PATH,
      dimahSurvey,
      guardRespondent,
      memoryAdapter,
    } from "@dimah-survey/server";

    const database = memoryAdapter();

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

    export const fill = dimahSurvey({
      audience: "fill",
      database,
      basePath: SURVEY_API_BASE_PATH,
      guard: guardRespondent("demo"),
    });
    ```

    The fill guard stamps `"demo"` onto every response operation. With the default
    `"one-open"` policy, another start resumes the open draft instead of creating a
    second one.
  </Step>

  <Step>
    ### Mount both audiences [#mount-both-audiences]

    ```ts title="app/api/survey/[[...path]]/route.ts"
    import { toNextJsHandler } from "@dimah-survey/server/next";
    import { fill } from "@/lib/survey";

    export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(fill);
    ```

    ```ts title="app/api/admin/survey/[[...path]]/route.ts"
    import { toNextJsHandler } from "@dimah-survey/server/next";
    import { editor } from "@/lib/survey";

    export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(editor);
    ```

    The paths match the default base paths: `/api/survey` for fill and
    `/api/admin/survey` for editor. See [Mount the server](https://survey.dimah.dev/docs/integration.md) for
    other runtimes.
  </Step>

  <Step>
    ### Publish a SurveyJS document [#publish-a-surveyjs-document]

    Use the in-process editor API from server code. Saving changes `draftJson`;
    publishing explicitly copies that draft to `publishedJson`.

    ```ts title="lib/welcome.ts"
    import { isAPIError } from "@dimah-survey/server";
    import { editor } from "@/lib/survey";

    const definition = {
      title: "Welcome",
      pages: [
        {
          name: "welcome",
          elements: [
            { type: "text", name: "name", title: "Your name", isRequired: true },
            {
              type: "rating",
              name: "score",
              title: "How was it?",
              isRequired: true,
            },
          ],
        },
      ],
    };

    export async function ensureWelcomeSurvey() {
      try {
        await editor.api.getSurvey({ query: { id: "welcome" } });
      } catch (error) {
        if (!isAPIError(error) || error.code !== "SURVEY_NOT_FOUND") throw error;

        await editor.api.saveSurvey({
          body: { id: "welcome", slug: "welcome", draftJson: definition },
        });
        await editor.api.publishSurvey({ body: { id: "welcome" } });
      }
    }
    ```

    `getSurvey` throws `SURVEY_NOT_FOUND` when the row is missing; it does not
    return `null`.
  </Step>

  <Step>
    ### Create the browser client [#create-the-browser-client]

    ```ts title="lib/fill-client.ts"
    "use client";

    import { createFillClient } from "@dimah-survey/react";

    export const fillClient = createFillClient({
      baseURL: "/api/survey",
    });
    ```

    Browser clients take flat input objects. In-process APIs use `{ body }` or
    `{ query }`.
  </Step>

  <Step>
    ### Render the response snapshot [#render-the-response-snapshot]

    ```tsx title="components/fill.tsx"
    "use client";

    import dynamic from "next/dynamic";
    import { useSurveyResponse } from "@dimah-survey/react";
    import { fillClient } from "@/lib/fill-client";

    import "survey-core/survey-core.min.css";

    const Survey = dynamic(
      () => import("survey-react-ui").then((mod) => mod.Survey),
      { ssr: false },
    );

    export function Fill({ responseId }: { responseId: string }) {
      const { model, error, saveError, stale, reload } = useSurveyResponse({
        client: fillClient,
        responseId,
      });

      if (error) return <p>{error.message}</p>;
      if (!model) return null;

      return (
        <>
          {saveError ? <p>{saveError.message}</p> : null}
          {stale ? <button onClick={reload}>Reload</button> : null}
          <Survey model={model} />
        </>
      );
    }
    ```

    The hook loads `response.definition`, constructs a `Model`, and binds partial
    save and submit. It never renders UI. `partial` defaults to `"page"`, so moving
    to the next page persists the complete `survey.data` object.
  </Step>

  <Step>
    ### Start the response [#start-the-response]

    ```tsx title="app/page.tsx"
    import { Fill } from "@/components/fill";
    import { fill } from "@/lib/survey";
    import { ensureWelcomeSurvey } from "@/lib/welcome";

    export default async function Page() {
      await ensureWelcomeSurvey();
      const response = await fill.api.startResponse({
        body: { surveyId: "welcome" },
      });

      return <Fill responseId={response.id} />;
    }
    ```

    The first request creates a draft whose definition is a copy of the published
    document. Refreshing resumes that draft. A later publish cannot rewrite it.
  </Step>
</Steps>

## Make it production-ready [#make-it-production-ready]

* Replace `memoryAdapter()` with the SQL store or your own durable
  `SurveyStore`. See [Persistence](https://survey.dimah.dev/docs/persistence.md).
* Read the respondent from your session and protect the editor audience. See
  [Security](https://survey.dimah.dev/docs/security.md).
* Add Creator autosave without coupling it to publish. See
  [Survey Creator](https://survey.dimah.dev/docs/creator.md).


# When to use (https://survey.dimah.dev/docs/comparison)



Use dimah-survey when SurveyJS remains your schema, Creator, and renderer, but
the server must control which document a respondent sees and preserve that
document with the response.

<Callout>
  The deciding requirement is a durable response snapshot—not simply rendering a
  SurveyJS form in React.
</Callout>

## It is a good fit when [#it-is-a-good-fit-when]

* authors need an editable draft and an explicit publish action
* responses must remain reproducible after the survey changes
* partial saves, submit, abandon, and reopen belong to one server protocol
* submit must validate against the definition that started the response
* your application must keep ownership of auth, data, and deployment

Choose another approach when you only need the SurveyJS renderer, want a hosted
inbox and account system, or do not use SurveyJS JSON as the form definition.

## Compare the ownership model [#compare-the-ownership-model]

| Concern                        | dimah-survey              | SurveyJS alone          | Hosted product   |
| ------------------------------ | ------------------------- | ----------------------- | ---------------- |
| Survey renderer                | Your app uses SurveyJS    | Your app uses SurveyJS  | Product or embed |
| Publish workflow               | Built in                  | Your implementation     | Product workflow |
| Response definition snapshot   | Built in                  | Your persistence design | Vendor-specific  |
| Draft, submit, reopen, and CAS | Built in                  | Your implementation     | Product workflow |
| Authentication                 | Your application          | Your application        | Vendor account   |
| Data and migrations            | Your application          | Your application        | Vendor storage   |
| Authoring                      | Creator stays in your app | Survey Creator          | Hosted builder   |

This compares responsibility, not performance. The same application can use
SurveyJS with dimah-survey for surveys and a different form library for
unrelated UI.

## Deliberate non-goals [#deliberate-non-goals]

dimah-survey does not render questions, ship Creator, define question types,
store file bytes, or provide accounts, dashboards, PDF generation, or an
analytics warehouse. It also has no plugin system and no survey-version table;
`response.definition` is the historical record.

SurveyJS Analytics or PDF tooling can consume each response's `definition` and
`data`. Do not interpret old response data against the current survey draft.

Next, review the concrete [package boundaries](https://survey.dimah.dev/docs/packages.md) or build the
[quickstart](https://survey.dimah.dev/docs/quickstart.md).


# Survey lifecycle (https://survey.dimah.dev/docs/surveys)



Each survey stores three independent concerns:

* `draftJson` — the editable SurveyJS document
* `publishedJson` — the document new responses may start
* `settings` — collection policy owned by dimah-survey

Creator writes the draft. Publishing explicitly promotes that draft. Starting
a response copies the published document into `response.definition`.

<Callout>
  `saveSurvey` only replaces `draftJson`. It never publishes, changes collection
  settings, or updates an existing response.
</Callout>

## Status and availability [#status-and-availability]

| Status     | Meaning                                           | Can start a response?         |
| ---------- | ------------------------------------------------- | ----------------------------- |
| `draft`    | No published document exists                      | No; `NOT_PUBLISHED`           |
| `active`   | `publishedJson` is available to fill              | Yes, while collection is open |
| `archived` | Collection is stopped; published JSON is retained | No; `NOT_PUBLISHED`           |

`GET /survey/published` on the fill handler returns `publishedJson` and
`settings` for an active survey. It never exposes `draftJson`. The read remains
available outside the collection window so the application can render a closed
state; start and writes fail with `SURVEY_CLOSED`.

## Authoring operations [#authoring-operations]

| Operation            | Effect                                                                      |
| -------------------- | --------------------------------------------------------------------------- |
| `saveSurvey`         | Create the row or replace `draftJson`; insert default settings once         |
| `publishSurvey`      | Copy `draftJson` to `publishedJson`, set `active`, and update `publishedAt` |
| `archiveSurvey`      | Set `archived` without deleting either document                             |
| `resumeSurvey`       | Return an archived, previously published survey to `active`                 |
| `saveSurveySettings` | Replace the complete `settings` object without touching either document     |

`resumeSurvey` does not copy `draftJson`. An already active survey is returned
unchanged. An archived survey with no published document throws
`NOT_PUBLISHED`, and resume never runs publish hooks.

Publish runs `onPublish` before the write and `afterPublish` after it.
`onPublish` may abort; an `afterPublish` failure leaves the published row in
place. See [Configuration](https://survey.dimah.dev/docs/configuration.md).

## IDs and slugs [#ids-and-slugs]

The application chooses `id`. On create, `slug` defaults to that id and must be
unique; a conflicting write throws `SLUG_TAKEN`.

`getSurvey` and `getPublishedSurvey` accept an id or a slug in the `id` query.
`startResponse` accepts either value in `surveyId`.

## Protect concurrent changes [#protect-concurrent-changes]

Draft, publish, archive, settings, and resume accept `expectedUpdatedAt` from
the last read. The store compares it inside the write and rejects a stale
request with `STALE_UPDATE`.

Omit the token only when a last-write-wins update is intentional. Creating a
survey with `expectedUpdatedAt` set fails with `STALE_UPDATE`, because there is
no row to compare.

`useSurveyDraft` sends the token it last read. See [Creator](https://survey.dimah.dev/docs/creator.md).

## Publish changes the future, not history [#publish-changes-the-future-not-history]

Existing drafts, submitted responses, and abandoned responses keep their stored
`definition`. Only a newly created response copies the latest
`publishedJson`.

There is no separate survey-version table. The snapshot on each response is
the version history that matters.


# Collection settings (https://survey.dimah.dev/docs/settings)



`settings` is the server-owned collection policy for one survey. It decides
whether an identified respondent reuses a response, whether closed responses
can reopen, when writes are accepted, and how many submissions are allowed.

<Callout>
  Settings are not SurveyJS JSON and are never copied into
  `response.definition`. `saveSurveySettings` replaces the complete object.
</Callout>

## Fields [#fields]

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

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

`saveSurvey` inserts these defaults and then leaves the column alone.
`saveSurveySettings` replaces the whole object. Send every field. `closesAt`
must be later than `opensAt` when both are set. `maxResponses` is a positive
integer or `null`.

Dates are ISO strings. If legacy storage contains an invalid settings object,
the reader falls back to defaults rather than exposing malformed policy.

## Reuse identified responses [#reuse-identified-responses]

For an identified principal, `startResponse` applies `responses` inside the
store write:

| Policy       | Result                                                                 |
| ------------ | ---------------------------------------------------------------------- |
| `"one-open"` | Return the existing draft; otherwise create a new response             |
| `"single"`   | Return the latest response of any status; create only when none exists |

Anonymous starts always create a new response. Their response id is the
capability; they cannot list responses. See [Security](https://survey.dimah.dev/docs/security.md).

`"single"` can return a submitted or abandoned row. The fill `Model` opens in
display mode unless you reopen it. See [React](https://survey.dimah.dev/docs/react.md).

## Enforce the window and cap [#enforce-the-window-and-cap]

`opensAt` and `closesAt` are inclusive. Outside that window, start, partial
save, and submit throw `SURVEY_CLOSED`. Abandon, get, and reopen stay
available. `GET /survey/published` still returns the document, so the client
can show that the survey is closed.

`maxResponses` counts rows already stored as `submitted`. The check runs before
a new insert and before submit, inside the store lock. At the cap, both fail
with `RESPONSE_LIMIT`. Returning an existing identified row does not consume
capacity.

## Reopen a response [#reopen-a-response]

`reopen: true` lets `reopenResponse` move a submitted or abandoned row back to
`draft`. It clears `submittedAt` and does not change `definition` or `data`.

When reopening would create a second draft for the same survey and identified
respondent, the write fails with `OPEN_DRAFT`. `reopen: false` instead fails
with `RESPONSE_CLOSED`.


# Response lifecycle (https://survey.dimah.dev/docs/responses)



`startResponse` copies `publishedJson` into `response.definition`. Every draft
write and submit check remains tied to that snapshot—not the current survey
draft and not a later publish.

<Callout>
  A response is both the answer data and the SurveyJS definition needed to
  interpret it.
</Callout>

## States and transitions [#states-and-transitions]

| Status      | Meaning                                 | Allowed next actions             |
| ----------- | --------------------------------------- | -------------------------------- |
| `draft`     | Answers may be incomplete               | Partial save, submit, or abandon |
| `submitted` | The stored definition accepted the data | Reopen or idempotent resubmit    |
| `abandoned` | Closed without submission               | Reopen                           |

| Operation         | Effect                                                                |
| ----------------- | --------------------------------------------------------------------- |
| `startResponse`   | Create a draft or return the response selected by collection settings |
| `savePartial`     | Replace `data` on a draft                                             |
| `submitResponse`  | Validate the snapshot and store the accepted data                     |
| `abandonResponse` | Close a draft without changing `definition` or `data`                 |
| `reopenResponse`  | Move a submitted or abandoned response back to draft                  |

`savePartial` and `abandonResponse` throw `RESPONSE_CLOSED` unless the row is a
draft. `reopenResponse` throws `RESPONSE_CLOSED` on a draft, and when
`settings.reopen` is `false`.

## Save a draft [#save-a-draft]

Partial save replaces the entire `data` object; it is not a key patch.

With the default `sanitizePartial: "clear"`, the server loads the stored
definition, assigns the posted data, runs `clearIncorrectValues(true)`, and
persists the resulting `survey.data`. It does not run required-question
validation, so an incomplete draft remains valid.

`"replace"` stores the payload as sent.

Questions with `choicesByUrl` keep their posted value through the clear. The
server does not fetch that URL.

## Submit against the snapshot [#submit-against-the-snapshot]

The default `validateResult` loads `response.definition`, removes values that
cannot be assigned, runs SurveyJS `validate()`, and returns the resulting
`survey.data` to persist. An invalid result throws `VALIDATION_FAILED` with the
failing question names in `questions`.

Replace `validateResult` for application-specific checks. It still receives the
stored definition. Return the object to persist, return nothing to keep the
input, or throw an `APIError` to reject the submit. See
[Configuration](https://survey.dimah.dev/docs/configuration.md).

A repeated submit of a row that is already `submitted` returns that row when
the cleaned payload matches stored `data`. Omitting `data` also returns the
stored row. A different payload fails with `RESPONSE_CLOSED`.

## Lifecycle hooks [#lifecycle-hooks]

`onStart` runs only before a new insert; a resumed response skips both start
hooks. `onSubmit` runs after validation and before the submitted write.
Throwing from either `on*` callback aborts the write.

`afterStart` and `afterSubmit` run after the row has been stored. A failure
there leaves the write in place. An idempotent resubmit does not run submit
hooks again.

## Handle concurrent writes [#handle-concurrent-writes]

Partial save, submit, abandon, and reopen accept `expectedUpdatedAt` from the
last read. The comparison happens inside the write. A mismatch fails with
`STALE_UPDATE`.

`useSurveyResponse` sends the token it last read. On `STALE_UPDATE` it sets
`stale` and leaves the model mounted. Call `reload` to read the stored snapshot
again.

Omit the token only when a last-write-wins update is intentional.

## Read response lists [#read-response-lists]

`listResponses` defaults to a summary. Summary rows omit `definition` and
`data`. `include: "full"` returns the complete analytics snapshot and is only
available to the editor audience.

Both lists default to 50 rows and cap at 100. Results sort by `updatedAt`
descending. `total` ignores `limit` and `offset`.

`submittedFrom` and `submittedTo` are inclusive bounds on `submittedAt`. Rows
with no submit time are excluded. `updatedAfter` is an exclusive lower bound on
`updatedAt`.

Read full rows against `definition` on that row. Do not join them back to the
live survey document.


# Mount the server (https://survey.dimah.dev/docs/integration)



`dimahSurvey()` creates one audience at a time. Every instance exposes a Fetch
`handler` for HTTP and a typed `api` for in-process server calls. Both audiences
must use the same `SurveyStore`.

<Callout>
  Mount fill and editor on separate paths. A handler never exposes operations
  from the other audience.
</Callout>

## Create the instances [#create-the-instances]

```ts title="lib/survey.ts"
import {
  SURVEY_API_BASE_PATH,
  SURVEY_EDITOR_API_BASE_PATH,
  dimahSurvey,
  guardAnonymous,
  memoryAdapter,
} from "@dimah-survey/server";

const database = memoryAdapter();

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

export const fill = dimahSurvey({
  audience: "fill",
  database,
  basePath: SURVEY_API_BASE_PATH,
  guard: guardAnonymous(),
});
```

`basePath` is part of the request contract and must match the corresponding
browser client's `baseURL`. The defaults are `/api/survey` for fill and
`/api/admin/survey` for editor.

A fill instance without `guard` throws at startup. An editor guard returns
nothing or throws. See [Security](https://survey.dimah.dev/docs/security.md).

`memoryAdapter()` is for tests and local development. Choose SQL or a custom
`SurveyStore` in [Persistence](https://survey.dimah.dev/docs/persistence.md).

## Mount the HTTP handlers [#mount-the-http-handlers]

```ts title="app/api/survey/[[...path]]/route.ts"
import { toNextJsHandler } from "@dimah-survey/server/next";
import { fill } from "@/lib/survey";

export const { GET, POST, PUT, PATCH, DELETE } = toNextJsHandler(fill);
```

Mount the editor the same way at `app/api/admin/survey/[[...path]]/route.ts`
with `toNextJsHandler(editor)`.

Each framework adapter forwards the same Fetch handler; it never interprets
SurveyJS JSON.

| Runtime   | Export                                                   |
| --------- | -------------------------------------------------------- |
| Next.js   | `@dimah-survey/server/next` → `toNextJsHandler`          |
| Express   | `@dimah-survey/server/express` → `toExpressHandler`      |
| Hono      | `@dimah-survey/server/hono` → `toHonoHandler`            |
| Fastify   | `@dimah-survey/server/fastify` → `toFastifyHandler`      |
| Elysia    | `@dimah-survey/server/elysia` → `toElysiaHandler`        |
| SvelteKit | `@dimah-survey/server/svelte-kit` → `toSvelteKitHandler` |
| Node.js   | `@dimah-survey/server/node` → `toNodeHandler`            |

<Callout type="warn">
  The Node-based adapters need the unread request stream. Mount Express before
  `express.json()`. Configure Fastify so its JSON parser does not consume these
  paths before the adapter.
</Callout>

<Tabs items="[&#x22;Express&#x22;, &#x22;Hono&#x22;, &#x22;Fastify&#x22;, &#x22;Elysia&#x22;, &#x22;SvelteKit&#x22;, &#x22;Node.js&#x22;]">
  <Tab value="Express">
    ```ts
    import express from "express";
    import { toExpressHandler } from "@dimah-survey/server/express";
    import { fill } from "./survey";

    const app = express();
    app.all("/api/survey/*", toExpressHandler(fill));
    app.use(express.json());
    ```
  </Tab>

  <Tab value="Hono">
    ```ts
    import { Hono } from "hono";
    import { toHonoHandler } from "@dimah-survey/server/hono";
    import { fill } from "./survey";

    const app = new Hono();
    app.on(
      ["GET", "POST", "PUT", "PATCH", "DELETE"],
      "/api/survey/*",
      toHonoHandler(fill),
    );
    ```
  </Tab>

  <Tab value="Fastify">
    ```ts
    import Fastify from "fastify";
    import { toFastifyHandler } from "@dimah-survey/server/fastify";
    import { fill } from "./survey";

    const app = Fastify();
    app.all("/api/survey/*", toFastifyHandler(fill));
    ```
  </Tab>

  <Tab value="Elysia">
    ```ts
    import { Elysia } from "elysia";
    import { toElysiaHandler } from "@dimah-survey/server/elysia";
    import { fill } from "./survey";

    new Elysia()
      .all("/api/survey/*", toElysiaHandler(fill))
      .listen(3000);
    ```
  </Tab>

  <Tab value="SvelteKit">
    ```ts title="src/routes/api/survey/[...path]/+server.ts"
    import { toSvelteKitHandler } from "@dimah-survey/server/svelte-kit";
    import { fill } from "$lib/survey";

    const handler = toSvelteKitHandler(fill);
    export const GET = handler;
    export const POST = handler;
    export const PUT = handler;
    export const PATCH = handler;
    export const DELETE = handler;
    ```
  </Tab>

  <Tab value="Node.js">
    ```ts
    import { createServer } from "node:http";
    import { toNodeHandler } from "@dimah-survey/server/node";
    import { fill } from "./survey";

    createServer(toNodeHandler(fill)).listen(3000);
    ```
  </Tab>
</Tabs>

Repeat the same adapter setup for the editor path. Never send both audiences
through one instance.

## Call the API on the server [#call-the-api-on-the-server]

`fill.api` and `editor.api` take `{ body }` or `{ query }`. They do not perform
an HTTP round trip. Pass `headers` or a `Request` when the guard needs cookies
or other caller context.

```ts
await editor.api.publishSurvey({
  body: { id: "welcome" },
});
```

```ts
import { headers } from "next/headers";

await fill.api.startResponse({
  body: { surveyId: "welcome" },
  headers: await headers(),
});
```

Without forwarded request data, an in-process call has no session cookie.

## Call from the browser [#call-from-the-browser]

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

const fillClient = createFillClient({ baseURL: "/api/survey" });
await fillClient.startResponse({ surveyId: "welcome" });
```

Browser clients take flat objects and perform HTTP. Import them from
`@dimah-survey/react` in React code or `@dimah-survey/core` elsewhere. Do not
send a server instance to the client bundle.

See the complete method and route map in [HTTP protocol](https://survey.dimah.dev/docs/protocol.md).


# 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.


# Authorization and identity (https://survey.dimah.dev/docs/security)



dimah-survey has no session, account, or role system. Your application
authorizes requests in `guard`; the library turns the result into response
ownership and audience isolation.

<Flow
  label="Fill request"
  steps="[
  { name: &#x22;Guard&#x22;, kind: &#x22;server&#x22;, note: &#x22;Return a principal&#x22; },
  { name: &#x22;Stamp&#x22;, kind: &#x22;protocol&#x22;, note: &#x22;Respondent id&#x22; },
  { name: &#x22;Validate&#x22;, kind: &#x22;server&#x22;, note: &#x22;Snapshot&#x22; },
  { name: &#x22;Store&#x22;, kind: &#x22;data&#x22;, note: &#x22;Persist&#x22; },
]"
/>

## Isolate the audiences [#isolate-the-audiences]

| Audience | Guard contract                                     | Access                                                |
| -------- | -------------------------------------------------- | ----------------------------------------------------- |
| Fill     | Return `{ respondentId }` or `{ anonymous: true }` | Published read and response lifecycle                 |
| Editor   | Return nothing; throw to reject                    | Authoring, publish, settings, and full response reads |

A fill instance requires `guard`. Returning nothing from a fill guard is
invalid. An editor guard must not return a principal. Keep the routes and
instances separate even when both use the same store.

`getPublishedSurvey` is on the fill handler. It does not stamp a respondent and
does not require a response id. The editor draft is not on that route.

Only editor may request `include: "full"` and read response definitions and
data in bulk. Fill owns response mutations. Submit hooks and `validateResult`
therefore belong to fill; publish hooks belong to editor.

## Stamp an identified respondent [#stamp-an-identified-respondent]

`guardRespondent(id)` returns `{ respondentId }`. The server then:

* stamps that id onto `startResponse` and `listResponses`
* rejects a different id in the body or query
* rejects `include: "full"`
* rejects get, partial save, submit, abandon, and reopen when the stored
  `respondentId` differs

```ts title="lib/survey.ts"
import {
  APIError,
  SURVEY_ERROR_CODES,
  dimahSurvey,
  guardRespondent,
} from "@dimah-survey/server";

export const fill = dimahSurvey({
  audience: "fill",
  database,
  guard: async (context) => {
    const session = await getSession(context.request);
    if (!session) {
      throw APIError.from("FORBIDDEN", SURVEY_ERROR_CODES.FORBIDDEN);
    }
    return guardRespondent(session.user.id)(context);
  },
});
```

The browser never chooses ownership. A supplied `respondentId` must match the
guard; omitting it still stamps the server-established id.

Identified start follows `settings.responses`. See [Settings](https://survey.dimah.dev/docs/settings.md).

## Treat anonymous ids as capabilities [#treat-anonymous-ids-as-capabilities]

`guardAnonymous()` returns `{ anonymous: true }`. Each start inserts a row.
Listing and a body `respondentId` are refused. The response id itself becomes
the capability for read and mutation, and the row must have a null
`respondentId`.

```ts title="lib/survey.ts"
import { guardAnonymous, guardRespondent } from "@dimah-survey/server";

export const fill = dimahSurvey({
  audience: "fill",
  database,
  guard: (context) => {
    const userId = userIdFromSession(context.request);
    if (!userId) return guardAnonymous()(context);
    return guardRespondent(userId)(context);
  },
});
```

<Callout type="warn">
  Store and transmit an anonymous response id like a secret. Anyone who has it
  can read and mutate that response.
</Callout>

## Protect the editor route [#protect-the-editor-route]

```ts title="lib/survey.ts"
import { APIError, SURVEY_ERROR_CODES, dimahSurvey } from "@dimah-survey/server";

export const editor = dimahSurvey({
  audience: "editor",
  database,
  guard: ({ request }) => {
    if (!isEditor(request)) {
      throw APIError.from("FORBIDDEN", SURVEY_ERROR_CODES.FORBIDDEN);
    }
  },
});
```

Returning a principal from this guard is an error. Editor authorization only
allows the request or throws.

`include: "full"` belongs here. It returns `definition` and `data` for
analytics. Do not expose that route on the fill path.

## Place side effects deliberately [#place-side-effects-deliberately]

`onStart`, `onSubmit`, and `onPublish` may abort their write. Their `after*`
counterparts run after storage; throwing there does not roll the row back.
Email, webhooks, and queue work generally belong in `after*` callbacks and must
be safe to retry at the application boundary.

A successful idempotent resubmit does not run submit hooks again.
`resumeSurvey` does not run publish hooks.


# Fill with React (https://survey.dimah.dev/docs/react)



`useSurveyResponse()` reads a response, constructs a SurveyJS `Model` from its
stored definition, restores its data, and binds partial save and submit. Your
application still renders the model with `survey-react-ui`.

<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-survey/react react survey-core survey-react-ui
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @dimah-survey/react react survey-core survey-react-ui
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @dimah-survey/react react survey-core survey-react-ui
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @dimah-survey/react react survey-core survey-react-ui
    ```
  </CodeBlockTab>
</CodeBlockTabs>

<Callout>
  `@dimah-survey/react` is a lifecycle binding, not a renderer wrapper. You
  retain direct access to the SurveyJS model and component.
</Callout>

```tsx title="components/fill.tsx"
"use client";

import dynamic from "next/dynamic";
import { createFillClient, useSurveyResponse } from "@dimah-survey/react";

import "survey-core/survey-core.min.css";

const Survey = dynamic(
  () => import("survey-react-ui").then((mod) => mod.Survey),
  { ssr: false },
);

const client = createFillClient({ baseURL: "/api/survey" });

export function Fill({ responseId }: { responseId: string }) {
  const { model, error, saveError, stale, reload } = useSurveyResponse({
    client,
    responseId,
  });

  if (error) return <p>{error.message}</p>;
  if (!model) return null;

  return (
    <>
      {saveError ? <p>{saveError.message}</p> : null}
      {stale ? <button onClick={reload}>Reload</button> : null}
      <Survey model={model} />
    </>
  );
}
```

SurveyJS UI components require a browser, so Next.js applications should load
`<Survey>` dynamically. A submitted or abandoned response opens in `display`
mode and sends no writes.

## Options [#options]

```ts
import type { UseSurveyResponseOptions } from "@dimah-survey/react";
```

<AutoTypeTable path="packages/react/src/use-survey-response.ts" name="UseSurveyResponseOptions" />

## Handle state [#handle-state]

```ts
import type { SurveyResponseBinding } from "@dimah-survey/react";
```

<AutoTypeTable path="packages/react/src/use-survey-response.ts" name="SurveyResponseBinding" />

Completion waits for the server. If submit fails, SurveyJS does not complete
and the respondent keeps their mounted model.

Each partial save sends the complete `survey.data` object; it is not a
key-level patch.

Writes are queued. Each one sends `expectedUpdatedAt` from the last successful
read. See [Responses](https://survey.dimah.dev/docs/responses.md).

## Bind an existing model [#bind-an-existing-model]

Use `bindSurveyModel()` when your application already owns the `Model`.
`useSurveyResponse()` is the load plus that binding.

```ts
import { bindSurveyModel } from "@dimah-survey/react";

const dispose = bindSurveyModel(model, {
  savePartial: (data) => client.savePartial({ id, data, expectedUpdatedAt }),
  submit: (data) => client.submitResponse({ id, data, expectedUpdatedAt }),
  onWriteError: (error) => {
    console.error(error);
  },
});
```

```ts
import type {
  SurveyModelActions,
  SurveyModelBindOptions,
} from "@dimah-survey/react";
```

<AutoTypeTable path="packages/react/src/bind-survey-model.ts" name="SurveyModelActions" />

<AutoTypeTable path="packages/react/src/bind-survey-model.ts" name="SurveyModelBindOptions" />

Call `dispose` on unmount. It removes the SurveyJS handlers it added.

`bindSurveyModel` also sets `storeDataAsText` to `false` on file and signature
questions, including questions added later.

## Keep file storage in your app [#keep-file-storage-in-your-app]

File bytes stay in your application. There is no upload route.

Handle `onUploadFiles`, `onDownloadFile`, and `onClearFiles` on the `Model`.
Give SurveyJS `{ file, content }` where `content` is your URL. Partial save
writes that locator into `data`. The binding does not store the bytes.

Start with the [Quickstart](https://survey.dimah.dev/docs/quickstart.md) for the complete load and render
flow. Creator autosave uses a separate binding described in
[Survey Creator](https://survey.dimah.dev/docs/creator.md).


# Survey Creator (https://survey.dimah.dev/docs/creator)



`useSurveyDraft()` connects a Creator instance to `saveSurvey`. Autosave
replaces `draftJson`; it never changes `publishedJson` or any response
definition.

<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-survey/react survey-creator-core survey-creator-react
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @dimah-survey/react survey-creator-core survey-creator-react
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @dimah-survey/react survey-creator-core survey-creator-react
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @dimah-survey/react survey-creator-core survey-creator-react
    ```
  </CodeBlockTab>
</CodeBlockTabs>

<Callout>
  Publishing is an explicit product action, not an autosave side effect. New
  responses see the draft only after `publishSurvey`.
</Callout>

```tsx title="components/design.tsx"
"use client";

import { useMemo } from "react";
import { createEditorClient, useSurveyDraft } from "@dimah-survey/react";
import { SurveyCreator, SurveyCreatorComponent } from "survey-creator-react";

const editorClient = createEditorClient({ baseURL: "/api/admin/survey" });

export function Design({
  surveyId,
  draftJson,
  updatedAt,
}: {
  surveyId: string;
  draftJson: object;
  updatedAt: string;
}) {
  const creator = useMemo(() => {
    const next = new SurveyCreator();
    next.JSON = draftJson;
    return next;
  }, [draftJson]);

  const { saveError, stale } = useSurveyDraft({
    client: editorClient,
    surveyId,
    creator,
    updatedAt,
  });

  return (
    <>
      {saveError ? <p>{saveError.message}</p> : null}
      {stale ? <p>This draft was saved somewhere else.</p> : null}
      <SurveyCreatorComponent creator={creator} />
    </>
  );
}
```

Load `draftJson` and `updatedAt` on the server with `editor.api.getSurvey`,
then pass both into the client component. `createEditorClient()` defaults to
`/api/admin/survey`.

```ts
import type { UseSurveyDraftOptions } from "@dimah-survey/react";
```

<AutoTypeTable path="packages/react/src/use-survey-draft.ts" name="UseSurveyDraftOptions" />

The hook sets `isAutoSave` and `saveSurveyFunc`. Each save sends
`expectedUpdatedAt` from the loaded survey, then from the last successful
write. A new `updatedAt` prop rebinds that token.

```ts
import type { SurveyDraftBinding } from "@dimah-survey/react";
```

<AutoTypeTable path="packages/react/src/use-survey-draft.ts" name="SurveyDraftBinding" />

Unlike the fill hook, `useSurveyDraft()` has no `reload()` method. Reload the
survey in your application before writing again.

## Publish explicitly [#publish-explicitly]

Autosave must not call `publishSurvey`. Publish copies the current draft onto
`publishedJson` and is an explicit action:

```ts
await editorClient.publishSurvey({ id: surveyId, expectedUpdatedAt });
```

On the server, the equivalent call is
`editor.api.publishSurvey({ body })`. Archive, resume, and settings are editor
calls too. See [Surveys](https://survey.dimah.dev/docs/surveys.md).

## Bind without React state [#bind-without-react-state]

`bindSurveyCreator()` exposes the same binding without React state. Pass
`initialUpdatedAt` from the load that produced the Creator instance. The app
still constructs, renders, and disposes Creator.

```ts
import type { SurveyCreatorActions } from "@dimah-survey/react";
```

<AutoTypeTable path="packages/react/src/bind-survey-creator.ts" name="SurveyCreatorActions" />

`survey-creator-core` and `survey-creator-react` stay in your application.
They are not dependencies of `@dimah-survey/react`.

See [Survey lifecycle](https://survey.dimah.dev/docs/surveys.md) for publish, archive, resume, and
compare-and-swap behavior.


# Packages (https://survey.dimah.dev/docs/packages)



The four packages version together but keep runtime boundaries separate. A
typical React application installs `server` and `react`, then adds `db` for the
bundled SQL store.

<Callout>
  Published on npm, still before `1.0.0`. A release may change the API.
</Callout>

| Package                | Install when you need                                       | Does not own       |
| ---------------------- | ----------------------------------------------------------- | ------------------ |
| `@dimah-survey/core`   | Protocol types, clients, schemas, errors, or a custom store | SurveyJS runtime   |
| `@dimah-survey/server` | Handlers, guards, validation, adapters, and local memory    | UI or SQL ORM      |
| `@dimah-survey/react`  | `Model` and Creator lifecycle bindings                      | SurveyJS renderers |
| `@dimah-survey/db`     | FumaDB-backed SQL storage and schema references             | Your migrations    |

<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-survey/server @dimah-survey/react survey-core survey-react-ui
    ```
  </CodeBlockTab>

  <CodeBlockTab value="pnpm">
    ```bash
    pnpm add @dimah-survey/server @dimah-survey/react survey-core survey-react-ui
    ```
  </CodeBlockTab>

  <CodeBlockTab value="yarn">
    ```bash
    yarn add @dimah-survey/server @dimah-survey/react survey-core survey-react-ui
    ```
  </CodeBlockTab>

  <CodeBlockTab value="bun">
    ```bash
    bun add @dimah-survey/server @dimah-survey/react survey-core survey-react-ui
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Add `@dimah-survey/db fumadb` for the bundled SQL store. Install
`survey-creator-core` and `survey-creator-react` only when your application
renders Creator.

## `@dimah-survey/server` [#dimah-surveyserver]

Start here for backend integration:

* `dimahSurvey()` for fill and editor instances
* `guardRespondent()` and `guardAnonymous()` for fill guards
* `memoryAdapter()` for local development and tests
* `checkSurveyResult()` and `clearSurveyResult()` for validation behavior
* adapters for Next.js, Node, Express, Hono, Fastify, Elysia, and SvelteKit

The package re-exports common error and client utilities for server
convenience. Browser bundles should import clients from `react` or `core`.

## `@dimah-survey/react` [#dimah-surveyreact]

Use this in React client code:

* `useSurveyResponse()` and `bindSurveyModel()` for fill sessions
* `useSurveyDraft()` and `bindSurveyCreator()` for Creator autosave
* `isStaleUpdate()` for write-state handling
* `createFillClient()` and `createEditorClient()` from `core`

It requires React and `survey-core` as peers. Your application imports
`survey-react-ui` and `survey-creator-react`, constructs their objects, and
renders their components.

## `@dimah-survey/core` [#dimah-surveycore]

Use `core` for non-React clients, shared protocol code, or custom stores. It
contains the fill/editor clients, route constants, Zod schemas, stable error
codes, list and settings helpers, and `SurveyStore` types.

Most applications use `createFillClient()` or `createEditorClient()`. Reach
for `createSurveyFetch()` only when you need the lower-level better-fetch
surface.

## `@dimah-survey/db` [#dimah-surveydb]

`db(client)` adapts a FumaDB client to `SurveyStore`. `DimahSurveyDB` describes
the versioned schema; Drizzle, SQL, and Prisma exports are readable reference
files.

<Callout type="warn">
  Copy or generate those schema references into your application, then migrate
  the application-owned file. Do not import a reference schema into a running
  application.
</Callout>

The database package is optional. Any store must preserve immutable response
definitions, compare-and-swap inside writes, and one open draft per identified
respondent.

Continue with [Configuration](https://survey.dimah.dev/docs/configuration.md) for runtime options or
[HTTP protocol](https://survey.dimah.dev/docs/protocol.md) for the operation map.


# Configuration (https://survey.dimah.dev/docs/configuration)



Fill and editor have separate configuration types because they expose different
trust boundaries. Both require the same `database`; only fill accepts response
validation and only editor accepts publish hooks.

## Fill instance [#fill-instance]

```ts
import {
  dimahSurvey,
  guardAnonymous,
  memoryAdapter,
} from "@dimah-survey/server";

export const fill = dimahSurvey({
  audience: "fill",
  database: memoryAdapter(),
  guard: guardAnonymous(),
});
```

```ts
import type { DimahFillConfig } from "@dimah-survey/server";
```

<AutoTypeTable path="packages/server/src/dimah-survey.ts" name="DimahFillConfig" />

`checkSurveyResult` drops values that cannot be assigned, runs `validate`, and
returns the `survey.data` to persist. Questions with `choicesByUrl` keep their
posted value. The server does not fetch that list.

A custom `validateResult` receives `{ definition, data }`. Return the object to
store. Return nothing to keep `data`. Throw `APIError` with `VALIDATION_FAILED`
to reject the submit. See [Responses](https://survey.dimah.dev/docs/responses.md).

### guard [#guard]

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

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

Returning nothing from a fill guard is forbidden. `guardRespondent(id)` returns
`{ respondentId }`. `guardAnonymous()` returns `{ anonymous: true }`.

### Lifecycle hooks [#lifecycle-hooks]

```ts
import type { FillHooks } from "@dimah-survey/server";
```

<AutoTypeTable path="packages/server/src/dimah-survey.ts" name="FillHooks" />

A start that returns an existing row does not call `onStart` or `afterStart`.
A replayed submit does not call `onSubmit` or `afterSubmit`. The HTTP body
cannot carry these hooks.

## Editor instance [#editor-instance]

```ts
import { dimahSurvey } from "@dimah-survey/server";

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

```ts
import type { DimahEditorConfig } from "@dimah-survey/server";
```

<AutoTypeTable path="packages/server/src/dimah-survey.ts" name="DimahEditorConfig" />

```ts
import type { EditorHooks } from "@dimah-survey/server";
```

<AutoTypeTable path="packages/server/src/dimah-survey.ts" name="EditorHooks" />

## Browser clients [#browser-clients]

`createFillClient` and `createEditorClient` accept the same options. Only the
default `baseURL` differs.

```ts
import type { CreateSurveyClientOptions } from "@dimah-survey/react";
```

<AutoTypeTable path="packages/core/src/client.ts" name="CreateSurveyClientOptions" />

Browser methods take flat objects. `fill.api` and `editor.api` take `{ body }`
or `{ query }`, plus optional `headers` or `request` for guards. See
[Mount the server](https://survey.dimah.dev/docs/integration.md).

## React [#react]

```ts
import type { UseSurveyResponseOptions } from "@dimah-survey/react";
```

<AutoTypeTable path="packages/react/src/use-survey-response.ts" name="UseSurveyResponseOptions" />

Returned state is documented in [React](https://survey.dimah.dev/docs/react.md).

```ts
import type { UseSurveyDraftOptions } from "@dimah-survey/react";
```

<AutoTypeTable path="packages/react/src/use-survey-draft.ts" name="UseSurveyDraftOptions" />

The hook does not construct Creator. See [Creator](https://survey.dimah.dev/docs/creator.md).


# HTTP protocol (https://survey.dimah.dev/docs/protocol)



Route constants and payload schemas live in `@dimah-survey/core`.

| Audience | Default base path   | Operations                                          |
| -------- | ------------------- | --------------------------------------------------- |
| Fill     | `/api/survey`       | Published read and respondent response lifecycle    |
| Editor   | `/api/admin/survey` | Authoring, settings, publish, and response analysis |

The suffixes below are appended to those bases. Browser clients take flat
objects; `fill.api` and `editor.api` take `{ query }` or `{ body }`. An
operation from the wrong audience is not mounted and returns `NOT_FOUND`.

List endpoints default to 50 rows and cap at 100.

## Fill routes [#fill-routes]

| Method | Path                | Browser client                               | In-process API                  |
| ------ | ------------------- | -------------------------------------------- | ------------------------------- |
| `GET`  | `/survey/published` | `getPublishedSurvey(id)`                     | `getPublishedSurvey({ query })` |
| `POST` | `/response/start`   | `startResponse({ surveyId, respondentId? })` | `startResponse({ body })`       |
| `GET`  | `/response`         | `getResponse(id)`                            | `getResponse({ query })`        |
| `GET`  | `/responses`        | `listResponses(query)`                       | `listResponses({ query })`      |
| `POST` | `/response/partial` | `savePartial(input)`                         | `savePartial({ body })`         |
| `POST` | `/response/submit`  | `submitResponse(input)`                      | `submitResponse({ body })`      |
| `POST` | `/response/abandon` | `abandonResponse(input)`                     | `abandonResponse({ body })`     |
| `POST` | `/response/reopen`  | `reopenResponse(input)`                      | `reopenResponse({ body })`      |

On survey reads, `id` accepts a survey id or slug. `startResponse.surveyId`
accepts either as well. Every response mutation uses the response row id.

`getPublishedSurvey` returns `publishedJson` and `settings`. It omits
`draftJson`. It is available to both fill principals and does not stamp a
respondent.

The guard stamps `respondentId` for identified callers. A matching value in the
body is tolerated, but the browser cannot claim another owner. Anonymous
callers cannot list responses; identified callers cannot request
`include: "full"`.

## Editor routes [#editor-routes]

| Method | Path               | Browser client              | In-process API                 |
| ------ | ------------------ | --------------------------- | ------------------------------ |
| `GET`  | `/survey`          | `getSurvey(id)`             | `getSurvey({ query })`         |
| `POST` | `/survey`          | `saveSurvey(input)`         | `saveSurvey({ body })`         |
| `GET`  | `/surveys`         | `listSurveys(query)`        | `listSurveys({ query })`       |
| `POST` | `/survey/publish`  | `publishSurvey(input)`      | `publishSurvey({ body })`      |
| `POST` | `/survey/archive`  | `archiveSurvey(input)`      | `archiveSurvey({ body })`      |
| `POST` | `/survey/settings` | `saveSurveySettings(input)` | `saveSurveySettings({ body })` |
| `POST` | `/survey/resume`   | `resumeSurvey(input)`       | `resumeSurvey({ body })`       |
| `GET`  | `/response`        | `getResponse(id)`           | `getResponse({ query })`       |
| `GET`  | `/responses`       | `listResponses(query)`      | `listResponses({ query })`     |

`saveSurvey` creates the survey when the id is new. `slug` defaults to `id`.
Pass `expectedUpdatedAt` when updating. A create that includes the token fails
with `STALE_UPDATE`.

`listSurveys` returns `{ surveys, limit, offset, nextOffset }`.
`listResponses` returns `{ responses, limit, offset, nextOffset, total }`.

## List queries [#list-queries]

`listResponses` accepts:

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

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

`"summary"` omits `definition` and `data`. `"full"` is the editor analytics
read. Rows sort by `updatedAt` descending.

`listSurveys` accepts:

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

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

## Compare-and-swap inputs [#compare-and-swap-inputs]

`expectedUpdatedAt` is optional on draft, publish, archive, settings, resume,
partial save, submit, abandon, and reopen. When it is set, the stored
`updatedAt` must match or the write fails with `STALE_UPDATE`. The React
bindings always send the token from their last successful read or write.

The exported Zod schemas are the authoritative payload definitions. See
[Errors](https://survey.dimah.dev/docs/errors.md) for the stable failure codes.


# Errors (https://survey.dimah.dev/docs/errors)



Failed HTTP requests return:

```ts
type SurveyErrorBody = {
  message: string;
  code?: SurveyErrorCode;
  questions?: string[];
};
```

Throw with `APIError.from` and a code from `SURVEY_ERROR_CODES`. A plain
`Error` is normalized to `INTERNAL_ERROR`. Treat `code` as the stable contract;
messages are for diagnostics.

```ts
import {
  APIError,
  SURVEY_ERROR_CODES,
  isAPIError,
} from "@dimah-survey/server";

throw APIError.from("NOT_FOUND", SURVEY_ERROR_CODES.SURVEY_NOT_FOUND);
throw APIError.from("FORBIDDEN", SURVEY_ERROR_CODES.FORBIDDEN);
throw APIError.from("CONFLICT", SURVEY_ERROR_CODES.STALE_UPDATE);

if (isAPIError(error)) {
  console.log(error.code, error.message);
}
```

`APIError` and `isAPIError` are also exported from `@dimah-survey/core`. There
is no `UNAUTHORIZED` code; guards reject callers with `FORBIDDEN`.

## Codes [#codes]

| Code                 | HTTP | Meaning                                                          |
| -------------------- | ---- | ---------------------------------------------------------------- |
| `NOT_FOUND`          | 404  | No route for the requested operation on this audience            |
| `SURVEY_NOT_FOUND`   | 404  | Survey id or slug is missing, or no active published read exists |
| `RESPONSE_NOT_FOUND` | 404  | Response id is missing                                           |
| `FORBIDDEN`          | 403  | Guard rejected the caller or ownership check failed              |
| `NOT_PUBLISHED`      | 409  | Start or resume has no eligible published document               |
| `SURVEY_CLOSED`      | 409  | Start, partial save, or submit is outside the collection window  |
| `RESPONSE_LIMIT`     | 409  | Submitted rows reached `maxResponses`                            |
| `SLUG_TAKEN`         | 409  | Another survey owns the slug                                     |
| `STALE_UPDATE`       | 409  | `expectedUpdatedAt` no longer matches                            |
| `RESPONSE_CLOSED`    | 409  | Response state forbids the write or reopen is disabled           |
| `OPEN_DRAFT`         | 409  | Another draft already exists for this survey and respondent      |
| `VALIDATION_ERROR`   | 400  | Request body or query failed schema validation                   |
| `VALIDATION_FAILED`  | 400  | The response definition rejected answer data                     |
| `INTERNAL_ERROR`     | 500  | An unhandled exception reached the handler                       |

`SURVEY_NOT_FOUND` on `getPublishedSurvey` also covers a draft or archived
survey. The editor `getSurvey` uses the same code when the id is missing, and
returns the draft when the row exists.

## SurveyJS validation failures [#surveyjs-validation-failures]

`VALIDATION_FAILED` adds `questions`: the SurveyJS question names that failed.
The message lists the same names and is not localized.

```json
{
  "message": "Survey result is invalid: name, score",
  "code": "VALIDATION_FAILED",
  "questions": ["name", "score"]
}
```

`VALIDATION_ERROR` is a bad request body or query, such as a settings object
whose `closesAt` is not after `opensAt`. It has no `questions` array.

A resubmit that fails validation on an already submitted row is
`RESPONSE_CLOSED`, not `VALIDATION_FAILED`. The row is already closed. See
[Responses](https://survey.dimah.dev/docs/responses.md).
