Auth
Guard every operation in your app. Use on* / after* hooks to stamp ownership and run side effects.
Auth is not a library feature. Put it in guard. Use on* / after* hooks to stamp respondentId and fire emails or webhooks after a successful write.
- 1. GuardAuthenticate & check permissions
- 2. ValidateValidate answers vs snapshot
- 3. on* HookPre-write mutation (e.g. set respondentId)
- 4. PersistAtomic database write & CAS
- 5. after* HookPost-write side effects (email, webhook)
The guard Security Hook
The guard hook executes before any database operation or endpoint handler. If guard throws an APIError, the request is immediately rejected:
import {
APIError,
FORM_ERROR_CODES,
dimahForm,
memoryAdapter,
} from "@dimah-form/server";
import { forms } from "@/lib/forms";
import { getAuthSession } from "@/lib/auth";
export const form = dimahForm({
database: memoryAdapter(),
forms,
guard: async ({ request, operation, formId, responseId, getResponse }) => {
// 1. Authenticate user from session cookies or Bearer token
const session = await getAuthSession(request);
// 2. Restrict administrative endpoints to Admins
const adminOperations = ["saveForm", "deleteForm", "listResponses"];
if (adminOperations.includes(operation)) {
if (!session || session.user.role !== "admin") {
throw APIError.from("FORBIDDEN", FORM_ERROR_CODES.FORBIDDEN);
}
}
// 3. Enforce Row-Level Respondent Ownership on response mutations
if (responseId) {
const record = await getResponse(responseId);
if (record && record.respondentId) {
if (!session || session.user.id !== record.respondentId) {
throw APIError.from("FORBIDDEN", FORM_ERROR_CODES.FORBIDDEN);
}
}
}
},
});Context Passed to guard
| Argument | Type | Description |
|---|---|---|
request | Request | Standard Fetch Request object with headers, cookies, and URL. |
operation | FormApiOperation | string | Name of the active operation (e.g., "startResponse", "submitResponse"). |
formId | string | undefined | Form identifier if present in the query or body payload. |
responseId | string | undefined | Response identifier if present in the query or body payload. |
getResponse | (id: string) => Promise<ResponseRecord | undefined> | Helper to fetch the current response record from the store. |
Sending Client Credentials & Headers
Configure createFormClient to automatically include auth tokens or session cookies with every request:
"use client";
import { createFormClient } from "@dimah-form/react";
import type { Form } from "@/lib/form";
import { getClientToken } from "@/lib/auth-client";
export const formClient = createFormClient<Form>({
basePath: "/api/form",
credentials: "include", // Include browser session cookies
headers: async () => {
const token = await getClientToken();
return token ? { Authorization: `Bearer ${token}` } : {};
},
});Pre-Write & Post-Write Lifecycle Hooks
Lifecycle hooks allow you to mutate records before persistence or run side-effects after persistence:
import { dimahForm, memoryAdapter } from "@dimah-form/server";
import { sendEmailNotification } from "@/lib/notifications";
import { getAuthSession } from "@/lib/auth";
export const form = dimahForm({
database: memoryAdapter(),
hooks: {
// 1. Pre-write: Runs before creating a new response in DB
onStart: async ({ request, response }) => {
const session = await getAuthSession(request);
if (session) {
response.respondentId = session.user.id;
}
},
// 2. Pre-write: Runs before saving a draft
onDraft: async ({ request, response, patch }) => {
console.log(`User updating draft for response ${response.id}`);
},
// 3. Post-write: Runs after successful submit transaction
afterSubmit: async ({ response }) => {
await sendEmailNotification({
to: "team@company.com",
subject: `New response submitted for form ${response.formId}`,
responseId: response.id,
});
},
// 4. Post-write: Runs after response is deleted
afterDelete: async ({ responseId }) => {
console.log(`Response ${responseId} was deleted.`);
},
},
});Hook Execution Rules
- Pre-write (
on*): Synchronously or asynchronously mutates theresponserecord before the database write commits. If an error is thrown, the write is aborted. - Post-write (
after*): Executes after the database write has succeeded. Safe for external network calls, transactional emails, or message queues.