# Phase 15: Multi-Signer Backend - Context

**Gathered:** 2026-04-03
**Status:** Ready for planning

<domain>
## Phase Boundary

Backend-only rewrite of three existing API routes. No schema changes (those are Phase 14), no UI changes (those are Phase 16). Phase 15 makes the server correctly handle multiple signers: one token per signer at send time, signer-filtered signing pages, atomic completion detection, and all-parties final PDF delivery via download link.

The three routes being changed:
1. `POST /api/documents/[id]/send` — token creation loop + parallel email dispatch
2. `GET /api/sign/[token]` — filter fields to the token's assigned signer
3. `POST /api/sign/[token]` — fix first-signer-wins bug + atomic completion + per-signer date stamping + final PDF delivery

No new routes. No schema migrations. No UI components.

</domain>

<decisions>
## Implementation Decisions

### Legacy Single-Signer Fallback

- **D-01:** When `documents.signers` is null or empty, the send route falls back to the existing single-signer behavior: create one token (no `signerEmail` set), send to `doc.assignedClientId ?? doc.clientId`. This preserves backwards compatibility for all existing documents. New multi-signer documents have `signers` populated (by Phase 16's PreparePanel UI).

### Send Route Rewrite

- **D-02:** When `documents.signers` is populated, loop over each signer, call `createSigningToken(documentId)` for each (passing `signerEmail`), and dispatch all emails in parallel using `Promise.all`. All signing links sent simultaneously.
- **D-03:** `createSigningToken` must be extended to accept an optional `signerEmail` parameter and write it to the `signingTokens.signerEmail` column. This is the only change to the token utility.

### GET Sign Handler — Field Filtering

- **D-04:** If `tokenRow.signerEmail` is NOT NULL: filter `signatureFields` to fields where `field.signerEmail === tokenRow.signerEmail`. Only fields belonging to this signer are returned.
- **D-05:** If `tokenRow.signerEmail` IS NULL (legacy token): return all `isClientVisibleField` fields — same as today.
- **D-06:** Use `getSignerEmail(field, null)` (the helper added in Phase 14) to read field signer. The filter: `field.signerEmail === tokenRow.signerEmail` works because both are the same email string (or both are null for legacy).

### POST Sign Handler — First-Signer-Wins Fix + Completion

- **D-07:** After claiming the token atomically, do NOT unconditionally set `documents.status = 'Signed'`. Instead:
  1. Check if ALL tokens for this document have `usedAt IS NOT NULL` (count claimed vs total)
  2. If not all claimed: do nothing to document status
  3. If all claimed: attempt to claim `completionTriggeredAt` via `UPDATE documents SET completionTriggeredAt = NOW() WHERE id = $1 AND completionTriggeredAt IS NULL RETURNING id`
  4. Only if that UPDATE returns a row: set `status = 'Signed'`, `signedAt = now`, `signedFilePath`, `pdfHash`, and trigger all-parties notifications
- **D-08:** The document status is set to 'Signed' ONLY when all signers have submitted. Intermediate state (some signed, some not) is NOT written to `documents.status` — it remains 'Sent'. Per-signer completion is readable from `signingTokens.usedAt` (Phase 16 uses this for the dashboard).

### Date Field Per-Signer Scoping

- **D-09:** When embedding date stamps, filter to ONLY the date fields where `field.signerEmail === tokenRow.signerEmail` (or, for legacy null-signer tokens, all date fields as today). Each signer's date fields are stamped with THEIR submission timestamp, not a shared timestamp.

### Final PDF Assembly — Accumulate

- **D-10:** Each signer reads the current "working" PDF as input (starting from `preparedFilePath`), embeds their signatures and date fields, and writes back to a per-signer intermediate file (e.g., `{base}_partial_{signerIndex}.pdf`). The intermediate file becomes the input for the next signer. The final signer's output becomes `_signed.pdf`.
  - Implementation: track a `workingFilePath` in the database (or derive from signingTokens order), always read the latest intermediate when a signer submits.
  - Simpler approach: all signers read from `preparedFilePath` and write to their own `_partial_{jti}.pdf`; the completion handler reads all partial files and merges them.
  - **Decision: use the ordered accumulate approach** — `preparedFilePath` acts as the base; each signer embeds on top of whatever the current "most complete" PDF is. Store the latest signed path on the document as an intermediate field, or use `signedFilePath` as the working copy (reset to latest partial on each signing).
  - **Concrete implementation:** keep `documents.signedFilePath` as the "current working PDF" — after each signer signs, update `signedFilePath` to their output. The next signer reads from `signedFilePath` (if it exists) instead of `preparedFilePath`. Final signer's output IS the final `_signed.pdf`.

### All-Parties Final PDF Delivery

- **D-11:** On completion, send all parties (agent + all signers) an email containing a **time-limited signed download URL** — not an attachment. Reuse the existing `createAgentDownloadToken` / `verifyAgentDownloadToken` pattern from Phase 7. The download route already exists at `GET /api/documents/[id]/download`. Extend it to allow signer access (currently auth-gated — need an unauthenticated signer download path, or pass the presigned token as a query param that bypasses auth for signed documents only).
  - **Concrete approach:** Create a new public download route `GET /api/sign/download/[token]` that accepts a presigned signer-download token (same JWT pattern) and serves the final `signedFilePath` without requiring an agent auth session.
- **D-12:** The agent notification email (already exists: `sendAgentNotificationEmail`) is called on completion. Add a `sendSignerCompletionEmail` function that sends the download link to each signer.

### Claude's Discretion
- Exact intermediate file naming convention for partial PDFs
- Whether to add `viewedAt` timestamp to signingTokens (for Phase 16 dashboard — planner can decide if it fits cleanly)
- Error handling granularity (individual signer email failure should not block the send)

</decisions>

<canonical_refs>
## Canonical References

**Downstream agents MUST read these before planning or implementing.**

### Files Being Modified
- `teressa-copeland-homes/src/app/api/documents/[id]/send/route.ts` — Send route (single-token → token loop)
- `teressa-copeland-homes/src/app/api/sign/[token]/route.ts` — GET + POST sign handler (full rewrite of both)
- `teressa-copeland-homes/src/lib/signing/token.ts` — `createSigningToken` needs optional `signerEmail` param
- `teressa-copeland-homes/src/lib/signing/signing-mailer.tsx` — Add `sendSignerCompletionEmail` function

### Schema (Phase 14 output — critical)
- `teressa-copeland-homes/src/lib/db/schema.ts` — `DocumentSigner` interface, `getSignerEmail()` helper, new nullable columns on `documents` and `signingTokens`

### Research
- `.planning/research/ARCHITECTURE.md` — Completion detection pattern (advisory lock), multi-signer token loop
- `.planning/research/PITFALLS.md` — First-signer-wins bug (line 254-258 in sign handler), race condition on completion

### Phase 14 Context
- `.planning/phases/14-multi-signer-schema/14-CONTEXT.md` — All schema decisions (D-01 through D-07)

</canonical_refs>

<code_context>
## Existing Code Insights

### Reusable Assets
- `createSigningToken(documentId)` in `token.ts` — Extend with optional `signerEmail` param; writes to `signingTokens.jti` with the new `signerEmail` column
- `sendSigningRequestEmail()` in `signing-mailer.tsx` — Already accepts `to`, `clientName`, `documentName`, `signingUrl`, `expiresAt` — reusable as-is in the token loop
- `sendAgentNotificationEmail()` — Already called in POST handler; keep calling it on final completion only (not per-signer)
- `embedSignatureInPdf()` in `signing/embed-signature.ts` — Reuse as-is; accumulate pattern just changes the INPUT path per signer
- `createAgentDownloadToken()` / `verifyAgentDownloadToken()` — JWT pattern for presigned downloads; extend for signer-download tokens

### Established Patterns
- **Atomic token claim:** `UPDATE ... WHERE usedAt IS NULL RETURNING` (line 127-131 in sign route) — same pattern for `completionTriggeredAt`
- **Fire-and-forget emails:** `db.query...then(async () => sendEmail()).catch(...)` pattern (lines 267-286) — follow same pattern for signer completion emails
- **Path traversal guard:** `!path.startsWith(UPLOADS_DIR)` check (lines 169-171) — replicate for the new signer download route
- **Audit logging:** `logAuditEvent()` called at each stage — continue logging on each signer's submission

### Integration Points
- `documents.signers` (Phase 14) — populated by Phase 16's PreparePanel; read here to loop over signers at send time
- `signingTokens.signerEmail` (Phase 14) — written here (send route), read here (GET/POST sign handler)
- `documents.completionTriggeredAt` (Phase 14) — written here ONLY; used as atomic completion guard
- `documents.signedFilePath` — repurposed as "working PDF path" during accumulate; final value = the last signer's output

</code_context>

<specifics>
## Specific Ideas

- The accumulate PDF approach: after each signer signs, update `documents.signedFilePath` to point to the latest partial PDF. The next signer reads from `signedFilePath` (if not null) rather than `preparedFilePath`. This means `signedFilePath` holds an intermediate value during multi-signing — document status remains 'Sent' to indicate it's not complete.
- New public download route for signers: `GET /api/sign/download/[token]` — accepts a short-lived JWT, serves `signedFilePath`, no agent auth required.

</specifics>

<deferred>
## Deferred Ideas

None — discussion stayed within phase scope.

</deferred>

---

*Phase: 15-multi-signer-backend*
*Context gathered: 2026-04-03*
