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