dimah-formv0.2.0

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.

Security & Execution Pipeline
  1. 1. GuardAuthenticate & check permissions
  2. 2. ValidateValidate answers vs snapshot
  3. 3. on* HookPre-write mutation (e.g. set respondentId)
  4. 4. PersistAtomic database write & CAS
  5. 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:

lib/form.ts
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

ArgumentTypeDescription
requestRequestStandard Fetch Request object with headers, cookies, and URL.
operationFormApiOperation | stringName of the active operation (e.g., "startResponse", "submitResponse").
formIdstring | undefinedForm identifier if present in the query or body payload.
responseIdstring | undefinedResponse 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:

lib/form-client.ts
"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:

lib/form.ts
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 the response record 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.

Next Steps

On this page