Authorization and identity
Guard fill and editor independently, then establish response ownership on the server.
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.
- GuardReturn a principal
- StampRespondent id
- ValidateSnapshot
- StorePersist
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
guardRespondent(id) returns { respondentId }. The server then:
- stamps that id onto
startResponseandlistResponses - rejects a different id in the body or query
- rejects
include: "full" - rejects get, partial save, submit, abandon, and reopen when the stored
respondentIddiffers
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.
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.
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);
},
});Store and transmit an anonymous response id like a secret. Anyone who has it can read and mutate that response.
Protect the editor route
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
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.