# Documenso integration audit

Reference for the multi-port Documenso signing pipeline in this CRM. Mirrors the legacy client portal's flow ([generate-quick-eoi.ts](../client-portal/server/api/eoi/generate-quick-eoi.ts), [documeso.ts](../client-portal/server/utils/documeso.ts), [documenso.post.ts](../client-portal/server/api/webhooks/documenso.post.ts), [website /sign/[type]/[token].vue](../../Port%20Nimara/Website/pages/sign/%5Btype%5D/%5Btoken%5D.vue)) but rewired for multi-tenant + better-auth + Drizzle.

---

## Per-port configuration

All Documenso settings live in `system_settings` keyed by `(key, port_id)` and are read via [`getPortDocumensoConfig(portId)`](../src/lib/services/port-config.ts). Falls back to env vars when no per-port row exists. Surfaced in the admin UI at `/[portSlug]/admin/documenso`.

| Setting key                         | Type                              | Purpose                                                                             |
| ----------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------- |
| `documenso_api_url_override`        | string                            | Per-port Documenso instance URL. Falls back to `DOCUMENSO_API_URL` env.             |
| `documenso_api_key_override`        | string                            | API key. Stored plaintext.                                                          |
| `documenso_api_version_override`    | `'v1' \| 'v2'`                    | Different ports may run different Documenso versions.                               |
| `documenso_eoi_template_id`         | int                               | Template ID for EOI generation.                                                     |
| `documenso_client_recipient_id`     | int                               | Template recipient slot — client (signing order 1).                                 |
| `documenso_developer_recipient_id`  | int                               | Template recipient slot — developer (signing order 2).                              |
| `documenso_approval_recipient_id`   | int                               | Template recipient slot — approver (signing order 3).                               |
| `documenso_developer_name`          | string                            | Display name for developer signer (legacy hardcoded "David Mizrahi").               |
| `documenso_developer_email`         | string                            | Developer signer email.                                                             |
| `documenso_approver_name`           | string                            | Approver display name.                                                              |
| `documenso_approver_email`          | string                            | Approver email.                                                                     |
| `documenso_webhook_secret`          | string                            | Per-port webhook secret. Receiver tries each enabled secret with timing-safe equal. |
| `eoi_default_pathway`               | `'documenso-template' \| 'inapp'` | Which path is used when EOI is generated without explicit choice.                   |
| `eoi_send_mode`                     | `'auto' \| 'manual'`              | Auto = send branded invitation email immediately; manual = rep clicks Send.         |
| `embedded_signing_host`             | string                            | Public host that wraps Documenso URLs into `{host}/sign/<type>/<token>`.            |
| `documenso_contract_template_id`    | int (optional)                    | Optional template for sales contracts. Blank = upload-and-place-fields per deal.    |
| `documenso_reservation_template_id` | int (optional)                    | Optional template for reservation agreements. Same logic as contract.               |

---

## Document type matrix

| Type            | Generation flow                                                   | Signers                                        | Field placement                                       |
| --------------- | ----------------------------------------------------------------- | ---------------------------------------------- | ----------------------------------------------------- |
| **EOI**         | Documenso template (`eoi_template_id`) + form-fill values         | Static: client, developer, approver (per-port) | Templated — fields baked into Documenso template      |
| **Contract**    | Per-deal upload (drafted custom). Template fallback if configured | Custom per deal — rep specifies                | Per-deal placement — default footer-anchored fallback |
| **Reservation** | Per-deal upload OR template if configured                         | Custom per deal                                | Per-deal placement                                    |

## Documenso field types

Custom-uploaded documents (contracts, reservations) need a per-deal field placement step — different documents need different mixes. The CRM exposes the full Documenso-supported field palette so reps can place whatever the document calls for without code changes.

