# Security (https://form.dimah.dev/docs/security)



dimah-form does not ship sessions or roles. Your application authorizes each
operation in `guard`, sets trusted ownership in `onStart`, and runs side effects
in `after*`.

<Flow
  label="Authorized write"
  steps="[
  { name: &#x22;Guard&#x22;, kind: &#x22;server&#x22;, note: &#x22;Reject early&#x22; },
  { name: &#x22;Validate&#x22;, kind: &#x22;protocol&#x22;, note: &#x22;Snapshot&#x22; },
  { name: &#x22;on*&#x22;, kind: &#x22;server&#x22;, note: &#x22;Set ownership&#x22; },
  { name: &#x22;Store&#x22;, kind: &#x22;data&#x22;, note: &#x22;Persist&#x22; },
  { name: &#x22;after*&#x22;, kind: &#x22;server&#x22;, note: &#x22;Side effects&#x22; },
]"
/>

## Authorize requests [#authorize-requests]

`guard` runs before form logic. Throw an `APIError` with a stable code to reject
the request. Use `getResponse()` and `getForm()` for ownership checks; they read
the store and do not call HTTP or re-enter `guard`.

```ts title="lib/form.ts"
import {
  APIError,
  FORM_ERROR_CODES,
  dimahForm,
  memoryAdapter,
} from "@dimah-form/server";

export const form = dimahForm({
  database: memoryAdapter(),
  forms,
  guard: async ({ request, operation, responseId, getResponse }) => {
    const session = await getSession(request);
    if (!session) {
      throw APIError.from("UNAUTHORIZED", FORM_ERROR_CODES.UNAUTHORIZED);
    }

    if (operation === "listResponses" && session.user.role !== "admin") {
      throw APIError.from("FORBIDDEN", FORM_ERROR_CODES.FORBIDDEN);
    }

    if (!responseId) return;
    const response = await getResponse(responseId);
    if (response?.respondentId !== session.user.id) {
      throw APIError.from("FORBIDDEN", FORM_ERROR_CODES.FORBIDDEN);
    }
  },
});
```

Plugin endpoints use their endpoint key as `operation`, so apply the same
authorization policy to them.

## Trust the server, not the browser [#trust-the-server-not-the-browser]

Set `respondentId` after validation and before persistence. Do not accept an
owner sent from client code.

```ts title="lib/form.ts"
export const form = dimahForm({
  database,
  hooks: {
    onStart: async ({ request, response }) => {
      const session = await getSession(request);
      if (session) response.respondentId = session.user.id;
    },
    afterSubmit: async ({ response }) => {
      await sendReceipt(response);
    },
  },
});
```

`on*` hooks run after validation and before the write; throwing aborts it.
`after*` hooks run after a successful write. An `after*` failure can make the
HTTP request fail even though the row is already stored, so design side effects
to be idempotent.

`onStart` stamps newly created rows. When `resume: true` finds an existing
draft, that row is returned as-is, so the respondent id used for lookup must
already be trusted.

## Resume authenticated drafts [#resume-authenticated-drafts]

Resolve authenticated ownership on the server. Call `form.api.startResponse`
with the respondent id from the session, then pass the returned response to
`useFormResponse`. This keeps the lookup key out of browser control.

```ts title="app/page.tsx"
import { headers } from "next/headers";
import { form } from "@/lib/form";

const session = await requireSession();
const response = await form.api.startResponse({
  body: {
    formId: "feedback",
    respondentId: session.user.id,
    resume: true,
  },
  headers: await headers(),
});
```

For an anonymous flow, use an app-issued, unguessable resume token and treat it
as a credential. Never use an email address or another predictable identifier
as a public resume key.

## Browser credentials [#browser-credentials]

Configure the client for your own session scheme:

```ts title="lib/form-client.ts"
export const formClient = createFormClient<Form>({
  basePath: "/api/form",
  credentials: "include",
  headers: async () => {
    const token = await getClientToken();
    return token ? { Authorization: `Bearer ${token}` } : {};
  },
});
```

`form.api` has no browser context; pass request headers explicitly when its
`guard` needs them.

## Hook context [#hook-context]

```ts
import type {
  DimahFormGuardContext,
  DimahFormHooks,
  ResponseHookContext,
} from "@dimah-form/server";
```

<AutoTypeTable path="packages/server/src/types.ts" name="DimahFormGuardContext" />

<AutoTypeTable path="packages/server/src/types.ts" name="DimahFormHooks" />

<AutoTypeTable path="packages/server/src/types.ts" name="ResponseHookContext" />

## Next [#next]

<Cards>
  <Card title="Errors" href="/docs/errors" description="Use stable error codes in guards and clients." />

  <Card title="Plugins" href="/docs/plugins/scoring" description="Authorize optional plugin endpoints in the same guard." />
</Cards>