| Field type       | Use case                                                | Needs `fieldMeta`? | What goes in meta                                   |
| ---------------- | ------------------------------------------------------- | ------------------ | --------------------------------------------------- |
| `SIGNATURE`      | Drawn signature — almost every signing flow             | No                 | —                                                   |
| `FREE_SIGNATURE` | Type-or-draw signature variant                          | No                 | —                                                   |
| `INITIALS`       | Per-page initials block                                 | No                 | —                                                   |
| `DATE`           | Auto-fills the date when the recipient signs            | No                 | —                                                   |
| `EMAIL`          | Auto-fills the recipient's email                        | No                 | —                                                   |
| `NAME`           | Auto-fills the recipient's name                         | No                 | —                                                   |
| `TEXT`           | Free text input (e.g. address, notes, place of signing) | Yes                | `{ text?, label?, required?, readOnly? }`           |
| `NUMBER`         | Numeric input with optional min/max                     | Yes                | `{ numberFormat?, min?, max?, required? }`          |
| `CHECKBOX`       | Boolean / single checkbox                               | Yes                | `{ values: [{ checked, value }], validationRule? }` |
| `DROPDOWN`       | Pick from a fixed list                                  | Yes                | `{ values: [{ value }], defaultValue? }`            |
| `RADIO`          | Mutually-exclusive options                              | Yes                | `{ values: [{ checked, value }] }`                  |

Helper: [`fieldTypeNeedsMeta(type)`](../src/lib/services/documenso-client.ts) returns true for the configurable types so the placement UI knows when to surface a config side-panel.

`fieldMeta` is forwarded verbatim by [`placeFields()`](../src/lib/services/documenso-client.ts) on the v2 path. v1 silently ignores the property — fields render as blank inputs. Configurable behaviour (validation, defaults) only fires on v2 instances.

---

## Documenso v1 vs v2 endpoint mapping

The [`documenso-client.ts`](../src/lib/services/documenso-client.ts) abstracts both. Each function picks v1 or v2 from `getPortDocumensoConfig(portId).apiVersion`.

| Operation                       | v1 (1.13–1.32)                                                              | v2.x                                                      |
| ------------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------- |
| Create document from upload     | `POST /api/v1/documents` (body: `{ title, document, recipients }`)          | `POST /api/v2/envelope`                                   |
| Generate document from template | `POST /api/v1/templates/{id}/generate-document`                             | (template-from-envelope path)                             |
| Send for signing                | `POST /api/v1/documents/{id}/send`                                          | `POST /api/v2/envelope/{id}/send`                         |
| Place a field                   | `POST /api/v1/documents/{id}/fields` (PIXEL coords, one at a time)          | `POST /api/v2/envelope/field/create-many` (PERCENT, bulk) |
| Get document state              | `GET /api/v1/documents/{id}`                                                | `GET /api/v2/envelope/{id}`                               |
| Send reminder to one recipient  | `POST /api/v1/documents/{id}/recipients/{rid}/remind`                       | `POST /api/v2/envelope/{id}/recipient/{rid}/remind`       |
| Download finalized PDF          | `GET /api/v1/documents/{id}/download` → `{ downloadUrl }` then GET that URL | `GET /api/v2/envelope/{id}/download` (same shape)         |
| Cancel / void                   | `DELETE /api/v1/documents/{id}`                                             | `DELETE /api/v2/envelope/{id}`                            |
| Healthcheck                     | `GET /api/v1/health`                                                        | (v1 path used)                                            |

**Field key rename in v2 responses**: `id` → `documentId` and recipient `id` → `recipientId`. Our [`normalizeDocument()`](../src/lib/services/documenso-client.ts) handles both shapes.

---

## Signing-flow lifecycle

```
[rep clicks Generate]                       (CRM)
       │
       ▼
buildEoiContext(interestId, portId)         service
       │
       ▼
generateAndSign(templateId, ctx, signers)   creates Documenso doc
       │
       ▼
POST /documents/{id}/send {sendEmail:false} Documenso starts the chain;
                                            it does NOT email signers
       │
       ▼
extract signing URLs from response          service
       │
       ▼
transformSigningUrl(url, host, role)        wrap as {host}/sign/<role>/<token>
       │
       ▼
if eoi_send_mode === 'auto':
    sendSigningInvitation(client)           our branded HTML email goes out
else:
    UI shows the URL + Send button          rep dispatches manually
```

When the client signs:

```
Documenso fires DOCUMENT_SIGNED webhook ──► /api/webhooks/documenso
       │
       ▼
verify x-documenso-secret (per-port lookup)
       │
       ▼
update document_signers row: status='signed', signedAt=...
       │
       ▼
if next signer in chain has not been notified:
    sendSigningInvitation(developer)        cascading "your turn" email
```

When the document reaches fully-signed:

```
Documenso fires DOCUMENT_COMPLETED webhook
       │
       ▼
download signed PDF from Documenso
       │
       ▼
store in storage backend → creates files row
       │
       ▼
update document: status='completed', completedAt=...
       │
       ▼
sendSigningCompleted([client, developer, approver], pdfFileId)
                                            all parties get the signed PDF
       │
       ▼
update interest: pipelineStage='eoi_signed' (or contract_signed, etc)
```

---

## Embedded signing on the marketing website

The CRM emits signing URLs in the form `{embeddedSigningHost}/sign/<role>/<token>`. The marketing website ([Port Nimara/Website/pages/sign/[type]/[token].vue](../../Port%20Nimara/Website/pages/sign/%5Btype%5D/%5Btoken%5D.vue)) hosts the page, embeds Documenso via `@documenso/embed-vue`'s `<EmbedSignDocument>`, and POSTs back to the CRM webhook on completion.

For the embed to work, the Documenso instance MUST send `Access-Control-Allow-Origin` headers permitting the website origin.

### nginx CORS block to apply on `signatures.portnimara.dev`

Add to the relevant `server { ... }` block:

```nginx
location / {
    # CORS for embedded signing — allow the marketing-website origin
    # to load the Documenso signing iframe.
    add_header 'Access-Control-Allow-Origin' 'https://portnimara.com' always;
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
    add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;

    # Preflight
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://portnimara.com' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain charset=UTF-8';
        add_header 'Content-Length' 0;
        return 204;
    }

    # ... your existing proxy_pass block to Documenso
}
```

To support multiple website origins (e.g. Port Amador hosted on a different domain), use a regex:

```nginx
set $cors_origin "";
if ($http_origin ~* "^https://(portnimara\.com|portamador\.com)$") {
    set $cors_origin $http_origin;
}
add_header 'Access-Control-Allow-Origin' $cors_origin always;
```

---

## What's deferred vs landed in this build

**Landed:**

- Per-port admin settings — every Documenso config knob is exposed at `/admin/documenso`
- Branded invitation, completion, and reminder email templates
- `transformSigningUrl()` for `{host}/sign/<role>/<token>` URL wrapping
- Documenso v1 + v2 dual-version client (existing)
- Webhook handler with timing-safe per-port secret resolution (existing)
- Contract + Reservation tab UI shells with paper-signed upload + "send for signing" placeholder
- Stage-conditional tab visibility for EOI / Contract / Reservation

**Deferred (separate sessions):**

- Custom document upload-to-Documenso service for contract/reservation (POST PDF → place fields → send). The tabs currently surface a "coming soon" dialog.
- Recipient + signing order configurator UI (rep specifies signers per deal for custom-uploaded docs).
- Drag-and-drop field placement UI on uploaded PDF previews. The fallback when this lands will be `computeDefaultSignatureLayout()` (footer-anchored fields).
- Webhook handler enhancements to track per-signer `sent_at`/`opened_at`/`signed_at` and trigger the cascading "your turn" branded emails. Currently the webhook just updates document status.
- Auto-store signed PDFs in storage backend and trigger `sendSigningCompleted()` on `DOCUMENT_COMPLETED`. Old system has this; needs porting.

**Manual ops work for you:**

- Apply the nginx CORS block above on your prod Documenso instance.
- Decide whether to upgrade prod Documenso to v2 (would unlock cleaner field placement + better envelope semantics).
- Configure each port's developer/approver names and template IDs at `/[portSlug]/admin/documenso`.
