mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 12:22:14 +10:00
Compare commits
368 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a05251d5ee | |||
| 454f73f2a9 | |||
| 24a5c85b6c | |||
| b92c53dbb2 | |||
| f8ac782f2e | |||
| 194660d847 | |||
| 0d3bd59ec6 | |||
| 92d82c0423 | |||
| 484e1c20d0 | |||
| 6f5014a561 | |||
| c112392da9 | |||
| bc72d9cb17 | |||
| 3ad3216c4c | |||
| 36eef79b1a | |||
| 6fb88fede5 | |||
| 653ab3678a | |||
| 006b1d0a57 | |||
| f3ec8ddc57 | |||
| 9a66d0ebf6 | |||
| 29622d3151 | |||
| 5de2527e54 | |||
| 6fcf0a638c | |||
| ff9e6acb7a | |||
| a60c6a90ab | |||
| f35c19d098 | |||
| cf8e21bf35 | |||
| 3f7c4df1b1 | |||
| ca199e7885 | |||
| 435d61ea57 | |||
| 34f14ba69a | |||
| 51916cd3f0 | |||
| f158305499 | |||
| 2e3d22c856 | |||
| d66c330d46 | |||
| 9bcb240895 | |||
| 066e6bc847 | |||
| 0d65693d55 | |||
| e3dee5e565 | |||
| f1c91c4951 | |||
| a5ef1d23e6 | |||
| d91414697d | |||
| e222a872d2 | |||
| e3b0087be6 | |||
| da89ce7c9a | |||
| b762561f11 | |||
| 9b190ef582 | |||
| 1669216a91 | |||
| 594a0f0c3f | |||
| 39ebc8184a | |||
| 2df41b9f01 | |||
| 8704c731c0 | |||
| eaee0d4bc6 | |||
| 0f8b7670f4 | |||
| 25e148d459 | |||
| 97ceb317a8 | |||
| c83109628d | |||
| a4d0e3e873 | |||
| 59a514c238 | |||
| 1b0df2d082 | |||
| d18dcb4d60 | |||
| d77f81163b | |||
| 62fb9e5248 | |||
| 53b0131740 | |||
| 155310b028 | |||
| 28bc2dc975 | |||
| eb3b3b18ce | |||
| 8bc4f1a713 | |||
| d3c898e317 | |||
| d08049ed3b | |||
| 7a583aa7af | |||
| b590076d85 | |||
| 65e30b88be | |||
| 9c6ee88cc4 | |||
| 6028ad9158 | |||
| 7fc6f5bb6e | |||
| 17b261df1f | |||
| c732c85082 | |||
| 7d38e18f93 | |||
| 0a3e0b8727 | |||
| b538580a1e | |||
| 42d6e1cbbd | |||
| 67da488f63 | |||
| fd3ebc08ec | |||
| a7963b385a | |||
| 9035240b4d | |||
| ed7a0011c7 | |||
| 158b36a9b7 | |||
| fabd69bd62 | |||
| c976e747e3 | |||
| 34f512bd55 | |||
| db913e95b6 | |||
| bb3e9583e4 | |||
| 5bc73a7471 | |||
| 06d7849146 | |||
| cef7987a72 | |||
| cf6f6bcea0 | |||
| 2f27304750 | |||
| 912530ca17 | |||
| a995961c4e | |||
| 6b041c23b4 | |||
| 7b6e948aa2 | |||
| f6d81b22bd | |||
| c861dd2ee2 | |||
| 7eabae4b4b | |||
| ae4272a6b6 | |||
| fd672943d1 | |||
| c2ea5e5859 | |||
| c1217c5a58 | |||
| 27eb2d65d4 | |||
| ef407cb0b4 | |||
| 1e20561e91 | |||
| a2ec5f0fa1 | |||
| de8d13a4c1 | |||
| 495d61a11d | |||
| 90fdba8000 | |||
| aa1cada79b | |||
| 790b385849 | |||
| baa2c51123 | |||
| 1e585e06e6 | |||
| 5624484631 | |||
| 810e00da03 | |||
| eeeee2fa0e | |||
| c50a31a503 | |||
| 7360709795 | |||
| df678d7d69 | |||
| 6739242554 | |||
| a5e5eecf8b | |||
| b0248c20eb | |||
| f129968968 | |||
| c5c87e3fd1 | |||
| 24a74c7b57 | |||
| f0a5a7e816 | |||
| 8462cd13fd | |||
| 576846de32 | |||
| 06071ea035 | |||
| b45a2691ba | |||
| f31cc575d0 | |||
| 05d7015ef0 | |||
| 2ca5d6cfaa | |||
| 04814ca14e | |||
| dd1dccdb6a | |||
| df4316ac5c | |||
| 02f1264eea | |||
| 928edb8645 | |||
| 54b0e4964e | |||
| 68e6ccdd19 | |||
| 09ab7e9a09 | |||
| 3bb0777914 | |||
| 4d6389e901 | |||
| 51e3d5030d | |||
| 0cebdec637 | |||
| 43486d8448 | |||
| 4d3d1b8d14 | |||
| 0387f3c20a | |||
| c5032d0c43 | |||
| 3bd34964cd | |||
| fe93b11a2c | |||
| 7638faf27b | |||
| 8fca029d96 | |||
| bac2bf11f4 | |||
| d93b2a70a7 | |||
| 5da915da38 | |||
| dcaecf1fc5 | |||
| f70b76d8b8 | |||
| 93137c6396 | |||
| d058b7c705 | |||
| b51f562224 | |||
| f80aa4bf72 | |||
| 9238f759a6 | |||
| 74ad6af47d | |||
| 18902ed59d | |||
| 3f70082146 | |||
| 31ba6d5f00 | |||
| c4f89a87a2 | |||
| 89d6dd5b0e | |||
| 08a9ab3aaf | |||
| e66bd422e3 | |||
| 0f5814ff89 | |||
| 1275a15571 | |||
| 22d99c7410 | |||
| 26a36487d4 | |||
| 2ee6b90c99 | |||
| f70e6ac50a | |||
| 7a94ee3b83 | |||
| e39924714a | |||
| c9604fee64 | |||
| 90f8340af4 | |||
| 28b8d2d415 | |||
| 978a2047d4 | |||
| 0dfa953f54 | |||
| 4774324e07 | |||
| bc19699a58 | |||
| 55480826de | |||
| 327b0eaf86 | |||
| 2de5c1992f | |||
| df0c03816e | |||
| a610a06372 | |||
| d5e085d7ee | |||
| c322356654 | |||
| b16862b480 | |||
| 7065b0dd88 | |||
| dff9cfec05 | |||
| d84cf0e58d | |||
| 5d8b147199 | |||
| 7d28295d42 | |||
| 94646cd48a | |||
| 14db9b8203 | |||
| 6ae672c16b | |||
| e9a9d65937 | |||
| d857dfdb38 | |||
| 11a56f3228 | |||
| 91642ddf0b | |||
| e364b08b6a | |||
| 5df3932958 | |||
| ae31860b16 | |||
| 16ee6b7a6d | |||
| 921c3d1ff3 | |||
| 2d7a4d0dde | |||
| d2176627ca | |||
| 17c6098638 | |||
| e5bde53ee4 | |||
| 0663605ffd | |||
| 1bbe561162 | |||
| fbc156722a | |||
| f5d63fb76c | |||
| 374477e692 | |||
| 11d9bde8f8 | |||
| fa1680aaf1 | |||
| 798b6bd750 | |||
| 8fbace0f61 | |||
| 1bbd04be9b | |||
| 6aa56fe7e0 | |||
| a07f3090cf | |||
| de3e6d2115 | |||
| dabd2564cd | |||
| 5811716d12 | |||
| 56526f9448 | |||
| 6ec1c3a3fb | |||
| 47466a2db9 | |||
| e4e04cdddc | |||
| 74a03077b7 | |||
| 58bff33275 | |||
| 0cb23be27a | |||
| 4304fc1d35 | |||
| ce53bcea8c | |||
| 5a3d5b8b4a | |||
| 3d1fe85d62 | |||
| 1772c3ee36 | |||
| f55b902a01 | |||
| 50db4e39be | |||
| 2802813c76 | |||
| 29d40f1cca | |||
| d67f32eae2 | |||
| a33233443b | |||
| 68a3608aee | |||
| 378dd605b9 | |||
| 211ae6c9e9 | |||
| f931885a95 | |||
| 4ade408001 | |||
| 3d0e3c6e8e | |||
| 936d8d90b3 | |||
| c6b08d8594 | |||
| 575634e326 | |||
| c66eda4aae | |||
| ef52b35f79 | |||
| 95a647034a | |||
| 34dba0b6ff | |||
| fccd97e124 | |||
| 3dbbcefddf | |||
| 2aea3c4de0 | |||
| ff44ffbc03 | |||
| 441842d2bd | |||
| ca0b83579f | |||
| 6c0d1da91e | |||
| 805982f3e8 | |||
| e2f5e570cf | |||
| 9fd9613076 | |||
| 0977c16e33 | |||
| 88d5a636c3 | |||
| 1e6292b1d9 | |||
| d65866156d | |||
| fe8915162f | |||
| 37a2634aca | |||
| eff7d90f43 | |||
| db5524f8ce | |||
| 3d539b20ad | |||
| 48626b9169 | |||
| 88371b665a | |||
| 1650c55b19 | |||
| 60d73e0921 | |||
| 4a779ec81e | |||
| 7f19ec1265 | |||
| d6a2f5a4c9 | |||
| d05bfa9fed | |||
| d2a009d52e | |||
| 9350c53c7d | |||
| ffce7a2c81 | |||
| 353bdce86b | |||
| e13b9f7c84 | |||
| 9908580bf1 | |||
| b0b07106b4 | |||
| 35250fa308 | |||
| 5cdd7f8623 | |||
| 47bdcd833f | |||
| 03eb6af69a | |||
| 88836404d1 | |||
| 2eebc0e439 | |||
| 4a3859ec60 | |||
| 49b792503f | |||
| c3dc76b1b4 | |||
| daab8461c7 | |||
| 1ffc4bd703 | |||
| f15c0778b5 | |||
| 06cb8b1f23 | |||
| 7f09ba72f4 | |||
| 7b17156e56 | |||
| 86e89e137e | |||
| 26f65dbdd7 | |||
| a902bec96d | |||
| 399f91de73 | |||
| 995bc9c362 | |||
| 3467317271 | |||
| a5eaa8ad47 | |||
| 577691214b | |||
| c7d21c6587 | |||
| 2aa391f917 | |||
| 681540b501 | |||
| f3305ac306 | |||
| 68b4305b6a | |||
| 3de1ea0a02 | |||
| b8fc47b719 | |||
| cfceebd78f | |||
| b9b3ddfb98 | |||
| 8590502338 | |||
| 53f29daf50 | |||
| 197d17ed7b | |||
| 3c646d9475 | |||
| ed4dfc9b55 | |||
| 32ce573de4 | |||
| 2ecfdbdde5 | |||
| a3005f8616 | |||
| 2c0d4f8789 | |||
| 7c8e93b53e | |||
| 93a3809f6a | |||
| 4550bca3d3 | |||
| 9ac7b94d9a | |||
| 374f2c45b4 | |||
| bb5c2edefd | |||
| 19565c1821 | |||
| 2603ae8b90 | |||
| 7d257236a6 | |||
| 31c1a9a783 | |||
| 657db3bc84 | |||
| 184ebdedf1 | |||
| 4012022f55 | |||
| 44f5da95b3 | |||
| 7eb882aea8 | |||
| dbf10e5b7b | |||
| fe4d3ed1fd | |||
| b8d07fd1a6 | |||
| 49fabeb0ec | |||
| 5a5bfe6e34 | |||
| d7e5a9eec7 | |||
| adefac81e2 | |||
| 67501b45cf | |||
| 17b36ac8e4 | |||
| 80e452afa2 | |||
| 1cb9de8083 |
@@ -0,0 +1,161 @@
|
||||
---
|
||||
date: 2026-01-28
|
||||
title: Pdf Placeholder Field Positioning
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This feature enables automatic field placement in PDFs using placeholder text, eliminating the need for manual coordinate-based positioning. It supports two complementary workflows:
|
||||
|
||||
1. **Automatic detection on upload** - PDFs containing structured placeholders like `{{signature, r1}}` have fields created automatically when uploaded
|
||||
2. **API placeholder positioning** - Developers can reference any text in a PDF to position fields instead of calculating coordinates
|
||||
|
||||
## Goals
|
||||
|
||||
- Allow users to prepare documents in Word/Google Docs with placeholders that become signature fields
|
||||
- Reduce friction for document preparation workflows
|
||||
- Provide API developers with a simpler alternative to coordinate-based field positioning
|
||||
- Support documents with repeated placeholders (e.g., initials on every page)
|
||||
|
||||
## Placeholder Format (Automatic Detection)
|
||||
|
||||
```
|
||||
{{FIELD_TYPE, RECIPIENT, option1=value1, option2=value2}}
|
||||
```
|
||||
|
||||
### Components
|
||||
|
||||
- **FIELD_TYPE** (required): One of `signature`, `initials`, `name`, `email`, `date`, `text`, `number`, `radio`, `checkbox`, `dropdown`
|
||||
- **RECIPIENT** (required): `r1`, `r2`, `r3`, etc. - identifies which recipient the field belongs to
|
||||
- **OPTIONS** (optional): Key-value pairs like `required=true`, `fontSize=14`, `readOnly=true`
|
||||
|
||||
### Examples
|
||||
|
||||
- `{{signature, r1}}` - Signature field for first recipient
|
||||
- `{{text, r1, required=true, label=Company Name}}` - Required text field with label
|
||||
- `{{number, r2, minValue=0, maxValue=100}}` - Number field with validation
|
||||
|
||||
### Behavior
|
||||
|
||||
- Placeholders without recipient identifiers (e.g., `{{signature}}`) are skipped during automatic detection - reserved for API use
|
||||
- Invalid field types are silently skipped
|
||||
- Placeholder text is covered with white rectangles after field creation
|
||||
|
||||
## API Placeholder Positioning
|
||||
|
||||
The `/api/v2/envelope/field/create-many` endpoint accepts `placeholder` as an alternative to coordinates:
|
||||
|
||||
```json
|
||||
{
|
||||
"recipientId": 123,
|
||||
"type": "SIGNATURE",
|
||||
"placeholder": "{{signature}}"
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------- | ------- | -------------------------------------------- |
|
||||
| `placeholder` | string | Text to search for in the PDF |
|
||||
| `width` | number | Optional override (percentage) |
|
||||
| `height` | number | Optional override (percentage) |
|
||||
| `matchAll` | boolean | When true, creates fields at ALL occurrences |
|
||||
|
||||
### matchAll Behavior
|
||||
|
||||
- Default (`false`): Only first occurrence gets a field
|
||||
- `true`: Creates a field at every occurrence of the placeholder text
|
||||
|
||||
This is useful for documents requiring initials on every page.
|
||||
|
||||
## Implementation Components
|
||||
|
||||
### Core Functions
|
||||
|
||||
- `extractPlaceholdersFromPDF()` - Scans PDF for `{{...}}` patterns with recipient identifiers
|
||||
- `removePlaceholdersFromPDF()` - Covers placeholder text with white rectangles
|
||||
- `whiteoutRegions()` - Low-level helper for drawing white boxes on PDF pages
|
||||
- `parseFieldTypeFromPlaceholder()` - Converts placeholder field type to FieldType enum
|
||||
- `parseFieldMetaFromPlaceholder()` - Parses options into fieldMeta format
|
||||
|
||||
### Integration Points
|
||||
|
||||
1. **Upload flow** (`create-envelope.ts`, `create-envelope-items.ts`)
|
||||
- Extract placeholders at upload time (before saving to storage)
|
||||
- Pass placeholders in-memory to envelope creation
|
||||
- Create placeholder recipients if none provided
|
||||
- Create fields within the same transaction
|
||||
|
||||
2. **API field creation** (`create-envelope-fields.ts`)
|
||||
- Accept `placeholder` as alternative to coordinates
|
||||
- Search PDF for placeholder text
|
||||
- Resolve position from bounding box
|
||||
- Support `matchAll` for multiple occurrences
|
||||
|
||||
### Field Meta Parsing
|
||||
|
||||
The following properties are explicitly parsed:
|
||||
|
||||
- `required`, `readOnly` → boolean
|
||||
- `fontSize`, `minValue`, `maxValue`, `characterLimit` → number
|
||||
- Other properties pass through as strings
|
||||
|
||||
Note: Signature fields do not support fieldMeta options.
|
||||
|
||||
## Testing
|
||||
|
||||
### E2E Tests
|
||||
|
||||
**UI Tests** (`e2e/auto-placing-fields/`):
|
||||
|
||||
- Single recipient placeholder detection
|
||||
- Multiple recipient placeholder detection
|
||||
- Field configuration from placeholder options
|
||||
- Skipping placeholders without recipient identifiers
|
||||
- Skipping invalid field types
|
||||
|
||||
**API Tests** (`e2e/api/v2/placeholder-fields-api.spec.ts`):
|
||||
|
||||
- Placeholder-based field positioning
|
||||
- Width/height overrides
|
||||
- Error on placeholder not found
|
||||
- Mixed coordinate and placeholder positioning
|
||||
- First occurrence only (default)
|
||||
- All occurrences with `matchAll: true`
|
||||
|
||||
## Documentation
|
||||
|
||||
### User Documentation
|
||||
|
||||
`/users/documents/pdf-placeholders` - Explains:
|
||||
|
||||
- Placeholder format and syntax
|
||||
- Supported field types
|
||||
- Recipient identifiers
|
||||
- Available options per field type
|
||||
- Troubleshooting
|
||||
|
||||
### Developer Documentation
|
||||
|
||||
`/developers/public-api/reference` - Documents:
|
||||
|
||||
- Coordinate-based positioning (existing)
|
||||
- Placeholder-based positioning (new)
|
||||
- matchAll parameter
|
||||
- Mixing both methods
|
||||
|
||||
## Edge Cases Handled
|
||||
|
||||
1. **No placeholders found** - Original PDF returned unchanged
|
||||
2. **Placeholder not found (API)** - Returns error with placeholder text
|
||||
3. **Multiple occurrences** - First only by default, all with `matchAll: true`
|
||||
4. **No recipient identifier** - Skipped during auto-detection, works for API
|
||||
5. **Invalid field type** - Skipped during auto-detection
|
||||
6. **Signature field with options** - Options ignored (signature doesn't support fieldMeta)
|
||||
|
||||
## Future Considerations
|
||||
|
||||
- Support for placeholder text styles (bold, underline) to indicate field properties
|
||||
- Template-level placeholder mapping for reusable configurations
|
||||
- Placeholder validation in document editor before sending
|
||||
@@ -0,0 +1,519 @@
|
||||
---
|
||||
date: 2026-02-10
|
||||
title: Envelope Expiration
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Envelopes (documents sent for signing) should automatically expire after a configurable period, preventing recipients from completing stale documents. Expiration is tracked **per-recipient** — when a recipient's signing window lapses, the document owner is notified and can resend (extending the deadline) or cancel. The document itself stays PENDING so other recipients can continue signing.
|
||||
|
||||
**Settings cascade**: Organisation → Team → Document (each level can override the prior).
|
||||
**Default**: 1 month from when the envelope is sent (transitions to PENDING).
|
||||
|
||||
---
|
||||
|
||||
## 1. Database Schema Changes
|
||||
|
||||
### 1.1 Expiration period data shape
|
||||
|
||||
Store expiration as a structured JSON object rather than an enum or raw milliseconds. This avoids the enum treadmill (adding `FOUR_MONTHS` later requires a migration) while keeping values validated and meaningful.
|
||||
|
||||
**Zod schema** (defined in `packages/lib/constants/envelope-expiration.ts`):
|
||||
|
||||
```typescript
|
||||
export const ZEnvelopeExpirationPeriod = z.union([
|
||||
z.object({ unit: z.enum(['day', 'week', 'month', 'year']), amount: z.number().int().min(1) }),
|
||||
z.object({ disabled: z.literal(true) }),
|
||||
]);
|
||||
|
||||
export type TEnvelopeExpirationPeriod = z.infer<typeof ZEnvelopeExpirationPeriod>;
|
||||
```
|
||||
|
||||
Semantics:
|
||||
|
||||
- `null` on `DocumentMeta` / `TeamGlobalSettings` = inherit from parent
|
||||
- `{ disabled: true }` = explicitly never expires
|
||||
- `{ unit: 'month', amount: 1 }` = expires in 1 month
|
||||
|
||||
No Prisma enum is needed — the period is stored as `Json?` on the relevant models (see sections 1.3 and 1.4).
|
||||
|
||||
### 1.2 Add expiration fields to `Recipient`
|
||||
|
||||
```prisma
|
||||
model Recipient {
|
||||
// ... existing fields
|
||||
expiresAt DateTime?
|
||||
expirationNotifiedAt DateTime? // null = not yet notified; set when owner notification sent
|
||||
|
||||
@@index([expiresAt])
|
||||
}
|
||||
```
|
||||
|
||||
`expiresAt` is a computed timestamp set when the envelope transitions to PENDING (at send time). It is calculated from the effective expiration period. Storing the concrete timestamp rather than a relative duration means:
|
||||
|
||||
- Sweep queries are simple (`WHERE expiresAt <= NOW() AND expirationNotifiedAt IS NULL`)
|
||||
- No need to re-resolve the settings cascade at query time
|
||||
- The sender can see the exact deadline in the UI
|
||||
- The index on `expiresAt` ensures the expiration sweep query is efficient
|
||||
|
||||
`expirationNotifiedAt` tracks whether the owner has already been notified about this recipient's expiration, making the notification job idempotent.
|
||||
|
||||
### 1.3 Add expiration period to settings models
|
||||
|
||||
**OrganisationGlobalSettings** (JSON, application-level default):
|
||||
|
||||
```prisma
|
||||
model OrganisationGlobalSettings {
|
||||
// ... existing fields
|
||||
envelopeExpirationPeriod Json?
|
||||
}
|
||||
```
|
||||
|
||||
Prisma `@default` doesn't work for `Json` columns, so the application-level default (`{ unit: 'month', amount: 1 }`) is applied in `extractDerivedTeamSettings` / `extractDerivedDocumentMeta` when the value is null. The migration should backfill existing rows with `{ "unit": "month", "amount": 1 }`.
|
||||
|
||||
**TeamGlobalSettings** (nullable, null = inherit from org):
|
||||
|
||||
```prisma
|
||||
model TeamGlobalSettings {
|
||||
// ... existing fields
|
||||
envelopeExpirationPeriod Json?
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 Add expiration period to DocumentMeta
|
||||
|
||||
This allows per-document override during the document editing flow:
|
||||
|
||||
```prisma
|
||||
model DocumentMeta {
|
||||
// ... existing fields
|
||||
envelopeExpirationPeriod Json?
|
||||
}
|
||||
```
|
||||
|
||||
When null on DocumentMeta, the resolved team/org setting is used at send time. Validated at write time using `ZEnvelopeExpirationPeriod.nullable()`.
|
||||
|
||||
**Important**: `envelopeExpirationPeriod` on `DocumentMeta` is a user-facing preference that may be set during the draft editing flow. It does NOT determine the final expiration — that is resolved at send time (see section 2.3). The value stored here is just the user's selection in the document editor.
|
||||
|
||||
---
|
||||
|
||||
## 2. Expiration Period Resolution
|
||||
|
||||
### 2.1 Duration mapping
|
||||
|
||||
Add to `packages/lib/constants/envelope-expiration.ts` alongside the Zod schema:
|
||||
|
||||
```typescript
|
||||
import { Duration } from 'luxon';
|
||||
|
||||
const UNIT_TO_LUXON_KEY: Record<TEnvelopeExpirationPeriod['unit'], string> = {
|
||||
day: 'days',
|
||||
week: 'weeks',
|
||||
month: 'months',
|
||||
year: 'years',
|
||||
};
|
||||
|
||||
export const DEFAULT_ENVELOPE_EXPIRATION_PERIOD: TEnvelopeExpirationPeriod = {
|
||||
unit: 'month',
|
||||
amount: 1,
|
||||
};
|
||||
|
||||
export const getEnvelopeExpirationDuration = (period: TEnvelopeExpirationPeriod): Duration => {
|
||||
return Duration.fromObject({ [UNIT_TO_LUXON_KEY[period.unit]]: period.amount });
|
||||
};
|
||||
```
|
||||
|
||||
### 2.2 Settings cascade integration
|
||||
|
||||
`extractDerivedTeamSettings()` in `packages/lib/utils/teams.ts` needs **no code changes** — it iterates `Object.keys(derivedSettings)` and overrides with non-null team values at runtime. The new `envelopeExpirationPeriod` field on both `OrganisationGlobalSettings` and `TeamGlobalSettings` will be automatically picked up.
|
||||
|
||||
Update `extractDerivedDocumentMeta()` in `packages/lib/utils/document.ts` to include the new field:
|
||||
|
||||
```typescript
|
||||
envelopeExpirationPeriod: meta.envelopeExpirationPeriod ?? settings.envelopeExpirationPeriod,
|
||||
```
|
||||
|
||||
### 2.3 Compute `expiresAt` at send time
|
||||
|
||||
The expiration period is **locked at send time** — when the envelope transitions to PENDING. The concrete `expiresAt` timestamp is computed for each recipient when the document is actually sent.
|
||||
|
||||
In `packages/lib/server-only/document/send-document.ts`:
|
||||
|
||||
```typescript
|
||||
// Resolve effective period: document meta -> team/org settings -> default
|
||||
const rawPeriod =
|
||||
envelope.documentMeta?.envelopeExpirationPeriod ?? settings.envelopeExpirationPeriod;
|
||||
|
||||
const expiresAt = resolveExpiresAt(rawPeriod);
|
||||
|
||||
// Inside the $transaction, for each recipient:
|
||||
await tx.recipient.updateMany({
|
||||
where: { envelopeId: envelope.id },
|
||||
data: { expiresAt },
|
||||
});
|
||||
```
|
||||
|
||||
### 2.4 Compute `expiresAt` in the direct template flow
|
||||
|
||||
`create-document-from-direct-template.ts` creates envelopes directly as PENDING and then calls `sendDocument` afterward. Since `sendDocument` handles setting `expiresAt` on recipients, the direct template flow doesn't need to set it directly — `sendDocument` handles it.
|
||||
|
||||
---
|
||||
|
||||
## 3. Cron Job Infrastructure (New)
|
||||
|
||||
The current job system is purely event-triggered. Inngest natively supports cron-triggered functions, but the local provider (used in dev and by self-hosters who don't want a third-party dependency) has no scheduling capability. This section adds cron support to the local provider to maintain feature parity.
|
||||
|
||||
### 3.1 Extend `JobDefinition` with cron support
|
||||
|
||||
Add an optional `cron` field to the trigger type in `packages/lib/jobs/client/_internal/job.ts`:
|
||||
|
||||
```typescript
|
||||
export type JobDefinition<Name extends string = string, Schema = any> = {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
enabled?: boolean;
|
||||
optimizeParallelism?: boolean;
|
||||
trigger: {
|
||||
name: Name;
|
||||
schema?: z.ZodType<Schema>;
|
||||
/** Cron expression (e.g. "* * * * *"). When set, the job runs on a schedule. */
|
||||
cron?: string;
|
||||
};
|
||||
handler: (options: { payload: Schema; io: JobRunIO }) => Promise<Json | void>;
|
||||
};
|
||||
```
|
||||
|
||||
### 3.2 Inngest provider: wire up native cron
|
||||
|
||||
In `packages/lib/jobs/client/inngest.ts`, when defining a function, check for `cron`:
|
||||
|
||||
```typescript
|
||||
defineJob(job) {
|
||||
if (job.trigger.cron) {
|
||||
this._functions.push(
|
||||
this._client.createFunction(
|
||||
{ id: job.id, name: job.name },
|
||||
{ cron: job.trigger.cron },
|
||||
async ({ step, logger }) => {
|
||||
const io = convertInngestIoToJobRunIo(step, logger, this);
|
||||
await job.handler({ payload: {} as any, io });
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Existing event-triggered logic (unchanged)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Local provider: poller + deterministic `BackgroundJob` IDs
|
||||
|
||||
Use the existing `BackgroundJob` table for multi-instance dedupe instead of advisory locks. This approach keeps implementation Prisma-only (no raw SQL), works for single-instance and multi-instance deployments, and preserves existing retry/visibility behavior.
|
||||
|
||||
**On `defineJob()`**: If the job has a `cron` field, register an in-process scheduler entry and start a lightweight poller (every 30s with jitter).
|
||||
|
||||
**Each poll tick**:
|
||||
|
||||
1. Evaluate whether the cron schedule has one or more due run slots since the last tick (use a real cron parser, e.g. `cron-parser`)
|
||||
2. For each due slot, build a deterministic run ID from job ID + scheduled slot time
|
||||
3. Create a `BackgroundJob` row with that deterministic ID using Prisma
|
||||
4. If insert succeeds → enqueue via the existing local job pipeline
|
||||
5. If insert fails with Prisma `P2002` (unique violation) → another node already enqueued that run, skip
|
||||
|
||||
### 3.4 Summary of changes to the job system
|
||||
|
||||
| File | Change |
|
||||
| ------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| `packages/lib/jobs/client/_internal/job.ts` | Add optional `cron` field to `trigger` type |
|
||||
| `packages/lib/jobs/client/local.ts` | Add cron poller + deterministic `BackgroundJob.id` dedupe |
|
||||
| `packages/lib/jobs/client/inngest.ts` | Wire up `{ cron: ... }` in `createFunction` for cron jobs |
|
||||
| `packages/lib/jobs/client/_internal/*` | Add cron helper utilities (`getDueCronSlots`, run ID generation) |
|
||||
|
||||
---
|
||||
|
||||
## 4. Expiration Processing
|
||||
|
||||
### 4.1 Two-job architecture
|
||||
|
||||
Expiration uses two jobs: a **sweep dispatcher** that runs on a cron schedule and finds expired recipients, and an **individual notification job** that handles the audit log, owner notification email, and webhook for a single recipient. This separation means:
|
||||
|
||||
- The sweep is lightweight and fast (just a query + N job triggers)
|
||||
- Each recipient's expiration notification is independently retryable
|
||||
- The individual jobs are idempotent — they check `expirationNotifiedAt IS NULL` before processing
|
||||
|
||||
### 4.2 Sweep job: `EXPIRE_RECIPIENTS_SWEEP_JOB`
|
||||
|
||||
A cron-triggered job that runs every minute to find and dispatch notifications for expired recipients.
|
||||
|
||||
**Definition:** `packages/lib/jobs/definitions/internal/expire-recipients-sweep.ts`
|
||||
|
||||
**Handler:** `packages/lib/jobs/definitions/internal/expire-recipients-sweep.handler.ts`
|
||||
|
||||
```typescript
|
||||
const expiredRecipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
expiresAt: { lte: new Date() },
|
||||
expirationNotifiedAt: null,
|
||||
signingStatus: { notIn: [SigningStatus.SIGNED, SigningStatus.REJECTED] },
|
||||
envelope: { status: DocumentStatus.PENDING },
|
||||
},
|
||||
select: { id: true },
|
||||
take: 100,
|
||||
});
|
||||
|
||||
for (const recipient of expiredRecipients) {
|
||||
await jobs.triggerJob({
|
||||
name: 'internal.notify-recipient-expired',
|
||||
payload: { recipientId: recipient.id },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 Individual notification job: `NOTIFY_RECIPIENT_EXPIRED_JOB`
|
||||
|
||||
An event-triggered job that handles a single recipient's expiration.
|
||||
|
||||
**Definition:** `packages/lib/jobs/definitions/internal/notify-recipient-expired.ts`
|
||||
|
||||
**Handler:** `packages/lib/jobs/definitions/internal/notify-recipient-expired.handler.ts`
|
||||
|
||||
The handler:
|
||||
|
||||
1. Fetches the recipient (with guard: `expirationNotifiedAt IS NULL` + not signed/rejected)
|
||||
2. Sets `recipient.expirationNotifiedAt = now()` (idempotency)
|
||||
3. Creates audit log entry with `DOCUMENT_RECIPIENT_EXPIRED` type
|
||||
4. Sends email notification to the **document owner** (inline — no separate email job)
|
||||
5. The document stays PENDING — the owner decides whether to resend or cancel
|
||||
|
||||
### 4.4 Register in job client
|
||||
|
||||
Add `EXPIRE_RECIPIENTS_SWEEP_JOB_DEFINITION` and `NOTIFY_RECIPIENT_EXPIRED_JOB_DEFINITION` to the job registry in `packages/lib/jobs/client.ts`.
|
||||
|
||||
### 4.5 Email template: Recipient Expired
|
||||
|
||||
Target the **document owner**:
|
||||
|
||||
- Subject: `Signing window expired for "{recipientName}" on "{documentTitle}"`
|
||||
- Body: "The signing window for {recipientName} ({recipientEmail}) on document {title} has expired. You can resend the document to extend their deadline or cancel the document."
|
||||
- Include a "View Document" link to the document page in the app
|
||||
|
||||
Template files:
|
||||
|
||||
- `packages/email/templates/recipient-expired.tsx` — wrapper
|
||||
- `packages/email/template-components/template-recipient-expired.tsx` — body
|
||||
|
||||
### 4.6 Recipient signing guard
|
||||
|
||||
In the signing flow, check `recipient.expiresAt` before allowing any signing action. Note that the document stays PENDING even after recipient expiration, so the existing `status !== PENDING` guard does not block expired recipients — an explicit expiration check is required:
|
||||
|
||||
```typescript
|
||||
if (recipient.expiresAt && recipient.expiresAt <= new Date()) {
|
||||
throw new AppError(AppErrorCode.RECIPIENT_EXPIRED, {
|
||||
message: 'Recipient signing window has expired',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**Files to update:**
|
||||
|
||||
- `packages/lib/server-only/document/complete-document-with-token.ts`
|
||||
- `packages/lib/server-only/field/sign-field-with-token.ts`
|
||||
- `packages/lib/server-only/field/remove-signed-field-with-token.ts`
|
||||
- `packages/lib/server-only/document/reject-document-with-token.ts`
|
||||
|
||||
---
|
||||
|
||||
## 5. UI Design
|
||||
|
||||
### 5.1 Expiration Period Selector Component
|
||||
|
||||
Use a number input + unit selector combo. This gives organisations full flexibility to configure any duration without needing schema changes for new options.
|
||||
|
||||
**Layout**: A horizontal group with:
|
||||
|
||||
- A number `<Input>` (min 1, integer)
|
||||
- A `<Select>` for the unit (`day`, `week`, `month`, `year`)
|
||||
- A "Never expires" toggle/checkbox that disables the duration inputs and sets the value to `{ disabled: true }`
|
||||
|
||||
At the team level, include an "Inherit from organisation" option that clears the value to `null`.
|
||||
|
||||
**Validation**: Use `ZEnvelopeExpirationPeriod` for form validation.
|
||||
|
||||
### 5.2 Organisation Settings → Document Preferences
|
||||
|
||||
Add a "Default Envelope Expiration" field to the `DocumentPreferencesForm` component. At the org level, there is no "Inherit" option — it must have a concrete value (default: `{ unit: 'month', amount: 1 }`).
|
||||
|
||||
### 5.3 Team Settings → Document Preferences
|
||||
|
||||
Same field as org, but with the additional "Inherit from organisation" option (stored as `null`).
|
||||
|
||||
### 5.4 Document Editor → Settings Step
|
||||
|
||||
Add the expiration selector to `packages/ui/primitives/document-flow/add-settings.tsx` inside the "Advanced Options" accordion.
|
||||
|
||||
Label: **"Expiration"**
|
||||
Description: _"How long recipients have to complete this document after it is sent."_
|
||||
|
||||
### 5.5 Recipient Signing Page — Expired State
|
||||
|
||||
When a recipient visits a signing link for an expired recipient:
|
||||
|
||||
- Redirect to `/sign/{token}/expired`
|
||||
- Show a clear, non-alarming message: "Your signing window has expired. Please contact the sender for a new invitation."
|
||||
- Do not show the signing form or fields
|
||||
- The `isExpired` flag in `get-envelope-for-recipient-signing.ts` is derived from `recipient.expiresAt`
|
||||
|
||||
### 5.6 Embed Signing — Expired State
|
||||
|
||||
Embed signing routes handle recipient expiration by throwing `embed-recipient-expired`:
|
||||
|
||||
- `apps/remix/app/routes/embed+/_v0+/sign.$token.tsx` — both V1 and V2 loaders check expiration
|
||||
- The embed error boundary renders an `EmbedRecipientExpired` component
|
||||
- Direct templates (`direct.$token.tsx`) create fresh recipients so `isExpired` is always `false`
|
||||
|
||||
---
|
||||
|
||||
## 6. API / TRPC Changes
|
||||
|
||||
### 6.1 Update settings mutation schemas
|
||||
|
||||
- `packages/trpc/server/organisation-router/update-organisation-settings.types.ts` — add `envelopeExpirationPeriod: ZEnvelopeExpirationPeriod` (non-nullable at org level)
|
||||
- `packages/trpc/server/team-router/update-team-settings.types.ts` — add `envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullable()` (null = inherit from org)
|
||||
|
||||
### 6.2 Update document mutation schemas
|
||||
|
||||
- `packages/lib/types/document-meta.ts` — add `envelopeExpirationPeriod: ZEnvelopeExpirationPeriod.nullable()` to the meta schema
|
||||
- `packages/trpc/server/document-router/create-document.types.ts` — include in meta
|
||||
- `packages/trpc/server/document-router/update-document.types.ts` — include in meta
|
||||
- `packages/trpc/server/document-router/distribute-document.types.ts` — include in meta
|
||||
|
||||
### 6.3 Expose `expiresAt` in recipient responses
|
||||
|
||||
Ensure `expiresAt` and `expirationNotifiedAt` are returned when fetching recipients/documents so the UI can display expiration status.
|
||||
|
||||
### 6.4 Webhook / API schema updates
|
||||
|
||||
- Recipient schema includes `expiresAt` and `expirationNotifiedAt` fields (replacing the old `expired` field)
|
||||
- Update `packages/api/v1/schema.ts`, webhook payload types, zapier integration, and sample data generators
|
||||
|
||||
---
|
||||
|
||||
## 7. Edge Cases & Considerations
|
||||
|
||||
### 7.1 Already-sent documents
|
||||
|
||||
The migration should NOT retroactively expire existing recipients. `expiresAt` will be null for all existing recipients, meaning they never expire (backward-compatible).
|
||||
|
||||
### 7.2 Re-sending / redistributing
|
||||
|
||||
When `redistribute` is called on a PENDING document, `expiresAt` should be refreshed on all eligible recipients. Redistributing signals active intent, so the clock should restart.
|
||||
|
||||
**Implementation**: `resendDocument` refreshes `recipient.expiresAt` for all recipients that haven't signed/rejected yet.
|
||||
|
||||
### 7.3 Multi-recipient partial expiration
|
||||
|
||||
If some recipients have signed and others expire, the document stays PENDING. This is the key advantage over document-level expiration — the owner can resend to extend the expired recipients' deadlines without affecting those who've already signed.
|
||||
|
||||
### 7.4 Partial completion
|
||||
|
||||
Partial signatures are preserved. The document is not sealed/completed until all required recipients have signed (or the owner cancels).
|
||||
|
||||
### 7.5 Timezone handling
|
||||
|
||||
`expiresAt` is stored as UTC. Display in the sender's configured timezone.
|
||||
|
||||
### 7.6 Race condition: signing at expiration time
|
||||
|
||||
The signing guard checks `recipient.expiresAt` in application code before the signing operation. The notification job's guard (`expirationNotifiedAt IS NULL` + `signingStatus NOT IN (SIGNED, REJECTED)`) prevents double-notifications. If a recipient signs just before expiration, the sweep's `signingStatus` filter skips them.
|
||||
|
||||
### 7.7 Direct template flow
|
||||
|
||||
`create-document-from-direct-template.ts` creates envelopes directly as PENDING then calls `sendDocument`. Since `sendDocument` sets `recipient.expiresAt`, no special handling is needed in the direct template flow.
|
||||
|
||||
---
|
||||
|
||||
## 8. Migration Plan
|
||||
|
||||
1. Add Prisma schema changes (`expiresAt` + `expirationNotifiedAt` on Recipient, `Json?` fields on settings models, index)
|
||||
2. Generate and run migration
|
||||
3. Backfill: set `envelopeExpirationPeriod` to `{ "unit": "month", "amount": 1 }` on all existing `OrganisationGlobalSettings` rows
|
||||
4. No backfill on `Recipient.expiresAt` — existing recipients keep null (never expire)
|
||||
5. Deploy backend changes (jobs, guards, email template)
|
||||
6. Deploy frontend changes (settings UI, document editor, signing page, embeds)
|
||||
|
||||
---
|
||||
|
||||
## 9. Files to Create or Modify
|
||||
|
||||
### New Files
|
||||
|
||||
- `packages/lib/constants/envelope-expiration.ts` — `ZEnvelopeExpirationPeriod` schema, types, `DEFAULT_ENVELOPE_EXPIRATION_PERIOD`, `getEnvelopeExpirationDuration()`, `resolveExpiresAt()` helper
|
||||
- `packages/lib/jobs/definitions/internal/expire-recipients-sweep.ts` — cron sweep job definition
|
||||
- `packages/lib/jobs/definitions/internal/expire-recipients-sweep.handler.ts` — cron sweep handler
|
||||
- `packages/lib/jobs/definitions/internal/notify-recipient-expired.ts` — individual notification job definition
|
||||
- `packages/lib/jobs/definitions/internal/notify-recipient-expired.handler.ts` — notification handler (includes inline email sending)
|
||||
- `packages/email/templates/recipient-expired.tsx` — email template wrapper
|
||||
- `packages/email/template-components/template-recipient-expired.tsx` — email template body
|
||||
- `apps/remix/app/components/embed/embed-recipient-expired.tsx` — embed expired component
|
||||
|
||||
### Modified Files
|
||||
|
||||
**Job system (cron infrastructure):**
|
||||
|
||||
- `packages/lib/jobs/client/_internal/job.ts` — add optional `cron` field to `trigger` type
|
||||
- `packages/lib/jobs/client/local.ts` — add cron poller + deterministic `BackgroundJob.id` dedupe
|
||||
- `packages/lib/jobs/client/inngest.ts` — wire up `{ cron: ... }` in `createFunction`
|
||||
- `packages/lib/jobs/client/_internal/*` — add cron helper utilities (slot calc + run ID)
|
||||
- `packages/lib/jobs/client.ts` — register new jobs
|
||||
|
||||
**Schema & data layer:**
|
||||
|
||||
- `packages/prisma/schema.prisma` — model changes + index
|
||||
- `packages/lib/utils/document.ts` — `extractDerivedDocumentMeta` (add `envelopeExpirationPeriod`)
|
||||
- `packages/lib/server-only/document/send-document.ts` — resolve settings + compute and set `recipient.expiresAt`
|
||||
- `packages/lib/server-only/template/create-document-from-direct-template.ts` — no changes (sendDocument handles it)
|
||||
- `packages/lib/server-only/document/resend-document.ts` — refresh `recipient.expiresAt` on redistribute
|
||||
- `packages/lib/server-only/document/complete-document-with-token.ts` — recipient expiration guard
|
||||
- `packages/lib/server-only/field/sign-field-with-token.ts` — recipient expiration guard
|
||||
- `packages/lib/server-only/field/remove-signed-field-with-token.ts` — recipient expiration guard
|
||||
- `packages/lib/server-only/document/reject-document-with-token.ts` — recipient expiration guard
|
||||
|
||||
**Error handling:**
|
||||
|
||||
- `packages/lib/errors/app-error.ts` — add `RECIPIENT_EXPIRED` error code
|
||||
|
||||
**Audit logs:**
|
||||
|
||||
- `packages/lib/types/document-audit-logs.ts` — add `DOCUMENT_RECIPIENT_EXPIRED` type with `recipientEmail`/`recipientName` data fields
|
||||
- `packages/lib/utils/document-audit-logs.ts` — add human-readable rendering for `DOCUMENT_RECIPIENT_EXPIRED`
|
||||
|
||||
**Signing page:**
|
||||
|
||||
- `packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts` — derive `isExpired` from `recipient.expiresAt`
|
||||
- `apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx` — keep redirect to expired page using `isExpired`
|
||||
|
||||
**Embeds:**
|
||||
|
||||
- `apps/remix/app/routes/embed+/_v0+/sign.$token.tsx` — check recipient expiration in V1/V2 loaders
|
||||
- `apps/remix/app/routes/embed+/_v0+/_layout.tsx` — handle `embed-recipient-expired` in error boundary
|
||||
|
||||
**Webhook / API:**
|
||||
|
||||
- `packages/lib/types/recipient.ts` — add `expiresAt`/`expirationNotifiedAt` to recipient type
|
||||
- `packages/lib/types/webhook-payload.ts` — add `expiresAt`/`expirationNotifiedAt` to webhook recipient
|
||||
- `packages/lib/server-only/webhooks/trigger/generate-sample-data.ts` — update sample data
|
||||
- `packages/lib/server-only/webhooks/zapier/list-documents.ts` — update zapier recipient shape
|
||||
- `packages/api/v1/schema.ts` — add `expiresAt` to API recipient schema
|
||||
|
||||
**TRPC / settings:**
|
||||
|
||||
- `packages/trpc/server/organisation-router/update-organisation-settings.types.ts`
|
||||
- `packages/trpc/server/team-router/update-team-settings.types.ts`
|
||||
- `packages/lib/types/document-meta.ts`
|
||||
|
||||
**UI:**
|
||||
|
||||
- `apps/remix/app/components/forms/document-preferences-form.tsx` — add expiration period picker
|
||||
- `packages/ui/primitives/document-flow/add-settings.tsx` — add expiration field
|
||||
- `packages/ui/primitives/document-flow/add-settings.types.ts` — add to schema
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
date: 2026-01-26
|
||||
title: Validate Signer Fields On Distribute
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Validate that signers have at least one signature field before allowing document/envelope distribution via API, matching the existing UI behavior.
|
||||
|
||||
## Background
|
||||
|
||||
The API originally allowed distributing documents/envelopes without validating that signers had signature fields assigned. This was intentional - we thought API users might have specific flows where this flexibility was needed.
|
||||
|
||||
However, after running it this way for a while, we've observed that more often than not, API users inadvertently send documents without fields assigned. This causes confusion for their recipients (who receive a document with nothing to sign) and breaks their own systems expecting a completed signing flow.
|
||||
|
||||
## Problem
|
||||
|
||||
The API allowed distributing documents/envelopes even when signers had no signature fields assigned. This was inconsistent with the UI which validates this condition before allowing distribution.
|
||||
|
||||
## Solution
|
||||
|
||||
### 1. Create centralized validation helper
|
||||
|
||||
**File**: `packages/lib/utils/recipients.ts`
|
||||
|
||||
- Added `RECIPIENT_ROLES_THAT_REQUIRE_FIELDS` constant (currently only `SIGNER`)
|
||||
- Added `getRecipientsWithMissingFields()` function that returns recipients missing required fields
|
||||
- Uses existing `isSignatureFieldType` guard from `packages/prisma/guards/is-signature-field.ts`
|
||||
|
||||
### 2. Add server-side validation
|
||||
|
||||
**File**: `packages/lib/server-only/document/send-document.ts`
|
||||
|
||||
- Added validation check that throws `AppError` with `INVALID_REQUEST` code when signers are missing signature fields
|
||||
- This blocks both v1 and v2 API distribution endpoints since they both use `sendDocument()`
|
||||
|
||||
### 3. Fix v1 API error handling
|
||||
|
||||
**File**: `packages/api/v1/implementation.ts`
|
||||
|
||||
- Changed `sendDocument` endpoint to use `AppError.toRestAPIError(err)` instead of always returning 500
|
||||
- Now returns 400 for validation errors
|
||||
|
||||
### 4. Update UI to use shared helper
|
||||
|
||||
**Files**:
|
||||
|
||||
- `apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx`
|
||||
- `packages/ui/primitives/document-flow/add-fields.tsx`
|
||||
|
||||
### 5. Consolidate `hasSignatureField` checks
|
||||
|
||||
Updated to use `isSignatureFieldType` guard (checks both `SIGNATURE` and `FREE_SIGNATURE`):
|
||||
|
||||
- `apps/remix/app/components/general/document-signing/document-signing-form.tsx`
|
||||
- `apps/remix/app/components/general/envelope-signing/envelope-signer-form.tsx`
|
||||
- `apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx`
|
||||
- `apps/remix/app/components/embed/embed-direct-template-client-page.tsx`
|
||||
- `apps/remix/app/components/embed/embed-document-signing-page-v1.tsx`
|
||||
|
||||
### 6. Add E2E tests
|
||||
|
||||
**Files**:
|
||||
|
||||
- `packages/app-tests/e2e/api/v1/document-sending.spec.ts` - 5 new tests
|
||||
- `packages/app-tests/e2e/api/v2/distribute-validation.spec.ts` - 8 new tests
|
||||
|
||||
## Test Coverage
|
||||
|
||||
- Distribution fails when signer has no fields
|
||||
- Distribution fails when signer has only non-signature fields
|
||||
- Distribution succeeds with SIGNATURE field
|
||||
- Distribution succeeds with FREE_SIGNATURE field (v1 only via Prisma)
|
||||
- Distribution succeeds when VIEWER/CC/APPROVER have no fields
|
||||
- Distribution fails when one of multiple signers is missing signature field
|
||||
- Distribution succeeds when all signers have signature fields
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
date: 2026-02-11
|
||||
title: Cert Page Width Mismatch
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
Certificate and audit log pages are generated with hardcoded A4 dimensions (`PDF_SIZE_A4_72PPI`: 595×842) regardless of the actual document page sizes. When the source document uses a different page size (e.g., Letter, Legal, or custom dimensions), the certificate/audit log pages end up with a different width than the document pages. This causes problems with courts that expect uniform page dimensions throughout a PDF.
|
||||
|
||||
**Both width and height must match** the last page of the document so the entire PDF prints uniformly.
|
||||
|
||||
**Root cause**: In `seal-document.handler.ts` (lines 186-187), the certificate payload always uses:
|
||||
|
||||
```ts
|
||||
pageWidth: PDF_SIZE_A4_72PPI.width, // 595
|
||||
pageHeight: PDF_SIZE_A4_72PPI.height, // 842
|
||||
```
|
||||
|
||||
These hardcoded values flow into `generateCertificatePdf`, `generateAuditLogPdf`, `renderCertificate`, and `renderAuditLogs` — all of which use `pageWidth`/`pageHeight` to set Konva stage dimensions and layout content.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Role |
|
||||
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| `packages/lib/jobs/definitions/internal/seal-document.handler.ts` | Orchestrates sealing; passes page dimensions to cert/audit generators |
|
||||
| `packages/lib/constants/pdf.ts` | Defines `PDF_SIZE_A4_72PPI` (595×842) |
|
||||
| `packages/lib/server-only/pdf/generate-certificate-pdf.ts` | Generates certificate PDF; accepts `pageWidth`/`pageHeight` |
|
||||
| `packages/lib/server-only/pdf/generate-audit-log-pdf.ts` | Generates audit log PDF; accepts `pageWidth`/`pageHeight` |
|
||||
| `packages/lib/server-only/pdf/render-certificate.ts` | Renders certificate pages via Konva; uses `pageWidth`/`pageHeight` for stage + layout |
|
||||
| `packages/lib/server-only/pdf/render-audit-logs.ts` | Renders audit log pages via Konva; uses `pageWidth`/`pageHeight` for stage + layout |
|
||||
| `packages/lib/server-only/pdf/get-page-size.ts` | Existing utility — extend with `@libpdf/core` version |
|
||||
| `packages/trpc/server/document-router/download-document-certificate.ts` | Standalone certificate download (also hardcodes A4) |
|
||||
| `packages/trpc/server/document-router/download-document-audit-logs.ts` | Standalone audit log download (also hardcodes A4) |
|
||||
|
||||
## Architecture
|
||||
|
||||
### Current Flow
|
||||
|
||||
1. **One cert PDF + one audit log PDF** generated per envelope with hardcoded A4 dims
|
||||
2. Both appended to **every** envelope item (document) via `decorateAndSignPdf` → `pdfDoc.copyPagesFrom()`
|
||||
3. The audit log is envelope-level (all recipients, all events across all docs) — one per envelope, not per document
|
||||
|
||||
### Multi-Document Envelopes
|
||||
|
||||
- V1 envelopes: single document only
|
||||
- V2 envelopes: support multiple documents (envelope items)
|
||||
- Each envelope item gets both cert + audit log pages appended to it
|
||||
- If documents have different page sizes → need size-matched cert/audit for each
|
||||
|
||||
### Reading Page Dimensions (`@libpdf/core` only)
|
||||
|
||||
Use `@libpdf/core`'s `PDF` class — NOT `@cantoo/pdf-lib`:
|
||||
|
||||
```ts
|
||||
const pdfDoc = await PDF.load(pdfData);
|
||||
const lastPage = pdfDoc.getPage(pdfDoc.getPageCount() - 1);
|
||||
const { width, height } = lastPage; // e.g. 612, 792 for Letter
|
||||
```
|
||||
|
||||
Already used this way in `seal-document.handler.ts` lines 403-410 for V2 field insertion.
|
||||
"Last page" = last page of the original document, before cert/audit pages are appended.
|
||||
|
||||
### Content Layout Adaptation
|
||||
|
||||
Both renderers already handle variable dimensions gracefully:
|
||||
|
||||
- **Width**: `render-certificate.ts:713` / `render-audit-logs.ts:588` — `Math.min(pageWidth - minimumMargin * 2, contentMaxWidth)` with `contentMaxWidth = 768`. Wider pages get more margin, narrower pages tighter margins.
|
||||
- **Height**: Both renderers paginate content into pages using `groupRowsIntoPages()` which respects `pageHeight` via `maxTableHeight = pageHeight - pageTopMargin - pageBottomMargin`. Shorter pages just mean more pages; taller pages fit more rows per page.
|
||||
|
||||
### Playwright PDF Path — Out of Scope
|
||||
|
||||
The `NEXT_PRIVATE_USE_PLAYWRIGHT_PDF` toggle enables a deprecated Playwright-based PDF generation path (`get-certificate-pdf.ts`, `get-audit-logs-pdf.ts`) that also hardcodes `format: 'A4'` in `page.pdf()`. This path is **not being updated** as part of this fix:
|
||||
|
||||
- Both files are marked `@deprecated`
|
||||
- The Konva-based path is the default and recommended path
|
||||
- The Playwright path is behind a feature flag and will be removed
|
||||
|
||||
No changes needed. Add a code comment noting the A4 limitation if the Playwright path is ever re-enabled.
|
||||
|
||||
## Plan
|
||||
|
||||
### 1. Extend `get-page-size.ts` with `@libpdf/core` utility
|
||||
|
||||
Add a `getLastPageDimensions` function to the existing `packages/lib/server-only/pdf/get-page-size.ts` file. This consolidates page-size logic in one place (the file already has the legacy `@cantoo/pdf-lib` version).
|
||||
|
||||
```ts
|
||||
export const getLastPageDimensions = (pdfDoc: PDF): { width: number; height: number } => {
|
||||
const lastPage = pdfDoc.getPage(pdfDoc.getPageCount() - 1);
|
||||
const width = Math.round(lastPage.width);
|
||||
const height = Math.round(lastPage.height);
|
||||
|
||||
if (width < MIN_CERT_PAGE_WIDTH || height < MIN_CERT_PAGE_HEIGHT) {
|
||||
return { width: PDF_SIZE_A4_72PPI.width, height: PDF_SIZE_A4_72PPI.height };
|
||||
}
|
||||
|
||||
return { width, height };
|
||||
};
|
||||
```
|
||||
|
||||
**Dimension rounding**: `Math.round()` both width and height. PDF points at 72ppi are typically whole numbers; rounding avoids spurious float-precision mismatches (e.g., 612.0 vs 612.00001) that would cause unnecessary duplicate cert/audit PDF generation.
|
||||
|
||||
**Minimum page dimensions**: Enforce a minimum threshold (e.g., 300pt for both width and height). If either dimension falls below the minimum, fall back to A4 (595×842). The certificate and audit log renderers have headers, table rows, margins, and QR codes that require a minimum viable area.
|
||||
|
||||
### 2. Read last page dimensions from each envelope item's PDF
|
||||
|
||||
In `seal-document.handler.ts`, before generating cert/audit PDFs:
|
||||
|
||||
- For each `envelopeItem`, load the PDF and read the **last page's width and height** using `getLastPageDimensions`
|
||||
- Use `PDF.load()` then pass the loaded doc to the utility
|
||||
|
||||
**Resealing consideration**: When `isResealing` is true, envelope items are remapped to use `initialData` (lines 152-158) before this point. Page-size extraction must operate on the same data source that `decorateAndSignPdf` will use. Since the `envelopeItems` array is already remapped by the time we read dimensions, reading from `envelopeItem.documentData` will naturally give the correct (initial) data. No special handling needed beyond ensuring the dimension read happens **after** the resealing remap.
|
||||
|
||||
### 3. Generate cert/audit PDFs per unique page size
|
||||
|
||||
Current flow generates one cert + one audit log doc per envelope. Change to:
|
||||
|
||||
1. Collect `{ width, height }` of the last page for each envelope item
|
||||
2. Deduplicate by `"${width}x${height}"` key (using the already-rounded integers)
|
||||
3. For each unique size, generate cert PDF and audit log PDF with those dimensions
|
||||
4. Store in a `Map<string, { certificateDoc, auditLogDoc }>` keyed by `"${width}x${height}"`
|
||||
|
||||
For the common single-document case, this is one generation — same perf as today.
|
||||
|
||||
### 4. Thread the correct docs into `decorateAndSignPdf`
|
||||
|
||||
In the envelope item loop, look up the item's last-page dimensions in the map and pass the matching cert/audit docs. Signature of `decorateAndSignPdf` doesn't change — it still receives a single `certificateDoc` and `auditLogDoc`, just the right ones per item.
|
||||
|
||||
### 5. Update standalone download routes
|
||||
|
||||
`download-document-certificate.ts` and `download-document-audit-logs.ts` also hardcode A4:
|
||||
|
||||
- Both routes have `documentId` which maps to a specific envelope item
|
||||
- Fetch **that specific document's** PDF data, load it, read last page width + height via `getLastPageDimensions`
|
||||
- Pass `{ pageWidth, pageHeight }` to the generator
|
||||
- This ensures the standalone download matches the dimensions the user would see in the sealed PDF for that document
|
||||
|
||||
### 6. Edge cases
|
||||
|
||||
| Scenario | Behavior |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| Mixed page sizes within one PDF | Use last page's dimensions (per spec) |
|
||||
| Page dimensions below minimum threshold | Fall back to A4 (595×842) |
|
||||
| Landscape pages | width/height just swap roles; renderers adapt via `Math.min()` capping. No special handling |
|
||||
| Fallback if page dims unreadable | Default to A4 (595×842) |
|
||||
| Resealing | Dimensions read after `initialData` remap — correct source automatically |
|
||||
| Playwright PDF path enabled | Remains A4 — out of scope, deprecated |
|
||||
| Single-doc envelope (most common) | One generation, same perf as today |
|
||||
| Multi-doc envelope, same page sizes | Dedup key matches → one generation |
|
||||
| Multi-doc envelope, different sizes | One generation per unique size |
|
||||
|
||||
### 7. Tests
|
||||
|
||||
- Add assertion-based E2E test (no visual regression / reference images needed)
|
||||
- Seal a Letter-size (612×792) PDF through the full flow
|
||||
- Load the sealed output and assert all pages (document + cert + audit) have matching width/height
|
||||
- Can be added to `envelope-alignment.spec.ts` or as a new focused test
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. **Extend `get-page-size.ts`** — add `getLastPageDimensions(pdfDoc: PDF): { width: number; height: number }` using `@libpdf/core`, with `Math.round()` and minimum dimension enforcement
|
||||
2. **In `seal-document.handler.ts`**:
|
||||
a. After the resealing remap (line ~159), load each envelope item's PDF via `PDF.load()` and collect last page `{ width, height }` using `getLastPageDimensions`
|
||||
b. Deduplicate by `"${width}x${height}"` key
|
||||
c. Generate cert/audit PDFs per unique size (parallel via `Promise.all`)
|
||||
d. In envelope item loop, look up matching cert/audit doc by size key
|
||||
3. **Fix `download-document-certificate.ts`** — load the specific document's PDF, read last page dims via `getLastPageDimensions`, pass to generator
|
||||
4. **Fix `download-document-audit-logs.ts`** — same as above, using the specific `documentId`'s PDF
|
||||
5. **Add E2E test** — assertion-based test with a Letter-size document verifying all page dimensions match after sealing
|
||||
@@ -0,0 +1,312 @@
|
||||
---
|
||||
date: 2026-02-26
|
||||
title: pnpm Migration
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Migrate from npm to pnpm to eliminate dependency resolution duplication issues that cause bundler problems. The current npm workspace setup results in nested `node_modules` copies that don't deduplicate reliably, requiring manual hoisting and `npm dedupe` cycles. pnpm's content-addressable store and strict symlink structure eliminates this class of problem entirely.
|
||||
|
||||
## Current State
|
||||
|
||||
- **Package manager:** npm@10.7.0 with `legacy-peer-deps=true` and `prefer-dedupe=true`
|
||||
- **Workspaces:** 18 total (3 apps, 15 packages) declared in root `package.json` `workspaces` field
|
||||
- **Lockfile:** `package-lock.json`
|
||||
- **Patches:** `patch-package` with one patch (`@ai-sdk+google-vertex+3.0.81`)
|
||||
- **Overrides:** `lodash`, `pdfjs-dist`, `typescript`, `zod` in root `package.json`
|
||||
- **Syncpack:** installed but unconfigured (no `.syncpackrc`)
|
||||
- **Heavy duplication:** `zod` in 7 workspaces, `ts-pattern` in 9, `luxon` in 8, `react` in 6, etc.
|
||||
- **Docker:** `turbo prune` → `npm ci` → `npm ci --only=production` multi-stage build
|
||||
- **Existing Dockerfiles:** `docker/Dockerfile` (primary, npm), `apps/remix/Dockerfile.pnpm` (already exists, needs review)
|
||||
|
||||
## Migration Steps
|
||||
|
||||
### Phase 1: Core Migration
|
||||
|
||||
#### Step 1: Enable pnpm via corepack
|
||||
|
||||
```bash
|
||||
corepack enable pnpm
|
||||
corepack use pnpm@latest
|
||||
```
|
||||
|
||||
This adds a `"packageManager"` field to root `package.json` (e.g. `"packageManager": "pnpm@10.x.x"`). Remove the existing `"engines"` npm constraint if present.
|
||||
|
||||
#### Step 2: Create `pnpm-workspace.yaml`
|
||||
|
||||
```yaml
|
||||
packages:
|
||||
- apps/*
|
||||
- packages/*
|
||||
```
|
||||
|
||||
Remove the `"workspaces"` field from root `package.json` — pnpm uses `pnpm-workspace.yaml` instead.
|
||||
|
||||
#### Step 3: Convert lockfile
|
||||
|
||||
```bash
|
||||
pnpm import
|
||||
```
|
||||
|
||||
This reads `package-lock.json` and generates `pnpm-lock.yaml`. After verifying, delete `package-lock.json`.
|
||||
|
||||
#### Step 4: Create `.npmrc` for pnpm
|
||||
|
||||
Replace the current `.npmrc` contents. The existing settings (`legacy-peer-deps=true`, `prefer-dedupe=true`) are npm-specific.
|
||||
|
||||
```ini
|
||||
# Hoist packages that expect to be resolvable from any workspace.
|
||||
# Start strict, add patterns here only as needed.
|
||||
shamefully-hoist=true
|
||||
```
|
||||
|
||||
> **Note:** `shamefully-hoist=true` is the pragmatic starting point. It mimics npm's flat `node_modules` layout. Once the migration is stable, this can be tightened to `hoist-pattern[]` entries for specific packages that need it, moving toward pnpm's strict isolation model.
|
||||
|
||||
#### Step 5: Clean install
|
||||
|
||||
```bash
|
||||
rm -rf node_modules apps/*/node_modules packages/*/node_modules
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Verify the install completes without errors. Fix any peer dependency warnings — pnpm is stricter than npm with `legacy-peer-deps=true`.
|
||||
|
||||
#### Step 6: Convert `overrides` to `pnpm.overrides`
|
||||
|
||||
In root `package.json`, move the `overrides` block under `pnpm`:
|
||||
|
||||
```json
|
||||
{
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"lodash": "4.17.23",
|
||||
"pdfjs-dist": "5.4.296",
|
||||
"typescript": "5.6.2",
|
||||
"zod": "^3.25.76"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Remove the top-level `overrides` field (that's npm-specific).
|
||||
|
||||
#### Step 7: Convert patch-package to pnpm patches
|
||||
|
||||
pnpm has native patching. Convert the existing `@ai-sdk+google-vertex+3.0.81` patch:
|
||||
|
||||
```bash
|
||||
# Remove patch-package dependency and postinstall script
|
||||
# Then use pnpm's native patching:
|
||||
pnpm patch @ai-sdk/google-vertex@3.0.81
|
||||
# Apply the same changes from patches/@ai-sdk+google-vertex+3.0.81.patch
|
||||
pnpm patch-commit <temp-dir>
|
||||
```
|
||||
|
||||
This adds a `pnpm.patchedDependencies` entry to root `package.json` and stores the patch in a `patches/` directory (pnpm's own format). Remove `patch-package` from dependencies and the `postinstall` script.
|
||||
|
||||
### Phase 2: Catalogs
|
||||
|
||||
#### Step 8: Identify catalog candidates
|
||||
|
||||
Packages duplicated across 3+ workspaces are prime candidates:
|
||||
|
||||
| Package | Workspaces | Catalog? |
|
||||
| ----------------------------------------------- | ---------- | ---------------------- |
|
||||
| `zod` | 7 | Yes |
|
||||
| `ts-pattern` | 9 | Yes |
|
||||
| `luxon` | 8 | Yes |
|
||||
| `react` / `react-dom` | 6 / 3 | Yes |
|
||||
| `typescript` | 6 | Yes |
|
||||
| `nanoid` | 4 | Yes |
|
||||
| `@lingui/core` / `macro` / `react` | 2-3 | Yes |
|
||||
| `@simplewebauthn/server` | 3 | Yes |
|
||||
| `@documenso/*` (internal) | varies | No (use `workspace:*`) |
|
||||
| `@aws-sdk/*` | 2 | Yes |
|
||||
| `hono` | 2 | Yes |
|
||||
| `posthog-node` / `posthog-js` | 2 | Yes |
|
||||
| `remeda` | 3 | Yes |
|
||||
| `@tanstack/react-query` | 2 | Yes |
|
||||
| `@trpc/*` | 2 | Yes |
|
||||
| `superjson` | 2 | Yes |
|
||||
| `kysely` | 2 | Yes |
|
||||
| `@types/react` / `@types/node` / `@types/luxon` | 3-4 | Yes |
|
||||
|
||||
#### Step 9: Define catalogs in `pnpm-workspace.yaml`
|
||||
|
||||
```yaml
|
||||
packages:
|
||||
- apps/*
|
||||
- packages/*
|
||||
|
||||
catalog:
|
||||
# Core
|
||||
react: ^18
|
||||
react-dom: ^18
|
||||
typescript: 5.6.2
|
||||
zod: ^3.25.76
|
||||
|
||||
# Shared utilities
|
||||
ts-pattern: <current-version>
|
||||
luxon: ^3.7.2
|
||||
nanoid: ^5.1.6
|
||||
remeda: <current-version>
|
||||
superjson: ^2.2.5
|
||||
|
||||
# AWS
|
||||
'@aws-sdk/client-s3': ^3.998.0
|
||||
'@aws-sdk/client-sesv2': ^3.998.0
|
||||
'@aws-sdk/cloudfront-signer': ^3.998.0
|
||||
'@aws-sdk/s3-request-presigner': ^3.998.0
|
||||
'@aws-sdk/signature-v4-crt': ^3.998.0
|
||||
|
||||
# Framework
|
||||
hono: 4.12.2
|
||||
'@tanstack/react-query': <current-version>
|
||||
'@trpc/client': 11.8.1
|
||||
'@trpc/react-query': 11.8.1
|
||||
'@trpc/server': 11.8.1
|
||||
|
||||
# i18n
|
||||
'@lingui/core': ^5.6.0
|
||||
'@lingui/macro': ^5.6.0
|
||||
'@lingui/react': ^5.6.0
|
||||
|
||||
# Auth
|
||||
'@simplewebauthn/server': <current-version>
|
||||
|
||||
# Observability
|
||||
posthog-node: 4.18.0
|
||||
posthog-js: <current-version>
|
||||
|
||||
# Database
|
||||
kysely: <current-version>
|
||||
'@prisma/client': ^6.19.0
|
||||
prisma: ^6.19.0
|
||||
|
||||
# Types
|
||||
'@types/react': <current-version>
|
||||
'@types/react-dom': <current-version>
|
||||
'@types/node': ^20
|
||||
'@types/luxon': <current-version>
|
||||
```
|
||||
|
||||
#### Step 10: Update workspace `package.json` files
|
||||
|
||||
Replace pinned versions with `catalog:` protocol for all cataloged packages:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"zod": "catalog:",
|
||||
"ts-pattern": "catalog:",
|
||||
"luxon": "catalog:"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is a mechanical find-and-replace across all workspace `package.json` files.
|
||||
|
||||
### Phase 3: Internal Workspace References
|
||||
|
||||
#### Step 11: Convert internal references to `workspace:*`
|
||||
|
||||
All `@documenso/*` internal package references currently use `"*"`. Convert to pnpm's `workspace:*` protocol:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": {
|
||||
"@documenso/lib": "workspace:*",
|
||||
"@documenso/prisma": "workspace:*"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This makes the workspace resolution explicit and prevents accidental resolution to a published version.
|
||||
|
||||
### Phase 4: Docker & CI
|
||||
|
||||
#### Step 12: Update primary Dockerfile (`docker/Dockerfile`)
|
||||
|
||||
The existing multi-stage build needs to change:
|
||||
|
||||
1. **base:** Add pnpm — `corepack enable pnpm` or install via `npm i -g pnpm`
|
||||
2. **builder:** `turbo prune` still works with pnpm. Output structure is the same.
|
||||
3. **installer:**
|
||||
- Replace `npm ci` with `pnpm install --frozen-lockfile`
|
||||
- Copy `pnpm-lock.yaml` and `pnpm-workspace.yaml` instead of `package-lock.json`
|
||||
- Remove `patch-package` from postinstall (pnpm patches are applied natively)
|
||||
4. **runner:**
|
||||
- Replace `npm ci --only=production` with `pnpm install --frozen-lockfile --prod`
|
||||
- Or use `pnpm deploy` for standalone output (copies only production deps to a flat directory)
|
||||
|
||||
Review `apps/remix/Dockerfile.pnpm` — it already exists and may have most of this solved. Reconcile with the primary `docker/Dockerfile`.
|
||||
|
||||
#### Step 13: Update CI workflows
|
||||
|
||||
Search for all `npm ci`, `npm install`, `npm run` in CI config files (`.github/workflows/`, etc.) and replace with `pnpm install --frozen-lockfile`, `pnpm run`, etc.
|
||||
|
||||
Ensure corepack is enabled in CI runners:
|
||||
|
||||
```yaml
|
||||
- run: corepack enable pnpm
|
||||
```
|
||||
|
||||
#### Step 14: Update turborepo config
|
||||
|
||||
Turbo works with pnpm out of the box. The `turbo.json` should not need changes. Verify `turbo prune` generates correct output with pnpm lockfile.
|
||||
|
||||
### Phase 5: Cleanup & Tighten
|
||||
|
||||
#### Step 15: Remove npm-specific tooling
|
||||
|
||||
- Remove `syncpack` (catalogs replace its purpose)
|
||||
- Remove `patch-package` (pnpm native patches replace it)
|
||||
- Remove `"workspaces"` from root `package.json` if not already done
|
||||
- Delete `package-lock.json`
|
||||
- Update `.gitignore` if needed (pnpm store is outside the repo by default)
|
||||
|
||||
#### Step 16: Tighten hoisting (optional, future)
|
||||
|
||||
Once stable, replace `shamefully-hoist=true` with targeted hoist patterns:
|
||||
|
||||
```ini
|
||||
shamefully-hoist=false
|
||||
hoist-pattern[]=*eslint*
|
||||
hoist-pattern[]=*prettier*
|
||||
# Add others as discovered
|
||||
```
|
||||
|
||||
This moves toward strict isolation where each package can only import what it declares. Catches phantom dependency issues. Do this incrementally — let the bundler tell you what breaks.
|
||||
|
||||
#### Step 17: Remove root-level dependency hoisting
|
||||
|
||||
With catalogs and strict resolution, dependencies currently hoisted to root `package.json` for deduplication purposes can be moved back to the workspaces that actually use them. The root `package.json` should only contain tooling deps (`turbo`, `prettier`, `eslint`, etc.) and `pnpm.overrides`.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
1. **Phantom dependencies surface:** pnpm's strict isolation will expose imports that work today only because npm hoisted them. `shamefully-hoist=true` defers this, but tightening later will reveal them.
|
||||
- **Mitigation:** Start with `shamefully-hoist=true`. Tighten incrementally after the migration is stable.
|
||||
|
||||
2. **Peer dependency strictness:** pnpm enforces peer deps by default. The current `.npmrc` has `legacy-peer-deps=true` which suppresses all peer dep errors.
|
||||
- **Mitigation:** Run `pnpm install` and address peer dep warnings. Most will be resolvable by adding missing peer deps to the relevant workspace.
|
||||
|
||||
3. **Docker build breakage:** The `turbo prune` + `npm ci` pipeline is battle-tested. Switching to pnpm changes the install semantics.
|
||||
- **Mitigation:** The existing `Dockerfile.pnpm` in `apps/remix/` provides a reference. Test the Docker build in CI before merging.
|
||||
|
||||
4. **CI cache invalidation:** Switching lockfiles invalidates all CI dependency caches.
|
||||
- **Mitigation:** Update cache keys to use `pnpm-lock.yaml` hash. First CI run will be slower, subsequent runs will cache normally.
|
||||
|
||||
5. **Turbo + pnpm compatibility:** Turbo has first-class pnpm support, but `turbo prune` output format may differ slightly.
|
||||
- **Mitigation:** Test `turbo prune --scope=@documenso/remix --docker` and verify output structure before updating Dockerfile.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
- [ ] `pnpm install` succeeds with no errors
|
||||
- [ ] `pnpm run build` succeeds (all workspaces)
|
||||
- [ ] `pnpm run lint` passes
|
||||
- [ ] `pnpm run dev` starts correctly
|
||||
- [ ] Docker build produces a working image
|
||||
- [ ] E2E tests pass (`pnpm run test:e2e`)
|
||||
- [ ] No duplicate package copies in `node_modules` for key deps (`zod`, `react`, `typescript`)
|
||||
- [ ] `pnpm audit` shows same or better results than current npm audit
|
||||
- [ ] CI pipeline passes end-to-end
|
||||
@@ -0,0 +1,551 @@
|
||||
---
|
||||
date: 2026-02-19
|
||||
title: Database Rate Limiting
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Replace the in-memory `hono-rate-limiter` with a database-backed rate limiting system using Prisma and PostgreSQL. The current in-memory approach is ineffective in multi-instance deployments since there are no sticky sessions. The new system uses **bucketed counters** (one row per key/action/time-bucket with atomic increment) to efficiently handle both high-throughput API rate limiting and granular auth/email rate limiting.
|
||||
|
||||
### Design Decisions
|
||||
|
||||
- **Bucketed counters** over row-per-request: high-throughput consumers would create thousands of rows per minute; bucketed counters reduce this to one row per key per time bucket
|
||||
- **Fixed time windows**: simpler than sliding windows, the 2x burst-at-boundary scenario is acceptable for rate limiting purposes
|
||||
- **Dual-key rate limiting**: per-identifier (`max`) and per-IP (`globalMax`) checked independently via separate rows with a `key` prefix (`id:` / `ip:`)
|
||||
- **Accept slight over-count**: the upsert is atomic (increment + return count in one operation) but concurrent requests near the limit may both see a count just under the threshold before either commits, allowing a slight overshoot
|
||||
- **Fail-open on errors**: if the rate limit DB query fails, allow the request through rather than blocking legitimate users
|
||||
- **Prisma upsert** with `{ increment: 1 }` for atomic counter updates, returns the updated row so count check is a single operation
|
||||
- **Application cron job** for cleanup of expired bucket rows
|
||||
|
||||
### Rate Limit Check Flow
|
||||
|
||||
```
|
||||
check({ ip, identifier }) ->
|
||||
1. Upsert IP row (ip:{ip} / action / bucket) with count + 1, RETURNING count
|
||||
-> if globalMax is set and count >= globalMax, return { isLimited: true }
|
||||
2. Upsert identifier row (id:{identifier} / action / bucket) with count + 1, RETURNING count
|
||||
-> if count >= max, return { isLimited: true }
|
||||
3. Neither limited -> return { isLimited: false }
|
||||
```
|
||||
|
||||
Each upsert atomically increments and returns the new count in a single operation. Both counters always increment on every check — there's no conditional logic to skip one based on the other. This keeps the implementation simple and avoids read-then-write race conditions. If only IP is provided (API rate limiting), only step 1 runs.
|
||||
|
||||
---
|
||||
|
||||
## 1. Database Schema
|
||||
|
||||
### 1.1 Prisma model
|
||||
|
||||
Add to `packages/prisma/schema.prisma` after the `Counter` model:
|
||||
|
||||
```prisma
|
||||
model RateLimit {
|
||||
key String
|
||||
action String
|
||||
bucket DateTime
|
||||
count Int @default(1)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@id([key, action, bucket])
|
||||
@@index([createdAt])
|
||||
}
|
||||
```
|
||||
|
||||
- **Composite primary key** `(key, action, bucket)` serves as both the unique constraint for upserts and the lookup index
|
||||
- **`key`** is prefixed: `ip:1.2.3.4` or `id:user@example.com`
|
||||
- **`action`** is the rate limit action name: `auth.forgot-password`, `api.v1`, etc.
|
||||
- **`bucket`** is the start of the time window, truncated to the window size (e.g., `2026-02-19T10:05:00Z` for a 5-minute bucket)
|
||||
- **`createdAt` index** is for the cleanup job to efficiently delete old rows
|
||||
- **`count`** starts at 1 (set by the create side of the upsert)
|
||||
|
||||
### 1.2 Migration
|
||||
|
||||
Generate with `npx prisma migrate dev --name add-rate-limits`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Rate Limit Library
|
||||
|
||||
### 2.1 Core module
|
||||
|
||||
Create `packages/lib/server-only/rate-limit/rate-limit.ts`:
|
||||
|
||||
```typescript
|
||||
type WindowUnit = 's' | 'm' | 'h' | 'd';
|
||||
type WindowStr = `${number}${WindowUnit}`;
|
||||
|
||||
type RateLimitConfig = {
|
||||
action: string;
|
||||
max: number;
|
||||
globalMax?: number;
|
||||
window: WindowStr;
|
||||
};
|
||||
|
||||
type CheckParams = {
|
||||
ip: string;
|
||||
identifier?: string;
|
||||
};
|
||||
|
||||
export const rateLimit = (config: RateLimitConfig) => {
|
||||
return {
|
||||
async check(params: CheckParams): Promise<{
|
||||
isLimited: boolean;
|
||||
remaining: number;
|
||||
limit: number;
|
||||
reset: Date;
|
||||
}> { ... }
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 2.2 Window parsing and bucket computation
|
||||
|
||||
```typescript
|
||||
const parseWindow = (window: WindowStr): number => {
|
||||
const value = parseInt(window.slice(0, -1), 10);
|
||||
const unit = window.slice(-1) as WindowUnit;
|
||||
const multipliers: Record<WindowUnit, number> = {
|
||||
s: 1000,
|
||||
m: 60 * 1000,
|
||||
h: 60 * 60 * 1000,
|
||||
d: 24 * 60 * 60 * 1000,
|
||||
};
|
||||
return value * multipliers[unit];
|
||||
};
|
||||
|
||||
const getBucket = (windowMs: number): Date => {
|
||||
const now = Date.now();
|
||||
return new Date(now - (now % windowMs));
|
||||
};
|
||||
```
|
||||
|
||||
### 2.3 Check implementation
|
||||
|
||||
The `check()` method:
|
||||
|
||||
1. Compute the current bucket from the window
|
||||
2. Compute `reset` as `bucket + windowMs` (the start of the next window)
|
||||
3. If `globalMax` is set, upsert the IP row and check count
|
||||
4. If `identifier` is provided, upsert the identifier row and check count
|
||||
5. Wrap in try/catch — **fail-open** on any database error (log the error, return `{ isLimited: false }`)
|
||||
|
||||
Each upsert uses Prisma's `upsert` with `{ increment: 1 }`:
|
||||
|
||||
```typescript
|
||||
const result = await prisma.rateLimit.upsert({
|
||||
where: {
|
||||
key_action_bucket: {
|
||||
key: `ip:${params.ip}`,
|
||||
action: config.action,
|
||||
bucket,
|
||||
},
|
||||
},
|
||||
create: {
|
||||
key: `ip:${params.ip}`,
|
||||
action: config.action,
|
||||
bucket,
|
||||
count: 1,
|
||||
},
|
||||
update: {
|
||||
count: { increment: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
if (config.globalMax && result.count >= config.globalMax) {
|
||||
return { isLimited: true, remaining: 0, limit: config.globalMax };
|
||||
}
|
||||
```
|
||||
|
||||
### 2.4 Rate limit definitions
|
||||
|
||||
Create `packages/lib/server-only/rate-limit/rate-limits.ts` with all rate limit instances:
|
||||
|
||||
```typescript
|
||||
// ---- Auth (Tier 1 - Critical, sends emails) ----
|
||||
export const signupRateLimit = rateLimit({
|
||||
action: 'auth.signup',
|
||||
max: 5,
|
||||
globalMax: 10,
|
||||
window: '1h',
|
||||
});
|
||||
|
||||
export const forgotPasswordRateLimit = rateLimit({
|
||||
action: 'auth.forgot-password',
|
||||
max: 3,
|
||||
globalMax: 20,
|
||||
window: '1h',
|
||||
});
|
||||
|
||||
export const resendVerifyEmailRateLimit = rateLimit({
|
||||
action: 'auth.resend-verify-email',
|
||||
max: 3,
|
||||
globalMax: 20,
|
||||
window: '1h',
|
||||
});
|
||||
|
||||
export const request2FAEmailRateLimit = rateLimit({
|
||||
action: 'auth.request-2fa-email',
|
||||
max: 5,
|
||||
globalMax: 20,
|
||||
window: '15m',
|
||||
});
|
||||
|
||||
// ---- Auth (Tier 2 - Unauthenticated) ----
|
||||
export const loginRateLimit = rateLimit({
|
||||
action: 'auth.login',
|
||||
max: 10,
|
||||
globalMax: 50,
|
||||
window: '15m',
|
||||
});
|
||||
|
||||
export const resetPasswordRateLimit = rateLimit({
|
||||
action: 'auth.reset-password',
|
||||
max: 5,
|
||||
globalMax: 20,
|
||||
window: '1h',
|
||||
});
|
||||
|
||||
export const verifyEmailRateLimit = rateLimit({
|
||||
action: 'auth.verify-email',
|
||||
max: 5,
|
||||
globalMax: 20,
|
||||
window: '15m',
|
||||
});
|
||||
|
||||
export const passkeyRateLimit = rateLimit({
|
||||
action: 'auth.passkey',
|
||||
max: 10,
|
||||
globalMax: 50,
|
||||
window: '15m',
|
||||
});
|
||||
|
||||
export const oauthRateLimit = rateLimit({
|
||||
action: 'auth.oauth',
|
||||
max: 10,
|
||||
globalMax: 50,
|
||||
window: '15m',
|
||||
});
|
||||
|
||||
export const linkOrgAccountRateLimit = rateLimit({
|
||||
action: 'auth.link-org-account',
|
||||
max: 5,
|
||||
globalMax: 20,
|
||||
window: '1h',
|
||||
});
|
||||
|
||||
// ---- API (Tier 4 - Standard) ----
|
||||
export const apiV1RateLimit = rateLimit({
|
||||
action: 'api.v1',
|
||||
max: 100,
|
||||
window: '1m',
|
||||
});
|
||||
|
||||
export const apiV2RateLimit = rateLimit({
|
||||
action: 'api.v2',
|
||||
max: 100,
|
||||
window: '1m',
|
||||
});
|
||||
|
||||
export const apiTrpcRateLimit = rateLimit({
|
||||
action: 'api.trpc',
|
||||
max: 100,
|
||||
window: '1m',
|
||||
});
|
||||
|
||||
export const aiRateLimit = rateLimit({
|
||||
action: 'api.ai',
|
||||
max: 3,
|
||||
window: '1m',
|
||||
});
|
||||
|
||||
export const fileUploadRateLimit = rateLimit({
|
||||
action: 'api.file-upload',
|
||||
max: 20,
|
||||
window: '1m',
|
||||
});
|
||||
```
|
||||
|
||||
Exact limits are initial values — tune based on observed traffic patterns. These should be easy to adjust.
|
||||
|
||||
---
|
||||
|
||||
## 3. Integration Points
|
||||
|
||||
### 3.1 Hono middleware for API routes
|
||||
|
||||
Create a reusable Hono middleware factory in `packages/lib/server-only/rate-limit/rate-limit-middleware.ts` that wraps the `rateLimit` checker into Hono middleware:
|
||||
|
||||
```typescript
|
||||
import { type MiddlewareHandler } from 'hono';
|
||||
|
||||
import { getIpAddress } from '@documenso/lib/universal/get-ip-address';
|
||||
|
||||
export const createRateLimitMiddleware = (
|
||||
limiter: ReturnType<typeof rateLimit>,
|
||||
options?: { identifierFn?: (c: Context) => string | undefined },
|
||||
): MiddlewareHandler => {
|
||||
return async (c, next) => {
|
||||
let ip: string;
|
||||
try {
|
||||
ip = getIpAddress(c.req.raw);
|
||||
} catch {
|
||||
ip = 'unknown';
|
||||
}
|
||||
|
||||
const identifier = options?.identifierFn?.(c);
|
||||
|
||||
const result = await limiter.check({ ip, identifier });
|
||||
|
||||
c.header('X-RateLimit-Limit', String(result.limit));
|
||||
c.header('X-RateLimit-Remaining', String(result.remaining));
|
||||
c.header('X-RateLimit-Reset', String(Math.ceil(result.reset.getTime() / 1000)));
|
||||
|
||||
if (result.isLimited) {
|
||||
c.header('Retry-After', String(Math.ceil((result.reset.getTime() - Date.now()) / 1000)));
|
||||
return c.json({ error: 'Too many requests, please try again later.' }, 429);
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 3.2 Replace existing Hono rate limiters
|
||||
|
||||
In `apps/remix/server/router.ts`:
|
||||
|
||||
- Remove `hono-rate-limiter` import and both `rateLimiter()` instances
|
||||
- Replace with `createRateLimitMiddleware()` calls using the defined rate limits
|
||||
- API routes use IP-only limiting (no identifier)
|
||||
- AI route uses IP-only limiting with the stricter 3/min limit
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
import { rateLimiter } from 'hono-rate-limiter';
|
||||
const rateLimitMiddleware = rateLimiter({ ... });
|
||||
|
||||
// After
|
||||
import { createRateLimitMiddleware } from '@documenso/lib/server-only/rate-limit/rate-limit-middleware';
|
||||
import { apiV1RateLimit, apiV2RateLimit, aiRateLimit } from '@documenso/lib/server-only/rate-limit/rate-limits';
|
||||
|
||||
const apiV1RateLimitMiddleware = createRateLimitMiddleware(apiV1RateLimit);
|
||||
const apiV2RateLimitMiddleware = createRateLimitMiddleware(apiV2RateLimit);
|
||||
const aiRateLimitMiddleware = createRateLimitMiddleware(aiRateLimit);
|
||||
```
|
||||
|
||||
### 3.3 Response helpers for inline checks
|
||||
|
||||
For auth routes (Hono handlers) and tRPC routes where rate limiting is applied inline rather than via middleware, provide helpers that handle the response formatting and headers consistently.
|
||||
|
||||
**Hono helper** — returns a 429 `Response` with headers if limited, or `null` if allowed:
|
||||
|
||||
```typescript
|
||||
export const rateLimitResponse = (c: Context, result: RateLimitCheckResult): Response | null => {
|
||||
c.header('X-RateLimit-Limit', String(result.limit));
|
||||
c.header('X-RateLimit-Remaining', String(result.remaining));
|
||||
c.header('X-RateLimit-Reset', String(Math.ceil(result.reset.getTime() / 1000)));
|
||||
|
||||
if (result.isLimited) {
|
||||
c.header('Retry-After', String(Math.ceil((result.reset.getTime() - Date.now()) / 1000)));
|
||||
return c.json({ error: 'Too many requests, please try again later.' }, 429);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
```
|
||||
|
||||
Usage in auth routes:
|
||||
|
||||
```typescript
|
||||
const result = await loginRateLimit.check({
|
||||
ip: requestMetadata.ipAddress ?? 'unknown',
|
||||
identifier: input.email,
|
||||
});
|
||||
|
||||
const limited = rateLimitResponse(c, result);
|
||||
if (limited) return limited;
|
||||
```
|
||||
|
||||
**tRPC helper** — throws a `TRPCError` with rate limit headers if limited:
|
||||
|
||||
```typescript
|
||||
export const assertRateLimit = (result: RateLimitCheckResult): void => {
|
||||
if (result.isLimited) {
|
||||
throw new TRPCError({
|
||||
code: 'TOO_MANY_REQUESTS',
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Usage in tRPC routes:
|
||||
|
||||
```typescript
|
||||
const result = await request2FAEmailRateLimit.check({
|
||||
ip: ctx.requestMetadata.ipAddress ?? 'unknown',
|
||||
identifier: input.recipientId,
|
||||
});
|
||||
|
||||
assertRateLimit(result);
|
||||
```
|
||||
|
||||
Both helpers live in `packages/lib/server-only/rate-limit/rate-limit-middleware.ts` alongside the Hono middleware.
|
||||
|
||||
### 3.4 Auth endpoint rate limiting
|
||||
|
||||
In `packages/auth/server/routes/email-password.ts`, add rate limit checks at the start of each handler using the `rateLimitResponse` helper.
|
||||
|
||||
Apply to each endpoint per the tier list:
|
||||
|
||||
| Endpoint | Rate Limit |
|
||||
| --------------------------- | ----------------------------------------------------- |
|
||||
| `POST /signup` | `signupRateLimit` with `identifier: email` |
|
||||
| `POST /authorize` (login) | `loginRateLimit` with `identifier: email` |
|
||||
| `POST /forgot-password` | `forgotPasswordRateLimit` with `identifier: email` |
|
||||
| `POST /resend-verify-email` | `resendVerifyEmailRateLimit` with `identifier: email` |
|
||||
| `POST /verify-email` | `verifyEmailRateLimit` with `identifier: token` |
|
||||
| `POST /reset-password` | `resetPasswordRateLimit` with `identifier: token` |
|
||||
| `POST /passkey/authorize` | `passkeyRateLimit` (IP only, no identifier) |
|
||||
| `POST /oauth/authorize/*` | `oauthRateLimit` (IP only) |
|
||||
|
||||
### 3.4 tRPC unauthenticated route rate limiting
|
||||
|
||||
For unauthenticated tRPC routes that send emails, add rate limit checks at the start of the route handler:
|
||||
|
||||
| Route | Rate Limit | Identifier |
|
||||
| ---------------------------------------------------------- | ------------------------------------ | ---------------------- |
|
||||
| `document.accessAuth.request2FAEmail` | `request2FAEmailRateLimit` | `recipientId` or token |
|
||||
| `enterprise.organisation.authenticationPortal.linkAccount` | `linkOrgAccountRateLimit` | email |
|
||||
| `template.createDocumentFromDirectTemplate` | Dedicated direct template rate limit | IP only |
|
||||
|
||||
Access `requestMetadata` from the tRPC context (`ctx.requestMetadata.ipAddress`).
|
||||
|
||||
### 3.5 tRPC and file routes — general API rate limiting
|
||||
|
||||
Add rate limit middleware for currently unprotected routes:
|
||||
|
||||
- `/api/trpc/*` — apply `apiTrpcRateLimit` middleware
|
||||
- `/api/files/*` — apply `fileUploadRateLimit` middleware
|
||||
|
||||
---
|
||||
|
||||
## 4. Cleanup Job
|
||||
|
||||
### 4.1 Job definition
|
||||
|
||||
Create `packages/lib/jobs/definitions/internal/cleanup-rate-limits.ts`:
|
||||
|
||||
```typescript
|
||||
export const CLEANUP_RATE_LIMITS_JOB_DEFINITION = {
|
||||
id: 'internal.cleanup-rate-limits',
|
||||
name: 'Cleanup Rate Limits',
|
||||
version: '1.0.0',
|
||||
trigger: {
|
||||
name: 'internal.cleanup-rate-limits',
|
||||
schema: z.object({}),
|
||||
cron: '*/15 * * * *', // Every 15 minutes
|
||||
},
|
||||
handler: async ({ payload, io }) => {
|
||||
const handler = await import('./cleanup-rate-limits.handler');
|
||||
await handler.run({ payload, io });
|
||||
},
|
||||
} as const satisfies JobDefinition<...>;
|
||||
```
|
||||
|
||||
### 4.2 Job handler
|
||||
|
||||
Create `packages/lib/jobs/definitions/internal/cleanup-rate-limits.handler.ts`:
|
||||
|
||||
- Delete all `RateLimit` rows where `createdAt` is older than 24 hours (covers all possible windows with margin)
|
||||
- Use batched deletes to avoid long-running transactions
|
||||
- Batch in chunks of 10,000 rows
|
||||
|
||||
```typescript
|
||||
export const run = async () => {
|
||||
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
let deleted = 0;
|
||||
do {
|
||||
// Prisma doesn't support DELETE with LIMIT, so use raw SQL for batching
|
||||
deleted = await prisma.$executeRaw`
|
||||
DELETE FROM "RateLimit"
|
||||
WHERE "createdAt" < ${cutoff}
|
||||
AND ctid IN (
|
||||
SELECT ctid FROM "RateLimit"
|
||||
WHERE "createdAt" < ${cutoff}
|
||||
LIMIT 10000
|
||||
)
|
||||
`;
|
||||
} while (deleted > 0);
|
||||
};
|
||||
```
|
||||
|
||||
### 4.3 Register in job client
|
||||
|
||||
Add `CLEANUP_RATE_LIMITS_JOB_DEFINITION` to the job registry in `packages/lib/jobs/client.ts`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Remove hono-rate-limiter Dependency
|
||||
|
||||
After the migration is complete:
|
||||
|
||||
- Remove `hono-rate-limiter` from `apps/remix/package.json`
|
||||
- Run `npm install` to clean up
|
||||
|
||||
---
|
||||
|
||||
## 6. Files to Create or Modify
|
||||
|
||||
### New Files
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `packages/lib/server-only/rate-limit/rate-limit.ts` | Core rate limit factory (`rateLimit()`) with window parsing, bucket computation, Prisma upsert, fail-open |
|
||||
| `packages/lib/server-only/rate-limit/rate-limits.ts` | All rate limit instances (auth, API, AI, file upload) |
|
||||
| `packages/lib/server-only/rate-limit/rate-limit-middleware.ts` | Hono middleware factory, `rateLimitResponse` helper for Hono handlers, `assertRateLimit` helper for tRPC routes |
|
||||
| `packages/lib/jobs/definitions/internal/cleanup-rate-limits.ts` | Cleanup cron job definition |
|
||||
| `packages/lib/jobs/definitions/internal/cleanup-rate-limits.handler.ts` | Cleanup handler (batched deletes) |
|
||||
|
||||
### Modified Files
|
||||
|
||||
| File | Change |
|
||||
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `packages/prisma/schema.prisma` | Add `RateLimit` model |
|
||||
| `apps/remix/server/router.ts` | Replace `hono-rate-limiter` with DB-backed middleware, add rate limits for `/api/trpc/*` and `/api/files/*` |
|
||||
| `apps/remix/package.json` | Remove `hono-rate-limiter` dependency |
|
||||
| `packages/auth/server/routes/email-password.ts` | Add rate limit checks to signup, login, forgot-password, resend-verify-email, verify-email, reset-password |
|
||||
| `packages/auth/server/routes/passkey.ts` | Add rate limit check to passkey authorize |
|
||||
| `packages/auth/server/routes/oauth.ts` | Add rate limit check to OAuth authorize endpoints |
|
||||
| `packages/trpc/server/document-router/access-auth-request-2fa-email.ts` | Add rate limit check (sends email, unauthenticated) |
|
||||
| `packages/trpc/server/enterprise-router/link-organisation-account.ts` | Add rate limit check (sends email, unauthenticated) |
|
||||
| `packages/lib/jobs/client.ts` | Register cleanup-rate-limits job definition |
|
||||
|
||||
---
|
||||
|
||||
## 7. Considerations
|
||||
|
||||
### 7.1 Fail-open
|
||||
|
||||
All rate limit checks must be wrapped in try/catch. On any DB error, log the error and allow the request through. Rate limiting should never block legitimate traffic due to infrastructure issues.
|
||||
|
||||
### 7.2 Performance
|
||||
|
||||
- Each API request adds 1 upsert query (~1ms)
|
||||
- Auth requests add 2 upsert queries (~2ms total)
|
||||
- The composite primary key ensures all lookups and upserts are index-only operations
|
||||
- No `COUNT(*)` queries — the count is stored directly in the row
|
||||
|
||||
### 7.3 Monitoring
|
||||
|
||||
Log rate limit hits at `warn` level with the action, key type (IP/identifier), and count. This provides visibility into traffic patterns and helps tune limits.
|
||||
|
||||
### 7.4 Testing
|
||||
|
||||
The rate limit module should be mockable in tests. Consider exporting the bucket computation and window parsing as standalone functions for unit testing. Integration tests can verify the upsert + count logic against a test database.
|
||||
|
||||
### 7.5 Future improvements
|
||||
|
||||
- **Redis backend**: if DB pressure from rate limiting becomes measurable, swap the Prisma upsert for Redis `INCR` + `EXPIRE` with no API changes
|
||||
- **System-wide circuit breaker**: add a `systemMax` config option that counts all requests for an action regardless of key
|
||||
@@ -0,0 +1,186 @@
|
||||
---
|
||||
date: 2026-01-14
|
||||
title: Simplewebauthn V13 Upgrade
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Upgrade SimpleWebAuthn packages from v9.x to v13.x to address the deprecation of `@simplewebauthn/types` and take advantage of new features and improvements.
|
||||
|
||||
## Current State
|
||||
|
||||
The codebase currently uses:
|
||||
- `@simplewebauthn/browser@9.x`
|
||||
- `@simplewebauthn/server@9.x`
|
||||
- `@simplewebauthn/types@9.x`
|
||||
|
||||
## Breaking Changes Summary (v9 → v13)
|
||||
|
||||
### v10.0.0 Breaking Changes
|
||||
1. **Minimum Node version raised to Node v20**
|
||||
2. **`generateRegistrationOptions()` now expects `Base64URLString` for `excludeCredentials` IDs** (no more `type: 'public-key'` needed)
|
||||
3. **`generateAuthenticationOptions()` now expects `Base64URLString` for `allowCredentials` IDs**
|
||||
4. **`credentialID` returned from verification methods is now `Base64URLString`** instead of `Uint8Array`
|
||||
5. **`AuthenticatorDevice.credentialID` is now `Base64URLString`**
|
||||
6. **`rpID` is now required when calling `generateAuthenticationOptions()`**
|
||||
7. **`generateRegistrationOptions()` will generate random user IDs** if not provided
|
||||
8. **`user.id` is treated as base64url string in `startRegistration()`**
|
||||
9. **`userHandle` is treated as base64url string in `startAuthentication()`**
|
||||
|
||||
### v11.0.0 Breaking Changes
|
||||
1. **Positional arguments in `startRegistration()` and `startAuthentication()` replaced by object**
|
||||
- Before: `startRegistration(options)`
|
||||
- After: `startRegistration({ optionsJSON: options })`
|
||||
- Before: `startAuthentication(options)`
|
||||
- After: `startAuthentication({ optionsJSON: options })`
|
||||
2. **`AuthenticatorDevice` type renamed to `WebAuthnCredential`**
|
||||
- `credentialID` → `credential.id`
|
||||
- `credentialPublicKey` → `credential.publicKey`
|
||||
3. **`verifyRegistrationResponse()` returns `registrationInfo.credential` instead of individual properties**
|
||||
- `credentialID` → `credential.id`
|
||||
- `credentialPublicKey` → `credential.publicKey`
|
||||
- `counter` → `credential.counter`
|
||||
- `transports` are now in `credential.transports`
|
||||
4. **`verifyAuthenticationResponse()` uses `credential` argument instead of `authenticator`**
|
||||
|
||||
### v13.0.0 Breaking Changes
|
||||
1. **`@simplewebauthn/types` package is retired**
|
||||
- Types are now exported from `@simplewebauthn/browser` and `@simplewebauthn/server`
|
||||
- Import types from `@simplewebauthn/server` instead
|
||||
|
||||
## Files to Update
|
||||
|
||||
### Package Changes
|
||||
1. Remove `@simplewebauthn/types` dependency
|
||||
2. Update `@simplewebauthn/browser` to `^13.2.2`
|
||||
3. Update `@simplewebauthn/server` to `^13.2.2`
|
||||
|
||||
### Server-side Files
|
||||
|
||||
#### 1. `packages/lib/server-only/auth/create-passkey-registration-options.ts`
|
||||
- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
|
||||
- Remove `type: 'public-key'` from `excludeCredentials` items
|
||||
- Update `userID` to use `isoUint8Array.fromUTF8String()` for proper encoding
|
||||
|
||||
#### 2. `packages/lib/server-only/auth/create-passkey-authentication-options.ts`
|
||||
- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
|
||||
- Remove `type: 'public-key'` from `allowCredentials` items
|
||||
|
||||
#### 3. `packages/lib/server-only/auth/create-passkey-signin-options.ts`
|
||||
- No changes needed (already using correct options)
|
||||
|
||||
#### 4. `packages/lib/server-only/auth/create-passkey.ts`
|
||||
- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
|
||||
- Update to use new `registrationInfo.credential` structure:
|
||||
- `credentialID` → `credential.id`
|
||||
- `credentialPublicKey` → `credential.publicKey`
|
||||
- `counter` → `credential.counter`
|
||||
- Note: `credential.id` is now a `Base64URLString`, so `Buffer.from(credentialID)` needs updating
|
||||
|
||||
#### 5. `packages/lib/server-only/document/is-recipient-authorized.ts`
|
||||
- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator`:
|
||||
- Change `authenticator: { credentialID, credentialPublicKey, counter }` to `credential: { id, publicKey, counter }`
|
||||
- Since `credential.id` is now base64url string, convert stored `credentialId` buffer to base64url
|
||||
|
||||
#### 6. `packages/auth/server/routes/passkey.ts`
|
||||
- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator`
|
||||
- Same changes as `is-recipient-authorized.ts`
|
||||
|
||||
#### 7. `packages/trpc/server/auth-router/create-passkey.ts`
|
||||
- Change import from `@simplewebauthn/types` to `@simplewebauthn/server`
|
||||
|
||||
### Browser-side Files
|
||||
|
||||
#### 8. `apps/remix/app/components/dialogs/passkey-create-dialog.tsx`
|
||||
- Update `startRegistration()` call:
|
||||
- Before: `startRegistration(passkeyRegistrationOptions)`
|
||||
- After: `startRegistration({ optionsJSON: passkeyRegistrationOptions })`
|
||||
|
||||
#### 9. `apps/remix/app/components/forms/signin.tsx`
|
||||
- Update `startAuthentication()` call:
|
||||
- Before: `startAuthentication(options)`
|
||||
- After: `startAuthentication({ optionsJSON: options })`
|
||||
|
||||
#### 10. `apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx`
|
||||
- Update `startAuthentication()` call:
|
||||
- Before: `startAuthentication(options)`
|
||||
- After: `startAuthentication({ optionsJSON: options })`
|
||||
|
||||
### Database/Schema Considerations
|
||||
|
||||
The database stores `credentialId` as `Bytes`. The new API returns `credential.id` as `Base64URLString`. We need to:
|
||||
1. When **storing** a new passkey: Convert from `Base64URLString` to `Buffer`
|
||||
2. When **passing to verification**: Convert from `Buffer` to `Base64URLString`
|
||||
|
||||
Use `isoBase64URL` helper from `@simplewebauthn/server/helpers` for these conversions.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Update package.json dependencies
|
||||
```bash
|
||||
npm uninstall @simplewebauthn/types
|
||||
npm install @simplewebauthn/browser@^13.2.2 @simplewebauthn/server@^13.2.2
|
||||
```
|
||||
|
||||
### Step 2: Update type imports
|
||||
Replace all `@simplewebauthn/types` imports with `@simplewebauthn/server`
|
||||
|
||||
### Step 3: Update browser-side API calls
|
||||
- `startRegistration(options)` → `startRegistration({ optionsJSON: options })`
|
||||
- `startAuthentication(options)` → `startAuthentication({ optionsJSON: options })`
|
||||
|
||||
### Step 4: Update server-side registration
|
||||
- Update `excludeCredentials` format (remove `type: 'public-key'`)
|
||||
- Update `userID` encoding if needed
|
||||
- Update `verifyRegistrationResponse()` result handling for new `credential` structure
|
||||
|
||||
### Step 5: Update server-side authentication
|
||||
- Update `allowCredentials` format (remove `type: 'public-key'`)
|
||||
- Update `verifyAuthenticationResponse()` to use `credential` instead of `authenticator`
|
||||
- Handle `Base64URLString` for `credential.id`
|
||||
|
||||
### Step 6: Update credential storage/retrieval
|
||||
- When storing: Convert `Base64URLString` to `Buffer`
|
||||
- When reading: Convert `Buffer` to `Base64URLString`
|
||||
|
||||
### Step 7: Test passkey flows
|
||||
1. Test passkey creation
|
||||
2. Test passkey sign-in
|
||||
3. Test passkey authentication for document signing
|
||||
4. Test passkey deletion
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Converting stored Buffer to Base64URLString for verification
|
||||
```typescript
|
||||
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
||||
|
||||
// When reading from database (Buffer) and passing to verification
|
||||
const credential = {
|
||||
id: isoBase64URL.fromBuffer(passkey.credentialId),
|
||||
publicKey: new Uint8Array(passkey.credentialPublicKey),
|
||||
counter: Number(passkey.counter),
|
||||
transports: passkey.transports,
|
||||
};
|
||||
```
|
||||
|
||||
### Converting Base64URLString to Buffer for storage
|
||||
```typescript
|
||||
import { isoBase64URL } from '@simplewebauthn/server/helpers';
|
||||
|
||||
// When storing from registration response
|
||||
const credentialIdBuffer = Buffer.from(
|
||||
isoBase64URL.toBuffer(registrationInfo.credential.id)
|
||||
);
|
||||
```
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
1. **Database compatibility**: The `credentialId` is stored as `Bytes` in the database. The new API uses `Base64URLString`. We need proper conversion functions.
|
||||
- **Mitigation**: Use `isoBase64URL.fromBuffer()` and `isoBase64URL.toBuffer()` for conversions
|
||||
|
||||
2. **Existing passkeys**: Existing passkeys should continue to work as long as conversion is done correctly.
|
||||
- **Mitigation**: Test with existing passkeys after upgrade
|
||||
|
||||
3. **Browser compatibility**: v10+ requires newer browser APIs.
|
||||
- **Mitigation**: `browserSupportsWebAuthn()` already handles this check
|
||||
@@ -0,0 +1,263 @@
|
||||
---
|
||||
date: 2026-02-24
|
||||
title: Custom Email Domain Sync And Recovery
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Custom email domains configured via AWS SES can get stuck in a `PENDING` state or fail validation silently. Currently, there is **no automated verification** -- users must manually click "Sync" in the UI to check domain status. If a domain fails to validate, the only option is to delete it and recreate it, which generates new DKIM keys and requires the user to update their DNS records.
|
||||
|
||||
### Current Pain Points
|
||||
|
||||
1. **No background sync** -- Domain verification status is never checked automatically; users must manually click "Sync"
|
||||
2. **Stuck domains** -- Domains can remain in `PENDING` state indefinitely with no alerting or auto-recovery
|
||||
3. **Failed recovery requires DNS changes** -- Deleting and recreating a domain generates new keys, forcing the user to update DNS records
|
||||
4. **No visibility into failure duration** -- There's no tracking of how long a domain has been pending
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
### 1. Hourly Background Sync Job
|
||||
|
||||
Create a new cron job (`internal.sync-email-domains`) that runs every hour to automatically verify all `PENDING` email domains.
|
||||
|
||||
**Job Definition:** `packages/lib/jobs/definitions/internal/sync-email-domains.ts`
|
||||
**Job Handler:** `packages/lib/jobs/definitions/internal/sync-email-domains.handler.ts`
|
||||
|
||||
**Pattern:** Follow the existing `cleanup-rate-limits` cron job pattern:
|
||||
|
||||
- `cron: '0 * * * *'` (every hour, on the hour)
|
||||
- Empty `z.object({})` schema (no payload needed)
|
||||
- Register in `packages/lib/jobs/client.ts`
|
||||
|
||||
**Handler Logic:**
|
||||
|
||||
1. Query all `EmailDomain` records with `status: 'PENDING'`
|
||||
2. For each domain, call `verifyEmailDomain(emailDomainId)` which:
|
||||
- Calls AWS SES `GetEmailIdentityCommand` to check current verification status
|
||||
- Updates DB status to `ACTIVE` if verified, keeps `PENDING` otherwise
|
||||
3. Log results via `io.logger` (how many checked, how many transitioned to ACTIVE)
|
||||
4. Process domains in batches to avoid overwhelming SES API rate limits
|
||||
5. Add error handling per-domain so one failure doesn't stop the entire sweep
|
||||
|
||||
### 2. Schema Changes -- Track Pending Duration
|
||||
|
||||
Add a `lastVerifiedAt` column to the `EmailDomain` model to track when verification was last attempted, enabling "stale domain" detection.
|
||||
|
||||
**File:** `packages/prisma/schema.prisma`
|
||||
|
||||
```prisma
|
||||
model EmailDomain {
|
||||
// ... existing fields ...
|
||||
lastVerifiedAt DateTime? // Last time verification was checked against SES
|
||||
}
|
||||
```
|
||||
|
||||
**Migration:** Create a new Prisma migration for this column addition.
|
||||
|
||||
**Updates needed:**
|
||||
|
||||
- `verify-email-domain.ts` -- Update `lastVerifiedAt` when verification is checked
|
||||
- The sync job handler -- Use `lastVerifiedAt` to avoid re-checking domains that were just verified
|
||||
|
||||
### 3. Domain Re-registration (Recovery) -- Delete & Recreate in SES Without Changing Keys
|
||||
|
||||
Add a new "Re-register" action that deletes the SES identity and recreates it using the **same** DKIM key pair stored in the database, so the user's DNS records remain valid.
|
||||
|
||||
#### 3a. New Service Function
|
||||
|
||||
**File:** `packages/ee/server-only/lib/reregister-email-domain.ts`
|
||||
|
||||
```typescript
|
||||
export const reregisterEmailDomain = async (options: { emailDomainId: string }) => {
|
||||
// 1. Fetch the EmailDomain record (including encrypted privateKey)
|
||||
// 2. Decrypt the private key using DOCUMENSO_ENCRYPTION_KEY
|
||||
// 3. Call DeleteEmailIdentityCommand on SES (ignore NotFoundException)
|
||||
// 4. Call CreateEmailIdentityCommand with BYODKIM using the SAME selector + private key
|
||||
// 5. Update EmailDomain status back to PENDING, update lastVerifiedAt
|
||||
// 6. Return the updated domain
|
||||
};
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- Uses the existing encrypted `privateKey` from the DB -- no new key generation
|
||||
- Uses the existing `selector` -- DNS records stay the same
|
||||
- Deletes first, then recreates -- handles cases where SES state is corrupted
|
||||
- Resets status to `PENDING` since verification will need to re-occur
|
||||
- Uses `verifyDomainWithDKIM()` from `create-email-domain.ts` (may need to extract/export this helper)
|
||||
|
||||
#### 3b. Admin TRPC Routes (Find, Get, Re-register)
|
||||
|
||||
All email domain admin routes use `adminProcedure` -- requires system-level `Role.ADMIN`.
|
||||
|
||||
**Find (list) route:**
|
||||
**File:** `packages/trpc/server/admin-router/find-email-domains.ts`
|
||||
**Types:** `packages/trpc/server/admin-router/find-email-domains.types.ts`
|
||||
|
||||
- Query route: `admin.emailDomain.find`
|
||||
- Input: `{ query?: string, page?: number, perPage?: number, status?: EmailDomainStatus }`
|
||||
- Extends `ZFindSearchParamsSchema` with optional `status` filter
|
||||
- Returns standard `ZFindResultResponse` with email domain data including: id, domain, status, selector, createdAt, lastVerifiedAt, organisation name, email count
|
||||
- Prisma query filters by domain name (LIKE search on `query`), optional status, joins organisation for name, counts emails
|
||||
|
||||
**Get (detail) route:**
|
||||
**File:** `packages/trpc/server/admin-router/get-email-domain.ts`
|
||||
**Types:** `packages/trpc/server/admin-router/get-email-domain.types.ts`
|
||||
|
||||
- Query route: `admin.emailDomain.get`
|
||||
- Input: `{ emailDomainId: string }`
|
||||
- Returns full email domain detail: all fields (except privateKey), organisation info, list of associated emails, DNS records (generated from publicKey + selector)
|
||||
- Omits `privateKey` from response
|
||||
|
||||
**Re-register (mutation) route:**
|
||||
**File:** `packages/trpc/server/admin-router/reregister-email-domain.ts`
|
||||
**Types:** `packages/trpc/server/admin-router/reregister-email-domain.types.ts`
|
||||
|
||||
- Mutation route: `admin.emailDomain.reregister`
|
||||
- Input: `{ emailDomainId: string }`
|
||||
- Calls `reregisterEmailDomain()`
|
||||
- Rationale: Re-registration is a recovery/operational action that deletes and recreates an SES identity. This is a privileged operation that should only be performed by platform operators, not self-service by org admins.
|
||||
|
||||
#### 3c. Register in Admin Router
|
||||
|
||||
**File:** `packages/trpc/server/admin-router/router.ts`
|
||||
|
||||
Add a new `emailDomain` namespace to the admin router:
|
||||
|
||||
```typescript
|
||||
emailDomain: {
|
||||
find: findEmailDomainsRoute,
|
||||
get: getEmailDomainRoute,
|
||||
reregister: reregisterEmailDomainRoute,
|
||||
},
|
||||
```
|
||||
|
||||
#### 3d. Admin Panel UI -- Email Domains Section
|
||||
|
||||
**List page:** `apps/remix/app/routes/_authenticated+/admin+/email-domains._index.tsx`
|
||||
|
||||
- New admin panel page at `/admin/email-domains`
|
||||
- Follow the existing admin documents list pattern (client-side TRPC data fetching)
|
||||
- Search input (debounced) filtering by domain name
|
||||
- Status filter dropdown (All / Pending / Active)
|
||||
- DataTable with columns: Domain, Organisation, Status (badge), Email Count, Created, Last Verified, Actions
|
||||
- Actions dropdown per row: View details, Re-register
|
||||
- Pagination via `DataTablePagination`
|
||||
|
||||
**Detail page:** `apps/remix/app/routes/_authenticated+/admin+/email-domains.$id.tsx`
|
||||
|
||||
- Shows full domain details: domain, selector, status, organisation, created date, last verified date
|
||||
- Shows DNS records (DKIM + SPF) with copy buttons (reuse `organisation-email-domain-records-dialog` pattern)
|
||||
- Table of associated organisation emails
|
||||
- "Re-register" button with confirmation dialog explaining the action (SES identity will be deleted and recreated with the same keys)
|
||||
- "Verify Now" button to manually trigger a verification check
|
||||
- Shows how long the domain has been pending (using `lastVerifiedAt` or `createdAt`)
|
||||
|
||||
**Navigation:** Add menu item to admin sidebar in `_layout.tsx`:
|
||||
|
||||
```tsx
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
'justify-start md:w-full',
|
||||
pathname?.startsWith('/admin/email-domains') && 'bg-secondary',
|
||||
)}
|
||||
asChild
|
||||
>
|
||||
<Link to="/admin/email-domains">
|
||||
<MailIcon className="mr-2 h-5 w-5" />
|
||||
<Trans>Email Domains</Trans>
|
||||
</Link>
|
||||
</Button>
|
||||
```
|
||||
|
||||
**Table component:** `apps/remix/app/components/tables/admin-email-domains-table.tsx` (optional -- can be inline in the route file like the documents page)
|
||||
|
||||
#### 3e. Automatic Re-registration in Sync Job (Optional Enhancement)
|
||||
|
||||
In the hourly sync job, after checking verification status, if a domain has been `PENDING` for more than 48 hours:
|
||||
|
||||
- Automatically call `reregisterEmailDomain()` to attempt recovery
|
||||
- Log the auto-recovery attempt
|
||||
- This provides a self-healing mechanism without user intervention
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Background Sync Job (Core)
|
||||
|
||||
1. Create `sync-email-domains.ts` job definition with hourly cron
|
||||
2. Create `sync-email-domains.handler.ts` with batch verification logic
|
||||
3. Register job in `packages/lib/jobs/client.ts`
|
||||
4. Add error handling and logging
|
||||
|
||||
### Phase 2: Schema Enhancement
|
||||
|
||||
5. Add `lastVerifiedAt` column to `EmailDomain` model
|
||||
6. Create Prisma migration
|
||||
7. Update `verifyEmailDomain()` to set `lastVerifiedAt` on each check
|
||||
8. Update sync job to use `lastVerifiedAt` for intelligent scheduling
|
||||
|
||||
### Phase 3: Admin Email Domains Panel
|
||||
|
||||
9. Create `find-email-domains` admin TRPC route + types (list/search with pagination and status filter)
|
||||
10. Create `get-email-domain` admin TRPC route + types (detail view with org info, emails, DNS records)
|
||||
11. Register find + get routes in admin router under `emailDomain` namespace
|
||||
12. Create admin list page (`admin+/email-domains._index.tsx`) with search, status filter, DataTable
|
||||
13. Create admin detail page (`admin+/email-domains.$id.tsx`) with domain info, emails table, DNS records
|
||||
14. Add "Email Domains" menu item to admin sidebar (`_layout.tsx`)
|
||||
|
||||
### Phase 4: Re-registration Feature
|
||||
|
||||
15. Extract `verifyDomainWithDKIM()` as a shared helper (if not already exported)
|
||||
16. Create `reregisterEmailDomain()` service function
|
||||
17. Create `reregister-email-domain` admin TRPC mutation route + types
|
||||
18. Register reregister route in admin router under `emailDomain.reregister`
|
||||
19. Add "Re-register" button + confirmation dialog on admin detail page
|
||||
|
||||
### Phase 5: Auto-Recovery (Optional)
|
||||
|
||||
20. Add 48-hour stale detection logic to sync job
|
||||
21. Auto-trigger re-registration for stale domains
|
||||
22. Add logging/notifications for auto-recovery events
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
### New Files
|
||||
|
||||
- `packages/lib/jobs/definitions/internal/sync-email-domains.ts`
|
||||
- `packages/lib/jobs/definitions/internal/sync-email-domains.handler.ts`
|
||||
- `packages/ee/server-only/lib/reregister-email-domain.ts`
|
||||
- `packages/trpc/server/admin-router/find-email-domains.ts`
|
||||
- `packages/trpc/server/admin-router/find-email-domains.types.ts`
|
||||
- `packages/trpc/server/admin-router/get-email-domain.ts`
|
||||
- `packages/trpc/server/admin-router/get-email-domain.types.ts`
|
||||
- `packages/trpc/server/admin-router/reregister-email-domain.ts`
|
||||
- `packages/trpc/server/admin-router/reregister-email-domain.types.ts`
|
||||
- `apps/remix/app/routes/_authenticated+/admin+/email-domains._index.tsx`
|
||||
- `apps/remix/app/routes/_authenticated+/admin+/email-domains.$id.tsx`
|
||||
|
||||
### Modified Files
|
||||
|
||||
- `packages/prisma/schema.prisma` -- Add `lastVerifiedAt` field
|
||||
- `packages/lib/jobs/client.ts` -- Register new sync job
|
||||
- `packages/ee/server-only/lib/verify-email-domain.ts` -- Update `lastVerifiedAt`
|
||||
- `packages/ee/server-only/lib/create-email-domain.ts` -- Export `verifyDomainWithDKIM` helper
|
||||
- `packages/trpc/server/admin-router/router.ts` -- Add `emailDomain.{find, get, reregister}` routes
|
||||
- `apps/remix/app/routes/_authenticated+/admin+/_layout.tsx` -- Add "Email Domains" nav item to sidebar
|
||||
- New Prisma migration file
|
||||
|
||||
## Technical Considerations
|
||||
|
||||
1. **SES API Rate Limits** -- AWS SES has rate limits on `GetEmailIdentityCommand`. The sync job should process domains in batches with small delays between calls (e.g., 5-10 per batch with 1s delay).
|
||||
|
||||
2. **Concurrency** -- The local job provider has deterministic deduplication via SHA-256 IDs, so multiple app instances won't run the same cron tick twice.
|
||||
|
||||
3. **Error Isolation** -- Each domain verification in the sync job should be wrapped in try/catch so one failing domain doesn't prevent others from being checked.
|
||||
|
||||
4. **Re-registration Safety** -- The re-register function should be idempotent. Deleting a non-existent SES identity should be handled gracefully (already done in `deleteEmailDomain`).
|
||||
|
||||
5. **Private Key Security** -- The private key is encrypted at rest and should only be decrypted transiently during re-registration. It should never be logged or exposed in API responses.
|
||||
|
||||
6. **Feature Gating** -- The sync job should only process domains belonging to organisations with active `emailDomains` claim flags. This prevents processing domains for orgs that have downgraded.
|
||||
|
||||
7. **Observability** -- Add structured logging to the sync job so operations teams can monitor domain verification health across all tenants.
|
||||
@@ -0,0 +1,210 @@
|
||||
---
|
||||
date: 2026-02-10
|
||||
title: Add Folder Support To V1 Api
|
||||
status: ready
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
The `GET /api/v1/documents` endpoint does not return documents inside folders. The underlying `findDocuments()` function defaults to `folderId: null` when no `folderId` is provided, meaning only root-level documents are returned. The V1 API never passes `folderId`, so folder documents are invisible to API consumers.
|
||||
|
||||
Additionally, neither the list endpoint nor the single-document endpoint exposes `folderId` in the response, so consumers cannot know which folder a document belongs to.
|
||||
|
||||
## Root Cause
|
||||
|
||||
In `packages/lib/server-only/document/find-documents.ts` (line 222-226):
|
||||
|
||||
```ts
|
||||
if (folderId !== undefined) {
|
||||
whereClause.folderId = folderId;
|
||||
} else {
|
||||
whereClause.folderId = null; // Only root documents returned
|
||||
}
|
||||
```
|
||||
|
||||
The V1 `getDocuments` handler in `packages/api/v1/implementation.ts` (line 61-70) only passes `page` and `perPage` to `findDocuments` — it never extracts or forwards a `folderId` from the query string.
|
||||
|
||||
## Decisions
|
||||
|
||||
These decisions were made during the spec interview:
|
||||
|
||||
1. **Fix V1 directly** — The V1 API is deprecated but still actively used. This is a quick, low-risk fix. No need to defer to a newer API.
|
||||
2. **Breaking change accepted** — Returning ALL documents by default (instead of root-only) is intentional. The current root-only behavior is a bug, not a feature.
|
||||
3. **No root-only query option needed** — Not all documents are in folders, so consumers can filter client-side using the `folderId` field in the response if needed.
|
||||
4. **No folder existence validation** — `?folderId=nonexistent` returns empty array, not 404. Consistent with V1 list endpoint patterns.
|
||||
5. **Add `folderId` to both endpoints** — Both `GET /api/v1/documents` (list) and `GET /api/v1/documents/:id` (single) will include `folderId` in the response.
|
||||
6. **Top-level `skipFolderFilter` is sufficient** — The inner helper filters (`findDocumentsFilter`, `findTeamDocumentsFilter`) receive `folderId: undefined` when skip is active. Prisma ignores `undefined` values in WHERE clauses, so these inner filters will not constrain by folder. No propagation needed.
|
||||
7. **Scope is minimal** — Only `folderId` support. No other filters (status, period, query, senderIds) added in this change.
|
||||
|
||||
## Scope
|
||||
|
||||
Three files need changes. No new files.
|
||||
|
||||
| File | Change |
|
||||
| ----------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `packages/api/v1/schema.ts` | Add `folderId` to query schema + both response schemas |
|
||||
| `packages/api/v1/implementation.ts` | Pass `folderId` through in `getDocuments`, add to `getDocument` response |
|
||||
| `packages/lib/server-only/document/find-documents.ts` | Add `skipFolderFilter` option |
|
||||
|
||||
## Changes
|
||||
|
||||
### 1. `packages/api/v1/schema.ts` — Add `folderId` to query + response schemas
|
||||
|
||||
**Query schema** (`ZGetDocumentsQuerySchema`, line 35-38):
|
||||
|
||||
```ts
|
||||
export const ZGetDocumentsQuerySchema = z.object({
|
||||
page: z.coerce.number().min(1).optional().default(1),
|
||||
perPage: z.coerce.number().min(1).optional().default(10),
|
||||
folderId: z
|
||||
.string()
|
||||
.describe(
|
||||
'Filter documents by folder ID. When omitted, returns all documents regardless of folder.',
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
```
|
||||
|
||||
**List response schema** (`ZSuccessfulDocumentResponseSchema`, line 46-56):
|
||||
|
||||
Add `folderId: z.string().nullish()` so consumers can see which folder each document belongs to.
|
||||
|
||||
**Single document response schema** (`ZSuccessfulGetDocumentResponseSchema`, line 58-79):
|
||||
|
||||
Add `folderId: z.string().nullish()` to the extended schema as well.
|
||||
|
||||
### 2. `packages/api/v1/implementation.ts` — Pass `folderId` through + add to responses
|
||||
|
||||
**`getDocuments` handler** (line 61-70):
|
||||
|
||||
```ts
|
||||
getDocuments: authenticatedMiddleware(async (args, user, team) => {
|
||||
const page = Number(args.query.page) || 1;
|
||||
const perPage = Number(args.query.perPage) || 10;
|
||||
|
||||
const { data: documents, totalPages } = await findDocuments({
|
||||
page,
|
||||
perPage,
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
folderId: args.query.folderId,
|
||||
skipFolderFilter: args.query.folderId === undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: {
|
||||
documents: documents.map((document) => ({
|
||||
id: mapSecondaryIdToDocumentId(document.secondaryId),
|
||||
externalId: document.externalId,
|
||||
userId: document.userId,
|
||||
teamId: document.teamId,
|
||||
folderId: document.folderId,
|
||||
title: document.title,
|
||||
status: document.status,
|
||||
createdAt: document.createdAt,
|
||||
updatedAt: document.updatedAt,
|
||||
completedAt: document.completedAt,
|
||||
})),
|
||||
totalPages,
|
||||
},
|
||||
};
|
||||
}),
|
||||
```
|
||||
|
||||
**`getDocument` handler** (line 91-197):
|
||||
|
||||
Add `folderId: envelope.folderId` to the response body mapping (alongside `id`, `externalId`, etc.).
|
||||
|
||||
### 3. `packages/lib/server-only/document/find-documents.ts` — Handle "return all" semantics
|
||||
|
||||
Add `skipFolderFilter` to the options type and modify the WHERE clause logic:
|
||||
|
||||
```ts
|
||||
export type FindDocumentsOptions = {
|
||||
// ... existing fields ...
|
||||
folderId?: string;
|
||||
skipFolderFilter?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
Modify the folderId logic (line 222-226):
|
||||
|
||||
```ts
|
||||
if (!skipFolderFilter) {
|
||||
if (folderId !== undefined) {
|
||||
whereClause.folderId = folderId;
|
||||
} else {
|
||||
whereClause.folderId = null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
When `skipFolderFilter` is true:
|
||||
|
||||
- The top-level `whereClause.folderId` is never set — no folder constraint at the top level.
|
||||
- The inner helpers (`findDocumentsFilter`, `findTeamDocumentsFilter`) receive `folderId: undefined`, which Prisma ignores in WHERE objects — no folder constraint at the inner level either.
|
||||
- Result: all documents returned regardless of folder.
|
||||
|
||||
When `skipFolderFilter` is false (default, used by UI/tRPC callers):
|
||||
|
||||
- Existing behavior is completely unchanged. `folderId: undefined` still defaults to root-only.
|
||||
|
||||
## Why `skipFolderFilter` (Option B)
|
||||
|
||||
Two approaches were considered:
|
||||
|
||||
**Option A: Change `folderId: undefined` semantics to mean "all documents"**
|
||||
|
||||
- Risky: would affect all callers of `findDocuments` (UI, tRPC) unless every caller is audited.
|
||||
- The UI intentionally shows root-only when no folder is selected.
|
||||
|
||||
**Option B (chosen): Add `skipFolderFilter` boolean**
|
||||
|
||||
- Additive — no existing callers pass this flag, so they're unaffected.
|
||||
- Explicit — the intent is clear in the code.
|
||||
- Safe — zero risk to UI/tRPC behavior.
|
||||
|
||||
## Behavior Matrix
|
||||
|
||||
| Request | Current Behavior | New Behavior |
|
||||
| -------------------------------------------- | ------------------- | ------------------------- |
|
||||
| `GET /api/v1/documents` | Root docs only | ALL docs (root + folders) |
|
||||
| `GET /api/v1/documents?folderId=abc` | Not supported | Docs in folder `abc` only |
|
||||
| `GET /api/v1/documents?folderId=nonexistent` | Not supported | Empty array, 200 OK |
|
||||
| `GET /api/v1/documents/:id` response | No `folderId` field | Includes `folderId` |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `folderId` is a `String?` on the `Envelope` model in Prisma, not a number.
|
||||
- The `findDocuments` function already accepts `folderId` in its options type — it just needs the `skipFolderFilter` escape hatch.
|
||||
- No need to propagate `skipFolderFilter` into `findDocumentsFilter` or `findTeamDocumentsFilter`. When `folderId` is `undefined`, those helpers embed `folderId: undefined` in their Prisma WHERE objects. Prisma strips `undefined` keys, so no folder constraint is applied. This is well-documented Prisma behavior.
|
||||
- The `createDocument` endpoint already supports `folderId` in the request body (line 139-144 of schema.ts), confirming the pattern.
|
||||
- The `getDocument` handler fetches from `prisma.envelope.findFirstOrThrow` which already includes `folderId` on the envelope — just needs to be added to the response mapping.
|
||||
|
||||
## Testing
|
||||
|
||||
Manual and automated test cases:
|
||||
|
||||
1. `GET /api/v1/documents` returns docs from root AND subfolders.
|
||||
2. `GET /api/v1/documents?folderId=<valid-id>` returns only docs in that folder.
|
||||
3. `GET /api/v1/documents?folderId=<nonexistent-id>` returns empty array with 200 status.
|
||||
4. List response includes `folderId` field on each document (null for root docs, string for folder docs).
|
||||
5. `GET /api/v1/documents/:id` response includes `folderId` field.
|
||||
6. Existing UI/tRPC callers of `findDocuments` are unaffected (they don't pass `skipFolderFilter`).
|
||||
7. Pagination: verify `totalPages` correctly reflects the larger result set when all docs are returned.
|
||||
|
||||
## Breaking Change Notice
|
||||
|
||||
This is a **breaking change** for existing V1 API consumers:
|
||||
|
||||
- **Before**: `GET /api/v1/documents` returned only root-level documents (those not in any folder).
|
||||
- **After**: `GET /api/v1/documents` returns all documents regardless of folder placement.
|
||||
|
||||
Impact:
|
||||
|
||||
- Consumers paginating through results will see more documents in the total count.
|
||||
- Consumers building UIs will now display folder documents they previously didn't see.
|
||||
- The new `folderId` field is additive and won't break existing response parsing.
|
||||
|
||||
This is considered a **bug fix**, not a feature removal. The previous behavior silently hid documents from API consumers.
|
||||
@@ -0,0 +1,207 @@
|
||||
---
|
||||
name: agent-browser
|
||||
description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction.
|
||||
allowed-tools: Bash(agent-browser:*)
|
||||
---
|
||||
|
||||
# Browser Automation with agent-browser
|
||||
|
||||
## Core Workflow
|
||||
|
||||
Every browser automation follows this pattern:
|
||||
|
||||
1. **Navigate**: `agent-browser open <url>`
|
||||
2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`)
|
||||
3. **Interact**: Use refs to click, fill, select
|
||||
4. **Re-snapshot**: After navigation or DOM changes, get fresh refs
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/form
|
||||
agent-browser snapshot -i
|
||||
# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit"
|
||||
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser snapshot -i # Check result
|
||||
```
|
||||
|
||||
## Essential Commands
|
||||
|
||||
```bash
|
||||
# Navigation
|
||||
agent-browser open <url> # Navigate (aliases: goto, navigate)
|
||||
agent-browser close # Close browser
|
||||
|
||||
# Snapshot
|
||||
agent-browser snapshot -i # Interactive elements with refs (recommended)
|
||||
agent-browser snapshot -s "#selector" # Scope to CSS selector
|
||||
|
||||
# Interaction (use @refs from snapshot)
|
||||
agent-browser click @e1 # Click element
|
||||
agent-browser fill @e2 "text" # Clear and type text
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser select @e1 "option" # Select dropdown option
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser press Enter # Press key
|
||||
agent-browser scroll down 500 # Scroll page
|
||||
|
||||
# Get information
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get title # Get page title
|
||||
|
||||
# Wait
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait --load networkidle # Wait for network idle
|
||||
agent-browser wait --url "**/page" # Wait for URL pattern
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
|
||||
# Capture
|
||||
agent-browser screenshot # Screenshot to temp dir
|
||||
agent-browser screenshot --full # Full page screenshot
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Form Submission
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/signup
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "Jane Doe"
|
||||
agent-browser fill @e2 "jane@example.com"
|
||||
agent-browser select @e3 "California"
|
||||
agent-browser check @e4
|
||||
agent-browser click @e5
|
||||
agent-browser wait --load networkidle
|
||||
```
|
||||
|
||||
### Authentication with State Persistence
|
||||
|
||||
```bash
|
||||
# Login once and save state
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "$USERNAME"
|
||||
agent-browser fill @e2 "$PASSWORD"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
agent-browser state save auth.json
|
||||
|
||||
# Reuse in future sessions
|
||||
agent-browser state load auth.json
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
### Data Extraction
|
||||
|
||||
```bash
|
||||
agent-browser open https://example.com/products
|
||||
agent-browser snapshot -i
|
||||
agent-browser get text @e5 # Get specific element text
|
||||
agent-browser get text body > page.txt # Get all page text
|
||||
|
||||
# JSON output for parsing
|
||||
agent-browser snapshot -i --json
|
||||
agent-browser get text @e1 --json
|
||||
```
|
||||
|
||||
### Parallel Sessions
|
||||
|
||||
```bash
|
||||
agent-browser --session site1 open https://site-a.com
|
||||
agent-browser --session site2 open https://site-b.com
|
||||
|
||||
agent-browser --session site1 snapshot -i
|
||||
agent-browser --session site2 snapshot -i
|
||||
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
### Visual Browser (Debugging)
|
||||
|
||||
```bash
|
||||
agent-browser --headed open https://example.com
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser record start demo.webm # Record session
|
||||
```
|
||||
|
||||
### iOS Simulator (Mobile Safari)
|
||||
|
||||
```bash
|
||||
# List available iOS simulators
|
||||
agent-browser device list
|
||||
|
||||
# Launch Safari on a specific device
|
||||
agent-browser -p ios --device "iPhone 16 Pro" open https://example.com
|
||||
|
||||
# Same workflow as desktop - snapshot, interact, re-snapshot
|
||||
agent-browser -p ios snapshot -i
|
||||
agent-browser -p ios tap @e1 # Tap (alias for click)
|
||||
agent-browser -p ios fill @e2 "text"
|
||||
agent-browser -p ios swipe up # Mobile-specific gesture
|
||||
|
||||
# Take screenshot
|
||||
agent-browser -p ios screenshot mobile.png
|
||||
|
||||
# Close session (shuts down simulator)
|
||||
agent-browser -p ios close
|
||||
```
|
||||
|
||||
**Requirements:** macOS with Xcode, Appium (`npm install -g appium && appium driver install xcuitest`)
|
||||
|
||||
**Real devices:** Works with physical iOS devices if pre-configured. Use `--device "<UDID>"` where UDID is from `xcrun xctrace list devices`.
|
||||
|
||||
## Ref Lifecycle (Important)
|
||||
|
||||
Refs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after:
|
||||
|
||||
- Clicking links or buttons that navigate
|
||||
- Form submissions
|
||||
- Dynamic content loading (dropdowns, modals)
|
||||
|
||||
```bash
|
||||
agent-browser click @e5 # Navigates to new page
|
||||
agent-browser snapshot -i # MUST re-snapshot
|
||||
agent-browser click @e1 # Use new refs
|
||||
```
|
||||
|
||||
## Semantic Locators (Alternative to Refs)
|
||||
|
||||
When refs are unavailable or unreliable, use semantic locators:
|
||||
|
||||
```bash
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find placeholder "Search" type "query"
|
||||
agent-browser find testid "submit-btn" click
|
||||
```
|
||||
|
||||
## Deep-Dive Documentation
|
||||
|
||||
| Reference | When to Use |
|
||||
|-----------|-------------|
|
||||
| [references/commands.md](references/commands.md) | Full command reference with all options |
|
||||
| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting |
|
||||
| [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping |
|
||||
| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse |
|
||||
| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation |
|
||||
| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies |
|
||||
|
||||
## Ready-to-Use Templates
|
||||
|
||||
| Template | Description |
|
||||
|----------|-------------|
|
||||
| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation |
|
||||
| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state |
|
||||
| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots |
|
||||
|
||||
```bash
|
||||
./templates/form-automation.sh https://example.com/form
|
||||
./templates/authenticated-session.sh https://app.example.com/login
|
||||
./templates/capture-workflow.sh https://example.com ./output
|
||||
```
|
||||
@@ -0,0 +1,202 @@
|
||||
# Authentication Patterns
|
||||
|
||||
Login flows, session persistence, OAuth, 2FA, and authenticated browsing.
|
||||
|
||||
**Related**: [session-management.md](session-management.md) for state persistence details, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Login Flow](#basic-login-flow)
|
||||
- [Saving Authentication State](#saving-authentication-state)
|
||||
- [Restoring Authentication](#restoring-authentication)
|
||||
- [OAuth / SSO Flows](#oauth--sso-flows)
|
||||
- [Two-Factor Authentication](#two-factor-authentication)
|
||||
- [HTTP Basic Auth](#http-basic-auth)
|
||||
- [Cookie-Based Auth](#cookie-based-auth)
|
||||
- [Token Refresh Handling](#token-refresh-handling)
|
||||
- [Security Best Practices](#security-best-practices)
|
||||
|
||||
## Basic Login Flow
|
||||
|
||||
```bash
|
||||
# Navigate to login page
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Get form elements
|
||||
agent-browser snapshot -i
|
||||
# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Sign In"
|
||||
|
||||
# Fill credentials
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
|
||||
# Submit
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Verify login succeeded
|
||||
agent-browser get url # Should be dashboard, not login
|
||||
```
|
||||
|
||||
## Saving Authentication State
|
||||
|
||||
After logging in, save state for reuse:
|
||||
|
||||
```bash
|
||||
# Login first (see above)
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
|
||||
# Save authenticated state
|
||||
agent-browser state save ./auth-state.json
|
||||
```
|
||||
|
||||
## Restoring Authentication
|
||||
|
||||
Skip login by loading saved state:
|
||||
|
||||
```bash
|
||||
# Load saved auth state
|
||||
agent-browser state load ./auth-state.json
|
||||
|
||||
# Navigate directly to protected page
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
|
||||
# Verify authenticated
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
## OAuth / SSO Flows
|
||||
|
||||
For OAuth redirects:
|
||||
|
||||
```bash
|
||||
# Start OAuth flow
|
||||
agent-browser open https://app.example.com/auth/google
|
||||
|
||||
# Handle redirects automatically
|
||||
agent-browser wait --url "**/accounts.google.com**"
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Fill Google credentials
|
||||
agent-browser fill @e1 "user@gmail.com"
|
||||
agent-browser click @e2 # Next button
|
||||
agent-browser wait 2000
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e3 "password"
|
||||
agent-browser click @e4 # Sign in
|
||||
|
||||
# Wait for redirect back
|
||||
agent-browser wait --url "**/app.example.com**"
|
||||
agent-browser state save ./oauth-state.json
|
||||
```
|
||||
|
||||
## Two-Factor Authentication
|
||||
|
||||
Handle 2FA with manual intervention:
|
||||
|
||||
```bash
|
||||
# Login with credentials
|
||||
agent-browser open https://app.example.com/login --headed # Show browser
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "user@example.com"
|
||||
agent-browser fill @e2 "password123"
|
||||
agent-browser click @e3
|
||||
|
||||
# Wait for user to complete 2FA manually
|
||||
echo "Complete 2FA in the browser window..."
|
||||
agent-browser wait --url "**/dashboard" --timeout 120000
|
||||
|
||||
# Save state after 2FA
|
||||
agent-browser state save ./2fa-state.json
|
||||
```
|
||||
|
||||
## HTTP Basic Auth
|
||||
|
||||
For sites using HTTP Basic Authentication:
|
||||
|
||||
```bash
|
||||
# Set credentials before navigation
|
||||
agent-browser set credentials username password
|
||||
|
||||
# Navigate to protected resource
|
||||
agent-browser open https://protected.example.com/api
|
||||
```
|
||||
|
||||
## Cookie-Based Auth
|
||||
|
||||
Manually set authentication cookies:
|
||||
|
||||
```bash
|
||||
# Set auth cookie
|
||||
agent-browser cookies set session_token "abc123xyz"
|
||||
|
||||
# Navigate to protected page
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
## Token Refresh Handling
|
||||
|
||||
For sessions with expiring tokens:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Wrapper that handles token refresh
|
||||
|
||||
STATE_FILE="./auth-state.json"
|
||||
|
||||
# Try loading existing state
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
agent-browser state load "$STATE_FILE"
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
|
||||
# Check if session is still valid
|
||||
URL=$(agent-browser get url)
|
||||
if [[ "$URL" == *"/login"* ]]; then
|
||||
echo "Session expired, re-authenticating..."
|
||||
# Perform fresh login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "$USERNAME"
|
||||
agent-browser fill @e2 "$PASSWORD"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --url "**/dashboard"
|
||||
agent-browser state save "$STATE_FILE"
|
||||
fi
|
||||
else
|
||||
# First-time login
|
||||
agent-browser open https://app.example.com/login
|
||||
# ... login flow ...
|
||||
fi
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Never commit state files** - They contain session tokens
|
||||
```bash
|
||||
echo "*.auth-state.json" >> .gitignore
|
||||
```
|
||||
|
||||
2. **Use environment variables for credentials**
|
||||
```bash
|
||||
agent-browser fill @e1 "$APP_USERNAME"
|
||||
agent-browser fill @e2 "$APP_PASSWORD"
|
||||
```
|
||||
|
||||
3. **Clean up after automation**
|
||||
```bash
|
||||
agent-browser cookies clear
|
||||
rm -f ./auth-state.json
|
||||
```
|
||||
|
||||
4. **Use short-lived sessions for CI/CD**
|
||||
```bash
|
||||
# Don't persist state in CI
|
||||
agent-browser open https://app.example.com/login
|
||||
# ... login and perform actions ...
|
||||
agent-browser close # Session ends, nothing persisted
|
||||
```
|
||||
@@ -0,0 +1,259 @@
|
||||
# Command Reference
|
||||
|
||||
Complete reference for all agent-browser commands. For quick start and common patterns, see SKILL.md.
|
||||
|
||||
## Navigation
|
||||
|
||||
```bash
|
||||
agent-browser open <url> # Navigate to URL (aliases: goto, navigate)
|
||||
# Supports: https://, http://, file://, about:, data://
|
||||
# Auto-prepends https:// if no protocol given
|
||||
agent-browser back # Go back
|
||||
agent-browser forward # Go forward
|
||||
agent-browser reload # Reload page
|
||||
agent-browser close # Close browser (aliases: quit, exit)
|
||||
agent-browser connect 9222 # Connect to browser via CDP port
|
||||
```
|
||||
|
||||
## Snapshot (page analysis)
|
||||
|
||||
```bash
|
||||
agent-browser snapshot # Full accessibility tree
|
||||
agent-browser snapshot -i # Interactive elements only (recommended)
|
||||
agent-browser snapshot -c # Compact output
|
||||
agent-browser snapshot -d 3 # Limit depth to 3
|
||||
agent-browser snapshot -s "#main" # Scope to CSS selector
|
||||
```
|
||||
|
||||
## Interactions (use @refs from snapshot)
|
||||
|
||||
```bash
|
||||
agent-browser click @e1 # Click
|
||||
agent-browser dblclick @e1 # Double-click
|
||||
agent-browser focus @e1 # Focus element
|
||||
agent-browser fill @e2 "text" # Clear and type
|
||||
agent-browser type @e2 "text" # Type without clearing
|
||||
agent-browser press Enter # Press key (alias: key)
|
||||
agent-browser press Control+a # Key combination
|
||||
agent-browser keydown Shift # Hold key down
|
||||
agent-browser keyup Shift # Release key
|
||||
agent-browser hover @e1 # Hover
|
||||
agent-browser check @e1 # Check checkbox
|
||||
agent-browser uncheck @e1 # Uncheck checkbox
|
||||
agent-browser select @e1 "value" # Select dropdown option
|
||||
agent-browser select @e1 "a" "b" # Select multiple options
|
||||
agent-browser scroll down 500 # Scroll page (default: down 300px)
|
||||
agent-browser scrollintoview @e1 # Scroll element into view (alias: scrollinto)
|
||||
agent-browser drag @e1 @e2 # Drag and drop
|
||||
agent-browser upload @e1 file.pdf # Upload files
|
||||
```
|
||||
|
||||
## Get Information
|
||||
|
||||
```bash
|
||||
agent-browser get text @e1 # Get element text
|
||||
agent-browser get html @e1 # Get innerHTML
|
||||
agent-browser get value @e1 # Get input value
|
||||
agent-browser get attr @e1 href # Get attribute
|
||||
agent-browser get title # Get page title
|
||||
agent-browser get url # Get current URL
|
||||
agent-browser get count ".item" # Count matching elements
|
||||
agent-browser get box @e1 # Get bounding box
|
||||
agent-browser get styles @e1 # Get computed styles (font, color, bg, etc.)
|
||||
```
|
||||
|
||||
## Check State
|
||||
|
||||
```bash
|
||||
agent-browser is visible @e1 # Check if visible
|
||||
agent-browser is enabled @e1 # Check if enabled
|
||||
agent-browser is checked @e1 # Check if checked
|
||||
```
|
||||
|
||||
## Screenshots and PDF
|
||||
|
||||
```bash
|
||||
agent-browser screenshot # Save to temporary directory
|
||||
agent-browser screenshot path.png # Save to specific path
|
||||
agent-browser screenshot --full # Full page
|
||||
agent-browser pdf output.pdf # Save as PDF
|
||||
```
|
||||
|
||||
## Video Recording
|
||||
|
||||
```bash
|
||||
agent-browser record start ./demo.webm # Start recording
|
||||
agent-browser click @e1 # Perform actions
|
||||
agent-browser record stop # Stop and save video
|
||||
agent-browser record restart ./take2.webm # Stop current + start new
|
||||
```
|
||||
|
||||
## Wait
|
||||
|
||||
```bash
|
||||
agent-browser wait @e1 # Wait for element
|
||||
agent-browser wait 2000 # Wait milliseconds
|
||||
agent-browser wait --text "Success" # Wait for text (or -t)
|
||||
agent-browser wait --url "**/dashboard" # Wait for URL pattern (or -u)
|
||||
agent-browser wait --load networkidle # Wait for network idle (or -l)
|
||||
agent-browser wait --fn "window.ready" # Wait for JS condition (or -f)
|
||||
```
|
||||
|
||||
## Mouse Control
|
||||
|
||||
```bash
|
||||
agent-browser mouse move 100 200 # Move mouse
|
||||
agent-browser mouse down left # Press button
|
||||
agent-browser mouse up left # Release button
|
||||
agent-browser mouse wheel 100 # Scroll wheel
|
||||
```
|
||||
|
||||
## Semantic Locators (alternative to refs)
|
||||
|
||||
```bash
|
||||
agent-browser find role button click --name "Submit"
|
||||
agent-browser find text "Sign In" click
|
||||
agent-browser find text "Sign In" click --exact # Exact match only
|
||||
agent-browser find label "Email" fill "user@test.com"
|
||||
agent-browser find placeholder "Search" type "query"
|
||||
agent-browser find alt "Logo" click
|
||||
agent-browser find title "Close" click
|
||||
agent-browser find testid "submit-btn" click
|
||||
agent-browser find first ".item" click
|
||||
agent-browser find last ".item" click
|
||||
agent-browser find nth 2 "a" hover
|
||||
```
|
||||
|
||||
## Browser Settings
|
||||
|
||||
```bash
|
||||
agent-browser set viewport 1920 1080 # Set viewport size
|
||||
agent-browser set device "iPhone 14" # Emulate device
|
||||
agent-browser set geo 37.7749 -122.4194 # Set geolocation (alias: geolocation)
|
||||
agent-browser set offline on # Toggle offline mode
|
||||
agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers
|
||||
agent-browser set credentials user pass # HTTP basic auth (alias: auth)
|
||||
agent-browser set media dark # Emulate color scheme
|
||||
agent-browser set media light reduced-motion # Light mode + reduced motion
|
||||
```
|
||||
|
||||
## Cookies and Storage
|
||||
|
||||
```bash
|
||||
agent-browser cookies # Get all cookies
|
||||
agent-browser cookies set name value # Set cookie
|
||||
agent-browser cookies clear # Clear cookies
|
||||
agent-browser storage local # Get all localStorage
|
||||
agent-browser storage local key # Get specific key
|
||||
agent-browser storage local set k v # Set value
|
||||
agent-browser storage local clear # Clear all
|
||||
```
|
||||
|
||||
## Network
|
||||
|
||||
```bash
|
||||
agent-browser network route <url> # Intercept requests
|
||||
agent-browser network route <url> --abort # Block requests
|
||||
agent-browser network route <url> --body '{}' # Mock response
|
||||
agent-browser network unroute [url] # Remove routes
|
||||
agent-browser network requests # View tracked requests
|
||||
agent-browser network requests --filter api # Filter requests
|
||||
```
|
||||
|
||||
## Tabs and Windows
|
||||
|
||||
```bash
|
||||
agent-browser tab # List tabs
|
||||
agent-browser tab new [url] # New tab
|
||||
agent-browser tab 2 # Switch to tab by index
|
||||
agent-browser tab close # Close current tab
|
||||
agent-browser tab close 2 # Close tab by index
|
||||
agent-browser window new # New window
|
||||
```
|
||||
|
||||
## Frames
|
||||
|
||||
```bash
|
||||
agent-browser frame "#iframe" # Switch to iframe
|
||||
agent-browser frame main # Back to main frame
|
||||
```
|
||||
|
||||
## Dialogs
|
||||
|
||||
```bash
|
||||
agent-browser dialog accept [text] # Accept dialog
|
||||
agent-browser dialog dismiss # Dismiss dialog
|
||||
```
|
||||
|
||||
## JavaScript
|
||||
|
||||
```bash
|
||||
agent-browser eval "document.title" # Simple expressions only
|
||||
agent-browser eval -b "<base64>" # Any JavaScript (base64 encoded)
|
||||
agent-browser eval --stdin # Read script from stdin
|
||||
```
|
||||
|
||||
Use `-b`/`--base64` or `--stdin` for reliable execution. Shell escaping with nested quotes and special characters is error-prone.
|
||||
|
||||
```bash
|
||||
# Base64 encode your script, then:
|
||||
agent-browser eval -b "ZG9jdW1lbnQucXVlcnlTZWxlY3RvcignW3NyYyo9Il9uZXh0Il0nKQ=="
|
||||
|
||||
# Or use stdin with heredoc for multiline scripts:
|
||||
cat <<'EOF' | agent-browser eval --stdin
|
||||
const links = document.querySelectorAll('a');
|
||||
Array.from(links).map(a => a.href);
|
||||
EOF
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
```bash
|
||||
agent-browser state save auth.json # Save cookies, storage, auth state
|
||||
agent-browser state load auth.json # Restore saved state
|
||||
```
|
||||
|
||||
## Global Options
|
||||
|
||||
```bash
|
||||
agent-browser --session <name> ... # Isolated browser session
|
||||
agent-browser --json ... # JSON output for parsing
|
||||
agent-browser --headed ... # Show browser window (not headless)
|
||||
agent-browser --full ... # Full page screenshot (-f)
|
||||
agent-browser --cdp <port> ... # Connect via Chrome DevTools Protocol
|
||||
agent-browser -p <provider> ... # Cloud browser provider (--provider)
|
||||
agent-browser --proxy <url> ... # Use proxy server
|
||||
agent-browser --headers <json> ... # HTTP headers scoped to URL's origin
|
||||
agent-browser --executable-path <p> # Custom browser executable
|
||||
agent-browser --extension <path> ... # Load browser extension (repeatable)
|
||||
agent-browser --ignore-https-errors # Ignore SSL certificate errors
|
||||
agent-browser --help # Show help (-h)
|
||||
agent-browser --version # Show version (-V)
|
||||
agent-browser <command> --help # Show detailed help for a command
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```bash
|
||||
agent-browser --headed open example.com # Show browser window
|
||||
agent-browser --cdp 9222 snapshot # Connect via CDP port
|
||||
agent-browser connect 9222 # Alternative: connect command
|
||||
agent-browser console # View console messages
|
||||
agent-browser console --clear # Clear console
|
||||
agent-browser errors # View page errors
|
||||
agent-browser errors --clear # Clear errors
|
||||
agent-browser highlight @e1 # Highlight element
|
||||
agent-browser trace start # Start recording trace
|
||||
agent-browser trace stop trace.zip # Stop and save trace
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
AGENT_BROWSER_SESSION="mysession" # Default session name
|
||||
AGENT_BROWSER_EXECUTABLE_PATH="/path/chrome" # Custom browser path
|
||||
AGENT_BROWSER_EXTENSIONS="/ext1,/ext2" # Comma-separated extension paths
|
||||
AGENT_BROWSER_PROVIDER="browserbase" # Cloud browser provider
|
||||
AGENT_BROWSER_STREAM_PORT="9223" # WebSocket streaming port
|
||||
AGENT_BROWSER_HOME="/path/to/agent-browser" # Custom install location
|
||||
```
|
||||
@@ -0,0 +1,188 @@
|
||||
# Proxy Support
|
||||
|
||||
Proxy configuration for geo-testing, rate limiting avoidance, and corporate environments.
|
||||
|
||||
**Related**: [commands.md](commands.md) for global options, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Proxy Configuration](#basic-proxy-configuration)
|
||||
- [Authenticated Proxy](#authenticated-proxy)
|
||||
- [SOCKS Proxy](#socks-proxy)
|
||||
- [Proxy Bypass](#proxy-bypass)
|
||||
- [Common Use Cases](#common-use-cases)
|
||||
- [Verifying Proxy Connection](#verifying-proxy-connection)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Basic Proxy Configuration
|
||||
|
||||
Set proxy via environment variable before starting:
|
||||
|
||||
```bash
|
||||
# HTTP proxy
|
||||
export HTTP_PROXY="http://proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
|
||||
# HTTPS proxy
|
||||
export HTTPS_PROXY="https://proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
|
||||
# Both
|
||||
export HTTP_PROXY="http://proxy.example.com:8080"
|
||||
export HTTPS_PROXY="http://proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
## Authenticated Proxy
|
||||
|
||||
For proxies requiring authentication:
|
||||
|
||||
```bash
|
||||
# Include credentials in URL
|
||||
export HTTP_PROXY="http://username:password@proxy.example.com:8080"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
## SOCKS Proxy
|
||||
|
||||
```bash
|
||||
# SOCKS5 proxy
|
||||
export ALL_PROXY="socks5://proxy.example.com:1080"
|
||||
agent-browser open https://example.com
|
||||
|
||||
# SOCKS5 with auth
|
||||
export ALL_PROXY="socks5://user:pass@proxy.example.com:1080"
|
||||
agent-browser open https://example.com
|
||||
```
|
||||
|
||||
## Proxy Bypass
|
||||
|
||||
Skip proxy for specific domains:
|
||||
|
||||
```bash
|
||||
# Bypass proxy for local addresses
|
||||
export NO_PROXY="localhost,127.0.0.1,.internal.company.com"
|
||||
agent-browser open https://internal.company.com # Direct connection
|
||||
agent-browser open https://external.com # Via proxy
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### Geo-Location Testing
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Test site from different regions using geo-located proxies
|
||||
|
||||
PROXIES=(
|
||||
"http://us-proxy.example.com:8080"
|
||||
"http://eu-proxy.example.com:8080"
|
||||
"http://asia-proxy.example.com:8080"
|
||||
)
|
||||
|
||||
for proxy in "${PROXIES[@]}"; do
|
||||
export HTTP_PROXY="$proxy"
|
||||
export HTTPS_PROXY="$proxy"
|
||||
|
||||
region=$(echo "$proxy" | grep -oP '^\w+-\w+')
|
||||
echo "Testing from: $region"
|
||||
|
||||
agent-browser --session "$region" open https://example.com
|
||||
agent-browser --session "$region" screenshot "./screenshots/$region.png"
|
||||
agent-browser --session "$region" close
|
||||
done
|
||||
```
|
||||
|
||||
### Rotating Proxies for Scraping
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Rotate through proxy list to avoid rate limiting
|
||||
|
||||
PROXY_LIST=(
|
||||
"http://proxy1.example.com:8080"
|
||||
"http://proxy2.example.com:8080"
|
||||
"http://proxy3.example.com:8080"
|
||||
)
|
||||
|
||||
URLS=(
|
||||
"https://site.com/page1"
|
||||
"https://site.com/page2"
|
||||
"https://site.com/page3"
|
||||
)
|
||||
|
||||
for i in "${!URLS[@]}"; do
|
||||
proxy_index=$((i % ${#PROXY_LIST[@]}))
|
||||
export HTTP_PROXY="${PROXY_LIST[$proxy_index]}"
|
||||
export HTTPS_PROXY="${PROXY_LIST[$proxy_index]}"
|
||||
|
||||
agent-browser open "${URLS[$i]}"
|
||||
agent-browser get text body > "output-$i.txt"
|
||||
agent-browser close
|
||||
|
||||
sleep 1 # Polite delay
|
||||
done
|
||||
```
|
||||
|
||||
### Corporate Network Access
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Access internal sites via corporate proxy
|
||||
|
||||
export HTTP_PROXY="http://corpproxy.company.com:8080"
|
||||
export HTTPS_PROXY="http://corpproxy.company.com:8080"
|
||||
export NO_PROXY="localhost,127.0.0.1,.company.com"
|
||||
|
||||
# External sites go through proxy
|
||||
agent-browser open https://external-vendor.com
|
||||
|
||||
# Internal sites bypass proxy
|
||||
agent-browser open https://intranet.company.com
|
||||
```
|
||||
|
||||
## Verifying Proxy Connection
|
||||
|
||||
```bash
|
||||
# Check your apparent IP
|
||||
agent-browser open https://httpbin.org/ip
|
||||
agent-browser get text body
|
||||
# Should show proxy's IP, not your real IP
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Proxy Connection Failed
|
||||
|
||||
```bash
|
||||
# Test proxy connectivity first
|
||||
curl -x http://proxy.example.com:8080 https://httpbin.org/ip
|
||||
|
||||
# Check if proxy requires auth
|
||||
export HTTP_PROXY="http://user:pass@proxy.example.com:8080"
|
||||
```
|
||||
|
||||
### SSL/TLS Errors Through Proxy
|
||||
|
||||
Some proxies perform SSL inspection. If you encounter certificate errors:
|
||||
|
||||
```bash
|
||||
# For testing only - not recommended for production
|
||||
agent-browser open https://example.com --ignore-https-errors
|
||||
```
|
||||
|
||||
### Slow Performance
|
||||
|
||||
```bash
|
||||
# Use proxy only when necessary
|
||||
export NO_PROXY="*.cdn.com,*.static.com" # Direct CDN access
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use environment variables** - Don't hardcode proxy credentials
|
||||
2. **Set NO_PROXY appropriately** - Avoid routing local traffic through proxy
|
||||
3. **Test proxy before automation** - Verify connectivity with simple requests
|
||||
4. **Handle proxy failures gracefully** - Implement retry logic for unstable proxies
|
||||
5. **Rotate proxies for large scraping jobs** - Distribute load and avoid bans
|
||||
@@ -0,0 +1,193 @@
|
||||
# Session Management
|
||||
|
||||
Multiple isolated browser sessions with state persistence and concurrent browsing.
|
||||
|
||||
**Related**: [authentication.md](authentication.md) for login patterns, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Named Sessions](#named-sessions)
|
||||
- [Session Isolation Properties](#session-isolation-properties)
|
||||
- [Session State Persistence](#session-state-persistence)
|
||||
- [Common Patterns](#common-patterns)
|
||||
- [Default Session](#default-session)
|
||||
- [Session Cleanup](#session-cleanup)
|
||||
- [Best Practices](#best-practices)
|
||||
|
||||
## Named Sessions
|
||||
|
||||
Use `--session` flag to isolate browser contexts:
|
||||
|
||||
```bash
|
||||
# Session 1: Authentication flow
|
||||
agent-browser --session auth open https://app.example.com/login
|
||||
|
||||
# Session 2: Public browsing (separate cookies, storage)
|
||||
agent-browser --session public open https://example.com
|
||||
|
||||
# Commands are isolated by session
|
||||
agent-browser --session auth fill @e1 "user@example.com"
|
||||
agent-browser --session public get text body
|
||||
```
|
||||
|
||||
## Session Isolation Properties
|
||||
|
||||
Each session has independent:
|
||||
- Cookies
|
||||
- LocalStorage / SessionStorage
|
||||
- IndexedDB
|
||||
- Cache
|
||||
- Browsing history
|
||||
- Open tabs
|
||||
|
||||
## Session State Persistence
|
||||
|
||||
### Save Session State
|
||||
|
||||
```bash
|
||||
# Save cookies, storage, and auth state
|
||||
agent-browser state save /path/to/auth-state.json
|
||||
```
|
||||
|
||||
### Load Session State
|
||||
|
||||
```bash
|
||||
# Restore saved state
|
||||
agent-browser state load /path/to/auth-state.json
|
||||
|
||||
# Continue with authenticated session
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
```
|
||||
|
||||
### State File Contents
|
||||
|
||||
```json
|
||||
{
|
||||
"cookies": [...],
|
||||
"localStorage": {...},
|
||||
"sessionStorage": {...},
|
||||
"origins": [...]
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Authenticated Session Reuse
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Save login state once, reuse many times
|
||||
|
||||
STATE_FILE="/tmp/auth-state.json"
|
||||
|
||||
# Check if we have saved state
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
agent-browser state load "$STATE_FILE"
|
||||
agent-browser open https://app.example.com/dashboard
|
||||
else
|
||||
# Perform login
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "$USERNAME"
|
||||
agent-browser fill @e2 "$PASSWORD"
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Save for future use
|
||||
agent-browser state save "$STATE_FILE"
|
||||
fi
|
||||
```
|
||||
|
||||
### Concurrent Scraping
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Scrape multiple sites concurrently
|
||||
|
||||
# Start all sessions
|
||||
agent-browser --session site1 open https://site1.com &
|
||||
agent-browser --session site2 open https://site2.com &
|
||||
agent-browser --session site3 open https://site3.com &
|
||||
wait
|
||||
|
||||
# Extract from each
|
||||
agent-browser --session site1 get text body > site1.txt
|
||||
agent-browser --session site2 get text body > site2.txt
|
||||
agent-browser --session site3 get text body > site3.txt
|
||||
|
||||
# Cleanup
|
||||
agent-browser --session site1 close
|
||||
agent-browser --session site2 close
|
||||
agent-browser --session site3 close
|
||||
```
|
||||
|
||||
### A/B Testing Sessions
|
||||
|
||||
```bash
|
||||
# Test different user experiences
|
||||
agent-browser --session variant-a open "https://app.com?variant=a"
|
||||
agent-browser --session variant-b open "https://app.com?variant=b"
|
||||
|
||||
# Compare
|
||||
agent-browser --session variant-a screenshot /tmp/variant-a.png
|
||||
agent-browser --session variant-b screenshot /tmp/variant-b.png
|
||||
```
|
||||
|
||||
## Default Session
|
||||
|
||||
When `--session` is omitted, commands use the default session:
|
||||
|
||||
```bash
|
||||
# These use the same default session
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser close # Closes default session
|
||||
```
|
||||
|
||||
## Session Cleanup
|
||||
|
||||
```bash
|
||||
# Close specific session
|
||||
agent-browser --session auth close
|
||||
|
||||
# List active sessions
|
||||
agent-browser session list
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Name Sessions Semantically
|
||||
|
||||
```bash
|
||||
# GOOD: Clear purpose
|
||||
agent-browser --session github-auth open https://github.com
|
||||
agent-browser --session docs-scrape open https://docs.example.com
|
||||
|
||||
# AVOID: Generic names
|
||||
agent-browser --session s1 open https://github.com
|
||||
```
|
||||
|
||||
### 2. Always Clean Up
|
||||
|
||||
```bash
|
||||
# Close sessions when done
|
||||
agent-browser --session auth close
|
||||
agent-browser --session scrape close
|
||||
```
|
||||
|
||||
### 3. Handle State Files Securely
|
||||
|
||||
```bash
|
||||
# Don't commit state files (contain auth tokens!)
|
||||
echo "*.auth-state.json" >> .gitignore
|
||||
|
||||
# Delete after use
|
||||
rm /tmp/auth-state.json
|
||||
```
|
||||
|
||||
### 4. Timeout Long Sessions
|
||||
|
||||
```bash
|
||||
# Set timeout for automated scripts
|
||||
timeout 60 agent-browser --session long-task get text body
|
||||
```
|
||||
@@ -0,0 +1,194 @@
|
||||
# Snapshot and Refs
|
||||
|
||||
Compact element references that reduce context usage dramatically for AI agents.
|
||||
|
||||
**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [How Refs Work](#how-refs-work)
|
||||
- [Snapshot Command](#the-snapshot-command)
|
||||
- [Using Refs](#using-refs)
|
||||
- [Ref Lifecycle](#ref-lifecycle)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Ref Notation Details](#ref-notation-details)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## How Refs Work
|
||||
|
||||
Traditional approach:
|
||||
```
|
||||
Full DOM/HTML → AI parses → CSS selector → Action (~3000-5000 tokens)
|
||||
```
|
||||
|
||||
agent-browser approach:
|
||||
```
|
||||
Compact snapshot → @refs assigned → Direct interaction (~200-400 tokens)
|
||||
```
|
||||
|
||||
## The Snapshot Command
|
||||
|
||||
```bash
|
||||
# Basic snapshot (shows page structure)
|
||||
agent-browser snapshot
|
||||
|
||||
# Interactive snapshot (-i flag) - RECOMMENDED
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
### Snapshot Output Format
|
||||
|
||||
```
|
||||
Page: Example Site - Home
|
||||
URL: https://example.com
|
||||
|
||||
@e1 [header]
|
||||
@e2 [nav]
|
||||
@e3 [a] "Home"
|
||||
@e4 [a] "Products"
|
||||
@e5 [a] "About"
|
||||
@e6 [button] "Sign In"
|
||||
|
||||
@e7 [main]
|
||||
@e8 [h1] "Welcome"
|
||||
@e9 [form]
|
||||
@e10 [input type="email"] placeholder="Email"
|
||||
@e11 [input type="password"] placeholder="Password"
|
||||
@e12 [button type="submit"] "Log In"
|
||||
|
||||
@e13 [footer]
|
||||
@e14 [a] "Privacy Policy"
|
||||
```
|
||||
|
||||
## Using Refs
|
||||
|
||||
Once you have refs, interact directly:
|
||||
|
||||
```bash
|
||||
# Click the "Sign In" button
|
||||
agent-browser click @e6
|
||||
|
||||
# Fill email input
|
||||
agent-browser fill @e10 "user@example.com"
|
||||
|
||||
# Fill password
|
||||
agent-browser fill @e11 "password123"
|
||||
|
||||
# Submit the form
|
||||
agent-browser click @e12
|
||||
```
|
||||
|
||||
## Ref Lifecycle
|
||||
|
||||
**IMPORTANT**: Refs are invalidated when the page changes!
|
||||
|
||||
```bash
|
||||
# Get initial snapshot
|
||||
agent-browser snapshot -i
|
||||
# @e1 [button] "Next"
|
||||
|
||||
# Click triggers page change
|
||||
agent-browser click @e1
|
||||
|
||||
# MUST re-snapshot to get new refs!
|
||||
agent-browser snapshot -i
|
||||
# @e1 [h1] "Page 2" ← Different element now!
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Always Snapshot Before Interacting
|
||||
|
||||
```bash
|
||||
# CORRECT
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i # Get refs first
|
||||
agent-browser click @e1 # Use ref
|
||||
|
||||
# WRONG
|
||||
agent-browser open https://example.com
|
||||
agent-browser click @e1 # Ref doesn't exist yet!
|
||||
```
|
||||
|
||||
### 2. Re-Snapshot After Navigation
|
||||
|
||||
```bash
|
||||
agent-browser click @e5 # Navigates to new page
|
||||
agent-browser snapshot -i # Get new refs
|
||||
agent-browser click @e1 # Use new refs
|
||||
```
|
||||
|
||||
### 3. Re-Snapshot After Dynamic Changes
|
||||
|
||||
```bash
|
||||
agent-browser click @e1 # Opens dropdown
|
||||
agent-browser snapshot -i # See dropdown items
|
||||
agent-browser click @e7 # Select item
|
||||
```
|
||||
|
||||
### 4. Snapshot Specific Regions
|
||||
|
||||
For complex pages, snapshot specific areas:
|
||||
|
||||
```bash
|
||||
# Snapshot just the form
|
||||
agent-browser snapshot @e9
|
||||
```
|
||||
|
||||
## Ref Notation Details
|
||||
|
||||
```
|
||||
@e1 [tag type="value"] "text content" placeholder="hint"
|
||||
│ │ │ │ │
|
||||
│ │ │ │ └─ Additional attributes
|
||||
│ │ │ └─ Visible text
|
||||
│ │ └─ Key attributes shown
|
||||
│ └─ HTML tag name
|
||||
└─ Unique ref ID
|
||||
```
|
||||
|
||||
### Common Patterns
|
||||
|
||||
```
|
||||
@e1 [button] "Submit" # Button with text
|
||||
@e2 [input type="email"] # Email input
|
||||
@e3 [input type="password"] # Password input
|
||||
@e4 [a href="/page"] "Link Text" # Anchor link
|
||||
@e5 [select] # Dropdown
|
||||
@e6 [textarea] placeholder="Message" # Text area
|
||||
@e7 [div class="modal"] # Container (when relevant)
|
||||
@e8 [img alt="Logo"] # Image
|
||||
@e9 [checkbox] checked # Checked checkbox
|
||||
@e10 [radio] selected # Selected radio
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Ref not found" Error
|
||||
|
||||
```bash
|
||||
# Ref may have changed - re-snapshot
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
### Element Not Visible in Snapshot
|
||||
|
||||
```bash
|
||||
# Scroll to reveal element
|
||||
agent-browser scroll --bottom
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Or wait for dynamic content
|
||||
agent-browser wait 1000
|
||||
agent-browser snapshot -i
|
||||
```
|
||||
|
||||
### Too Many Elements
|
||||
|
||||
```bash
|
||||
# Snapshot specific container
|
||||
agent-browser snapshot @e5
|
||||
|
||||
# Or use get text for content-only extraction
|
||||
agent-browser get text @e5
|
||||
```
|
||||
@@ -0,0 +1,173 @@
|
||||
# Video Recording
|
||||
|
||||
Capture browser automation as video for debugging, documentation, or verification.
|
||||
|
||||
**Related**: [commands.md](commands.md) for full command reference, [SKILL.md](../SKILL.md) for quick start.
|
||||
|
||||
## Contents
|
||||
|
||||
- [Basic Recording](#basic-recording)
|
||||
- [Recording Commands](#recording-commands)
|
||||
- [Use Cases](#use-cases)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Output Format](#output-format)
|
||||
- [Limitations](#limitations)
|
||||
|
||||
## Basic Recording
|
||||
|
||||
```bash
|
||||
# Start recording
|
||||
agent-browser record start ./demo.webm
|
||||
|
||||
# Perform actions
|
||||
agent-browser open https://example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser click @e1
|
||||
agent-browser fill @e2 "test input"
|
||||
|
||||
# Stop and save
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
## Recording Commands
|
||||
|
||||
```bash
|
||||
# Start recording to file
|
||||
agent-browser record start ./output.webm
|
||||
|
||||
# Stop current recording
|
||||
agent-browser record stop
|
||||
|
||||
# Restart with new file (stops current + starts new)
|
||||
agent-browser record restart ./take2.webm
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Debugging Failed Automation
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Record automation for debugging
|
||||
|
||||
agent-browser record start ./debug-$(date +%Y%m%d-%H%M%S).webm
|
||||
|
||||
# Run your automation
|
||||
agent-browser open https://app.example.com
|
||||
agent-browser snapshot -i
|
||||
agent-browser click @e1 || {
|
||||
echo "Click failed - check recording"
|
||||
agent-browser record stop
|
||||
exit 1
|
||||
}
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
### Documentation Generation
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Record workflow for documentation
|
||||
|
||||
agent-browser record start ./docs/how-to-login.webm
|
||||
|
||||
agent-browser open https://app.example.com/login
|
||||
agent-browser wait 1000 # Pause for visibility
|
||||
|
||||
agent-browser snapshot -i
|
||||
agent-browser fill @e1 "demo@example.com"
|
||||
agent-browser wait 500
|
||||
|
||||
agent-browser fill @e2 "password"
|
||||
agent-browser wait 500
|
||||
|
||||
agent-browser click @e3
|
||||
agent-browser wait --load networkidle
|
||||
agent-browser wait 1000 # Show result
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
### CI/CD Test Evidence
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Record E2E test runs for CI artifacts
|
||||
|
||||
TEST_NAME="${1:-e2e-test}"
|
||||
RECORDING_DIR="./test-recordings"
|
||||
mkdir -p "$RECORDING_DIR"
|
||||
|
||||
agent-browser record start "$RECORDING_DIR/$TEST_NAME-$(date +%s).webm"
|
||||
|
||||
# Run test
|
||||
if run_e2e_test; then
|
||||
echo "Test passed"
|
||||
else
|
||||
echo "Test failed - recording saved"
|
||||
fi
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Add Pauses for Clarity
|
||||
|
||||
```bash
|
||||
# Slow down for human viewing
|
||||
agent-browser click @e1
|
||||
agent-browser wait 500 # Let viewer see result
|
||||
```
|
||||
|
||||
### 2. Use Descriptive Filenames
|
||||
|
||||
```bash
|
||||
# Include context in filename
|
||||
agent-browser record start ./recordings/login-flow-2024-01-15.webm
|
||||
agent-browser record start ./recordings/checkout-test-run-42.webm
|
||||
```
|
||||
|
||||
### 3. Handle Recording in Error Cases
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
cleanup() {
|
||||
agent-browser record stop 2>/dev/null || true
|
||||
agent-browser close 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
agent-browser record start ./automation.webm
|
||||
# ... automation steps ...
|
||||
```
|
||||
|
||||
### 4. Combine with Screenshots
|
||||
|
||||
```bash
|
||||
# Record video AND capture key frames
|
||||
agent-browser record start ./flow.webm
|
||||
|
||||
agent-browser open https://example.com
|
||||
agent-browser screenshot ./screenshots/step1-homepage.png
|
||||
|
||||
agent-browser click @e1
|
||||
agent-browser screenshot ./screenshots/step2-after-click.png
|
||||
|
||||
agent-browser record stop
|
||||
```
|
||||
|
||||
## Output Format
|
||||
|
||||
- Default format: WebM (VP8/VP9 codec)
|
||||
- Compatible with all modern browsers and video players
|
||||
- Compressed but high quality
|
||||
|
||||
## Limitations
|
||||
|
||||
- Recording adds slight overhead to automation
|
||||
- Large recordings can consume significant disk space
|
||||
- Some headless environments may have codec limitations
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/bin/bash
|
||||
# Template: Authenticated Session Workflow
|
||||
# Purpose: Login once, save state, reuse for subsequent runs
|
||||
# Usage: ./authenticated-session.sh <login-url> [state-file]
|
||||
#
|
||||
# Environment variables:
|
||||
# APP_USERNAME - Login username/email
|
||||
# APP_PASSWORD - Login password
|
||||
#
|
||||
# Two modes:
|
||||
# 1. Discovery mode (default): Shows form structure so you can identify refs
|
||||
# 2. Login mode: Performs actual login after you update the refs
|
||||
#
|
||||
# Setup steps:
|
||||
# 1. Run once to see form structure (discovery mode)
|
||||
# 2. Update refs in LOGIN FLOW section below
|
||||
# 3. Set APP_USERNAME and APP_PASSWORD
|
||||
# 4. Delete the DISCOVERY section
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
LOGIN_URL="${1:?Usage: $0 <login-url> [state-file]}"
|
||||
STATE_FILE="${2:-./auth-state.json}"
|
||||
|
||||
echo "Authentication workflow: $LOGIN_URL"
|
||||
|
||||
# ================================================================
|
||||
# SAVED STATE: Skip login if valid saved state exists
|
||||
# ================================================================
|
||||
if [[ -f "$STATE_FILE" ]]; then
|
||||
echo "Loading saved state from $STATE_FILE..."
|
||||
agent-browser state load "$STATE_FILE"
|
||||
agent-browser open "$LOGIN_URL"
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
CURRENT_URL=$(agent-browser get url)
|
||||
if [[ "$CURRENT_URL" != *"login"* ]] && [[ "$CURRENT_URL" != *"signin"* ]]; then
|
||||
echo "Session restored successfully"
|
||||
agent-browser snapshot -i
|
||||
exit 0
|
||||
fi
|
||||
echo "Session expired, performing fresh login..."
|
||||
rm -f "$STATE_FILE"
|
||||
fi
|
||||
|
||||
# ================================================================
|
||||
# DISCOVERY MODE: Shows form structure (delete after setup)
|
||||
# ================================================================
|
||||
echo "Opening login page..."
|
||||
agent-browser open "$LOGIN_URL"
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
echo ""
|
||||
echo "Login form structure:"
|
||||
echo "---"
|
||||
agent-browser snapshot -i
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " 1. Note the refs: username=@e?, password=@e?, submit=@e?"
|
||||
echo " 2. Update the LOGIN FLOW section below with your refs"
|
||||
echo " 3. Set: export APP_USERNAME='...' APP_PASSWORD='...'"
|
||||
echo " 4. Delete this DISCOVERY MODE section"
|
||||
echo ""
|
||||
agent-browser close
|
||||
exit 0
|
||||
|
||||
# ================================================================
|
||||
# LOGIN FLOW: Uncomment and customize after discovery
|
||||
# ================================================================
|
||||
# : "${APP_USERNAME:?Set APP_USERNAME environment variable}"
|
||||
# : "${APP_PASSWORD:?Set APP_PASSWORD environment variable}"
|
||||
#
|
||||
# agent-browser open "$LOGIN_URL"
|
||||
# agent-browser wait --load networkidle
|
||||
# agent-browser snapshot -i
|
||||
#
|
||||
# # Fill credentials (update refs to match your form)
|
||||
# agent-browser fill @e1 "$APP_USERNAME"
|
||||
# agent-browser fill @e2 "$APP_PASSWORD"
|
||||
# agent-browser click @e3
|
||||
# agent-browser wait --load networkidle
|
||||
#
|
||||
# # Verify login succeeded
|
||||
# FINAL_URL=$(agent-browser get url)
|
||||
# if [[ "$FINAL_URL" == *"login"* ]] || [[ "$FINAL_URL" == *"signin"* ]]; then
|
||||
# echo "Login failed - still on login page"
|
||||
# agent-browser screenshot /tmp/login-failed.png
|
||||
# agent-browser close
|
||||
# exit 1
|
||||
# fi
|
||||
#
|
||||
# # Save state for future runs
|
||||
# echo "Saving state to $STATE_FILE"
|
||||
# agent-browser state save "$STATE_FILE"
|
||||
# echo "Login successful"
|
||||
# agent-browser snapshot -i
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# Template: Content Capture Workflow
|
||||
# Purpose: Extract content from web pages (text, screenshots, PDF)
|
||||
# Usage: ./capture-workflow.sh <url> [output-dir]
|
||||
#
|
||||
# Outputs:
|
||||
# - page-full.png: Full page screenshot
|
||||
# - page-structure.txt: Page element structure with refs
|
||||
# - page-text.txt: All text content
|
||||
# - page.pdf: PDF version
|
||||
#
|
||||
# Optional: Load auth state for protected pages
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_URL="${1:?Usage: $0 <url> [output-dir]}"
|
||||
OUTPUT_DIR="${2:-.}"
|
||||
|
||||
echo "Capturing: $TARGET_URL"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
# Optional: Load authentication state
|
||||
# if [[ -f "./auth-state.json" ]]; then
|
||||
# echo "Loading authentication state..."
|
||||
# agent-browser state load "./auth-state.json"
|
||||
# fi
|
||||
|
||||
# Navigate to target
|
||||
agent-browser open "$TARGET_URL"
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Get metadata
|
||||
TITLE=$(agent-browser get title)
|
||||
URL=$(agent-browser get url)
|
||||
echo "Title: $TITLE"
|
||||
echo "URL: $URL"
|
||||
|
||||
# Capture full page screenshot
|
||||
agent-browser screenshot --full "$OUTPUT_DIR/page-full.png"
|
||||
echo "Saved: $OUTPUT_DIR/page-full.png"
|
||||
|
||||
# Get page structure with refs
|
||||
agent-browser snapshot -i > "$OUTPUT_DIR/page-structure.txt"
|
||||
echo "Saved: $OUTPUT_DIR/page-structure.txt"
|
||||
|
||||
# Extract all text content
|
||||
agent-browser get text body > "$OUTPUT_DIR/page-text.txt"
|
||||
echo "Saved: $OUTPUT_DIR/page-text.txt"
|
||||
|
||||
# Save as PDF
|
||||
agent-browser pdf "$OUTPUT_DIR/page.pdf"
|
||||
echo "Saved: $OUTPUT_DIR/page.pdf"
|
||||
|
||||
# Optional: Extract specific elements using refs from structure
|
||||
# agent-browser get text @e5 > "$OUTPUT_DIR/main-content.txt"
|
||||
|
||||
# Optional: Handle infinite scroll pages
|
||||
# for i in {1..5}; do
|
||||
# agent-browser scroll down 1000
|
||||
# agent-browser wait 1000
|
||||
# done
|
||||
# agent-browser screenshot --full "$OUTPUT_DIR/page-scrolled.png"
|
||||
|
||||
# Cleanup
|
||||
agent-browser close
|
||||
|
||||
echo ""
|
||||
echo "Capture complete:"
|
||||
ls -la "$OUTPUT_DIR"
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Template: Form Automation Workflow
|
||||
# Purpose: Fill and submit web forms with validation
|
||||
# Usage: ./form-automation.sh <form-url>
|
||||
#
|
||||
# This template demonstrates the snapshot-interact-verify pattern:
|
||||
# 1. Navigate to form
|
||||
# 2. Snapshot to get element refs
|
||||
# 3. Fill fields using refs
|
||||
# 4. Submit and verify result
|
||||
#
|
||||
# Customize: Update the refs (@e1, @e2, etc.) based on your form's snapshot output
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
FORM_URL="${1:?Usage: $0 <form-url>}"
|
||||
|
||||
echo "Form automation: $FORM_URL"
|
||||
|
||||
# Step 1: Navigate to form
|
||||
agent-browser open "$FORM_URL"
|
||||
agent-browser wait --load networkidle
|
||||
|
||||
# Step 2: Snapshot to discover form elements
|
||||
echo ""
|
||||
echo "Form structure:"
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Step 3: Fill form fields (customize these refs based on snapshot output)
|
||||
#
|
||||
# Common field types:
|
||||
# agent-browser fill @e1 "John Doe" # Text input
|
||||
# agent-browser fill @e2 "user@example.com" # Email input
|
||||
# agent-browser fill @e3 "SecureP@ss123" # Password input
|
||||
# agent-browser select @e4 "Option Value" # Dropdown
|
||||
# agent-browser check @e5 # Checkbox
|
||||
# agent-browser click @e6 # Radio button
|
||||
# agent-browser fill @e7 "Multi-line text" # Textarea
|
||||
# agent-browser upload @e8 /path/to/file.pdf # File upload
|
||||
#
|
||||
# Uncomment and modify:
|
||||
# agent-browser fill @e1 "Test User"
|
||||
# agent-browser fill @e2 "test@example.com"
|
||||
# agent-browser click @e3 # Submit button
|
||||
|
||||
# Step 4: Wait for submission
|
||||
# agent-browser wait --load networkidle
|
||||
# agent-browser wait --url "**/success" # Or wait for redirect
|
||||
|
||||
# Step 5: Verify result
|
||||
echo ""
|
||||
echo "Result:"
|
||||
agent-browser get url
|
||||
agent-browser snapshot -i
|
||||
|
||||
# Optional: Capture evidence
|
||||
agent-browser screenshot /tmp/form-result.png
|
||||
echo "Screenshot saved: /tmp/form-result.png"
|
||||
|
||||
# Cleanup
|
||||
agent-browser close
|
||||
echo "Done"
|
||||
@@ -1,3 +1,6 @@
|
||||
# The license key to enable enterprise features for self hosters
|
||||
NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=
|
||||
|
||||
# [[AUTH]]
|
||||
NEXTAUTH_SECRET="secret"
|
||||
|
||||
@@ -13,18 +16,30 @@ NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="DEADBEEF"
|
||||
# https://docs.documenso.com/developers/self-hosting/setting-up-oauth-providers#google-oauth-gmail
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
|
||||
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""
|
||||
# Find documentation on setting up Microsoft OAuth here:
|
||||
# https://docs.documenso.com/developers/self-hosting/setting-up-oauth-providers#microsoft-oauth-azure-ad
|
||||
NEXT_PRIVATE_MICROSOFT_CLIENT_ID=""
|
||||
NEXT_PRIVATE_MICROSOFT_CLIENT_SECRET=""
|
||||
|
||||
NEXT_PRIVATE_OIDC_WELL_KNOWN=""
|
||||
NEXT_PRIVATE_OIDC_CLIENT_ID=""
|
||||
NEXT_PRIVATE_OIDC_CLIENT_SECRET=""
|
||||
NEXT_PRIVATE_OIDC_PROVIDER_LABEL="OIDC"
|
||||
NEXT_PRIVATE_OIDC_SKIP_VERIFY=""
|
||||
# Specifies the prompt to use for OIDC signin, explicitly setting
|
||||
# an empty string will omit the prompt parameter.
|
||||
# See: https://www.cerberauth.com/blog/openid-connect-oauth2-prompts/
|
||||
NEXT_PRIVATE_OIDC_PROMPT="login"
|
||||
|
||||
# [[URLS]]
|
||||
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
||||
# URL used by the web app to request itself (e.g. local background jobs)
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000"
|
||||
|
||||
# [[SERVER]]
|
||||
# OPTIONAL: The port the server will listen on. Defaults to 3000.
|
||||
PORT=3000
|
||||
|
||||
# [[DATABASE]]
|
||||
NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
|
||||
# Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool.
|
||||
@@ -47,6 +62,18 @@ NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_PATH=
|
||||
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_PUBLIC_CRT_FILE_CONTENTS=
|
||||
# OPTIONAL: The path to the Google Cloud Credentials file to use for the gcloud-hsm signing transport.
|
||||
NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS=
|
||||
# OPTIONAL: The path to the certificate chain file for the gcloud-hsm signing transport.
|
||||
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_FILE_PATH=
|
||||
# OPTIONAL: The base64-encoded contents of the certificate chain for the gcloud-hsm signing transport.
|
||||
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS=
|
||||
# OPTIONAL: The Google Secret Manager path to retrieve the certificate for the gcloud-hsm signing transport.
|
||||
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH=
|
||||
# OPTIONAL: Comma-separated list of timestamp authority URLs for PDF signing (enables LTV and archival timestamps).
|
||||
NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=
|
||||
# OPTIONAL: Contact info to embed in PDF signatures. Defaults to the webapp URL.
|
||||
NEXT_PUBLIC_SIGNING_CONTACT_INFO=
|
||||
# OPTIONAL: Set to "true" to use the legacy adbe.pkcs7.detached subfilter instead of ETSI.CAdES.detached.
|
||||
NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER=
|
||||
|
||||
# [[STORAGE]]
|
||||
# OPTIONAL: Defines the storage transport to use. Available options: database (default) | s3
|
||||
@@ -126,11 +153,30 @@ NEXT_PUBLIC_POSTHOG_KEY=""
|
||||
NEXT_PUBLIC_FEATURE_BILLING_ENABLED=
|
||||
# OPTIONAL: Leave blank to allow users to signup through /signup page.
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP=
|
||||
# OPTIONAL: Set to true to use internal webapp url in browserless requests.
|
||||
NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false
|
||||
|
||||
# [[TELEMETRY]]
|
||||
# OPTIONAL: Set to "true" to disable anonymous telemetry for self-hosted instances.
|
||||
# Telemetry helps us understand how Documenso is being used and improve the product.
|
||||
# We only collect: app version, installation ID, and node ID. No personal data is collected.
|
||||
DOCUMENSO_DISABLE_TELEMETRY=
|
||||
|
||||
# [[AI]]
|
||||
# OPTIONAL: Google Cloud Project ID for Vertex AI.
|
||||
GOOGLE_VERTEX_PROJECT_ID=""
|
||||
# OPTIONAL: Google Cloud region for Vertex AI. Defaults to "global".
|
||||
GOOGLE_VERTEX_LOCATION="global"
|
||||
# OPTIONAL: API key for Google Vertex AI (Gemini). Get your key from:
|
||||
# https://console.cloud.google.com/vertex-ai/studio/settings/api-keys
|
||||
GOOGLE_VERTEX_API_KEY=""
|
||||
|
||||
# [[E2E Tests]]
|
||||
E2E_TEST_AUTHENTICATE_USERNAME="Test User"
|
||||
E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
|
||||
E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
|
||||
# OPTIONAL: Set to "true" to disable all rate limiting. Only use for E2E tests.
|
||||
DANGEROUS_BYPASS_RATE_LIMITS=
|
||||
|
||||
# [[LOGGER]]
|
||||
# OPTIONAL: The file to save the logger output to. Will disable stdout if provided.
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
name: Playwright Tests
|
||||
on:
|
||||
push:
|
||||
branches: ['main', 'feat/rr7']
|
||||
branches: ['main']
|
||||
pull_request:
|
||||
branches: ['main']
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
e2e_tests:
|
||||
name: 'E2E Tests'
|
||||
timeout-minutes: 60
|
||||
runs-on: warp-ubuntu-2204-x64-16x
|
||||
runs-on: warp-ubuntu-2204-x64-8x
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -28,9 +33,6 @@ jobs:
|
||||
- name: Seed the database
|
||||
run: npm run prisma:seed
|
||||
|
||||
- name: Build app
|
||||
run: npm run build
|
||||
|
||||
- name: Install playwright browsers
|
||||
run: npx playwright install --with-deps
|
||||
|
||||
@@ -39,13 +41,14 @@ jobs:
|
||||
env:
|
||||
# Needed since we use next start which will set the NODE_ENV to production
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: './example/cert.p12'
|
||||
DANGEROUS_BYPASS_RATE_LIMITS: 'true'
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results
|
||||
path: 'packages/app-tests/**/test-results/*'
|
||||
retention-days: 30
|
||||
retention-days: 7
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
||||
|
||||
@@ -3,6 +3,12 @@ name: Publish Docker
|
||||
on:
|
||||
push:
|
||||
branches: ['release']
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Git tag to build and publish (e.g., v1.0.0)'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build_and_publish_platform_containers:
|
||||
@@ -18,6 +24,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag || github.ref }}
|
||||
fetch-tags: true
|
||||
|
||||
- name: Login to DockerHub
|
||||
@@ -36,13 +43,20 @@ jobs:
|
||||
- name: Build the docker image
|
||||
env:
|
||||
BUILD_PLATFORM: ${{ matrix.os == 'warp-ubuntu-latest-arm64-4x' && 'arm64' || 'amd64' }}
|
||||
NEXT_PRIVATE_TELEMETRY_KEY: ${{ secrets.NEXT_PRIVATE_TELEMETRY_KEY }}
|
||||
NEXT_PRIVATE_TELEMETRY_HOST: ${{ secrets.NEXT_PRIVATE_TELEMETRY_HOST }}
|
||||
APP_VERSION: ${{ inputs.tag || '' }}
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
if [ -z "$APP_VERSION" ]; then
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
fi
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
docker build \
|
||||
-f ./docker/Dockerfile \
|
||||
--progress=plain \
|
||||
--build-arg NEXT_PRIVATE_TELEMETRY_KEY="${NEXT_PRIVATE_TELEMETRY_KEY:-}" \
|
||||
--build-arg NEXT_PRIVATE_TELEMETRY_HOST="${NEXT_PRIVATE_TELEMETRY_HOST:-}" \
|
||||
-t "documenso/documenso-$BUILD_PLATFORM:latest" \
|
||||
-t "documenso/documenso-$BUILD_PLATFORM:$GIT_SHA" \
|
||||
-t "documenso/documenso-$BUILD_PLATFORM:$APP_VERSION" \
|
||||
@@ -69,6 +83,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ inputs.tag || github.ref }}
|
||||
fetch-tags: true
|
||||
|
||||
- name: Login to DockerHub
|
||||
@@ -85,8 +100,12 @@ jobs:
|
||||
password: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
- name: Create and push DockerHub manifest
|
||||
env:
|
||||
APP_VERSION: ${{ inputs.tag || '' }}
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
if [ -z "$APP_VERSION" ]; then
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
fi
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
# Check if the version is stable (no rc or beta in the version)
|
||||
@@ -122,8 +141,12 @@ jobs:
|
||||
docker manifest push documenso/documenso:$APP_VERSION
|
||||
|
||||
- name: Create and push Github Container Registry manifest
|
||||
env:
|
||||
APP_VERSION: ${{ inputs.tag || '' }}
|
||||
run: |
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
if [ -z "$APP_VERSION" ]; then
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
fi
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
# Check if the version is stable (no rc or beta in the version)
|
||||
|
||||
@@ -17,6 +17,7 @@ jobs:
|
||||
environment: Translations
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -26,12 +27,54 @@ jobs:
|
||||
- name: Extract translations
|
||||
run: npm run translate:extract
|
||||
|
||||
- name: Check and commit any files created
|
||||
- name: Commit changes and push to reserved branch
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BRANCH="chore/extract-translations"
|
||||
|
||||
git config --global user.name 'github-actions'
|
||||
git config --global user.email 'github-actions@documenso.com'
|
||||
|
||||
git fetch origin
|
||||
|
||||
# Create branch locally (always reset to main)
|
||||
git checkout -B "$BRANCH" origin/main
|
||||
|
||||
# Stage translation output
|
||||
git add packages/lib/translations
|
||||
git diff --staged --quiet --exit-code || (git commit -m "chore: extract translations" && git push)
|
||||
|
||||
# If no changes, exit early
|
||||
if git diff --staged --quiet; then
|
||||
echo "No translation changes found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Commit fresh snapshot
|
||||
git commit -m "chore: extract translations"
|
||||
|
||||
# Force push reserved branch
|
||||
git push origin "$BRANCH" --force
|
||||
|
||||
# Does a PR already exist?
|
||||
EXISTING_PR=$(gh pr list \
|
||||
--state open \
|
||||
--head "$BRANCH" \
|
||||
--json number \
|
||||
--jq '.[0].number // empty')
|
||||
|
||||
if [ -z "$EXISTING_PR" ]; then
|
||||
echo "No existing PR — creating new one."
|
||||
gh pr create \
|
||||
--title "chore: extract translations" \
|
||||
--body "Automated translation extraction" \
|
||||
--base main \
|
||||
--head "$BRANCH"
|
||||
else
|
||||
echo "PR #$EXISTING_PR already exists — not creating a new one."
|
||||
fi
|
||||
|
||||
- name: Compile translations
|
||||
id: compile_translations
|
||||
|
||||
+14
-1
@@ -56,4 +56,17 @@ logs.json
|
||||
|
||||
# claude
|
||||
.claude
|
||||
CLAUDE.md
|
||||
CLAUDE.md
|
||||
|
||||
# agents
|
||||
.specs
|
||||
|
||||
# scripts
|
||||
scripts/output*
|
||||
|
||||
# license
|
||||
.documenso-license.json
|
||||
.documenso-license-backup.json
|
||||
|
||||
# tmp
|
||||
tmp/
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
auto-install-peers = true
|
||||
legacy-peer-deps = true
|
||||
prefer-dedupe = true
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
description: Add and commit changes using conventional commits
|
||||
allowed-tools: Bash, Read, Glob, Grep
|
||||
---
|
||||
|
||||
Create a git commit for the current changes using the Conventional Commits standard.
|
||||
|
||||
## Process
|
||||
|
||||
1. **Analyze the changes** by running:
|
||||
- `git status` to see all modified/untracked files
|
||||
- `git diff` to see unstaged changes
|
||||
- `git diff --staged` to see already-staged changes
|
||||
- `git log --oneline -5` to see recent commit style
|
||||
|
||||
2. **Stage appropriate files**:
|
||||
- Stage all related changes with `git add`
|
||||
- Do NOT stage files that appear to contain secrets (.env, credentials, API keys, tokens)
|
||||
- If you detect potential secrets, warn the user and skip those files
|
||||
|
||||
3. **Determine the commit type** based on the changes:
|
||||
- `feat`: New feature or capability
|
||||
- `fix`: Bug fix
|
||||
- `docs`: Documentation only
|
||||
- `style`: Formatting, whitespace (not CSS)
|
||||
- `refactor`: Code restructuring without behavior change
|
||||
- `perf`: Performance improvement
|
||||
- `test`: Adding or updating tests
|
||||
- `build`: Build system or dependencies
|
||||
- `ci`: CI/CD configuration
|
||||
- `chore`: Maintenance tasks, tooling, config
|
||||
|
||||
NOTE: Do not use a scope for commits
|
||||
|
||||
4. **Write the commit message**:
|
||||
- **Subject line**: `<type>: <description>`
|
||||
- Use imperative mood ("add" not "added")
|
||||
- Lowercase, no period at end
|
||||
- Max 50 characters if possible, 72 hard limit
|
||||
- **Body** (if needed): Explain _why_, not _what_
|
||||
- Wrap at 72 characters
|
||||
- Separate from subject with blank line
|
||||
|
||||
## Commit Format
|
||||
|
||||
```
|
||||
<type>[scope]: <subject>
|
||||
|
||||
[optional body explaining WHY this change was made]
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Simple change:
|
||||
|
||||
```
|
||||
fix: handle empty input in parser without throwing
|
||||
```
|
||||
|
||||
With body:
|
||||
|
||||
```
|
||||
feat: add streaming response support
|
||||
|
||||
Large responses were causing memory issues in production.
|
||||
Streaming allows processing chunks incrementally.
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
- NEVER commit files that may contain secrets
|
||||
- NEVER use `git commit --amend` unless the user explicitly requests it
|
||||
- NEVER use `--no-verify` to skip hooks
|
||||
- If the pre-commit hook fails, fix the issues and create a NEW commit
|
||||
- If there are no changes to commit, inform the user and stop
|
||||
- Use a HEREDOC to pass the commit message to ensure proper formatting
|
||||
|
||||
## Execute
|
||||
|
||||
Run the git commands to analyze, stage, and commit the changes now.
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
description: Continue implementing a spec from a previous session
|
||||
argument-hint: <spec-file-path>
|
||||
---
|
||||
|
||||
You are continuing implementation of a specification that was started in a previous session. Work autonomously until the feature is complete and tests pass.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Read the spec** at `$ARGUMENTS`
|
||||
2. **Read CODE_STYLE.md** for formatting conventions
|
||||
3. **Assess current state**:
|
||||
- Check git status for uncommitted changes
|
||||
- Run tests to see what's passing/failing (if E2E tests exist)
|
||||
- Review any existing implementation
|
||||
4. **Determine what remains** by comparing the spec to the current state
|
||||
5. **Plan remaining work** using TodoWrite
|
||||
6. **Continue implementing** until complete
|
||||
|
||||
## Assessing Current State
|
||||
|
||||
Run these commands to understand where the previous session left off:
|
||||
|
||||
```bash
|
||||
git status # See uncommitted changes
|
||||
git log --oneline -10 # See recent commits
|
||||
npm run typecheck -w @documenso/remix # Check for type errors
|
||||
npm run lint:fix # Check for linting issues
|
||||
```
|
||||
|
||||
Review the code that's already been written to understand:
|
||||
|
||||
- What's already implemented
|
||||
- What's partially done
|
||||
- What's not started yet
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### During Implementation
|
||||
|
||||
- Follow CODE_STYLE.md strictly (2-space indent, double quotes, braces always, etc.)
|
||||
- Follow workspace rules for TypeScript, React, TRPC patterns, and Remix conventions
|
||||
- Mark todos complete as you finish each task
|
||||
- Commit logical chunks of work
|
||||
|
||||
### Code Quality
|
||||
|
||||
- No stubbed implementations
|
||||
- Handle edge cases and error conditions
|
||||
- Include descriptive error messages with context
|
||||
- Use async/await for all I/O operations
|
||||
- Use AppError class when throwing errors
|
||||
- Use Zod for validation and react-hook-form for forms
|
||||
|
||||
### Testing
|
||||
|
||||
**Important**: E2E tests are time-consuming. Only write tests for non-trivial functionality.
|
||||
|
||||
- Write E2E tests in `packages/app-tests/e2e/` using Playwright
|
||||
- Test critical user flows and edge cases
|
||||
- Follow existing E2E test patterns in the codebase
|
||||
- Use descriptive test names that explain what is being tested
|
||||
- Skip tests for trivial changes (simple UI tweaks, minor refactors, etc.)
|
||||
|
||||
## Autonomous Workflow
|
||||
|
||||
Work continuously through these steps:
|
||||
|
||||
1. **Implement** - Write the code for the current task
|
||||
2. **Typecheck** - Run `npm run typecheck -w @documenso/remix` to verify types
|
||||
3. **Lint** - Run `npm run lint:fix` to fix linting issues
|
||||
4. **Test** - If non-trivial, run E2E tests: `npm run test:dev -w @documenso/app-tests`
|
||||
5. **Fix** - If tests fail, fix and re-run
|
||||
6. **Repeat** - Move to next task
|
||||
|
||||
## Stopping Conditions
|
||||
|
||||
**Stop and report success when:**
|
||||
|
||||
- All spec requirements are implemented
|
||||
- Typecheck passes
|
||||
- Lint passes
|
||||
- E2E tests pass (if written for non-trivial functionality)
|
||||
|
||||
**Stop and ask for help when:**
|
||||
|
||||
- The spec is ambiguous and you need clarification
|
||||
- You encounter a blocking issue you cannot resolve
|
||||
- You need to make a decision that significantly deviates from the spec
|
||||
- External dependencies are missing
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Type checking
|
||||
npm run typecheck -w @documenso/remix
|
||||
|
||||
# Linting
|
||||
npm run lint:fix
|
||||
|
||||
# E2E Tests (only for non-trivial work)
|
||||
npm run test:dev -w @documenso/app-tests # Run E2E tests in dev mode
|
||||
npm run test-ui:dev -w @documenso/app-tests # Run E2E tests with UI
|
||||
npm run test:e2e # Run full E2E test suite
|
||||
|
||||
# Development
|
||||
npm run dev # Start dev server
|
||||
```
|
||||
|
||||
## Begin
|
||||
|
||||
Read the spec file and CODE_STYLE.md, assess the current implementation state, then continue where the previous session left off. Use TodoWrite to track your progress throughout.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
description: Create a new justification file in .agents/justifications/
|
||||
argument-hint: <justification-slug> [content]
|
||||
---
|
||||
|
||||
You are creating a new justification file in the `.agents/justifications/` directory.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
|
||||
2. **Gather content** - Collect or generate the justification content
|
||||
3. **Create the file** - Use the create-justification script to generate the file
|
||||
|
||||
## Usage
|
||||
|
||||
The script will automatically:
|
||||
- Generate a unique three-word ID (e.g., `swift-emerald-river`)
|
||||
- Create frontmatter with current date and formatted title
|
||||
- Save the file as `{id}-{slug}.md` in `.agents/justifications/`
|
||||
|
||||
## Creating the File
|
||||
|
||||
### Option 1: Direct Content
|
||||
|
||||
If you have the content ready, run:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-justification.ts "$ARGUMENTS" "Your justification content here"
|
||||
```
|
||||
|
||||
### Option 2: Multi-line Content (Heredoc)
|
||||
|
||||
For multi-line content, use heredoc:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-justification.ts "$ARGUMENTS" << HEREDOC
|
||||
Your multi-line
|
||||
justification content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
### Option 3: Pipe Content
|
||||
|
||||
You can also pipe content:
|
||||
|
||||
```bash
|
||||
echo "Your content" | npx tsx scripts/create-justification.ts "$ARGUMENTS"
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
The created file will have:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Justification Title
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
The title is automatically formatted from the slug (e.g., `architecture-decision` → `Architecture Decision`).
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Use descriptive slugs in kebab-case (e.g., `tech-stack-choice`, `api-design-rationale`)
|
||||
- Include clear reasoning and context for the decision
|
||||
- The unique ID ensures no filename conflicts
|
||||
- Files are automatically dated for organization
|
||||
|
||||
## Begin
|
||||
|
||||
Create a justification file using the slug from `$ARGUMENTS` and appropriate content documenting the reasoning or justification.
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
description: Create a new plan file in .agents/plans/
|
||||
argument-hint: <plan-slug> [content]
|
||||
---
|
||||
|
||||
You are creating a new plan file in the `.agents/plans/` directory.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
|
||||
2. **Gather content** - Collect or generate the plan content
|
||||
3. **Create the file** - Use the create-plan script to generate the file
|
||||
|
||||
## Usage
|
||||
|
||||
The script will automatically:
|
||||
|
||||
- Generate a unique three-word ID (e.g., `happy-blue-moon`)
|
||||
- Create frontmatter with current date and formatted title
|
||||
- Save the file as `{id}-{slug}.md` in `.agents/plans/`
|
||||
|
||||
## Creating the File
|
||||
|
||||
### Option 1: Direct Content
|
||||
|
||||
If you have the content ready, run:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-plan.ts "$ARGUMENTS" "Your plan content here"
|
||||
```
|
||||
|
||||
### Option 2: Multi-line Content (Heredoc)
|
||||
|
||||
For multi-line content, use heredoc:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-plan.ts "$ARGUMENTS" << HEREDOC
|
||||
Your multi-line
|
||||
plan content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
### Option 3: Pipe Content
|
||||
|
||||
You can also pipe content:
|
||||
|
||||
```bash
|
||||
echo "Your content" | npx tsx scripts/create-plan.ts "$ARGUMENTS"
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
The created file will have:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Plan Title
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
The title is automatically formatted from the slug (e.g., `my-feature` → `My Feature`).
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Use descriptive slugs in kebab-case (e.g., `user-authentication`, `api-integration`)
|
||||
- Include clear, actionable plan content
|
||||
- The unique ID ensures no filename conflicts
|
||||
- Files are automatically dated for organization
|
||||
|
||||
## Begin
|
||||
|
||||
Create a plan file using the slug from `$ARGUMENTS` and appropriate content for the planning task.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
description: Create a new scratch file in .agents/scratches/
|
||||
argument-hint: <scratch-slug> [content]
|
||||
---
|
||||
|
||||
You are creating a new scratch file in the `.agents/scratches/` directory.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
|
||||
2. **Gather content** - Collect or generate the scratch content
|
||||
3. **Create the file** - Use the create-scratch script to generate the file
|
||||
|
||||
## Usage
|
||||
|
||||
The script will automatically:
|
||||
- Generate a unique three-word ID (e.g., `calm-teal-cloud`)
|
||||
- Create frontmatter with current date and formatted title
|
||||
- Save the file as `{id}-{slug}.md` in `.agents/scratches/`
|
||||
|
||||
## Creating the File
|
||||
|
||||
### Option 1: Direct Content
|
||||
|
||||
If you have the content ready, run:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-scratch.ts "$ARGUMENTS" "Your scratch content here"
|
||||
```
|
||||
|
||||
### Option 2: Multi-line Content (Heredoc)
|
||||
|
||||
For multi-line content, use heredoc:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-scratch.ts "$ARGUMENTS" << HEREDOC
|
||||
Your multi-line
|
||||
scratch content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
### Option 3: Pipe Content
|
||||
|
||||
You can also pipe content:
|
||||
|
||||
```bash
|
||||
echo "Your content" | npx tsx scripts/create-scratch.ts "$ARGUMENTS"
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
The created file will have:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Scratch Title
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
The title is automatically formatted from the slug (e.g., `quick-notes` → `Quick Notes`).
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Use descriptive slugs in kebab-case (e.g., `exploration-ideas`, `temporary-notes`)
|
||||
- Scratch files are for temporary notes, explorations, or ideas
|
||||
- The unique ID ensures no filename conflicts
|
||||
- Files are automatically dated for organization
|
||||
|
||||
## Begin
|
||||
|
||||
Create a scratch file using the slug from `$ARGUMENTS` and appropriate content for notes or exploration.
|
||||
@@ -0,0 +1,201 @@
|
||||
---
|
||||
description: Generate MDX documentation for a module or feature
|
||||
argument-hint: <module-path-or-feature>
|
||||
---
|
||||
|
||||
You are creating proper MDX documentation for a module or feature in Documenso using Nextra.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Identify the scope** - What does `$ARGUMENTS` refer to? (file, directory, or feature name)
|
||||
2. **Read the source code** - Understand the public API, types, and behavior
|
||||
3. **Read existing docs** - Check if there's documentation to update or reference
|
||||
4. **Write comprehensive documentation** - Create or update MDX docs in the appropriate location
|
||||
5. **Update navigation** - Add entry to `_meta.js` if creating a new page
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
Create documentation in the appropriate location:
|
||||
|
||||
- **Developer docs**: `apps/documentation/pages/developers/`
|
||||
- **User docs**: `apps/documentation/pages/users/`
|
||||
|
||||
### File Format
|
||||
|
||||
All documentation files must be `.mdx` files with frontmatter:
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Page Title
|
||||
description: Brief description for SEO and meta tags
|
||||
---
|
||||
|
||||
# Page Title
|
||||
|
||||
Content starts here...
|
||||
```
|
||||
|
||||
### Navigation
|
||||
|
||||
Each directory should have a `_meta.js` file that defines the navigation structure:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
index: 'Introduction',
|
||||
'feature-name': 'Feature Name',
|
||||
'another-feature': 'Another Feature',
|
||||
};
|
||||
```
|
||||
|
||||
If creating a new page, add it to the appropriate `_meta.js` file.
|
||||
|
||||
### Documentation Format
|
||||
|
||||
````mdx
|
||||
---
|
||||
title: <Module|Feature Name>
|
||||
description: Brief description of what this does and when to use it
|
||||
---
|
||||
|
||||
# <Module|Feature Name>
|
||||
|
||||
Brief description of what this module/feature does and when to use it.
|
||||
|
||||
## Installation
|
||||
|
||||
If there are specific packages or imports needed:
|
||||
|
||||
```bash
|
||||
npm install @documenso/package-name
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```jsx
|
||||
// Minimal working example
|
||||
import { Component } from '@documenso/package';
|
||||
|
||||
const Example = () => {
|
||||
return <Component />;
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Component/Function Name
|
||||
|
||||
Description of what it does.
|
||||
|
||||
#### Props/Parameters
|
||||
|
||||
| Prop/Param | Type | Description |
|
||||
| ---------- | -------------------- | ------------------------- |
|
||||
| prop | `string` | Description of the prop |
|
||||
| optional | `boolean` (optional) | Optional prop description |
|
||||
|
||||
#### Example
|
||||
|
||||
```jsx
|
||||
import { Component } from '@documenso/package';
|
||||
|
||||
<Component prop="value" optional={true} />;
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
#### `TypeName`
|
||||
|
||||
```typescript
|
||||
type TypeName = {
|
||||
property: string;
|
||||
optional?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Common Use Case
|
||||
|
||||
```jsx
|
||||
// Full working example
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
```jsx
|
||||
// More complex example
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [Link to related documentation](/developers/path)
|
||||
- [Another related page](/users/path)
|
||||
````
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Content Quality
|
||||
|
||||
- **Be accurate** - Verify behavior by reading the code
|
||||
- **Be complete** - Document all public API surface
|
||||
- **Be practical** - Include real, working examples
|
||||
- **Be concise** - Don't over-explain obvious things
|
||||
- **Be user-focused** - Write for the target audience (developers or users)
|
||||
|
||||
### Code Examples
|
||||
|
||||
- Use appropriate language tags: `jsx`, `tsx`, `typescript`, `bash`, `json`
|
||||
- Show imports when not obvious
|
||||
- Include expected output in comments where helpful
|
||||
- Progress from simple to complex
|
||||
- Use real examples from the codebase when possible
|
||||
|
||||
### Formatting
|
||||
|
||||
- Always include frontmatter with `title` and `description`
|
||||
- Use proper markdown headers (h1 for title, h2 for sections)
|
||||
- Use tables for props/parameters documentation (matching existing style)
|
||||
- Use code fences with appropriate language tags
|
||||
- Use Nextra components when appropriate:
|
||||
- `<Callout type="info">` for notes
|
||||
- `<Steps>` for step-by-step instructions
|
||||
- Use relative links for internal documentation (e.g., `/developers/embedding/react`)
|
||||
|
||||
### Nextra Components
|
||||
|
||||
You can import and use Nextra components:
|
||||
|
||||
```jsx
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
<Callout type="info">
|
||||
This is an informational note.
|
||||
</Callout>
|
||||
|
||||
<Steps>
|
||||
<Steps.Step>First step</Steps.Step>
|
||||
<Steps.Step>Second step</Steps.Step>
|
||||
</Steps>
|
||||
```
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Include types inline so docs don't get stale
|
||||
- Reference source file locations for complex behavior
|
||||
- Keep examples up-to-date with the codebase
|
||||
- Update `_meta.js` when adding new pages
|
||||
|
||||
## Process
|
||||
|
||||
1. **Explore the code** - Read source files to understand the API
|
||||
2. **Identify the audience** - Is this for developers or users?
|
||||
3. **Check existing docs** - Look for similar pages to match style
|
||||
4. **Draft the structure** - Outline sections before writing
|
||||
5. **Write content** - Fill in each section with frontmatter
|
||||
6. **Add examples** - Create working code samples
|
||||
7. **Update navigation** - Add to `_meta.js` if needed
|
||||
8. **Review** - Read through for clarity and accuracy
|
||||
|
||||
## Begin
|
||||
|
||||
Analyze `$ARGUMENTS`, read the relevant source code, check existing documentation patterns, and create comprehensive MDX documentation following the Documenso documentation style.
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
description: Implement a spec from the plans directory
|
||||
argument-hint: <spec-file-path>
|
||||
---
|
||||
|
||||
You are implementing a specification from the `.agents/plans/` directory. Work autonomously until the feature is complete and tests pass.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Read the spec** at `$ARGUMENTS`
|
||||
2. **Read CODE_STYLE.md** for formatting conventions
|
||||
3. **Plan the implementation** using the TodoWrite tool to break down the work
|
||||
4. **Implement the feature** following the spec and code style
|
||||
5. **Write E2E tests** only for non-trivial functionality (E2E tests are time-consuming)
|
||||
6. **Run tests** and fix any failures
|
||||
7. **Run typecheck and lint** and fix any issues
|
||||
|
||||
## Implementation Guidelines
|
||||
|
||||
### Before Coding
|
||||
|
||||
- Understand the spec's goals and scope
|
||||
- Identify the desired API from usage examples in the spec
|
||||
- Review related existing code to understand patterns
|
||||
- Break the work into discrete tasks using TodoWrite
|
||||
|
||||
### During Implementation
|
||||
|
||||
- Follow CODE_STYLE.md strictly (2-space indent, double quotes, braces always, etc.)
|
||||
- Follow workspace rules for TypeScript, React, TRPC patterns, and Remix conventions
|
||||
- Mark todos complete as you finish each task
|
||||
- Commit logical chunks of work
|
||||
|
||||
### Code Quality
|
||||
|
||||
- No stubbed implementations
|
||||
- Handle edge cases and error conditions
|
||||
- Include descriptive error messages with context
|
||||
- Use async/await for all I/O operations
|
||||
- Use AppError class when throwing errors
|
||||
- Use Zod for validation and react-hook-form for forms
|
||||
|
||||
### Testing
|
||||
|
||||
**Important**: E2E tests are time-consuming. Only write tests for non-trivial functionality.
|
||||
|
||||
- Write E2E tests in `packages/app-tests/e2e/` using Playwright
|
||||
- Test critical user flows and edge cases
|
||||
- Follow existing E2E test patterns in the codebase
|
||||
- Use descriptive test names that explain what is being tested
|
||||
- Skip tests for trivial changes (simple UI tweaks, minor refactors, etc.)
|
||||
|
||||
## Autonomous Workflow
|
||||
|
||||
Work continuously through these steps:
|
||||
|
||||
1. **Implement** - Write the code for the current task
|
||||
2. **Typecheck** - Run `npm run typecheck -w @documenso/remix` to verify types
|
||||
3. **Lint** - Run `npm run lint:fix` to fix linting issues
|
||||
4. **Test** - If non-trivial, run E2E tests: `npm run test:dev -w @documenso/app-tests`
|
||||
5. **Fix** - If tests fail, fix and re-run
|
||||
6. **Repeat** - Move to next task
|
||||
|
||||
## Stopping Conditions
|
||||
|
||||
**Stop and report success when:**
|
||||
|
||||
- All spec requirements are implemented
|
||||
- Typecheck passes
|
||||
- Lint passes
|
||||
- E2E tests pass (if written for non-trivial functionality)
|
||||
|
||||
**Stop and ask for help when:**
|
||||
|
||||
- The spec is ambiguous and you need clarification
|
||||
- You encounter a blocking issue you cannot resolve
|
||||
- You need to make a decision that significantly deviates from the spec
|
||||
- External dependencies are missing
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Type checking
|
||||
npm run typecheck -w @documenso/remix
|
||||
|
||||
# Linting
|
||||
npm run lint:fix
|
||||
|
||||
# E2E Tests (only for non-trivial work)
|
||||
npm run test:dev -w @documenso/app-tests # Run E2E tests in dev mode
|
||||
npm run test-ui:dev -w @documenso/app-tests # Run E2E tests with UI
|
||||
npm run test:e2e # Run full E2E test suite
|
||||
|
||||
# Development
|
||||
npm run dev # Start dev server
|
||||
```
|
||||
|
||||
## Begin
|
||||
|
||||
Read the spec file and CODE_STYLE.md, then start implementing. Use TodoWrite to track your progress throughout.
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
description: Deep-dive interview to flesh out a spec or design document
|
||||
agent: build
|
||||
argument-hint: <file-path>
|
||||
---
|
||||
|
||||
You are conducting a thorough interview to help flesh out and complete a specification or design document.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Read the document** at `$ARGUMENTS`
|
||||
2. **Analyze it deeply** - identify gaps, ambiguities, unexplored edge cases, and areas needing clarification
|
||||
3. **Interview the user** by providing a question with some pre-determined options
|
||||
4. **Write the completed spec** back to the file when the interview is complete
|
||||
|
||||
## Interview Guidelines
|
||||
|
||||
### Question Quality
|
||||
- Ask **non-obvious, insightful questions** - avoid surface-level queries
|
||||
- Focus on: technical implementation details, architectural decisions, edge cases, error handling, UX implications, security considerations, performance tradeoffs, integration points, migration strategies, rollback plans
|
||||
- Each question should reveal something that would otherwise be missed
|
||||
- Challenge assumptions embedded in the document
|
||||
- Explore second and third-order consequences of design decisions
|
||||
- Use the Web Search and other tools where required to ground questions (e.g. package recommendations)
|
||||
|
||||
### Question Strategy
|
||||
- Start by identifying the 3-5 most critical unknowns or ambiguities
|
||||
- Use the AskUserQuestion tool with well-crafted options that represent real tradeoffs
|
||||
- When appropriate, offer multiple valid approaches with their pros/cons as options
|
||||
- Don't ask about things that are already clearly specified
|
||||
- Probe deeper when answers reveal new areas of uncertainty
|
||||
|
||||
### Topics to Explore (as relevant)
|
||||
- **Technical**: Data models, API contracts, state management, concurrency, caching, validation
|
||||
- **UX**: Error states, loading states, empty states, edge cases, accessibility, mobile considerations
|
||||
- **Operations**: Deployment, monitoring, alerting, debugging, logging, feature flags
|
||||
- **Security**: Auth, authz, input validation, rate limiting, audit trails
|
||||
- **Scale**: Performance bottlenecks, data growth, traffic spikes, graceful degradation
|
||||
- **Integration**: Dependencies, backwards compatibility, versioning, migration path
|
||||
- **Failure modes**: What happens when X fails? How do we recover? What's the blast radius?
|
||||
|
||||
### Interview Flow
|
||||
1. Ask 2-4 questions at a time (use multiple questions in one when they're related)
|
||||
2. After each round, incorporate answers and identify follow-up questions
|
||||
3. Continue until all critical areas are addressed
|
||||
4. Signal when you believe the interview is complete, but offer to go deeper
|
||||
|
||||
## Output
|
||||
|
||||
When the interview is complete:
|
||||
1. Synthesize all gathered information
|
||||
2. Rewrite/expand the original document with the new details
|
||||
3. Preserve the document's original structure where sensible, but reorganize if needed
|
||||
4. Add new sections for areas that weren't originally covered
|
||||
5. Write the completed spec back to `$ARGUMENTS`
|
||||
|
||||
Begin by reading the file and identifying your first set of deep questions.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: create-justification
|
||||
description: Create a new justification file in .agents/justifications/ with a unique three-word ID, frontmatter, and formatted title
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
audience: agents
|
||||
workflow: decision-making
|
||||
---
|
||||
|
||||
## What I do
|
||||
|
||||
I help you create new justification files in the `.agents/justifications/` directory. Each justification file gets:
|
||||
|
||||
- A unique three-word identifier (e.g., `swift-emerald-river`)
|
||||
- Frontmatter with the current date and formatted title
|
||||
- Content you provide
|
||||
|
||||
## How to use
|
||||
|
||||
Run the script with a slug and content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-justification.ts "decision-name" "Justification content here"
|
||||
```
|
||||
|
||||
Or use heredoc for multi-line content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-justification.ts "decision-name" << HEREDOC
|
||||
Multi-line
|
||||
justification content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
## File format
|
||||
|
||||
Files are created as: `{three-word-id}-{slug}.md`
|
||||
|
||||
Example: `swift-emerald-river-decision-name.md`
|
||||
|
||||
The file includes frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Decision Name
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
## When to use me
|
||||
|
||||
Use this skill when you need to document the reasoning or justification for a decision, approach, or architectural choice. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: create-plan
|
||||
description: Create a new plan file in .agents/plans/ with a unique three-word ID, frontmatter, and formatted title
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
audience: agents
|
||||
workflow: planning
|
||||
---
|
||||
|
||||
## What I do
|
||||
|
||||
I help you create new plan files in the `.agents/plans/` directory. Each plan file gets:
|
||||
|
||||
- A unique three-word identifier (e.g., `happy-blue-moon`)
|
||||
- Frontmatter with the current date and formatted title
|
||||
- Content you provide
|
||||
|
||||
## How to use
|
||||
|
||||
Run the script with a slug and content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-plan.ts "feature-name" "Plan content here"
|
||||
```
|
||||
|
||||
Or use heredoc for multi-line content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-plan.ts "feature-name" << HEREDOC
|
||||
Multi-line
|
||||
plan content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
## File format
|
||||
|
||||
Files are created as: `{three-word-id}-{slug}.md`
|
||||
|
||||
Example: `happy-blue-moon-feature-name.md`
|
||||
|
||||
The file includes frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Feature Name
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
## When to use me
|
||||
|
||||
Use this skill when you need to create a new plan document for a feature, task, or project. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
name: create-scratch
|
||||
description: Create a new scratch file in .agents/scratches/ with a unique three-word ID, frontmatter, and formatted title
|
||||
license: MIT
|
||||
compatibility: opencode
|
||||
metadata:
|
||||
audience: agents
|
||||
workflow: exploration
|
||||
---
|
||||
|
||||
## What I do
|
||||
|
||||
I help you create new scratch files in the `.agents/scratches/` directory. Each scratch file gets:
|
||||
|
||||
- A unique three-word identifier (e.g., `calm-teal-cloud`)
|
||||
- Frontmatter with the current date and formatted title
|
||||
- Content you provide
|
||||
|
||||
## How to use
|
||||
|
||||
Run the script with a slug and content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-scratch.ts "note-name" "Scratch content here"
|
||||
```
|
||||
|
||||
Or use heredoc for multi-line content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-scratch.ts "note-name" << HEREDOC
|
||||
Multi-line
|
||||
scratch content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
## File format
|
||||
|
||||
Files are created as: `{three-word-id}-{slug}.md`
|
||||
|
||||
Example: `calm-teal-cloud-note-name.md`
|
||||
|
||||
The file includes frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Note Name
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
## When to use me
|
||||
|
||||
Use this skill when you need to create a temporary note, exploration document, or scratch pad for ideas. The unique ID ensures no filename conflicts, and the frontmatter provides metadata for organization.
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/agent-browser
|
||||
@@ -15,3 +15,6 @@ packages/lib/translations/**/*.js
|
||||
.prettierignore
|
||||
.DS_Store
|
||||
.eslintignore
|
||||
|
||||
# Docs MDX - Prettier strips indentation from code blocks inside components
|
||||
apps/docs/content/**/*.mdx
|
||||
|
||||
Vendored
+2
-1
@@ -17,5 +17,6 @@
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
}
|
||||
},
|
||||
"prisma.pinToPrisma6": true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# Agent Guidelines for Documenso
|
||||
|
||||
## Build/Test/Lint Commands
|
||||
|
||||
- `npm run build` - Build all packages
|
||||
- `npm run lint` - Lint all packages
|
||||
- `npm run lint:fix` - Auto-fix linting issues
|
||||
- `npm run test:e2e` - Run E2E tests with Playwright
|
||||
- `npm run test:dev -w @documenso/app-tests` - Run single E2E test in dev mode
|
||||
- `npm run test-ui:dev -w @documenso/app-tests` - Run E2E tests with UI
|
||||
- `npm run format` - Format code with Prettier
|
||||
- `npm run dev` - Start development server for Remix app
|
||||
|
||||
**Important:** Do not run `npm run build` to verify changes unless explicitly asked. Builds take a long time (~2 minutes). Use `npx tsc --noEmit` for type checking specific packages if needed.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
- Use TypeScript for all code; prefer `type` over `interface`
|
||||
- Use functional components with `const Component = () => {}`
|
||||
- Never use classes; prefer functional/declarative patterns
|
||||
- Use descriptive variable names with auxiliary verbs (isLoading, hasError)
|
||||
- Directory names: lowercase with dashes (auth-wizard)
|
||||
- Use named exports for components
|
||||
- Never use 'use client' directive
|
||||
- Never use 1-line if statements
|
||||
- Structure files: exported component, subcomponents, helpers, static content, types
|
||||
|
||||
## Error Handling & Validation
|
||||
|
||||
- Use custom AppError class when throwing errors
|
||||
- When catching errors on the frontend use `const error = AppError.parse(error)` to get the error code
|
||||
- Use early returns and guard clauses
|
||||
- Use Zod for form validation and react-hook-form for forms
|
||||
- Use error boundaries for unexpected errors
|
||||
|
||||
## UI & Styling
|
||||
|
||||
- Use Shadcn UI, Radix, and Tailwind CSS with mobile-first approach
|
||||
- Use `<Form>` `<FormItem>` elements with fieldset having `:disabled` attribute when loading
|
||||
- Use Lucide icons with longhand names (HomeIcon vs Home)
|
||||
|
||||
## TRPC Routes
|
||||
|
||||
- Each route in own file: `routers/teams/create-team.ts`
|
||||
- Associated types file: `routers/teams/create-team.types.ts`
|
||||
- Request/response schemas: `Z[RouteName]RequestSchema`, `Z[RouteName]ResponseSchema`
|
||||
- Only use GET and POST methods in OpenAPI meta
|
||||
- Deconstruct input argument on its own line
|
||||
- Prefer route names such as get/getMany/find/create/update/delete
|
||||
- "create" routes request schema should have the ID and data in the top level
|
||||
- "update" routes request schema should have the ID in the top level and the data in a nested "data" object
|
||||
|
||||
## Translations & Remix
|
||||
|
||||
- Use `<Trans>string</Trans>` for JSX translations from `@lingui/react/macro`
|
||||
- Use `t\`string\`` macro for TypeScript translations
|
||||
- Use `(params: Route.Params)` and `(loaderData: Route.LoaderData)` for routes
|
||||
- Directly return data from loaders, don't use `json()`
|
||||
- Use `superLoaderJson` when sending complex data through loaders such as dates or prisma decimals
|
||||
+359
@@ -0,0 +1,359 @@
|
||||
# Documenso Architecture
|
||||
|
||||
This document provides a high-level overview of the Documenso codebase to help humans and agents understand how the application is structured.
|
||||
|
||||
## Overview
|
||||
|
||||
Documenso is an open-source document signing platform built as a **monorepo** using npm workspaces and Turborepo. The application enables users to create, send, and sign documents electronically.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Remix App (Hono Server) │
|
||||
│ apps/remix │
|
||||
├─────────────┬─────────────┬─────────────┬─────────────┬─────────────────────┤
|
||||
│ /api/v1/* │ /api/v2/* │ /api/trpc/* │ /api/jobs/* │ React Router UI │
|
||||
│ (ts-rest) │ (tRPC) │ (tRPC) │ (Jobs API) │ │
|
||||
├─────────────┴─────────────┴─────────────┴─────────────┴─────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │
|
||||
│ │ @api │ │ @trpc │ │ @lib │ │ @email │ │ @signing │ │
|
||||
│ │ (REST) │ │ (RPC) │ │ (CORE) │ │ │ │ │ │
|
||||
│ └─────────┘ └─────────┘ └────┬────┘ └─────────┘ └─────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────────────┼──────────────────┐ │
|
||||
│ │ │ │ │
|
||||
│ ┌────▼────┐ ┌─────▼─────┐ ┌─────▼─────┐ │
|
||||
│ │ Storage │ │ Jobs │ │ PDF │ │
|
||||
│ │Provider │ │ Provider │ │ Signing │ │
|
||||
│ └────┬────┘ └─────┬─────┘ └─────┬─────┘ │
|
||||
│ │ │ │ │
|
||||
└──────────────┼──────────────────┼──────────────────┼────────────────────────┘
|
||||
│ │ │
|
||||
┌──────┴──────┐ ┌──────┴──────┐ ┌──────┴──────┐
|
||||
│ Database │ │ Inngest/ │ │ Google KMS/ │
|
||||
│ S3 │ │ Local │ │ Local │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
## Monorepo Structure
|
||||
|
||||
### Applications (`apps/`)
|
||||
|
||||
| Package | Description | Port |
|
||||
| -------------------------- | -------------------------------------------------------- | ---- |
|
||||
| `@documenso/remix` | Main application - React Router (Remix) with Hono server | 3000 |
|
||||
| `@documenso/documentation` | Documentation site (Next.js + Nextra) | 3002 |
|
||||
| `@documenso/openpage-api` | Public analytics API | 3003 |
|
||||
|
||||
### Core Packages (`packages/`)
|
||||
|
||||
| Package | Description |
|
||||
| -------------------- | --------------------------------------------------------- |
|
||||
| `@documenso/lib` | Core business logic (server-only, client-only, universal) |
|
||||
| `@documenso/trpc` | tRPC API layer with OpenAPI support (API V2) |
|
||||
| `@documenso/api` | REST API layer using ts-rest (API V1) |
|
||||
| `@documenso/prisma` | Database layer (Prisma ORM + Kysely) |
|
||||
| `@documenso/ui` | UI component library (Shadcn + Radix + Tailwind) |
|
||||
| `@documenso/email` | Email templates and mailer (React Email) |
|
||||
| `@documenso/auth` | Authentication (OAuth via Arctic, WebAuthn/Passkeys) |
|
||||
| `@documenso/signing` | PDF signing (Local P12, Google Cloud KMS) |
|
||||
| `@documenso/ee` | Enterprise Edition features |
|
||||
| `@documenso/assets` | Static assets |
|
||||
|
||||
### Supporting Packages
|
||||
|
||||
| Package | Description |
|
||||
| ---------------------------- | ------------------------- |
|
||||
| `@documenso/app-tests` | E2E tests (Playwright) |
|
||||
| `@documenso/eslint-config` | Shared ESLint config |
|
||||
| `@documenso/prettier-config` | Shared Prettier config |
|
||||
| `@documenso/tailwind-config` | Shared Tailwind config |
|
||||
| `@documenso/tsconfig` | Shared TypeScript configs |
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Category | Technology |
|
||||
| -------- | --------------------------------- |
|
||||
| Frontend | React 18, React Router v7 (Remix) |
|
||||
| Server | Hono |
|
||||
| Database | PostgreSQL 15, Prisma, Kysely |
|
||||
| API | tRPC, ts-rest, OpenAPI |
|
||||
| Styling | Tailwind CSS, Radix UI, Shadcn UI |
|
||||
| Auth | Arctic (OAuth), WebAuthn/Passkeys |
|
||||
| Email | React Email, Nodemailer |
|
||||
| Jobs | Inngest / Local |
|
||||
| Storage | S3-compatible / Database |
|
||||
| PDF | @libpdf/core, pdfjs-dist |
|
||||
| i18n | Lingui |
|
||||
| Build | Turborepo, Vite |
|
||||
| Testing | Playwright |
|
||||
|
||||
## API Architecture
|
||||
|
||||
### API V1 (Deprecated)
|
||||
|
||||
- **Location**: `packages/api/v1/`
|
||||
- **Framework**: ts-rest (contract-based REST)
|
||||
- **Mount**: `/api/v1/*`
|
||||
- **Auth**: API Token (Bearer header)
|
||||
- **Status**: Deprecated but maintained
|
||||
|
||||
**Routes** (RESTful pattern):
|
||||
|
||||
- `GET/POST/DELETE /api/v1/documents/*`
|
||||
- `GET/POST/DELETE /api/v1/templates/*`
|
||||
- Recipients and fields nested under documents
|
||||
|
||||
### API V2 (Current)
|
||||
|
||||
- **Location**: `packages/trpc/server/`
|
||||
- **Framework**: tRPC with trpc-to-openapi
|
||||
- **Mount**: `/api/v2/*`, `/api/v2-beta/*`
|
||||
- **Auth**: API Token or Session Cookie
|
||||
- **Status**: Active
|
||||
|
||||
**Routes** (action-based pattern):
|
||||
|
||||
- `GET/POST /api/v2/document/*` - Document operations
|
||||
- `GET/POST /api/v2/template/*` - Template operations
|
||||
- `GET/POST /api/v2/envelope/*` - Envelope operations (multi-document)
|
||||
- `GET/POST /api/v2/folder/*` - Folder management
|
||||
|
||||
**Route Organization**:
|
||||
|
||||
```
|
||||
packages/trpc/server/
|
||||
├── document-router/
|
||||
│ ├── get-document.ts
|
||||
│ ├── get-document.types.ts
|
||||
│ └── ...
|
||||
├── template-router/
|
||||
├── envelope-router/
|
||||
├── recipient-router/
|
||||
├── field-router/
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Internal tRPC API
|
||||
|
||||
- **Mount**: `/api/trpc/*`
|
||||
- **Usage**: Frontend-to-backend communication
|
||||
- **Auth**: Session-based
|
||||
|
||||
## Background Jobs
|
||||
|
||||
Jobs handle async operations like email sending, document sealing, and webhooks.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌───────────────────────────────────────┐
|
||||
│ triggerJob() │────▶│ Job Provider │
|
||||
│ │ │ ┌─────────────┬─────────────────┐ │
|
||||
│ - name │ │ │ Inngest │ Local │ │
|
||||
│ - payload │ │ │ (Cloud) │ (Database) │ │
|
||||
└─────────────────┘ │ └─────────────┴─────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────┐ │
|
||||
│ │ Job Handler │ │
|
||||
│ │ (async processing) │ │
|
||||
│ └─────────────────────┘ │
|
||||
└───────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Location
|
||||
|
||||
- `packages/lib/jobs/client/` - Provider implementations
|
||||
- `packages/lib/jobs/definitions/` - Job definitions
|
||||
|
||||
### Job Types
|
||||
|
||||
**Email Jobs**:
|
||||
|
||||
- `send.signing.requested.email` - Signing invitation
|
||||
- `send-confirmation-email` - Email verification
|
||||
- `send-recipient-signed-email` - Notify on signature
|
||||
- `send-rejection-emails` - Rejection notifications
|
||||
- `send-document-cancelled-emails` - Cancellation notices
|
||||
|
||||
**Internal Jobs**:
|
||||
|
||||
- `internal.seal-document` - Finalize signed documents
|
||||
- `internal.bulk-send-template` - Bulk document sending
|
||||
- `internal.execute-webhook` - External webhook calls
|
||||
|
||||
## Swappable Providers
|
||||
|
||||
The codebase uses a **strategy pattern** with `ts-pattern` for provider selection via environment variables.
|
||||
|
||||
### Storage Provider
|
||||
|
||||
Handles file uploads and downloads.
|
||||
|
||||
| Provider | Description | Env Value |
|
||||
| -------- | ------------------------------------ | ---------- |
|
||||
| Database | Store files as Base64 in DB | `database` |
|
||||
| S3 | S3-compatible storage (+ CloudFront) | `s3` |
|
||||
|
||||
**Config**: `NEXT_PUBLIC_UPLOAD_TRANSPORT`
|
||||
|
||||
**Location**: `packages/lib/universal/upload/`
|
||||
|
||||
### PDF Signing Provider
|
||||
|
||||
Cryptographically signs PDF documents.
|
||||
|
||||
| Provider | Description | Env Value |
|
||||
| ---------------- | -------------------- | ------------ |
|
||||
| Local | P12 certificate file | `local` |
|
||||
| Google Cloud HSM | Google Cloud KMS | `gcloud-hsm` |
|
||||
|
||||
**Config**: `NEXT_PRIVATE_SIGNING_TRANSPORT`
|
||||
|
||||
**Location**: `packages/signing/`
|
||||
|
||||
### Email Provider
|
||||
|
||||
Sends transactional emails.
|
||||
|
||||
| Provider | Description | Env Value |
|
||||
| ------------ | ------------------------------ | -------------- |
|
||||
| SMTP Auth | Standard SMTP with credentials | `smtp-auth` |
|
||||
| SMTP API | SMTP with API key | `smtp-api` |
|
||||
| Resend | Resend API | `resend` |
|
||||
| MailChannels | MailChannels API | `mailchannels` |
|
||||
|
||||
**Config**: `NEXT_PRIVATE_SMTP_TRANSPORT`
|
||||
|
||||
**Location**: `packages/email/mailer.ts`
|
||||
|
||||
### Background Jobs Provider
|
||||
|
||||
Processes async jobs.
|
||||
|
||||
| Provider | Description | Env Value |
|
||||
| -------- | --------------------- | ----------------- |
|
||||
| Local | Database-backed queue | `local` (default) |
|
||||
| Inngest | Managed cloud service | `inngest` |
|
||||
|
||||
**Config**: `NEXT_PRIVATE_JOBS_PROVIDER`
|
||||
|
||||
**Location**: `packages/lib/jobs/client/`
|
||||
|
||||
## Request Flow
|
||||
|
||||
### Web Application Request
|
||||
|
||||
```
|
||||
Browser
|
||||
│
|
||||
▼
|
||||
Hono Server (apps/remix/server/)
|
||||
│
|
||||
├──▶ /api/v1/* ──▶ ts-rest handlers (packages/api/)
|
||||
│
|
||||
├──▶ /api/v2/* ──▶ tRPC OpenAPI handlers (packages/trpc/)
|
||||
│
|
||||
├──▶ /api/trpc/* ──▶ tRPC handlers (packages/trpc/)
|
||||
│
|
||||
├──▶ /api/jobs/* ──▶ Job handlers (packages/lib/jobs/)
|
||||
│
|
||||
└──▶ /* ──▶ React Router (apps/remix/app/routes/)
|
||||
│
|
||||
▼
|
||||
React Components (packages/ui/)
|
||||
```
|
||||
|
||||
### Document Signing Flow
|
||||
|
||||
```
|
||||
1. Upload Document ──▶ Storage Provider (DB/S3)
|
||||
│
|
||||
2. Add Recipients ────────────────┤
|
||||
│
|
||||
3. Add Fields ────────────────────┤
|
||||
│
|
||||
4. Send Document ─────────────────┤
|
||||
│ │
|
||||
▼ │
|
||||
Email Job ──▶ Email Provider |
|
||||
│ |
|
||||
5. Recipient Signs ───────────────┤
|
||||
│ │
|
||||
▼ │
|
||||
seal-document Job │
|
||||
│ │
|
||||
▼ │
|
||||
Signing Provider ◀─────────────┘
|
||||
│
|
||||
▼
|
||||
Signed PDF ──▶ Storage Provider
|
||||
```
|
||||
|
||||
## Key Directories
|
||||
|
||||
```
|
||||
documenso/
|
||||
├── apps/
|
||||
│ └── remix/
|
||||
│ ├── app/
|
||||
│ │ └── routes/ # React Router routes
|
||||
│ │ ├── _authenticated+/ # Protected routes
|
||||
│ │ ├── _unauthenticated+/ # Public routes
|
||||
│ │ └── _recipient+/ # Signing routes
|
||||
│ └── server/
|
||||
│ ├── router.ts # Hono route mounting
|
||||
│ └── main.js # Entry point
|
||||
├── packages/
|
||||
│ ├── api/v1/ # API V1 (ts-rest)
|
||||
│ ├── trpc/server/ # API V2 + Internal (tRPC)
|
||||
│ ├── lib/
|
||||
│ │ ├── server-only/ # Server business logic
|
||||
│ │ ├── client-only/ # Client utilities
|
||||
│ │ ├── universal/ # Shared code
|
||||
│ │ └── jobs/ # Background jobs
|
||||
│ ├── prisma/ # Database schema & client
|
||||
│ ├── signing/ # PDF signing
|
||||
│ ├── email/ # Email templates
|
||||
│ └── ui/ # Component library
|
||||
└── docker/ # Docker configs
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Full setup (install, docker, migrate, seed, dev)
|
||||
npm run d
|
||||
|
||||
# Start development server
|
||||
npm run dev
|
||||
|
||||
# Database GUI
|
||||
npm run prisma:studio
|
||||
|
||||
# Type checking (faster than build)
|
||||
npx tsc --noEmit
|
||||
|
||||
# E2E tests
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Docker Services (Development)
|
||||
|
||||
| Service | Port |
|
||||
| --------------- | ---------- |
|
||||
| PostgreSQL | 54320 |
|
||||
| Inbucket (Mail) | 9000 |
|
||||
| MinIO (S3) | 9001, 9002 |
|
||||
|
||||
## Environment Variables Summary
|
||||
|
||||
| Variable | Purpose | Options |
|
||||
| -------------------------------- | ---------------- | ------------------------------------------------- |
|
||||
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Storage provider | `database`, `s3` |
|
||||
| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Signing provider | `local`, `gcloud-hsm` |
|
||||
| `NEXT_PRIVATE_SMTP_TRANSPORT` | Email provider | `smtp-auth`, `smtp-api`, `resend`, `mailchannels` |
|
||||
| `NEXT_PRIVATE_JOBS_PROVIDER` | Jobs provider | `local`, `inngest` |
|
||||
|
||||
See `.env.example` for the complete list of configuration options.
|
||||
+692
@@ -0,0 +1,692 @@
|
||||
# Documenso Code Style Guide
|
||||
|
||||
This document captures the code style, patterns, and conventions used in the Documenso codebase. It covers both enforceable rules and subjective "taste" elements that make our code consistent and maintainable.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [General Principles](#general-principles)
|
||||
2. [TypeScript Conventions](#typescript-conventions)
|
||||
3. [Imports & Dependencies](#imports--dependencies)
|
||||
4. [Functions & Methods](#functions--methods)
|
||||
5. [React & Components](#react--components)
|
||||
6. [Error Handling](#error-handling)
|
||||
7. [Async/Await Patterns](#asyncawait-patterns)
|
||||
8. [Whitespace & Formatting](#whitespace--formatting)
|
||||
9. [Naming Conventions](#naming-conventions)
|
||||
10. [Pattern Matching](#pattern-matching)
|
||||
11. [Database & Prisma](#database--prisma)
|
||||
12. [TRPC Patterns](#trpc-patterns)
|
||||
|
||||
---
|
||||
|
||||
## General Principles
|
||||
|
||||
- **Functional over Object-Oriented**: Prefer functional programming patterns over classes
|
||||
- **Explicit over Implicit**: Be explicit about types, return values, and error cases
|
||||
- **Early Returns**: Use guard clauses and early returns to reduce nesting
|
||||
- **Immutability**: Favor `const` over `let`; avoid mutation where possible
|
||||
|
||||
---
|
||||
|
||||
## TypeScript Conventions
|
||||
|
||||
### Type Definitions
|
||||
|
||||
```typescript
|
||||
// ✅ Prefer `type` over `interface`
|
||||
type CreateDocumentOptions = {
|
||||
templateId: number;
|
||||
userId: number;
|
||||
recipients: Recipient[];
|
||||
};
|
||||
|
||||
// ❌ Avoid interfaces unless absolutely necessary
|
||||
interface CreateDocumentOptions {
|
||||
templateId: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Type Imports
|
||||
|
||||
```typescript
|
||||
// ✅ Use `type` keyword for type-only imports
|
||||
import type { Document, Recipient } from '@prisma/client';
|
||||
import { DocumentStatus } from '@prisma/client';
|
||||
|
||||
// Types in function signatures
|
||||
export const findDocuments = async ({ userId, teamId }: FindDocumentsOptions) => {
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
### Inline Types for Function Parameters
|
||||
|
||||
```typescript
|
||||
// ✅ Extract inline types to named types
|
||||
type FinalRecipient = Pick<Recipient, 'name' | 'email' | 'role' | 'authOptions'> & {
|
||||
templateRecipientId: number;
|
||||
fields: Field[];
|
||||
};
|
||||
|
||||
const finalRecipients: FinalRecipient[] = [];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Imports & Dependencies
|
||||
|
||||
### Import Organization
|
||||
|
||||
Imports should be organized in the following order with blank lines between groups:
|
||||
|
||||
```typescript
|
||||
// 1. React imports
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
|
||||
// 2. Third-party library imports (alphabetically)
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Document, Recipient } from '@prisma/client';
|
||||
import { DocumentStatus, RecipientRole } from '@prisma/client';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
// 3. Internal package imports (from @documenso/*)
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
|
||||
// 4. Relative imports
|
||||
import { getTeamById } from '../team/get-team';
|
||||
import type { FindResultResponse } from './types';
|
||||
```
|
||||
|
||||
### Destructuring Imports
|
||||
|
||||
```typescript
|
||||
// ✅ Destructure specific exports
|
||||
// ✅ Use type imports for types
|
||||
import type { Document } from '@prisma/client';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Functions & Methods
|
||||
|
||||
### Arrow Functions
|
||||
|
||||
```typescript
|
||||
// ✅ Always use arrow functions for functions
|
||||
export const createDocument = async ({
|
||||
userId,
|
||||
title,
|
||||
}: CreateDocumentOptions) => {
|
||||
// ...
|
||||
};
|
||||
|
||||
// ✅ Callbacks and handlers
|
||||
const onSubmit = useCallback(async () => {
|
||||
// ...
|
||||
}, [dependencies]);
|
||||
|
||||
// ❌ Avoid regular function declarations
|
||||
function createDocument() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Function Parameters
|
||||
|
||||
```typescript
|
||||
// ✅ Use destructured object parameters for multiple params
|
||||
export const findDocuments = async ({
|
||||
userId,
|
||||
teamId,
|
||||
status = ExtendedDocumentStatus.ALL,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
}: FindDocumentsOptions) => {
|
||||
// ...
|
||||
};
|
||||
|
||||
// ✅ Destructure on separate line when needed
|
||||
const onFormSubmit = form.handleSubmit(onSubmit);
|
||||
|
||||
// ✅ Deconstruct nested properties explicitly
|
||||
const { user } = ctx;
|
||||
const { templateId } = input;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## React & Components
|
||||
|
||||
### Component Definition
|
||||
|
||||
```typescript
|
||||
// ✅ Use const with arrow function
|
||||
export const AddSignersFormPartial = ({
|
||||
documentFlow,
|
||||
recipients,
|
||||
fields,
|
||||
onSubmit,
|
||||
}: AddSignersFormProps) => {
|
||||
// ...
|
||||
};
|
||||
|
||||
// ❌ Never use classes
|
||||
class MyComponent extends React.Component {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Hooks
|
||||
|
||||
```typescript
|
||||
// ✅ Group related hooks together with blank line separation
|
||||
const { _ } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const { currentStep, totalSteps, previousStep } = useStep();
|
||||
|
||||
const form = useForm<TFormSchema>({
|
||||
resolver: zodResolver(ZFormSchema),
|
||||
defaultValues: {
|
||||
// ...
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Event Handlers
|
||||
|
||||
```typescript
|
||||
// ✅ Use arrow functions with descriptive names
|
||||
const onFormSubmit = async () => {
|
||||
await form.trigger();
|
||||
// ...
|
||||
};
|
||||
|
||||
const onFieldCopy = useCallback(
|
||||
(event?: KeyboardEvent | null) => {
|
||||
event?.preventDefault();
|
||||
// ...
|
||||
},
|
||||
[dependencies],
|
||||
);
|
||||
|
||||
// ✅ Inline handlers for simple operations
|
||||
<Button onClick={() => setOpen(false)}>Close</Button>
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
```typescript
|
||||
// ✅ Descriptive state names with auxiliary verbs
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
|
||||
// ✅ Complex state in single useState when related
|
||||
const [coords, setCoords] = useState({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Try-Catch Blocks
|
||||
|
||||
```typescript
|
||||
// ✅ Use try-catch for operations that might fail
|
||||
try {
|
||||
const document = await getDocumentById({
|
||||
documentId: Number(documentId),
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
body: document,
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
status: 404,
|
||||
body: {
|
||||
message: 'Document not found',
|
||||
},
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Throwing Errors
|
||||
|
||||
```typescript
|
||||
// ✅ Use AppError for application errors
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
|
||||
// ✅ Use descriptive error messages
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: `Template with ID ${templateId} not found`,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Error Parsing on Frontend
|
||||
|
||||
```typescript
|
||||
// ✅ Parse errors on the frontend
|
||||
try {
|
||||
await updateOrganisation({ organisationId, data });
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
console.error(error);
|
||||
|
||||
toast({
|
||||
title: t`An error occurred`,
|
||||
description: error.message,
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Async/Await Patterns
|
||||
|
||||
### Async Function Definitions
|
||||
|
||||
```typescript
|
||||
// ✅ Mark async functions clearly
|
||||
export const createDocument = async ({
|
||||
userId,
|
||||
title,
|
||||
}: Options): Promise<Document> => {
|
||||
// ...
|
||||
};
|
||||
|
||||
// ✅ Use await for promises
|
||||
const document = await prisma.document.create({ data });
|
||||
|
||||
// ✅ Use Promise.all for parallel operations
|
||||
const [document, recipients] = await Promise.all([
|
||||
getDocumentById({ documentId }),
|
||||
getRecipientsForDocument({ documentId }),
|
||||
]);
|
||||
```
|
||||
|
||||
### Void for Fire-and-Forget
|
||||
|
||||
```typescript
|
||||
// ✅ Use void for intentionally unwaited promises
|
||||
void handleAutoSave();
|
||||
|
||||
// ✅ Or in event handlers
|
||||
onClick={() => void onFormSubmit()}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Whitespace & Formatting
|
||||
|
||||
### Blank Lines Between Concepts
|
||||
|
||||
```typescript
|
||||
// ✅ Blank line after imports
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export const findDocuments = async () => {
|
||||
// ...
|
||||
};
|
||||
|
||||
// ✅ Blank line between logical sections
|
||||
const user = await prisma.user.findFirst({ where: { id: userId } });
|
||||
|
||||
let team = null;
|
||||
|
||||
if (teamId !== undefined) {
|
||||
team = await getTeamById({ userId, teamId });
|
||||
}
|
||||
|
||||
// ✅ Blank line before return statements
|
||||
const result = await someOperation();
|
||||
|
||||
return result;
|
||||
```
|
||||
|
||||
### Function/Method Spacing
|
||||
|
||||
```typescript
|
||||
// ✅ No blank lines between chained methods in same operation
|
||||
const documents = await prisma.document
|
||||
.findMany({ where: { userId } })
|
||||
.then((docs) => docs.map(maskTokens));
|
||||
|
||||
// ✅ Blank line between different operations
|
||||
const document = await createDocument({ userId });
|
||||
|
||||
await sendDocument({ documentId: document.id });
|
||||
|
||||
return document;
|
||||
```
|
||||
|
||||
### Object and Array Formatting
|
||||
|
||||
```typescript
|
||||
// ✅ Multi-line when complex
|
||||
const options = {
|
||||
userId,
|
||||
teamId,
|
||||
status: ExtendedDocumentStatus.ALL,
|
||||
page: 1,
|
||||
};
|
||||
|
||||
// ✅ Single line when simple
|
||||
const coords = { x: 0, y: 0 };
|
||||
|
||||
// ✅ Array items on separate lines when objects
|
||||
const recipients = [
|
||||
{
|
||||
name: 'John',
|
||||
email: 'john@example.com',
|
||||
},
|
||||
{
|
||||
name: 'Jane',
|
||||
email: 'jane@example.com',
|
||||
},
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
### Variables
|
||||
|
||||
```typescript
|
||||
// ✅ camelCase for variables and functions
|
||||
const documentId = 123;
|
||||
const onSubmit = () => {};
|
||||
|
||||
// ✅ Descriptive names with auxiliary verbs for booleans
|
||||
const isLoading = false;
|
||||
const hasError = false;
|
||||
const canEdit = true;
|
||||
const shouldRender = true;
|
||||
|
||||
// ✅ Prefix with $ for DOM elements
|
||||
const $page = document.querySelector('.page');
|
||||
const $inputRef = useRef<HTMLInputElement>(null);
|
||||
```
|
||||
|
||||
### Types and Schemas
|
||||
|
||||
```typescript
|
||||
// ✅ PascalCase for types
|
||||
type CreateDocumentOptions = {
|
||||
userId: number;
|
||||
};
|
||||
|
||||
// ✅ Prefix Zod schemas with Z
|
||||
const ZCreateDocumentSchema = z.object({
|
||||
title: z.string(),
|
||||
});
|
||||
|
||||
// ✅ Prefix type from Zod schema with T
|
||||
type TCreateDocumentSchema = z.infer<typeof ZCreateDocumentSchema>;
|
||||
```
|
||||
|
||||
### Constants
|
||||
|
||||
```typescript
|
||||
// ✅ UPPER_SNAKE_CASE for true constants
|
||||
const DEFAULT_DOCUMENT_DATE_FORMAT = 'dd/MM/yyyy';
|
||||
const MAX_FILE_SIZE = 1024 * 1024 * 5;
|
||||
|
||||
// ✅ camelCase for const variables that aren't "constants"
|
||||
const userId = await getUserId();
|
||||
```
|
||||
|
||||
### Functions
|
||||
|
||||
```typescript
|
||||
// ✅ Verb-based names for functions
|
||||
const createDocument = async () => {};
|
||||
const findDocuments = async () => {};
|
||||
const updateDocument = async () => {};
|
||||
const deleteDocument = async () => {};
|
||||
|
||||
// ✅ On prefix for event handlers
|
||||
const onSubmit = () => {};
|
||||
const onClick = () => {};
|
||||
const onFieldCopy = () => {}; // 'on' is also acceptable
|
||||
```
|
||||
|
||||
### Clarity Over Brevity
|
||||
|
||||
```typescript
|
||||
// ✅ Prefer descriptive names over abbreviations
|
||||
const superLongMethodThatIsCorrect = () => {};
|
||||
const recipientAuthenticationOptions = {};
|
||||
const documentMetadata = {};
|
||||
|
||||
// ❌ Avoid abbreviations that sacrifice clarity
|
||||
const supLongMethThatIsCorrect = () => {};
|
||||
const recipAuthOpts = {};
|
||||
const docMeta = {};
|
||||
|
||||
// ✅ Common abbreviations that are widely understood are acceptable
|
||||
const userId = 123;
|
||||
const htmlElement = document.querySelector('div');
|
||||
const apiResponse = await fetch('/api');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pattern Matching
|
||||
|
||||
### Using ts-pattern
|
||||
|
||||
```typescript
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
// ✅ Use match for complex conditionals
|
||||
const result = match(status)
|
||||
.with(ExtendedDocumentStatus.DRAFT, () => ({
|
||||
status: 'draft',
|
||||
}))
|
||||
.with(ExtendedDocumentStatus.PENDING, () => ({
|
||||
status: 'pending',
|
||||
}))
|
||||
.with(ExtendedDocumentStatus.COMPLETED, () => ({
|
||||
status: 'completed',
|
||||
}))
|
||||
.exhaustive();
|
||||
|
||||
// ✅ Use .otherwise() for default case when not exhaustive
|
||||
const value = match(type)
|
||||
.with('text', () => 'Text field')
|
||||
.with('number', () => 'Number field')
|
||||
.otherwise(() => 'Unknown field');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database & Prisma
|
||||
|
||||
### Query Structure
|
||||
|
||||
```typescript
|
||||
// ✅ Destructure commonly used fields
|
||||
const { id, email, name } = user;
|
||||
|
||||
// ✅ Use select to limit returned fields
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ Use include for relations
|
||||
const document = await prisma.document.findFirst({
|
||||
where: { id: documentId },
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Transactions
|
||||
|
||||
```typescript
|
||||
// ✅ Use transactions for related operations
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const document = await tx.document.create({ data });
|
||||
|
||||
await tx.field.createMany({ data: fieldsData });
|
||||
|
||||
await tx.documentAuditLog.create({ data: auditData });
|
||||
|
||||
return document;
|
||||
});
|
||||
```
|
||||
|
||||
### Where Clauses
|
||||
|
||||
```typescript
|
||||
// ✅ Build complex where clauses separately
|
||||
const whereClause: Prisma.DocumentWhereInput = {
|
||||
AND: [
|
||||
{ userId: user.id },
|
||||
{ deletedAt: null },
|
||||
{ status: { in: [DocumentStatus.DRAFT, DocumentStatus.PENDING] } },
|
||||
],
|
||||
};
|
||||
|
||||
const documents = await prisma.document.findMany({
|
||||
where: whereClause,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## TRPC Patterns
|
||||
|
||||
### Router Structure
|
||||
|
||||
```typescript
|
||||
// ✅ Destructure context and input at start
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { teamId } = ctx;
|
||||
const { templateId } = input;
|
||||
|
||||
ctx.logger.info({
|
||||
input: { templateId },
|
||||
});
|
||||
|
||||
return await getTemplateById({
|
||||
id: templateId,
|
||||
userId: ctx.user.id,
|
||||
teamId,
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Request/Response Schemas
|
||||
|
||||
```typescript
|
||||
// ✅ Name schemas clearly
|
||||
const ZCreateDocumentRequestSchema = z.object({
|
||||
title: z.string(),
|
||||
recipients: z.array(ZRecipientSchema),
|
||||
});
|
||||
|
||||
const ZCreateDocumentResponseSchema = z.object({
|
||||
documentId: z.number(),
|
||||
status: z.string(),
|
||||
});
|
||||
```
|
||||
|
||||
### Error Handling in TRPC
|
||||
|
||||
```typescript
|
||||
// ✅ Catch and transform errors appropriately
|
||||
try {
|
||||
const result = await createDocument({ userId, data });
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
return AppError.toRestAPIError(err);
|
||||
}
|
||||
|
||||
// ✅ Or throw AppError directly
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Patterns
|
||||
|
||||
### Optional Chaining
|
||||
|
||||
```typescript
|
||||
// ✅ Use optional chaining for potentially undefined values
|
||||
const email = user?.email;
|
||||
const recipientToken = recipient?.token ?? '';
|
||||
|
||||
// ✅ Use nullish coalescing for defaults
|
||||
const pageSize = perPage ?? 10;
|
||||
const status = documentStatus ?? DocumentStatus.DRAFT;
|
||||
```
|
||||
|
||||
### Array Operations
|
||||
|
||||
```typescript
|
||||
// ✅ Use functional array methods
|
||||
const activeRecipients = recipients.filter((r) => r.signingStatus === 'SIGNED');
|
||||
const recipientEmails = recipients.map((r) => r.email);
|
||||
const hasSignedRecipients = recipients.some((r) => r.signingStatus === 'SIGNED');
|
||||
|
||||
// ✅ Use find instead of filter + [0]
|
||||
const recipient = recipients.find((r) => r.id === recipientId);
|
||||
```
|
||||
|
||||
### Conditional Rendering
|
||||
|
||||
```typescript
|
||||
// ✅ Use && for conditional rendering
|
||||
{isLoading && <Loader />}
|
||||
|
||||
// ✅ Use ternary for either/or
|
||||
{isLoading ? <Loader /> : <Content />}
|
||||
|
||||
// ✅ Extract complex conditions to variables
|
||||
const shouldShowAdvanced = isAdmin && hasPermission && !isDisabled;
|
||||
{shouldShowAdvanced && <AdvancedSettings />}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When in Doubt
|
||||
|
||||
- **Consistency**: Follow the patterns you see in similar files
|
||||
- **Readability**: Favor code that's easy to read over clever one-liners
|
||||
- **Explicitness**: Be explicit rather than implicit
|
||||
- **Whitespace**: Use blank lines to separate logical sections
|
||||
- **Early Returns**: Use guard clauses to reduce nesting
|
||||
- **Functional**: Prefer functional patterns over imperative ones
|
||||
@@ -52,3 +52,53 @@ You can build the project with:
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## AI-Assisted Development with OpenCode
|
||||
|
||||
We use [OpenCode](https://opencode.ai) for AI-assisted development. OpenCode provides custom commands and skills to help maintain consistency and streamline common workflows.
|
||||
|
||||
OpenCode works with most major AI providers (Anthropic, OpenAI, Google, etc.) or you can use [Zen](https://opencode.ai/zen) for optimized coding models. Configure your preferred provider in the OpenCode settings.
|
||||
|
||||
> **Important**: All AI-generated code must be thoroughly reviewed by the contributor before submitting a PR. You are responsible for understanding and validating every line of code you submit. If we detect that contributors are simply throwing AI-generated code over the wall without proper review, they will be blocked from the repository.
|
||||
|
||||
### Getting Started
|
||||
|
||||
1. Install OpenCode (see [opencode.ai](https://opencode.ai) for other install methods):
|
||||
```bash
|
||||
curl -fsSL https://opencode.ai/install | bash
|
||||
```
|
||||
2. Configure your AI provider (or use Zen for optimized models)
|
||||
3. Run `opencode` in the project root
|
||||
|
||||
### Available Commands
|
||||
|
||||
Use these commands in OpenCode by typing the command name:
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------ | -------------------------------------------------------- |
|
||||
| `/implement <spec-path>` | Implement a spec from `.agents/plans/` autonomously |
|
||||
| `/continue <spec-path>` | Continue implementing a spec from a previous session |
|
||||
| `/interview <file-path>` | Deep-dive interview to flesh out a spec or design |
|
||||
| `/document <module-path>` | Generate MDX documentation for a module or feature |
|
||||
| `/commit` | Create a conventional commit for staged changes |
|
||||
| `/create-plan <slug>` | Create a new plan file in `.agents/plans/` |
|
||||
| `/create-scratch <slug>` | Create a scratch file for notes in `.agents/scratches/` |
|
||||
| `/create-justification <slug>` | Create a justification file in `.agents/justifications/` |
|
||||
|
||||
### Typical Workflow
|
||||
|
||||
1. **Create a plan**: Use `/create-plan my-feature` to draft a spec for a new feature
|
||||
2. **Flesh out the spec**: Use `/interview .agents/plans/<file>.md` to refine requirements
|
||||
3. **Implement**: Use `/implement .agents/plans/<file>.md` to build the feature
|
||||
4. **Continue if needed**: Use `/continue .agents/plans/<file>.md` to pick up where you left off
|
||||
5. **Commit**: Use `/commit` to create a conventional commit
|
||||
|
||||
### Agent Files
|
||||
|
||||
The `.agents/` directory stores AI-generated artifacts:
|
||||
|
||||
- **`.agents/plans/`** - Feature specs and implementation plans
|
||||
- **`.agents/scratches/`** - Temporary notes and explorations
|
||||
- **`.agents/justifications/`** - Decision rationale and technical justifications
|
||||
|
||||
These files use a unique ID format (`{word}-{word}-{word}-{slug}.md`) to prevent conflicts.
|
||||
|
||||
@@ -171,9 +171,11 @@ git clone https://github.com/<your-username>/documenso
|
||||
|
||||
5. Create the database schema by running `npm run prisma:migrate-dev`
|
||||
|
||||
6. Run `npm run dev` in the root directory to start
|
||||
6. Run `npm run translate:compile` in the root directory to compile lingui
|
||||
|
||||
7. Register a new user at http://localhost:3000/signup
|
||||
7. Run `npm run dev` in the root directory to start
|
||||
|
||||
8. Register a new user at http://localhost:3000/signup
|
||||
|
||||
---
|
||||
|
||||
@@ -214,8 +216,6 @@ For detailed instructions on how to configure and run the Docker container, plea
|
||||
|
||||
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
||||
|
||||
> Please note that the below deployment methods are for v0.9, we will update these to v1.0 once it has been released.
|
||||
|
||||
### Fetch, configure, and build
|
||||
|
||||
First, clone the code from Github:
|
||||
@@ -258,7 +258,7 @@ npm run start
|
||||
|
||||
This will start the server on `localhost:3000`. For now, any reverse proxy can then do the frontend and SSL termination.
|
||||
|
||||
> If you want to run with another port than 3000, you can start the application with `next -p <ANY PORT>` from the `apps/web` folder.
|
||||
> If you want to run with another port than 3000, you can start the application with `next -p <ANY PORT>` from the `apps/remix` folder.
|
||||
|
||||
### Run as a service
|
||||
|
||||
@@ -308,7 +308,7 @@ The Web UI can be found at http://localhost:9000, while the SMTP port will be on
|
||||
|
||||
### Support IPv6
|
||||
|
||||
If you are deploying to a cluster that uses only IPv6, You can use a custom command to pass a parameter to the Next.js start command
|
||||
If you are deploying to a cluster that uses only IPv6, You can use a custom command to pass a parameter to the Remix start command
|
||||
|
||||
For local docker run
|
||||
|
||||
|
||||
+16
-5
@@ -10,15 +10,26 @@ For the digital signature of your documents you need a signing certificate in .p
|
||||
|
||||
`openssl req -new -x509 -key private.key -out certificate.crt -days 365`
|
||||
|
||||
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The -days parameter sets the number of days for which the certificate is valid.
|
||||
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The `-days` parameter sets the number of days for which the certificate is valid.
|
||||
|
||||
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following command to do this:
|
||||
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following commands to do this:
|
||||
|
||||
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
|
||||
```bash
|
||||
# Set certificate password securely (won't appear in command history)
|
||||
read -s -p "Enter certificate password: " CERT_PASS
|
||||
echo
|
||||
|
||||
# Create the p12 certificate using the environment variable
|
||||
openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt \
|
||||
-password env:CERT_PASS \
|
||||
-keypbe PBE-SHA1-3DES \
|
||||
-certpbe PBE-SHA1-3DES \
|
||||
-macalg sha1
|
||||
```
|
||||
|
||||
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
||||
4. **IMPORTANT**: A certificate password is required to prevent signing failures. Make sure to use a strong password (minimum 4 characters) when prompted. Certificates without passwords will cause "Failed to get private key bags" errors during document signing.
|
||||
|
||||
5. Place the certificate `/apps/web/resources/certificate.p12` (If the path does not exist, it needs to be created)
|
||||
5. Place the certificate `/apps/remix/resources/certificate.p12` (If the path does not exist, it needs to be created)
|
||||
|
||||
## Docker
|
||||
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
# Documentation Writing Style Guide
|
||||
|
||||
This document defines the writing conventions for Documenso documentation.
|
||||
|
||||
Documentation lives in `apps/docs/` as MDX files and uses [Fumadocs](https://fumadocs.dev).
|
||||
|
||||
## Core Principles
|
||||
|
||||
1. **Task-based navigation** - Organize by what users want to do, not by feature hierarchy
|
||||
2. **Progressive examples** - Start simple, build to complex
|
||||
3. **Explicit limitations** - List what's NOT supported clearly
|
||||
4. **Real-world context** - Explain document signing concepts with familiar comparisons
|
||||
|
||||
## Tone
|
||||
|
||||
- Direct and action-oriented
|
||||
- Second person ("you") with imperative voice
|
||||
- Technical but accessible
|
||||
- Acknowledge complexity without condescension
|
||||
- No emojis or excessive personality
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
- Assuming document signing domain knowledge
|
||||
- Hiding default values
|
||||
- Separate "TypeScript" sections (types integrated throughout)
|
||||
- Monolithic single-page references
|
||||
- Examples that don't work with current API
|
||||
|
||||
## Documentation Audiences
|
||||
|
||||
The docs serve three distinct audiences:
|
||||
|
||||
1. **Users** - People using the Documenso web application to send and sign documents
|
||||
2. **Developers** - Building integrations with the API or SDKs
|
||||
3. **Self-hosters** - Running their own Documenso instance
|
||||
|
||||
Tailor content to the audience:
|
||||
|
||||
- User docs: Focus on UI workflows, no code required
|
||||
- Developer docs: API/SDK examples, authentication, webhooks
|
||||
- Self-hosting docs: Deployment, configuration, infrastructure
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
apps/docs/
|
||||
├── index.mdx # Landing page with audience navigation
|
||||
├── getting-started/ # Quick starts for each audience
|
||||
├── users/ # Application usage guides
|
||||
│ ├── documents/ # Creating and managing documents
|
||||
│ ├── templates/ # Working with templates
|
||||
│ ├── signing/ # Signing documents
|
||||
│ └── settings/ # Account and team settings
|
||||
├── developers/ # API and SDK documentation
|
||||
│ ├── api/ # REST API reference
|
||||
│ ├── sdk/ # SDK guides
|
||||
│ ├── webhooks/ # Webhook integration
|
||||
│ └── examples/ # Code examples and recipes
|
||||
├── self-hosting/ # Self-hosting documentation
|
||||
│ ├── deployment/ # Deployment guides
|
||||
│ ├── configuration/ # Environment and settings
|
||||
│ └── maintenance/ # Upgrades and backups
|
||||
├── concepts/ # Shared concepts across audiences
|
||||
└── migration/ # Migration guides
|
||||
```
|
||||
|
||||
Each directory has a `meta.json` controlling navigation order:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Section Title",
|
||||
"pages": ["index", "page-one", "page-two"]
|
||||
}
|
||||
```
|
||||
|
||||
Use `---Label---` for section dividers in `meta.json`.
|
||||
|
||||
## MDX Frontmatter
|
||||
|
||||
Every page needs frontmatter for search and SEO:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Working with Pages
|
||||
description: Add, remove, reorder, copy, and merge PDF pages.
|
||||
---
|
||||
```
|
||||
|
||||
## Page Structure
|
||||
|
||||
### User Documentation
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Feature Name
|
||||
description: Brief description for SEO and previews.
|
||||
---
|
||||
|
||||
# Feature Name
|
||||
|
||||
Brief description of what this does and when to use it.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Navigate to **Settings > Feature**
|
||||
2. Click **Add New**
|
||||
3. Fill in the required fields
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Related Guide](/docs/users/related)
|
||||
```
|
||||
|
||||
### Developer Documentation
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Feature Name
|
||||
description: Brief description for SEO and previews.
|
||||
---
|
||||
|
||||
# Feature Name
|
||||
|
||||
Brief description of what this does and when to use it.
|
||||
|
||||
## Quick Start
|
||||
|
||||
\`\`\`typescript
|
||||
// Minimal working example
|
||||
\`\`\`
|
||||
|
||||
---
|
||||
|
||||
## Section Name
|
||||
|
||||
Content organized by task or concept.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Related Guide](/docs/developers/related)
|
||||
```
|
||||
|
||||
### Self-Hosting Documentation
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Configuration Topic
|
||||
description: Brief description for SEO and previews.
|
||||
---
|
||||
|
||||
# Configuration Topic
|
||||
|
||||
Brief description of what this configures.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Default | Description |
|
||||
| ---------- | -------- | ------- | ------------ |
|
||||
| `VAR_NAME` | Yes | - | What it does |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Related Guide](/docs/self-hosting/related)
|
||||
```
|
||||
|
||||
## Parameter Tables
|
||||
|
||||
Use Sharp-style nested parameter tables for developer documentation (API/SDK):
|
||||
|
||||
```markdown
|
||||
### methodName(param, options?)
|
||||
|
||||
Description of what the method does.
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
| ------------------- | --------- | -------- | --------------------- |
|
||||
| `param` | `string` | required | What it does |
|
||||
| `[options]` | `Options` | | |
|
||||
| `[options.setting]` | `boolean` | `false` | Nested option |
|
||||
| `[options.timeout]` | `number` | `5000` | Another nested option |
|
||||
|
||||
**Returns**: `Promise<Result>`
|
||||
|
||||
**Throws**:
|
||||
|
||||
- `SpecificError` - When something goes wrong
|
||||
```
|
||||
|
||||
Key conventions:
|
||||
|
||||
- Square brackets `[param]` indicate optional parameters
|
||||
- Nested options indented with `[options.name]` pattern
|
||||
- Always show default values
|
||||
- Group related options under their parent
|
||||
|
||||
## Code Examples
|
||||
|
||||
For developer documentation, use progressive complexity:
|
||||
|
||||
```typescript
|
||||
// Basic usage
|
||||
const document = await documenso.documents.create({
|
||||
title: "Contract",
|
||||
file: pdfBuffer,
|
||||
});
|
||||
|
||||
// With recipients
|
||||
const document = await documenso.documents.create({
|
||||
title: "Contract",
|
||||
file: pdfBuffer,
|
||||
recipients: [{ email: "signer@example.com", name: "John Doe" }],
|
||||
});
|
||||
|
||||
// Full example with error handling
|
||||
try {
|
||||
const document = await documenso.documents.create({
|
||||
title: "Contract",
|
||||
file: pdfBuffer,
|
||||
recipients: [{ email: "signer@example.com", name: "John Doe" }],
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof DocumentError) {
|
||||
// Handle document creation error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Guidelines
|
||||
|
||||
- All examples must be valid TypeScript
|
||||
- Show imports when not obvious
|
||||
- Include expected output in comments where helpful
|
||||
- Use realistic values, not `foo`/`bar`
|
||||
|
||||
## UI Instructions
|
||||
|
||||
For user documentation, use clear step-by-step instructions:
|
||||
|
||||
- Bold UI elements: **Settings**, **Save**, **Documents**
|
||||
- Use `>` for navigation paths: **Settings > Team > Members**
|
||||
- Number sequential steps
|
||||
- Include screenshots sparingly for complex workflows
|
||||
- Describe what the user should see after each action
|
||||
|
||||
## Callouts
|
||||
|
||||
Use Fumadocs callouts sparingly for important information:
|
||||
|
||||
```mdx
|
||||
<Callout type="info">Informational note about behavior.</Callout>
|
||||
|
||||
<Callout type="warn">Warning about potential issues or breaking changes.</Callout>
|
||||
|
||||
<Callout type="error">Critical warning about data loss or security.</Callout>
|
||||
```
|
||||
|
||||
Reserve callouts for:
|
||||
|
||||
- Beta/unstable features
|
||||
- Security considerations
|
||||
- Common mistakes
|
||||
- Breaking changes
|
||||
|
||||
## Tables
|
||||
|
||||
Use tables for:
|
||||
|
||||
- Feature matrices
|
||||
- Parameter documentation
|
||||
- Comparison charts
|
||||
- Error catalogs
|
||||
|
||||
```markdown
|
||||
| Feature | Status | Notes |
|
||||
| ---------------- | ------ | ------------------------ |
|
||||
| Email signing | Full | All recipient types |
|
||||
| Embedded signing | Full | Via SDK or direct links |
|
||||
| Templates | Full | Create and use templates |
|
||||
```
|
||||
|
||||
## Linking
|
||||
|
||||
- Link to related docs: `[Documents](/docs/api/documents)`
|
||||
- Use relative paths within docs
|
||||
- Add "See Also" sections for discoverability
|
||||
|
||||
## Error Documentation
|
||||
|
||||
Categorize errors by when they occur:
|
||||
|
||||
```markdown
|
||||
## Document Errors
|
||||
|
||||
Thrown when creating or updating documents.
|
||||
|
||||
### InvalidDocumentError
|
||||
|
||||
Document could not be processed.
|
||||
|
||||
**Common causes:**
|
||||
|
||||
- File is not a valid PDF
|
||||
- File exceeds size limits
|
||||
|
||||
**Solution:** Verify the file is a valid PDF within size limits.
|
||||
```
|
||||
|
||||
## Concept Explanations
|
||||
|
||||
Use analogies for document signing concepts:
|
||||
|
||||
```markdown
|
||||
Think of a **signing workflow** like passing a physical document around an office.
|
||||
|
||||
Each recipient gets the document in turn, adds their signature or initials,
|
||||
and passes it to the next person. The **document status** tracks where it
|
||||
is in this journey.
|
||||
```
|
||||
|
||||
## Self-Hosting Specific
|
||||
|
||||
For self-hosting documentation:
|
||||
|
||||
- Always specify required vs optional environment variables
|
||||
- Include example `.env` snippets
|
||||
- Document Docker and non-Docker approaches where applicable
|
||||
- Link to troubleshooting for common deployment issues
|
||||
- Specify minimum system requirements
|
||||
|
||||
## Maintenance
|
||||
|
||||
- Include types inline so docs don't get stale
|
||||
- Reference source file locations for complex behavior
|
||||
- Update examples when API changes
|
||||
- Test all code examples work
|
||||
- Keep environment variable documentation in sync with actual defaults
|
||||
@@ -0,0 +1,26 @@
|
||||
# deps
|
||||
/node_modules
|
||||
|
||||
# generated content
|
||||
.source
|
||||
|
||||
# test & build
|
||||
/coverage
|
||||
/.next/
|
||||
/out/
|
||||
/build
|
||||
*.tsbuildinfo
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
/.pnp
|
||||
.pnp.js
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# others
|
||||
.env*.local
|
||||
.vercel
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,45 @@
|
||||
# docs
|
||||
|
||||
This is a Next.js application generated with
|
||||
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
|
||||
|
||||
Run development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open http://localhost:3000 with your browser to see the result.
|
||||
|
||||
## Explore
|
||||
|
||||
In the project, you can see:
|
||||
|
||||
- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
|
||||
- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.
|
||||
|
||||
| Route | Description |
|
||||
| ------------------------- | ------------------------------------------------------ |
|
||||
| `app/(home)` | The route group for your landing page and other pages. |
|
||||
| `app/docs` | The documentation layout and pages. |
|
||||
| `app/api/search/route.ts` | The Route Handler for search. |
|
||||
|
||||
### Fumadocs MDX
|
||||
|
||||
A `source.config.ts` config file has been included, you can customise different options like frontmatter schema.
|
||||
|
||||
Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js and Fumadocs, take a look at the following
|
||||
resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
|
||||
features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "node_modules/@fumadocs/cli/dist/schema/src.json",
|
||||
"aliases": {
|
||||
"uiDir": "./components/ui",
|
||||
"componentsDir": "./components",
|
||||
"blockDir": "./components",
|
||||
"cssDir": "./styles",
|
||||
"libDir": "./lib"
|
||||
},
|
||||
"baseDir": "src",
|
||||
"uiLibrary": "radix-ui",
|
||||
"commands": {}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Certifications & Regulatory Compliance
|
||||
description: Documenso's compliance status for industry certifications and regulatory frameworks.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
### Compliance Status Overview
|
||||
|
||||
| Certification | Status |
|
||||
| -------------- | ---------------------- |
|
||||
| 21 CFR Part 11 | Compliant (Enterprise) |
|
||||
| SOC 2 | Compliant |
|
||||
| ISO 27001 | Planned |
|
||||
| HIPAA | Planned |
|
||||
|
||||
## 21 CFR Part 11
|
||||
|
||||
<Callout type="info">Status: Compliant (Enterprise License)</Callout>
|
||||
|
||||
21 CFR Part 11 is a regulation by the FDA that establishes the criteria for electronic records and electronic signatures to ensure their authenticity, integrity, and confidentiality in the pharmaceutical, medical device, and other FDA-regulated industries.
|
||||
|
||||
Read more about [21 CFR Part 11 with Documenso](https://documen.so/21-CFR-Part-11).
|
||||
|
||||
### Main Requirements
|
||||
|
||||
- Strong Identity Checks for each Signature
|
||||
- Signature and Audit Trails
|
||||
- User Access Management
|
||||
- Quality Assurance Documentation
|
||||
|
||||
## SOC 2
|
||||
|
||||
<Callout type="info">Status: [Compliant](https://documen.so/trust)</Callout>
|
||||
|
||||
SOC 2 is a framework for managing and auditing the security, availability, processing integrity, confidentiality, and data privacy in cloud and IT service organizations, established by the American Institute of Certified Public Accountants (AICPA).
|
||||
|
||||
## ISO 27001
|
||||
|
||||
<Callout type="warn">Status: [Planned](https://github.com/documenso/backlog/issues/26)</Callout>
|
||||
|
||||
ISO 27001 is an international standard for managing information security, specifying requirements for establishing, implementing, maintaining, and continually improving an information security management system (ISMS).
|
||||
|
||||
## HIPAA
|
||||
|
||||
<Callout type="warn">Status: [Planned](https://github.com/documenso/backlog/issues/25)</Callout>
|
||||
|
||||
The HIPAA (Health Insurance Portability and Accountability Act) is a U.S. law designed to protect patient health information's privacy and security and improve the healthcare system's efficiency and effectiveness.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Standards](/docs/compliance/standards) - Technical signing standards (PDF/A, PAdES, X.509)
|
||||
- [Signature Levels](/docs/compliance/signature-levels) - eIDAS and other signature level compliance
|
||||
- [Enterprise Edition](/docs/policies/enterprise-edition) - Enterprise licensing for compliance features
|
||||
- [GDPR](/docs/compliance/gdpr) - Data protection compliance
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
title: E-Sign Compliance
|
||||
description: Understand ESIGN, UETA, eIDAS, and other electronic signature laws that govern digital documents.
|
||||
---
|
||||
|
||||
## ESIGN Act (United States)
|
||||
|
||||
The Electronic Signatures in Global and National Commerce Act (ESIGN Act) is a U.S. federal law enacted in 2000. It ensures that electronic signatures and records have the same legal validity as paper documents and handwritten signatures in interstate and foreign commerce.
|
||||
|
||||
### Key Requirements
|
||||
|
||||
| Requirement | Description |
|
||||
| ----------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| **Intent to Sign** | Signers must demonstrate clear intent to sign the document |
|
||||
| **Consent** | All parties must agree to conduct the transaction electronically |
|
||||
| **Consumer Disclosure** | For consumer transactions, specific disclosures must be provided before obtaining consent |
|
||||
| **Record Retention** | Electronic records must be accurately preserved and accessible for later reference |
|
||||
| **Association** | The signature must be associated with the record being signed |
|
||||
|
||||
### Exclusions
|
||||
|
||||
The ESIGN Act does not apply to certain document types, including:
|
||||
|
||||
- Wills, codicils, and testamentary trusts
|
||||
- Family law documents (adoption, divorce)
|
||||
- Court orders and official court documents
|
||||
- Cancellation of utility services
|
||||
- Documents related to hazardous materials transportation
|
||||
|
||||
---
|
||||
|
||||
## UETA (United States)
|
||||
|
||||
The Uniform Electronic Transactions Act (UETA) is a model law adopted by 49 U.S. states (all except New York, which has its own Electronic Signatures and Records Act). UETA provides a legal framework for electronic signatures and records at the state level.
|
||||
|
||||
### Relationship to ESIGN
|
||||
|
||||
UETA and the ESIGN Act have similar requirements and purposes. The federal ESIGN Act allows states to modify or supersede certain ESIGN provisions if they adopt UETA or an equivalent law. In practice, the requirements for electronic signatures under both laws align closely.
|
||||
|
||||
### Key Requirements
|
||||
|
||||
- Intent to sign demonstrated by the signer
|
||||
- Consent to conduct transactions electronically
|
||||
- Retention of records in their original electronic form
|
||||
- Attribution of the signature to the signer
|
||||
|
||||
---
|
||||
|
||||
## eIDAS (European Union)
|
||||
|
||||
The Electronic Identification, Authentication and Trust Services (eIDAS) regulation governs electronic signatures across all EU member states. eIDAS establishes three levels of electronic signatures, each with different requirements and legal effects.
|
||||
|
||||
### Signature Levels
|
||||
|
||||
| Level | Description | Legal Effect |
|
||||
| ------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
||||
| **Simple (SES)** | Basic electronic signature with no specific technical requirements | Admissible as evidence; legal effect varies by use |
|
||||
| **Advanced (AES)** | Uniquely linked to signer, capable of identifying signer, under sole control | Higher evidentiary weight than SES |
|
||||
| **Qualified (QES)** | AES created by a qualified signature creation device, based on a qualified certificate | Equivalent to handwritten signature across the EU |
|
||||
|
||||
### Simple Electronic Signatures (SES)
|
||||
|
||||
SES is the baseline level. Any data in electronic form attached to or logically associated with other electronic data, used by the signatory to sign, qualifies as an SES. There are no specific technical requirements beyond demonstrating intent to sign.
|
||||
|
||||
### Advanced Electronic Signatures (AES)
|
||||
|
||||
AES must meet additional criteria:
|
||||
|
||||
- Uniquely linked to the signatory
|
||||
- Capable of identifying the signatory
|
||||
- Created using signature creation data under the signatory's sole control
|
||||
- Linked to the signed data in a way that detects subsequent changes
|
||||
|
||||
### Qualified Electronic Signatures (QES)
|
||||
|
||||
QES requires:
|
||||
|
||||
- A qualified certificate issued by a qualified trust service provider
|
||||
- Creation using a qualified electronic signature creation device
|
||||
- Identity verification compliant with eIDAS requirements
|
||||
|
||||
QES carries the same legal standing as a handwritten signature in all EU member states.
|
||||
|
||||
---
|
||||
|
||||
## Other Jurisdictions
|
||||
|
||||
Electronic signature laws exist in most countries. Below are selected examples:
|
||||
|
||||
| Jurisdiction | Framework | Notes |
|
||||
| ------------------ | --------------------------------------------- | ------------------------------------------------------------ |
|
||||
| **United Kingdom** | UK eIDAS / Electronic Communications Act 2000 | Post-Brexit, UK maintains eIDAS-like framework |
|
||||
| **Canada** | PIPEDA, provincial laws | Federal and provincial laws govern e-signatures |
|
||||
| **Australia** | Electronic Transactions Act 1999 | Generally technology-neutral approach |
|
||||
| **Switzerland** | ZertES | Swiss federal law with qualified signature requirements |
|
||||
| **Brazil** | MP 2200-2, ICP-Brasil | PKI-based framework for digital signatures |
|
||||
| **India** | IT Act 2000, Aadhaar e-KYC | Recognizes electronic signatures; Aadhaar-based verification |
|
||||
| **China** | Electronic Signature Law | Requires reliable electronic signatures for certain uses |
|
||||
| **Japan** | Electronic Signatures Act | Three-tier system similar to eIDAS |
|
||||
|
||||
Requirements vary significantly by jurisdiction. Some transactions may require specific signature types or have exclusions similar to the ESIGN Act.
|
||||
|
||||
---
|
||||
|
||||
## How Documenso Supports Compliance
|
||||
|
||||
Documenso provides features that support compliance with e-signature laws across jurisdictions:
|
||||
|
||||
### Intent to Sign
|
||||
|
||||
- Signers must actively interact with signature fields to apply their signature
|
||||
- The signing interface clearly indicates the document being signed
|
||||
- Signers receive a copy of the completed document
|
||||
|
||||
### Consent
|
||||
|
||||
- Recipients receive clear notification that they are being asked to sign electronically
|
||||
- The signing process requires affirmative action from the signer
|
||||
|
||||
### Record Retention
|
||||
|
||||
- Signed documents are stored and accessible to all parties
|
||||
- Original documents and audit trails are preserved
|
||||
- Documents can be downloaded in their signed form at any time
|
||||
|
||||
### Document Integrity
|
||||
|
||||
- All completed documents are cryptographically sealed
|
||||
- Any modification after signing invalidates the digital signature
|
||||
- PDF readers can verify the document has not been altered
|
||||
|
||||
### Signer Identification
|
||||
|
||||
- Email-based delivery establishes signer identity
|
||||
- Optional access codes add verification
|
||||
- Signing activity is logged with timestamps and metadata
|
||||
|
||||
---
|
||||
|
||||
## Audit Trails
|
||||
|
||||
Documenso maintains an audit trail for each document, recording:
|
||||
|
||||
| Event | Recorded Data |
|
||||
| ------------------ | -------------------------------------- |
|
||||
| Document creation | Timestamp, creator identity |
|
||||
| Recipient addition | Recipient details, assigned fields |
|
||||
| Document sent | Timestamp, delivery method |
|
||||
| Document viewed | Timestamp, viewer identity, IP address |
|
||||
| Field completed | Timestamp, field type, signer identity |
|
||||
| Document completed | Timestamp, final document hash |
|
||||
|
||||
The audit trail provides evidence of the signing process, including who signed, when they signed, and the sequence of events. This information supports the legal enforceability of the signed document.
|
||||
|
||||
---
|
||||
|
||||
## What Documenso Does NOT Provide
|
||||
|
||||
Documenso supports compliance with Simple Electronic Signature (SES) requirements. The following are not currently provided:
|
||||
|
||||
| Capability | Status |
|
||||
| ----------------------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| **Qualified Electronic Signatures (QES)** | Not supported; requires integration with qualified trust service providers |
|
||||
| **Advanced Electronic Signatures (AES)** | Partial support; full AES requires identity verification services |
|
||||
| **Identity Verification (KYC)** | Not built-in; optional integrations may be available |
|
||||
| **Qualified Certificates** | Not issued; would require becoming a qualified trust service provider |
|
||||
| **Industry-Specific Compliance** | Features for specific regulations (e.g., healthcare, finance) depend on configuration |
|
||||
|
||||
For transactions requiring AES or QES, consult with legal counsel about appropriate solutions.
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This page provides general information about electronic signature laws for educational purposes. It does not constitute legal advice.
|
||||
|
||||
Electronic signature requirements vary by jurisdiction, transaction type, and specific circumstances. Some documents may have specific legal requirements that electronic signatures cannot satisfy.
|
||||
|
||||
Consult qualified legal counsel in your jurisdiction to determine whether electronic signatures are appropriate for your specific use case and what requirements must be met.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [Signature Levels](/docs/compliance/signature-levels) - Simple, Advanced, and Qualified electronic signatures explained
|
||||
- [Standards & Regulations](/docs/compliance/standards) - SOC 2, 21 CFR Part 11, and other compliance frameworks
|
||||
- [Signing Certificates](/docs/concepts/signing-certificates) - How documents are digitally signed and verified
|
||||
@@ -0,0 +1,174 @@
|
||||
---
|
||||
title: GDPR
|
||||
description: Understand how Documenso handles GDPR compliance for data processing and storage.
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Documenso's Role
|
||||
|
||||
When using Documenso for document signing, two distinct data processing roles apply:
|
||||
|
||||
| Role | Description |
|
||||
| ------------------- | ------------------------------------------------------------------------------------- |
|
||||
| **Data Controller** | You (the organisation using Documenso) determine the purposes and means of processing |
|
||||
| **Data Processor** | Documenso processes personal data on your behalf according to your instructions |
|
||||
|
||||
As the data controller, you are responsible for:
|
||||
|
||||
- Obtaining appropriate consent or legal basis for processing
|
||||
- Informing data subjects about how their data is used
|
||||
- Responding to data subject access requests
|
||||
- Ensuring compliance with GDPR requirements
|
||||
|
||||
As the data processor, Documenso:
|
||||
|
||||
- Processes data only according to your instructions
|
||||
- Implements appropriate security measures
|
||||
- Assists with data subject requests when needed
|
||||
- Maintains records of processing activities
|
||||
|
||||
## Data Processing
|
||||
|
||||
Documenso processes personal data necessary to provide document signing services:
|
||||
|
||||
| Data Category | Examples | Purpose |
|
||||
| ------------------ | ---------------------------------------------- | --------------------------------------- |
|
||||
| **Identity Data** | Name, email address | User accounts, recipient identification |
|
||||
| **Document Data** | Uploaded PDFs, field values | Document storage and signing |
|
||||
| **Signature Data** | Signature images, signing timestamps | Recording signing actions |
|
||||
| **Audit Data** | IP addresses, browser information, action logs | Audit trail and verification |
|
||||
|
||||
Data is processed for the following purposes:
|
||||
|
||||
- Delivering documents to recipients
|
||||
- Recording signatures and other recipient actions
|
||||
- Generating signed documents with audit trails
|
||||
- Sending email notifications
|
||||
|
||||
## Data Storage Locations
|
||||
|
||||
Where your data is stored depends on how you use Documenso:
|
||||
|
||||
<Tabs items={['Documenso Cloud', 'Self-Hosted']}>
|
||||
<Tab value="Documenso Cloud">
|
||||
|
||||
For the hosted cloud service:
|
||||
|
||||
- Application data is stored in data centres within the European Union
|
||||
- Document storage uses EU-based infrastructure
|
||||
- Backups are maintained in geographically separate EU locations
|
||||
|
||||
Contact Documenso for specific information about sub-processors and data centre locations.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Self-Hosted">
|
||||
|
||||
When you self-host Documenso:
|
||||
|
||||
- You control all data storage locations
|
||||
- No data is transmitted to Documenso's infrastructure
|
||||
- You choose your own database, file storage, and backup locations
|
||||
|
||||
Self-hosting provides complete control over data residency, which may be required for certain compliance scenarios.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Data Subject Rights
|
||||
|
||||
GDPR grants individuals specific rights regarding their personal data. As the data controller, you are responsible for fulfilling these requests:
|
||||
|
||||
| Right | Description |
|
||||
| ----------------- | -------------------------------------------------------------------------- |
|
||||
| **Access** | Data subjects can request a copy of their personal data |
|
||||
| **Rectification** | Data subjects can request correction of inaccurate data |
|
||||
| **Erasure** | Data subjects can request deletion of their data ("right to be forgotten") |
|
||||
| **Portability** | Data subjects can request their data in a machine-readable format |
|
||||
| **Restriction** | Data subjects can request limited processing of their data |
|
||||
| **Objection** | Data subjects can object to certain types of processing |
|
||||
|
||||
When you receive a data subject request, you can:
|
||||
|
||||
- Export user and document data from your Documenso account
|
||||
- Delete user accounts and associated documents
|
||||
- Contact Documenso support for assistance with cloud-hosted data
|
||||
|
||||
## Data Deletion
|
||||
|
||||
Documenso supports data deletion to help fulfill erasure requests:
|
||||
|
||||
<Accordions type="multiple">
|
||||
<Accordion title="User Account Deletion">
|
||||
- Users can delete their own accounts
|
||||
- Account deletion removes profile data and authentication credentials
|
||||
- Team owners can remove members from teams
|
||||
</Accordion>
|
||||
<Accordion title="Document Deletion">
|
||||
- Document owners can delete documents in draft state
|
||||
- Completed documents can be deleted by the owner
|
||||
- Deletion removes the document, recipient data, and associated audit logs
|
||||
</Accordion>
|
||||
<Accordion title="Retention Considerations">
|
||||
For signed documents, you may need to balance deletion requests against:
|
||||
|
||||
- Legal requirements to retain signed contracts
|
||||
- Your organisation's record-keeping policies
|
||||
- The rights of other parties to the signed document
|
||||
|
||||
Consult with legal counsel to establish appropriate retention policies.
|
||||
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
## Self-Hosting for GDPR Compliance
|
||||
|
||||
Self-hosting Documenso can simplify GDPR compliance:
|
||||
|
||||
- **Data residency** - Store all data in your chosen jurisdiction
|
||||
- **Sub-processor control** - No third-party data processors beyond your own infrastructure
|
||||
- **Direct access** - Full database access for data subject requests
|
||||
- **Retention control** - Implement custom data retention and deletion policies
|
||||
|
||||
See the [Self-Hosting Guide](/docs/self-hosting) for deployment options.
|
||||
|
||||
## Data Processing Agreement
|
||||
|
||||
A Data Processing Agreement (DPA) is a contract required by GDPR when a data controller engages a data processor.
|
||||
|
||||
<Tabs items={['Documenso Cloud', 'Self-Hosted']}>
|
||||
<Tab value="Documenso Cloud">
|
||||
|
||||
- A DPA is available upon request
|
||||
- Contact [support@documenso.com](mailto:support@documenso.com) to request a DPA
|
||||
- The DPA covers Documenso's obligations as a data processor
|
||||
|
||||
</Tab>
|
||||
<Tab value="Self-Hosted">
|
||||
|
||||
No DPA with Documenso is required since no personal data is processed by Documenso.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This documentation is provided for informational purposes only and does not constitute legal advice. GDPR compliance depends on your specific circumstances, including how you use Documenso, what data you process, and your organisation's obligations.
|
||||
|
||||
Consult with qualified legal counsel to:
|
||||
|
||||
- Determine your GDPR obligations
|
||||
- Draft appropriate privacy notices
|
||||
- Establish lawful bases for processing
|
||||
- Implement compliant data handling procedures
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [Standards & Regulations](/docs/compliance/standards) - eIDAS, ESIGN Act, and other compliance frameworks
|
||||
- [Self-Hosting Guide](/docs/self-hosting) - Deploy Documenso on your own infrastructure
|
||||
- [Security Settings](/docs/users/settings/security) - Configure authentication and security options
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
title: Compliance
|
||||
description: Legal and regulatory compliance information for electronic signatures.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="E-Sign Compliance"
|
||||
description="ESIGN Act, UETA, eIDAS, and electronic signature laws by jurisdiction."
|
||||
href="/docs/compliance/esign"
|
||||
/>
|
||||
<Card
|
||||
title="GDPR"
|
||||
description="Data protection requirements for processing personal data in the EU."
|
||||
href="/docs/compliance/gdpr"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
## Additional Topics
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="Standards & Regulations"
|
||||
description="SOC 2, 21 CFR Part 11, and other compliance frameworks."
|
||||
href="/docs/compliance/standards"
|
||||
/>
|
||||
<Card
|
||||
title="Signature Levels"
|
||||
description="Simple, Advanced, and Qualified electronic signatures under eIDAS."
|
||||
href="/docs/compliance/signature-levels"
|
||||
/>
|
||||
<Card
|
||||
title="Certifications"
|
||||
description="Compliance status for industry certifications and regulatory frameworks."
|
||||
href="/docs/compliance/certifications"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This documentation is provided for informational purposes only. It does not constitute legal advice and should not be relied upon as such.
|
||||
|
||||
Compliance requirements vary based on:
|
||||
|
||||
- Your jurisdiction and applicable laws
|
||||
- The type of documents being signed
|
||||
- Industry-specific regulations
|
||||
- The parties involved in the transaction
|
||||
|
||||
Consult with qualified legal counsel to determine the specific requirements for your use case.
|
||||
|
||||
## Related
|
||||
|
||||
- [Privacy Policy](/docs/policies/privacy) - How Documenso handles personal data
|
||||
- [Security](/docs/policies/security) - Security practices and measures
|
||||
- [Terms of Service](/docs/policies/terms) - Terms governing use of Documenso
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Compliance",
|
||||
"pages": ["esign", "standards", "signature-levels", "gdpr", "certifications"]
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
---
|
||||
title: Signature Levels
|
||||
description: Understand the three eIDAS signature levels — SES, AES, and QES — their requirements, legal effect, and when to use each.
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
<Callout type="info">
|
||||
Documenso seals all signed documents cryptographically, regardless of signature level, to prevent
|
||||
any alterations after signing.
|
||||
</Callout>
|
||||
|
||||
### Compliance Status Overview
|
||||
|
||||
| Regulation | Status |
|
||||
| ------------ | --------- |
|
||||
| ESIGN / UETA | Compliant |
|
||||
| eIDAS SES | Compliant |
|
||||
| eIDAS AES | Planned |
|
||||
| eIDAS QES | Planned |
|
||||
| ZertES | Planned |
|
||||
|
||||
## U.S. ESIGN Act
|
||||
|
||||
<Callout type="info">Status: Compliant</Callout>
|
||||
|
||||
The Electronic Signatures in Global and National Commerce Act (ESIGN Act) is a U.S. federal law that ensures the legal validity and enforceability of electronic signatures and records in commerce.
|
||||
|
||||
### Main Requirements
|
||||
|
||||
- **Intent to Sign** - Parties must demonstrate their intent to sign
|
||||
- **Consent** - All parties must consent to the use of electronic signatures and records
|
||||
- **Consumer Disclosures** - Financial institutions must provide clear statements informing consumers before obtaining consent
|
||||
- **Record Retention** - Electronic records must be maintained for later access by signers
|
||||
- **Security** - Parties must take reasonable steps to ensure the security and integrity of electronic signatures and records
|
||||
|
||||
## UETA (Uniform Electronic Transactions Act)
|
||||
|
||||
<Callout type="info">Status: Compliant</Callout>
|
||||
|
||||
The Uniform Electronic Transactions Act provides a legal framework for the use of electronic signatures and records in electronic transactions, ensuring they have the same validity and enforceability as paper documents and handwritten signatures.
|
||||
|
||||
UETA shares the same core requirements as the [ESIGN Act](#us-esign-act).
|
||||
|
||||
## Simple Electronic Signatures (SES)
|
||||
|
||||
A Simple Electronic Signature is the most basic form of electronic signature. It includes any data in electronic form that is attached to or logically associated with other electronic data and used by the signatory to sign.
|
||||
|
||||
### Characteristics
|
||||
|
||||
| Aspect | Description |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------- |
|
||||
| **Technical Requirements** | No specific technical requirements beyond demonstrating intent to sign |
|
||||
| **Identity Verification** | None required; relies on email delivery or other indirect identification |
|
||||
| **Legal Status** | Admissible as evidence; cannot be denied legal effect solely because it is electronic |
|
||||
| **Examples** | Typed name, scanned signature image, checkbox acceptance, click-to-sign |
|
||||
|
||||
### When SES Is Appropriate
|
||||
|
||||
SES is suitable for many common business transactions:
|
||||
|
||||
- Standard contracts and agreements
|
||||
- Internal approvals and sign-offs
|
||||
- Terms of service acceptance
|
||||
- Non-disclosure agreements
|
||||
- Purchase orders and invoices
|
||||
- Employment documents (in most jurisdictions)
|
||||
|
||||
The legal validity of SES depends on the specific transaction and jurisdiction. Many everyday business documents do not require higher signature levels.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Electronic Signatures (AES)
|
||||
|
||||
An Advanced Electronic Signature meets additional technical and procedural requirements that provide stronger evidence of the signer's identity and the document's integrity.
|
||||
|
||||
### Requirements
|
||||
|
||||
Under eIDAS, an AES must satisfy four criteria:
|
||||
|
||||
1. **Uniquely linked to the signatory** - The signature is associated with a specific individual
|
||||
2. **Capable of identifying the signatory** - The signature data reveals who signed
|
||||
3. **Created using signature creation data under the signatory's sole control** - Only the signer can create the signature (e.g., private key, secure device)
|
||||
4. **Linked to the data in such a way that any subsequent change is detectable** - Tampering invalidates the signature
|
||||
|
||||
### Characteristics
|
||||
|
||||
| Aspect | Description |
|
||||
| -------------------------- | -------------------------------------------------------------------------- |
|
||||
| **Technical Requirements** | Cryptographic signature with signer identification |
|
||||
| **Identity Verification** | Required; must establish signer identity through verification process |
|
||||
| **Legal Status** | Higher evidentiary weight than SES; stronger presumption of validity |
|
||||
| **Implementation** | Typically requires identity verification service and personal certificates |
|
||||
|
||||
### Compliance Status
|
||||
|
||||
<Callout type="warn">
|
||||
Status: [Planned](https://github.com/documenso/backlog/issues/9) via third party until [Let's
|
||||
Sign](https://github.com/documenso/backlog/issues/21) is realized.
|
||||
</Callout>
|
||||
|
||||
Current AES progress:
|
||||
|
||||
- Cryptographic signature sealing the document against tampering
|
||||
- Signing using dedicated hardware (Hardware Security Module)
|
||||
- Embedding signer identity in the cryptographic signature (planned)
|
||||
- Being a government-audited trusted qualified services provider (planned)
|
||||
|
||||
### When AES Is Appropriate
|
||||
|
||||
AES is used when stronger proof of identity and intent is needed:
|
||||
|
||||
- Financial services agreements
|
||||
- Real estate transactions (in some jurisdictions)
|
||||
- Healthcare consent forms
|
||||
- Government submissions
|
||||
- High-value contracts
|
||||
- Cross-border agreements within the EU
|
||||
|
||||
---
|
||||
|
||||
## Qualified Electronic Signatures (QES)
|
||||
|
||||
A Qualified Electronic Signature is the highest level of electronic signature under eIDAS. It is legally equivalent to a handwritten signature in all EU member states and carries a presumption of validity.
|
||||
|
||||
### Requirements
|
||||
|
||||
QES must meet all AES requirements plus:
|
||||
|
||||
1. **Qualified Certificate** - Issued by a Qualified Trust Service Provider (QTSP) that is accredited by an EU member state
|
||||
2. **Qualified Electronic Signature Creation Device (QSCD)** - The signature is created using hardware or software that meets specific security standards
|
||||
3. **Identity Verification** - In-person or equivalent remote verification compliant with eIDAS requirements
|
||||
|
||||
### Characteristics
|
||||
|
||||
| Aspect | Description |
|
||||
| -------------------------- | --------------------------------------------------------------------- |
|
||||
| **Technical Requirements** | Qualified certificate + qualified signature creation device |
|
||||
| **Identity Verification** | Strict verification by a Qualified Trust Service Provider |
|
||||
| **Legal Status** | Equivalent to handwritten signature across all EU member states |
|
||||
| **Implementation** | Requires integration with a QTSP; typically involves external service |
|
||||
|
||||
### Compliance Status
|
||||
|
||||
<Callout type="warn">
|
||||
Status: [Planned](https://github.com/documenso/backlog/issues/32) via third party until [Let's
|
||||
Sign](https://github.com/documenso/backlog/issues/21) is realized.
|
||||
</Callout>
|
||||
|
||||
### When QES Is Required
|
||||
|
||||
Certain transactions require or benefit from QES:
|
||||
|
||||
- Documents that legally require a handwritten signature under national law
|
||||
- Court filings and legal documents
|
||||
- Company formation documents
|
||||
- Land registry transactions
|
||||
- Notarized documents
|
||||
- Regulated financial transactions
|
||||
- Cross-border transactions requiring guaranteed recognition
|
||||
|
||||
---
|
||||
|
||||
## Comparison of Signature Levels
|
||||
|
||||
| Aspect | SES | AES | QES |
|
||||
| ------------------------- | ------------ | ------------------------ | --------------------- |
|
||||
| **Technical Complexity** | Low | Medium | High |
|
||||
| **Identity Verification** | None | Required | Strict (QTSP) |
|
||||
| **Legal Effect (EU)** | Admissible | Higher evidentiary value | Equal to handwritten |
|
||||
| **Cost** | Low | Medium | Higher |
|
||||
| **User Experience** | Simple | More steps | Most steps |
|
||||
| **Signer Requirements** | Email access | Identity verification | Certificate from QTSP |
|
||||
|
||||
### Legal Recognition
|
||||
|
||||
| Jurisdiction | SES | AES | QES |
|
||||
| ------------------ | ------------------------------- | -------------------------- | ------------------------------------ |
|
||||
| **European Union** | Valid, evidentiary value varies | Enhanced evidentiary value | Equivalent to handwritten |
|
||||
| **United States** | Valid under ESIGN/UETA | No formal distinction | No formal distinction |
|
||||
| **United Kingdom** | Valid | Enhanced value | Equivalent to handwritten (UK eIDAS) |
|
||||
| **Switzerland** | Valid | Valid | Equivalent to handwritten (ZertES) |
|
||||
|
||||
---
|
||||
|
||||
## What Documenso Provides
|
||||
|
||||
Documenso supports Simple Electronic Signatures (SES) with features that enhance evidentiary value:
|
||||
|
||||
### SES Features
|
||||
|
||||
- **Intent to Sign** - Signers actively interact with signature fields
|
||||
- **Email-Based Delivery** - Documents sent to specific email addresses
|
||||
- **Audit Trail** - Complete record of signing events, timestamps, and IP addresses
|
||||
- **Document Integrity** - Cryptographic sealing detects any post-signing modifications
|
||||
- **Record Retention** - Signed documents stored and accessible to all parties
|
||||
|
||||
### Additional Verification Options
|
||||
|
||||
- **Access Codes** - Require signers to enter a code before accessing documents
|
||||
- **Signing Order** - Control the sequence of signatures
|
||||
|
||||
### What Documenso Does Not Provide
|
||||
|
||||
| Capability | Status |
|
||||
| ----------------------------------------- | --------------------------------------------------------- |
|
||||
| **Qualified Electronic Signatures (QES)** | Not supported; requires QTSP integration |
|
||||
| **Advanced Electronic Signatures (AES)** | Partial; full AES requires identity verification services |
|
||||
| **Identity Verification (KYC)** | Not built-in |
|
||||
| **Qualified Certificates** | Not issued; would require QTSP status |
|
||||
|
||||
For transactions requiring AES or QES, you would need to integrate with external identity verification services or Qualified Trust Service Providers.
|
||||
|
||||
---
|
||||
|
||||
## ZertES (Swiss Federal Law)
|
||||
|
||||
<Callout type="warn">Status: [Planned](https://github.com/documenso/backlog/issues/34)</Callout>
|
||||
|
||||
ZertES is a Swiss federal law that regulates electronic signature compliance. It defines requirements similar to eIDAS for qualified electronic signatures within Switzerland.
|
||||
|
||||
---
|
||||
|
||||
## When You Need Higher Signature Levels
|
||||
|
||||
Consider using AES or QES when:
|
||||
|
||||
<Accordions type="multiple">
|
||||
<Accordion title="Legal Requirements">
|
||||
- National law requires a handwritten signature (QES may substitute)
|
||||
- Regulations specify signature requirements (e.g., certain financial or healthcare documents)
|
||||
- Cross-border enforceability is critical
|
||||
</Accordion>
|
||||
<Accordion title="Risk Factors">
|
||||
- High contract value or significant liability
|
||||
- Higher likelihood of disputes
|
||||
- Need for stronger non-repudiation
|
||||
- Counterparty or regulatory requirements specify higher levels
|
||||
</Accordion>
|
||||
<Accordion title="Industry Standards">
|
||||
- Financial services with regulatory oversight
|
||||
- Healthcare with patient consent requirements
|
||||
- Government or public sector contracts
|
||||
- Real estate transactions in regulated markets
|
||||
</Accordion>
|
||||
<Accordion title="Evaluating Your Needs">
|
||||
Most business transactions do not require AES or QES. Consider:
|
||||
|
||||
1. What does your jurisdiction require for this document type?
|
||||
2. What do your counterparties or customers expect?
|
||||
3. What is the risk if the signature is disputed?
|
||||
4. Does your industry have specific requirements?
|
||||
|
||||
When in doubt, consult with legal counsel to determine the appropriate signature level for your specific use case.
|
||||
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
---
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This documentation is provided for informational purposes only and does not constitute legal advice.
|
||||
|
||||
The appropriate signature level for your documents depends on:
|
||||
|
||||
- Your jurisdiction and applicable laws
|
||||
- The type of document being signed
|
||||
- Industry-specific regulations
|
||||
- Contractual requirements from counterparties
|
||||
- Risk tolerance and dispute likelihood
|
||||
|
||||
Electronic signature requirements vary significantly across jurisdictions and document types. Some transactions have specific legal requirements that may mandate particular signature levels or exclude electronic signatures entirely.
|
||||
|
||||
Consult with qualified legal counsel to determine the signature level requirements for your specific use case.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [E-Sign Compliance](/docs/compliance/esign) - ESIGN Act, UETA, eIDAS, and electronic signature laws
|
||||
- [Signing Certificates](/docs/concepts/signing-certificates) - How documents are digitally signed and verified
|
||||
- [Standards & Regulations](/docs/compliance/standards) - SOC 2, 21 CFR Part 11, and other frameworks
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Standards & Regulations
|
||||
description: Key technical standards that ensure digital signatures are secure, interoperable, and valid long-term.
|
||||
---
|
||||
|
||||
## PDF/A for Archival
|
||||
|
||||
PDF/A is an ISO-standardized version of PDF designed for long-term archival of electronic documents. Unlike standard PDFs, PDF/A files are self-contained and do not rely on external resources.
|
||||
|
||||
Key characteristics:
|
||||
|
||||
- All fonts must be embedded
|
||||
- No external content references allowed
|
||||
- No encryption that would prevent future access
|
||||
- Metadata must be embedded in XMP format
|
||||
- Color spaces must be device-independent or include ICC profiles
|
||||
|
||||
PDF/A has several conformance levels (PDF/A-1, PDF/A-2, PDF/A-3) with increasing capabilities. PDF/A-3, for example, allows embedding of arbitrary file formats as attachments.
|
||||
|
||||
For signed documents intended for long-term storage, PDF/A ensures the document remains readable and verifiable years or decades after signing.
|
||||
|
||||
## PAdES (PDF Advanced Electronic Signatures)
|
||||
|
||||
PAdES is a set of standards (ETSI EN 319 142) that defines profiles for electronic signatures in PDF documents. It builds on the PDF signature capabilities defined in ISO 32000 and adds requirements for long-term validity.
|
||||
|
||||
PAdES defines several signature profiles:
|
||||
|
||||
| Profile | Description |
|
||||
| --------- | ---------------------------------------------------- |
|
||||
| PAdES-B | Basic signature with signing certificate |
|
||||
| PAdES-T | Adds a trusted timestamp |
|
||||
| PAdES-LT | Adds validation data (certificates, revocation info) |
|
||||
| PAdES-LTA | Adds long-term archival timestamps |
|
||||
|
||||
Each level builds upon the previous, with PAdES-LTA providing the strongest guarantees for long-term signature validity. The inclusion of validation data and archival timestamps allows signatures to be verified even after certificates expire or CAs cease operations.
|
||||
|
||||
## ISO 32000 (PDF Standard)
|
||||
|
||||
ISO 32000 is the international standard that defines the PDF format. It specifies the technical foundation for digital signatures in PDF documents.
|
||||
|
||||
Relevant signature capabilities defined in ISO 32000:
|
||||
|
||||
- Signature field dictionaries and appearance streams
|
||||
- Cryptographic signature handlers
|
||||
- Certificate and timestamp embedding
|
||||
- Incremental updates for signature preservation
|
||||
- Document modification detection
|
||||
|
||||
ISO 32000-2 (PDF 2.0) introduced additional features including support for more signature algorithms and improved encryption options.
|
||||
|
||||
## X.509 Certificates
|
||||
|
||||
X.509 is the standard format for public key certificates used in digital signatures. These certificates bind a public key to an identity and are issued by Certificate Authorities (CAs).
|
||||
|
||||
A typical X.509 certificate contains:
|
||||
|
||||
- Subject (identity information)
|
||||
- Issuer (the CA that issued the certificate)
|
||||
- Public key
|
||||
- Validity period (not before / not after dates)
|
||||
- Serial number
|
||||
- Signature algorithm
|
||||
- Extensions (key usage, policies, etc.)
|
||||
|
||||
For document signing, certificates typically include the "digital signature" key usage extension. Qualified certificates under eIDAS regulations have additional requirements and provide higher levels of assurance.
|
||||
|
||||
Certificate validation involves checking:
|
||||
|
||||
1. The certificate chain up to a trusted root CA
|
||||
2. That no certificate in the chain has expired
|
||||
3. Revocation status via CRL or OCSP
|
||||
|
||||
## RFC 3161 (Timestamping)
|
||||
|
||||
RFC 3161 defines the Internet X.509 Public Key Infrastructure Time-Stamp Protocol (TSP). Timestamps prove that a document existed in a specific state at a particular point in time.
|
||||
|
||||
A timestamp token contains:
|
||||
|
||||
- Hash of the signed data
|
||||
- Time of issuance (from a trusted time source)
|
||||
- Identifier of the Time Stamping Authority (TSA)
|
||||
- TSA's digital signature
|
||||
|
||||
Timestamps serve two purposes in document signing:
|
||||
|
||||
1. **Proof of existence**: Demonstrates the document was signed before a certain time
|
||||
2. **Signature validity extension**: Allows signature verification after the signing certificate expires
|
||||
|
||||
Without a trusted timestamp, a signature can only be verified while the signing certificate remains valid. With a timestamp, the signature remains verifiable as long as the timestamp can be validated.
|
||||
|
||||
## What Documenso Implements
|
||||
|
||||
Documenso implements digital signatures with the following characteristics:
|
||||
|
||||
- **PDF signatures**: Documents are signed using the PDF signature capabilities defined in ISO 32000
|
||||
- **X.509 certificates**: Signatures use X.509 certificates for signer identification
|
||||
- **Timestamps**: RFC 3161 timestamps can be applied to signatures
|
||||
- **Signature visualization**: Signed documents include visual signature representations
|
||||
|
||||
For specific implementation details and configuration options, refer to the [signing certificates](/signing-certificates/overview) documentation.
|
||||
|
||||
Self-hosted deployments can configure their own signing certificates and timestamp authorities to meet specific compliance requirements.
|
||||
|
||||
## Related
|
||||
|
||||
- [Legal Validity](/compliance/legal-validity) - Legal frameworks for electronic signatures
|
||||
- [Signing Certificates Overview](/signing-certificates/overview) - Certificate configuration
|
||||
- [Audit Log](/features/audit-log) - Document activity tracking
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: Document Lifecycle
|
||||
description: Track document progress through draft, pending, completed, and rejected states.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
## Document States
|
||||
|
||||
A document can be in one of four states:
|
||||
|
||||
| State | Description |
|
||||
| ------------- | ----------------------------------------------------------------- |
|
||||
| **Draft** | Document is being prepared and has not been sent |
|
||||
| **Pending** | Document has been sent and is awaiting recipient actions |
|
||||
| **Completed** | All recipients have completed their required actions |
|
||||
| **Rejected** | A recipient has rejected the document (when rejection is enabled) |
|
||||
|
||||
## How a Document Moves Through States
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Draft -- Send --> Pending
|
||||
Pending -- All recipients complete --> Completed
|
||||
Pending -- Recipient rejects --> Rejected
|
||||
```
|
||||
|
||||
## Draft
|
||||
|
||||
When you upload a document or create one from a template, it starts in the **Draft** state. In this state, you can:
|
||||
|
||||
- Add and remove recipients
|
||||
- Assign roles to recipients (signer, approver, viewer, CC)
|
||||
- Add, move, and configure fields
|
||||
- Set signing order
|
||||
- Configure document settings (expiration, reminders, rejection)
|
||||
- Delete the document
|
||||
|
||||
A draft document is only visible to you (the owner) and team members with appropriate permissions. Recipients cannot see or access the document until you send it.
|
||||
|
||||
**Transition:** A draft becomes **Pending** when you send it to recipients.
|
||||
|
||||
## Pending
|
||||
|
||||
Once sent, a document enters the **Pending** state. Recipients receive email notifications with links to view and complete their assigned actions.
|
||||
|
||||
While pending, you can:
|
||||
|
||||
- View recipient progress
|
||||
- Resend notifications to recipients
|
||||
- Void the document (cancels all pending actions)
|
||||
|
||||
<Callout type="info">
|
||||
You cannot modify the document content, recipients, or fields while it is pending.
|
||||
</Callout>
|
||||
|
||||
**Transitions:**
|
||||
|
||||
- Becomes **Completed** when all recipients finish their required actions
|
||||
- Becomes **Rejected** if any recipient rejects the document (requires rejection to be enabled)
|
||||
|
||||
## Completed
|
||||
|
||||
A document reaches the **Completed** state when all recipients have fulfilled their roles:
|
||||
|
||||
- Signers have signed
|
||||
- Approvers have approved
|
||||
- Viewers have viewed (if view confirmation is required)
|
||||
|
||||
At completion:
|
||||
|
||||
- All parties receive a copy of the signed document
|
||||
- The document is sealed with a digital certificate
|
||||
- An audit log is attached showing all actions taken
|
||||
|
||||
<Callout type="info">
|
||||
Completed documents cannot be modified. You can download the signed PDF or view the audit trail.
|
||||
</Callout>
|
||||
|
||||
## Rejected
|
||||
|
||||
If you enable document rejection in settings, recipients can reject instead of signing. When any recipient rejects:
|
||||
|
||||
- The document immediately moves to **Rejected** state
|
||||
- Other pending recipients can no longer act on the document
|
||||
- The document owner is notified
|
||||
|
||||
<Callout type="info">
|
||||
Rejected documents cannot be modified or reactivated. To proceed, you need to create a new
|
||||
document.
|
||||
</Callout>
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [Recipient Roles](/docs/concepts/recipient-roles) - The different roles recipients can have
|
||||
- [Field Types](/docs/concepts/field-types) - Fields you can add to documents
|
||||
- [Signing Workflow](/docs/concepts/signing-workflow) - How the signing process works for recipients
|
||||
@@ -0,0 +1,314 @@
|
||||
---
|
||||
title: Field Types
|
||||
description: Placeholder types for capturing signatures, text, dates, and selections during signing.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
## Field Types Overview
|
||||
|
||||
| Field Type | Description | Auto-filled |
|
||||
| ---------- | ------------------------------------------------- | ----------- |
|
||||
| Signature | Recipient's signature (drawn, typed, or uploaded) | No |
|
||||
| Initials | Recipient's initials | No |
|
||||
| Email | Recipient's email address | Yes |
|
||||
| Name | Recipient's full name | Yes |
|
||||
| Date | Date the field was completed | Yes |
|
||||
| Text | Free-form text input | No |
|
||||
| Number | Numeric input with optional validation | No |
|
||||
| Radio | Single selection from a list of options | No |
|
||||
| Checkbox | Multiple selections from a list of options | No |
|
||||
| Dropdown | Single selection from a dropdown menu | No |
|
||||
|
||||
## Signature
|
||||
|
||||

|
||||
|
||||
The signature field captures the recipient's legally binding signature. Recipients can:
|
||||
|
||||
- **Draw** their signature using a mouse or touchscreen
|
||||
- **Type** their name and select a font style
|
||||
- **Upload** an image of their signature
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| --------- | -------------------------------------------------- |
|
||||
| Required | Whether the field must be completed before signing |
|
||||
| Read-only | Lock the field with a pre-filled value |
|
||||
| Label | Display text shown above the field |
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
- Contract execution
|
||||
- Agreement acceptance
|
||||
- Authorization approvals
|
||||
|
||||
<Callout type="info">Each signer must have at least one Signature field assigned to them.</Callout>
|
||||
|
||||
## Initials
|
||||
|
||||
The initials field captures abbreviated signatures, typically used to acknowledge individual pages or clauses.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| -------------- | -------------------------------------- |
|
||||
| Required | Whether the field must be completed |
|
||||
| Read-only | Lock the field with a pre-filled value |
|
||||
| Label | Display text shown above the field |
|
||||
| Text alignment | Left, center, or right alignment |
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
- Page acknowledgment
|
||||
- Clause acceptance
|
||||
- Change or amendment approval
|
||||
|
||||
## Email
|
||||
|
||||
The email field displays the recipient's email address. This field is automatically populated with the email address used to send the signing request.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| -------------- | --------------------------------------------------- |
|
||||
| Required | Whether the field must be completed |
|
||||
| Read-only | Lock the field (recommended for auto-filled values) |
|
||||
| Label | Display text shown above the field |
|
||||
| Text alignment | Left, center, or right alignment |
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
- Contact information sections
|
||||
- Identity verification
|
||||
- Record keeping
|
||||
|
||||
## Name
|
||||
|
||||
The name field captures the recipient's full name. When the recipient has a name on file, the field can be auto-populated.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| -------------- | -------------------------------------- |
|
||||
| Required | Whether the field must be completed |
|
||||
| Read-only | Lock the field with a pre-filled value |
|
||||
| Label | Display text shown above the field |
|
||||
| Text alignment | Left, center, or right alignment |
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
- Signature blocks
|
||||
- Party identification
|
||||
- Contact details
|
||||
|
||||
## Date
|
||||
|
||||
The date field records when the recipient completed the field or signed the document. By default, it auto-fills with the current date.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| -------------- | -------------------------------------- |
|
||||
| Required | Whether the field must be completed |
|
||||
| Read-only | Lock the field with a pre-filled value |
|
||||
| Label | Display text shown above the field |
|
||||
| Text alignment | Left, center, or right alignment |
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
- Signature date
|
||||
- Agreement effective date
|
||||
- Timestamp records
|
||||
|
||||
## Text
|
||||
|
||||

|
||||
|
||||
The text field accepts free-form text input from recipients. Use this for any information that doesn't fit other field types.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| --------------- | ------------------------------------------ |
|
||||
| Required | Whether the field must be completed |
|
||||
| Read-only | Lock the field with a pre-filled value |
|
||||
| Label | Display text shown above the field |
|
||||
| Placeholder | Hint text shown when the field is empty |
|
||||
| Default value | Pre-filled text that recipients can modify |
|
||||
| Character limit | Maximum number of characters allowed |
|
||||
| Text alignment | Left, center, or right alignment |
|
||||
| Line height | Spacing between lines of text |
|
||||
| Letter spacing | Spacing between characters |
|
||||
|
||||
### Rules
|
||||
|
||||
- A field cannot be both required and read-only at the same time
|
||||
- A read-only field must have a default text value (it cannot be empty)
|
||||
- The field is inserted automatically into the document if there is a default text value
|
||||
- The text field character count cannot exceed the character limit
|
||||
- The signer cannot modify a read-only field
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
- Address input
|
||||
- Company names
|
||||
- Job titles
|
||||
- Custom information
|
||||
|
||||
## Number
|
||||
|
||||
The number field accepts numeric input with optional validation constraints.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| -------------- | -------------------------------------------- |
|
||||
| Required | Whether the field must be completed |
|
||||
| Read-only | Lock the field with a pre-filled value |
|
||||
| Label | Display text shown above the field |
|
||||
| Placeholder | Hint text shown when the field is empty |
|
||||
| Default value | Pre-filled number that recipients can modify |
|
||||
| Minimum value | Lowest allowed number |
|
||||
| Maximum value | Highest allowed number |
|
||||
| Number format | Display format for the number |
|
||||
| Text alignment | Left, center, or right alignment |
|
||||
|
||||
### Rules
|
||||
|
||||
- The value must be a number
|
||||
- A field cannot be both required and read-only at the same time
|
||||
- A read-only field must have a default number value
|
||||
- If a default number and a maximum value are set, the default must be less than the maximum
|
||||
- If a default number and a minimum value are set, the default must be greater than the minimum
|
||||
- The value must match the number format if a number format is set
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
- Quantities
|
||||
- Pricing
|
||||
- Phone numbers
|
||||
- Employee IDs
|
||||
|
||||
## Radio
|
||||
|
||||
The radio field presents a list of options where the recipient can select exactly one.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| ----------------- | ---------------------------------------- |
|
||||
| Required | Whether a selection must be made |
|
||||
| Read-only | Lock the field with a pre-selected value |
|
||||
| Label | Display text shown above the field |
|
||||
| Options | List of selectable values |
|
||||
| Default selection | Pre-selected option |
|
||||
| Direction | Vertical or horizontal layout |
|
||||
|
||||
### Rules
|
||||
|
||||
- A field cannot be both required and read-only at the same time
|
||||
- A read-only field must have at least one option
|
||||
- The field auto-signs if there is a default value
|
||||
- The signer cannot select a value that's not in the options list
|
||||
- Only one option can be selected at a time
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
- Yes/No questions
|
||||
- Single-choice selections
|
||||
- Status indicators
|
||||
- Plan or tier selection
|
||||
|
||||
## Checkbox
|
||||
|
||||

|
||||
|
||||
The checkbox field presents a list of options where the recipient can select multiple items.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| ------------------ | ------------------------------------------- |
|
||||
| Required | Whether at least one selection must be made |
|
||||
| Read-only | Lock the field with pre-selected values |
|
||||
| Label | Display text shown above the field |
|
||||
| Options | List of selectable values |
|
||||
| Default selections | Pre-selected options |
|
||||
| Validation rule | Rules for minimum/maximum selections |
|
||||
| Direction | Vertical or horizontal layout |
|
||||
|
||||
### Rules
|
||||
|
||||
- A field cannot be both required and read-only at the same time
|
||||
- A read-only field must have at least one checked option
|
||||
- The field auto-signs if there are default values
|
||||
- The validation rule enforces selection counts: "At least", "At most", or "Exactly" a specified number of options
|
||||
- The signer cannot select a value that's not in the options list
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
- Terms and conditions acceptance
|
||||
- Multiple acknowledgments
|
||||
- Feature selection
|
||||
- Preference lists
|
||||
|
||||
## Dropdown
|
||||
|
||||

|
||||
|
||||
The dropdown field presents a list of options in a collapsible menu. Recipients select one option from the list.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Description |
|
||||
| ------------- | ---------------------------------------- |
|
||||
| Required | Whether a selection must be made |
|
||||
| Read-only | Lock the field with a pre-selected value |
|
||||
| Label | Display text shown above the field |
|
||||
| Options | List of selectable values |
|
||||
| Default value | Pre-selected option |
|
||||
|
||||
### Rules
|
||||
|
||||
- A field cannot be both required and read-only at the same time
|
||||
- A read-only field must have a default value
|
||||
- The default value must be one of the options
|
||||
- The field auto-signs if there is a default value
|
||||
- The signer cannot select a value that's not in the options list
|
||||
|
||||
### Common Use Cases
|
||||
|
||||
- Country or state selection
|
||||
- Department selection
|
||||
- Category classification
|
||||
- Status selection
|
||||
|
||||
## Common Configuration Options
|
||||
|
||||
All field types share these base configuration options:
|
||||
|
||||
| Option | Description | Default |
|
||||
| --------- | --------------------------------------------------- | ------- |
|
||||
| Required | Recipient must complete the field to finish signing | `false` |
|
||||
| Read-only | Field value cannot be changed by the recipient | `false` |
|
||||
| Label | Text displayed above or near the field | None |
|
||||
| Font size | Size of the text in the field (8-96px) | 12px |
|
||||
|
||||
## Validation
|
||||
|
||||
Fields validate input based on their type and configuration:
|
||||
|
||||
- **Required fields** must be completed before the recipient can finish signing
|
||||
- **Read-only fields** display pre-filled values that cannot be modified
|
||||
- **Number fields** validate against minimum and maximum values when configured
|
||||
- **Checkbox fields** can enforce a minimum or maximum number of selections
|
||||
|
||||
If validation fails, the recipient sees an error message and must correct the input before proceeding.
|
||||
|
||||
## Related
|
||||
|
||||
- [Add Fields to Documents](/docs/users/documents/add-fields) - Learn how to place fields on your documents
|
||||
- [Recipient Roles](/docs/concepts/recipient-roles) - Understand who can be assigned fields
|
||||
- [Fields API](/docs/developers/api/fields) - Programmatically add fields via the API
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Concepts
|
||||
description: Foundational concepts behind document signing, recipient roles, field types, and certificates.
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="Document Lifecycle"
|
||||
description="How documents move from draft to pending to completed or rejected."
|
||||
href="/docs/concepts/document-lifecycle"
|
||||
/>
|
||||
<Card
|
||||
title="Recipient Roles"
|
||||
description="Signers, approvers, viewers, assistants, and CC recipients."
|
||||
href="/docs/concepts/recipient-roles"
|
||||
/>
|
||||
<Card
|
||||
title="Field Types"
|
||||
description="Signatures, text, dates, checkboxes, dropdowns, and more."
|
||||
href="/docs/concepts/field-types"
|
||||
/>
|
||||
<Card
|
||||
title="Signing Workflow"
|
||||
description="The complete process from preparing a document to collecting signatures and sealing the final PDF."
|
||||
href="/docs/concepts/signing-workflow"
|
||||
/>
|
||||
<Card
|
||||
title="Signing Certificates"
|
||||
description="How documents are digitally signed and verified."
|
||||
href="/docs/concepts/signing-certificates"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
---
|
||||
|
||||
## How These Concepts Apply
|
||||
|
||||
These concepts work consistently across all ways you interact with Documenso:
|
||||
|
||||
- **Web application**: When you create documents in the UI, you'll select recipient roles, add fields, and track documents through their lifecycle states.
|
||||
|
||||
- **API integration**: The same concepts map directly to API endpoints. Documents have status fields, recipients have role properties, and fields have type configurations.
|
||||
|
||||
- **Self-hosting**: The signing certificate concept becomes particularly relevant when you deploy your own instance and configure your own certificates for document signing.
|
||||
|
||||
Understanding these fundamentals will make the rest of the documentation easier to follow.
|
||||
|
||||
---
|
||||
|
||||
## Related Sections
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="User Guide"
|
||||
description="Apply these concepts when sending documents."
|
||||
href="/docs/users"
|
||||
/>
|
||||
<Card
|
||||
title="Developer Guide"
|
||||
description="Work with these concepts through the API."
|
||||
href="/docs/developers"
|
||||
/>
|
||||
<Card
|
||||
title="Compliance"
|
||||
description="How these concepts relate to legal standards."
|
||||
href="/docs/compliance"
|
||||
/>
|
||||
</Cards>
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"title": "Concepts",
|
||||
"pages": [
|
||||
"document-lifecycle",
|
||||
"recipient-roles",
|
||||
"field-types",
|
||||
"signing-workflow",
|
||||
"signing-certificates"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
---
|
||||
title: Recipient Roles
|
||||
description: Signers, approvers, viewers, assistants, and CC recipients.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Role Overview
|
||||
|
||||
| Role | Action Required | Can Sign | Description |
|
||||
| --------- | --------------- | -------- | ---------------------------------------- |
|
||||
| Signer | Yes | Yes | Must sign the document |
|
||||
| Approver | Yes | Optional | Must approve the document |
|
||||
| Viewer | Yes | No | Must view the document |
|
||||
| Assistant | Yes | No | Can pre-fill fields for other recipients |
|
||||
| CC | No | No | Receives a copy after completion |
|
||||
|
||||
## Role Details
|
||||
|
||||
<Tabs items={['Signer', 'Approver', 'Viewer', 'Assistant', 'CC']}>
|
||||
<Tab value="Signer">
|
||||
|
||||
Signers are the primary recipients of a document. They must complete all signature fields assigned to them before the document can be finalized.
|
||||
|
||||
**What they can do:**
|
||||
|
||||
- Sign signature fields assigned to them
|
||||
- Fill out any other fields assigned to them (text, date, checkbox, etc.)
|
||||
- Download the document after signing
|
||||
|
||||
**What they cannot do:**
|
||||
|
||||
- Sign on behalf of other recipients
|
||||
- Modify fields assigned to other recipients
|
||||
|
||||
**When to use this role:**
|
||||
|
||||
- Contracts requiring a legally binding signature
|
||||
- Agreements where the recipient must formally consent
|
||||
- Any document that requires a signature to be valid
|
||||
|
||||
</Tab>
|
||||
<Tab value="Approver">
|
||||
|
||||
Approvers must review and approve the document, but signing is optional. The document cannot be completed until all approvers have given their approval.
|
||||
|
||||
**What they can do:**
|
||||
|
||||
- Approve or reject the document
|
||||
- Optionally add a signature if signature fields are assigned
|
||||
- Fill out fields assigned to them
|
||||
- Download the document after approval
|
||||
|
||||
**What they cannot do:**
|
||||
|
||||
- Complete the document without explicitly approving it
|
||||
- Modify fields assigned to other recipients
|
||||
|
||||
**When to use this role:**
|
||||
|
||||
- Documents requiring manager or supervisor approval
|
||||
- Workflows where review is required before final signatures
|
||||
- Compliance processes requiring sign-off from multiple parties
|
||||
|
||||
</Tab>
|
||||
<Tab value="Viewer">
|
||||
|
||||
Viewers must acknowledge that they have viewed the document. They cannot add signatures but must confirm they have reviewed the content.
|
||||
|
||||
**What they can do:**
|
||||
|
||||
- View the complete document
|
||||
- Confirm they have viewed it
|
||||
- Download the document after viewing
|
||||
|
||||
**What they cannot do:**
|
||||
|
||||
- Sign the document
|
||||
- Fill out fields (no fields can be assigned to viewers)
|
||||
- Modify the document in any way
|
||||
|
||||
**When to use this role:**
|
||||
|
||||
- Informational documents that require acknowledgment
|
||||
- Policies or disclosures that recipients must review
|
||||
- Documents where you need proof of receipt without a signature
|
||||
|
||||
</Tab>
|
||||
<Tab value="Assistant">
|
||||
|
||||
Assistants can prepare the document by pre-filling fields on behalf of other signers. This role is only available when sequential signing is enabled.
|
||||
|
||||
**What they can do:**
|
||||
|
||||
- Pre-fill suggested values in fields assigned to later signers
|
||||
- Help prepare the document for the actual signers
|
||||
- Fill out any fields specifically assigned to them
|
||||
|
||||
**What they cannot do:**
|
||||
|
||||
- Sign on behalf of other recipients
|
||||
- Submit the document as complete
|
||||
- Be used in parallel signing mode
|
||||
|
||||
**When to use this role:**
|
||||
|
||||
- Administrative staff preparing documents for executives to sign
|
||||
- Workflows where one person gathers information and another signs
|
||||
- Situations where you want to reduce the burden on the final signer
|
||||
|
||||
<Callout type="info">
|
||||
The Assistant role requires sequential signing to be enabled. You cannot use this role when
|
||||
recipients sign in parallel.
|
||||
</Callout>
|
||||
|
||||
</Tab>
|
||||
<Tab value="CC">
|
||||
|
||||
CC recipients receive a copy of the completed document but do not need to take any action. They are notified when the document is fully signed.
|
||||
|
||||
**What they can do:**
|
||||
|
||||
- Receive a copy of the completed document
|
||||
- Download the signed document
|
||||
|
||||
**What they cannot do:**
|
||||
|
||||
- Sign or approve the document
|
||||
- View the document before it is completed
|
||||
- Take any action that affects document completion
|
||||
|
||||
**When to use this role:**
|
||||
|
||||
- Keeping stakeholders informed about signed agreements
|
||||
- Sending copies to legal or compliance teams
|
||||
- Archiving completed documents with relevant parties
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Signing Order
|
||||
|
||||
You can control the sequence in which recipients receive and act on a document by enabling signing order.
|
||||
|
||||
<Tabs items={['Parallel signing (default)', 'Sequential signing']}>
|
||||
<Tab value="Parallel signing (default)">
|
||||
|
||||
All recipients receive the document simultaneously and can act in any order. The document is completed when all required recipients have finished their actions.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Sequential signing">
|
||||
|
||||
Recipients receive the document one at a time, in the order you specify. Each recipient must complete their action before the next recipient is notified.
|
||||
|
||||
To enable sequential signing:
|
||||
|
||||
1. When adding recipients, check the "Enable signing order" option
|
||||
2. Assign an order number to each recipient
|
||||
3. Recipients with the same order number can act simultaneously
|
||||
4. The document proceeds to the next order number only when all recipients at the current level have completed their actions
|
||||
|
||||
<Callout type="info">Sequential signing is required if you want to use the Assistant role.</Callout>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Related
|
||||
|
||||
- [Add Recipients](/users/documents/add-recipients) - How to add recipients to a document
|
||||
- [Field Types](/concepts/field-types) - Learn about the different field types you can assign to recipients
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: Signing Certificates
|
||||
description: Documenso digitally signs completed documents using X.509 certificates, providing cryptographic proof of authenticity and integrity.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## How Documenso Signs Documents
|
||||
|
||||
Documenso applies a digital signature to the PDF when all recipients complete their actions.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Create hash
|
||||
|
||||
Creates a cryptographic hash of the document content.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Sign the hash
|
||||
|
||||
Signs the hash using the certificate's private key.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Embed signature
|
||||
|
||||
Embeds the signature and certificate information into the PDF.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
The signature is applied at the platform level, not by individual signers. Each signer's actions (signature image, text, checkboxes) are recorded and sealed together in the final signed document.
|
||||
|
||||
## What the Signature Proves
|
||||
|
||||
The digital signature provides two guarantees:
|
||||
|
||||
| Guarantee | Description |
|
||||
| ---------------- | -------------------------------------------------------------------------- |
|
||||
| **Integrity** | The document has not been altered since signing |
|
||||
| **Authenticity** | The document was signed by the certificate holder (the Documenso instance) |
|
||||
|
||||
If anyone modifies the PDF after signing, the signature becomes invalid. PDF readers will display a warning that the document has been changed.
|
||||
|
||||
## Timestamps
|
||||
|
||||
Documenso can include a trusted timestamp from a Time Stamping Authority (TSA) in the signature. This proves when the document was signed, independent of the signer's system clock. Timestamps are important for:
|
||||
|
||||
- Legal evidence of when signing occurred
|
||||
- Long-term validation (LTV) of signatures
|
||||
- Compliance with archival requirements
|
||||
|
||||
## Viewing the Signature in PDF Readers
|
||||
|
||||
You can verify a signed document's signature in any PDF reader that supports digital signatures.
|
||||
|
||||
<Tabs items={['Adobe Acrobat', 'Other PDF readers']}>
|
||||
<Tab value="Adobe Acrobat">
|
||||
|
||||
1. Open the signed PDF
|
||||
2. Click the signature panel on the left, or click on a signature field
|
||||
3. View certificate details, signing time, and validation status
|
||||
|
||||
</Tab>
|
||||
<Tab value="Other PDF readers">
|
||||
|
||||
Preview, Foxit, and other PDF readers also display signature information, though the interface varies. Look for a signatures or security panel in the application menu.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
The signature panel shows who signed (certificate subject), when it was signed, whether the document has been modified, and certificate trust status.
|
||||
|
||||
## Certificate Trust and Validation
|
||||
|
||||
PDF readers validate signatures against their list of trusted Certificate Authorities (CAs). You may see different validation results depending on the certificate type:
|
||||
|
||||
| Certificate Type | Validation Result |
|
||||
| ---------------- | ---------------------------------------------------------------------- |
|
||||
| **CA-issued** | Green checkmark in Adobe if the CA is on the Adobe Approved Trust List |
|
||||
| **Self-signed** | Warning that the certificate is not from a trusted source |
|
||||
|
||||
<Callout type="info">
|
||||
A self-signed certificate still provides integrity verification. The document cannot be modified without invalidating the signature. The warning only indicates that a third-party CA has not verified the certificate issuer's identity.
|
||||
|
||||
For most use cases, self-signed certificates are sufficient. The signature still proves the document came from your Documenso instance and has not been tampered with.
|
||||
|
||||
</Callout>
|
||||
|
||||
## Using Custom Certificates
|
||||
|
||||
If you self-host Documenso, you can use your own signing certificate.
|
||||
|
||||
<Tabs items={['Self-signed', 'CA-issued']}>
|
||||
<Tab value="Self-signed">
|
||||
|
||||
Free and suitable for most use cases. The signature still proves document integrity and authenticity.
|
||||
|
||||
You may see a warning in PDF readers that the certificate is not from a trusted source, but the document cannot be modified without invalidating the signature.
|
||||
|
||||
</Tab>
|
||||
<Tab value="CA-issued">
|
||||
|
||||
Provides trusted validation in PDF readers (e.g. green checkmark in Adobe) when the CA is on the Adobe Approved Trust List.
|
||||
|
||||
Required for some compliance scenarios where third-party verification of the certificate issuer is needed.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
See [Signing Certificate Configuration](/docs/self-hosting/configuration/signing-certificate) for setup instructions.
|
||||
|
||||
## Related
|
||||
|
||||
- [Signature Levels](/docs/compliance/signature-levels) - Simple, Advanced, and Qualified electronic signatures
|
||||
- [Standards and Regulations](/docs/compliance/standards) - ESIGN, eIDAS, and other compliance frameworks
|
||||
- [Signing Certificate Configuration](/docs/self-hosting/configuration/signing-certificate) - Self-hosting certificate setup
|
||||
@@ -0,0 +1,260 @@
|
||||
---
|
||||
title: Signing Workflow
|
||||
description: The complete process from preparing a document to collecting signatures and sealing the final PDF.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Workflow Overview
|
||||
|
||||
A typical signing workflow follows these steps:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Prepare Document] --> B[Send Document] --> C[Notify Recipients] --> D[Recipients Sign] --> E[Seal & Finalize] --> F[Document Completed]
|
||||
```
|
||||
|
||||
1. **Prepare** - Upload the document, add recipients, and place fields
|
||||
2. **Send** - Distribute the document to recipients
|
||||
3. **Notify** - Recipients receive signing requests
|
||||
4. **Sign** - Recipients complete their assigned fields
|
||||
5. **Complete** - Document is sealed and distributed to all parties
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Prepare the document
|
||||
|
||||
Document preparation involves three main tasks: uploading, adding recipients, and placing fields.
|
||||
|
||||
**Upload the document**
|
||||
|
||||
Start by uploading a PDF. You can upload directly:
|
||||
|
||||
- from your device
|
||||
- create from an existing template
|
||||
- or duplicate a previously sent document.
|
||||
|
||||
Once uploaded, the document enters the **Draft** state.
|
||||
|
||||
**Add recipients**
|
||||
|
||||
Add the people who need to interact with the document. Each recipient needs:
|
||||
|
||||
- an email address
|
||||
- a name
|
||||
- a role
|
||||
|
||||
Available roles are:
|
||||
|
||||
| Role | Purpose |
|
||||
| --------- | --------------------------------------------------- |
|
||||
| Signer | Must sign the document |
|
||||
| Approver | Must approve (signature optional) |
|
||||
| Viewer | Must confirm they viewed the document |
|
||||
| Assistant | Pre-fills fields for other recipients |
|
||||
| CC | Receives a copy after completion (no action needed) |
|
||||
|
||||
**Place fields**
|
||||
|
||||
Add fields that recipients will complete. At minimum, each signer needs one signature field. You can also add:
|
||||
|
||||
- name
|
||||
- email
|
||||
- date
|
||||
- text
|
||||
- number
|
||||
- dropdown
|
||||
- checkbox
|
||||
- radio
|
||||
- initials fields
|
||||
|
||||
Each field is assigned to a specific recipient, indicated by color coding in the editor.
|
||||
|
||||
<Callout type="info">
|
||||
The document cannot be sent until every signer has at least one signature field assigned to them.
|
||||
</Callout>
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Send the document
|
||||
|
||||
When the document is ready, you send it to recipients. You have two distribution options:
|
||||
|
||||
<Tabs items={['Email distribution', 'Manual distribution']}>
|
||||
<Tab value="Email distribution">
|
||||
|
||||
Recipients receive an email notification with a link to sign.
|
||||
|
||||
You can customize the email:
|
||||
|
||||
- subject line
|
||||
- message body with personalized variables
|
||||
- reply-to address for recipient responses
|
||||
|
||||
</Tab>
|
||||
<Tab value="Manual distribution">
|
||||
|
||||
Generate signing links without sending emails.
|
||||
|
||||
Use this when you want to:
|
||||
|
||||
- send links via SMS or messaging apps
|
||||
- embed links in your own application
|
||||
- control notification timing yourself
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
After sending, the document moves from **Draft** to **Pending** status.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Recipients are notified
|
||||
|
||||
When you send a document via email, each recipient receives a notification containing:
|
||||
|
||||
- the document title
|
||||
- your name and email (or team name)
|
||||
- your custom message (or a role-specific default)
|
||||
- a unique signing link
|
||||
|
||||
The signing link is specific to each recipient and cannot be used by others. Links remain active until the document is completed, deleted, or expired.
|
||||
|
||||
**Signing order**
|
||||
|
||||
By default, all recipients are notified simultaneously (parallel signing). If you enable sequential signing, only recipients in the first signing position receive notifications initially. When they complete their actions, the next group is notified.
|
||||
|
||||
This continues until all recipients have been notified and completed their actions.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Recipients sign
|
||||
|
||||
When a recipient clicks their signing link, they see the document with their assigned fields highlighted. The signing experience depends on their role:
|
||||
|
||||
<Tabs items={['Signer', 'Approver', 'Viewer']}>
|
||||
<Tab value="Signer">
|
||||
|
||||
Signers must complete all required fields before they can finish. For signature fields, they can:
|
||||
|
||||
- draw a signature using mouse or touchscreen
|
||||
- type their name and select a font style
|
||||
- upload an image of their existing signature
|
||||
|
||||
After completing all fields, the signer clicks a button to submit. They receive a confirmation and can download a copy of the document showing their completed fields.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Approver">
|
||||
|
||||
Approvers review the document and must explicitly approve it. If signature fields are assigned, they can optionally sign.
|
||||
|
||||
The document cannot proceed until all approvers have given approval.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Viewer">
|
||||
|
||||
Viewers see the full document and must confirm they have viewed it. They cannot add signatures or modify any content.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
**Authentication**
|
||||
|
||||
You can require recipients to verify their identity before signing through:
|
||||
|
||||
- email verification (confirm access to the email address)
|
||||
- access code (enter a code you provide separately)
|
||||
- passkey (authenticate with a hardware or software passkey)
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Document is completed
|
||||
|
||||
Once all recipients with required actions have completed them, the document is finalized.
|
||||
|
||||
| Aspect | Description |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Sealing** | The completed document is sealed with a digital certificate that cryptographically signs the PDF, prevents modification without detection, and provides proof of authenticity. |
|
||||
| **Audit trail** | An audit log is generated and can be attached to the document. It records when the document was created and sent, when each recipient viewed and signed, IP addresses and timestamps for each action, and any authentication methods used. |
|
||||
| **Distribution** | All parties receive the completed document: signers, approvers, and viewers receive their copy via email; CC recipients receive their first notification with the completed document; the document owner can download the signed PDF from their dashboard. |
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Workflow Variations
|
||||
|
||||
Documenso supports several workflow variations to handle different signing scenarios.
|
||||
|
||||
<Tabs items={['Sequential signing', 'Approval workflows', 'Assistants for pre-filling', 'Direct links']}>
|
||||
<Tab value="Sequential signing">
|
||||
|
||||
When recipients must sign in a specific order, enable signing order:
|
||||
|
||||
1. Assign each recipient a signing position (1, 2, 3, etc.)
|
||||
2. Recipients at position 1 sign first
|
||||
3. Recipients at position 2 are notified only after position 1 completes
|
||||
4. Multiple recipients can share the same position to sign in parallel within that step
|
||||
|
||||
Use sequential signing when later signers:
|
||||
|
||||
- need to see what earlier signers entered
|
||||
- approval must happen before final signatures
|
||||
- company policy requires a specific signing order
|
||||
|
||||
</Tab>
|
||||
<Tab value="Approval workflows">
|
||||
|
||||
Combine approver and signer roles to create approval workflows:
|
||||
|
||||
1. Add approvers at signing position 1
|
||||
2. Add signers at signing position 2
|
||||
3. Approvers review and approve first
|
||||
4. Signers are notified only after approval is complete
|
||||
|
||||
<Callout type="warn">
|
||||
If an approver rejects the document (when rejection is enabled), the workflow stops and signers
|
||||
are never notified.
|
||||
</Callout>
|
||||
|
||||
</Tab>
|
||||
<Tab value="Assistants for pre-filling">
|
||||
|
||||
Use assistants to have one person prepare the document for another:
|
||||
|
||||
1. Add an assistant at signing position 1
|
||||
2. Add the final signer at signing position 2
|
||||
3. The assistant pre-fills fields with suggested values
|
||||
4. The signer reviews and completes their signature
|
||||
|
||||
This is useful when administrative staff prepare documents for executives or when gathering information from one person while another signs.
|
||||
|
||||
<Callout type="info">
|
||||
The Assistant role is only available when sequential signing is enabled.
|
||||
</Callout>
|
||||
|
||||
</Tab>
|
||||
<Tab value="Direct links">
|
||||
|
||||
For high-volume signing scenarios, you can create direct links that allow anyone to sign without receiving an individual invitation:
|
||||
|
||||
- Generate a public signing link for a document or template
|
||||
- Share the link on your website, in emails, or through other channels
|
||||
- Each person who accesses the link creates their own signing instance
|
||||
- Useful for waivers, consent forms, and public agreements
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Related Concepts
|
||||
|
||||
- [Document Lifecycle](/docs/concepts/document-lifecycle) - Understanding document states from draft to completion
|
||||
- [Recipient Roles](/docs/concepts/recipient-roles) - Detailed explanation of each role type
|
||||
- [Field Types](/docs/concepts/field-types) - All available field types and their configuration options
|
||||
- [Signing Certificates](/docs/concepts/signing-certificates) - How documents are digitally sealed
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Developer Mode
|
||||
description: Advanced development tools for debugging field coordinates and integrating with the Documenso API.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Developer mode provides additional tools and features to help you integrate and debug Documenso.
|
||||
|
||||
## Field Coordinates
|
||||
|
||||
Field coordinates represent the position of a field in a document. They are returned in the `pageX`, `pageY`, `width` and `height` properties of the field.
|
||||
|
||||
To enable field coordinates, add the `devmode=true` query parameter to the editor URL.
|
||||
|
||||
```bash
|
||||
# Legacy editor
|
||||
|
||||
https://app.documenso.com/t/<team-url>/documents/<envelope-id>/legacy_editor?devmode=true
|
||||
```
|
||||
|
||||

|
||||
|
||||
```bash
|
||||
# New editor
|
||||
|
||||
https://app.documenso.com/t/<team-url>/documents/<envelope-id>/edit?step=addFields&devmode=true
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Fields API](/docs/developers/api/fields) - Create and position fields via API
|
||||
- [Field Types](/docs/concepts/field-types) - Detailed field type reference
|
||||
@@ -0,0 +1,815 @@
|
||||
---
|
||||
title: Documents API
|
||||
description: Create, manage, and send documents for signing via the API.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
<Callout type="warn">
|
||||
This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference,
|
||||
see the [OpenAPI Reference](https://openapi.documenso.com).
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
[Documents](/docs/users/documents) (called "envelopes" in the API) are the core resource in Documenso. You can:
|
||||
|
||||
1. create documents with recipients and fields
|
||||
2. send them for signing
|
||||
3. track their status
|
||||
4. retrieve the completed PDFs
|
||||
|
||||
Each document contains one or more PDF files, a list of recipients, and the fields they need to fill.
|
||||
|
||||
## Document Object
|
||||
|
||||
A document object contains the following properties:
|
||||
|
||||
| Property | Type | Description |
|
||||
| --------------- | -------------- | -------------------------------------------------------------- |
|
||||
| `id` | string | Unique identifier (e.g., `envelope_abc123`) |
|
||||
| `type` | string | `DOCUMENT` or `TEMPLATE` |
|
||||
| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED`, or `REJECTED` |
|
||||
| `title` | string | Document title |
|
||||
| `source` | string | How the document was created: `DOCUMENT`, `TEMPLATE`, `API` |
|
||||
| `visibility` | string | Who can view: `EVERYONE`, `ADMIN`, `MANAGER_AND_ABOVE` |
|
||||
| `externalId` | string \| null | Your custom identifier for the document |
|
||||
| `createdAt` | string | ISO 8601 timestamp |
|
||||
| `updatedAt` | string | ISO 8601 timestamp |
|
||||
| `completedAt` | string \| null | Timestamp when all recipients completed signing |
|
||||
| `deletedAt` | string \| null | Timestamp if soft-deleted |
|
||||
| `recipients` | array | List of recipients and their signing status |
|
||||
| `fields` | array | Signature and form fields on the document |
|
||||
| `envelopeItems` | array | PDF files attached to the document |
|
||||
| `documentMeta` | object | Email settings, redirect URL, signing options |
|
||||
|
||||
### Example Document Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "envelope_abc123xyz",
|
||||
"type": "DOCUMENT",
|
||||
"status": "PENDING",
|
||||
"source": "API",
|
||||
"visibility": "EVERYONE",
|
||||
"title": "Service Agreement",
|
||||
"externalId": "contract-2025-001",
|
||||
"createdAt": "2025-01-15T10:30:00.000Z",
|
||||
"updatedAt": "2025-01-15T10:35:00.000Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"recipients": [
|
||||
{
|
||||
"id": 1,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Smith",
|
||||
"role": "SIGNER",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"signingOrder": 1
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"id": "field_123",
|
||||
"type": "SIGNATURE",
|
||||
"page": 1,
|
||||
"positionX": 10,
|
||||
"positionY": 80,
|
||||
"width": 30,
|
||||
"height": 5,
|
||||
"recipientId": 1
|
||||
}
|
||||
],
|
||||
"envelopeItems": [
|
||||
{
|
||||
"id": "envelope_item_xyz",
|
||||
"title": "contract.pdf",
|
||||
"order": 1
|
||||
}
|
||||
],
|
||||
"documentMeta": {
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hi, please review and sign this agreement.",
|
||||
"timezone": "America/New_York",
|
||||
"redirectUrl": "https://example.com/thank-you"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## List Documents
|
||||
|
||||
Retrieve a paginated list of documents.
|
||||
|
||||
```
|
||||
GET /envelope
|
||||
```
|
||||
|
||||
### Query Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------------ | ------- | ------------------------------------------------------------- |
|
||||
| `page` | integer | Page number (default: 1) |
|
||||
| `perPage` | integer | Results per page (default: 10, max: 100) |
|
||||
| `type` | string | Filter by `DOCUMENT` or `TEMPLATE` |
|
||||
| `status` | string | Filter by status: `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED` |
|
||||
| `source` | string | Filter by creation source |
|
||||
| `folderId` | string | Filter by folder ID |
|
||||
| `orderByColumn` | string | Sort field (only `createdAt` supported) |
|
||||
| `orderByDirection` | string | Sort direction: `asc` or `desc` (default: `desc`) |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
# List all documents
|
||||
curl -X GET "https://app.documenso.com/api/v2/envelope" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx"
|
||||
|
||||
# Filter by status and paginate
|
||||
|
||||
curl -X GET "https://app.documenso.com/api/v2/envelope?status=PENDING&page=1&perPage=20" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx"
|
||||
|
||||
# List only documents (not templates)
|
||||
|
||||
curl -X GET "https://app.documenso.com/api/v2/envelope?type=DOCUMENT" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx"
|
||||
|
||||
````
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const API_TOKEN = process.env.DOCUMENSO_API_TOKEN;
|
||||
const BASE_URL = 'https://app.documenso.com/api/v2';
|
||||
|
||||
// List all documents
|
||||
const response = await fetch(`${BASE_URL}/envelope`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: API_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
const { data, pagination } = await response.json();
|
||||
console.log(`Found ${pagination.totalItems} documents`);
|
||||
|
||||
// Filter by status
|
||||
const pendingResponse = await fetch(
|
||||
`${BASE_URL}/envelope?status=PENDING&page=1&perPage=20`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: API_TOKEN,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const pendingDocs = await pendingResponse.json();
|
||||
````
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "envelope_abc123",
|
||||
"type": "DOCUMENT",
|
||||
"status": "PENDING",
|
||||
"title": "Service Agreement",
|
||||
"createdAt": "2025-01-15T10:30:00.000Z",
|
||||
"updatedAt": "2025-01-15T10:35:00.000Z",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 1,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Smith",
|
||||
"role": "SIGNER",
|
||||
"signingStatus": "NOT_SIGNED"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"perPage": 10,
|
||||
"totalPages": 5,
|
||||
"totalItems": 42
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Get Document
|
||||
|
||||
Retrieve a single document by ID.
|
||||
|
||||
```
|
||||
GET /envelope/{envelopeId}
|
||||
```
|
||||
|
||||
### Path Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------ | ------ | ----------------------------------------- |
|
||||
| `envelopeId` | string | The document ID (e.g., `envelope_abc123`) |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X GET "https://app.documenso.com/api/v2/envelope/envelope_abc123" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx"
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const envelopeId = 'envelope_abc123';
|
||||
|
||||
const response = await fetch(`https://app.documenso.com/api/v2/envelope/${envelopeId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
},
|
||||
});
|
||||
|
||||
const document = await response.json();
|
||||
console.log(document.title, document.status);
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
Returns the full document object including recipients, fields, and envelope items.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "envelope_abc123",
|
||||
"type": "DOCUMENT",
|
||||
"status": "PENDING",
|
||||
"title": "Service Agreement",
|
||||
"recipients": [...],
|
||||
"fields": [...],
|
||||
"envelopeItems": [...],
|
||||
"documentMeta": {...}
|
||||
}
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## Create Document
|
||||
|
||||
Create a new document with optional recipients and fields in a single request.
|
||||
|
||||
```
|
||||
POST /envelope/create
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
The request uses `multipart/form-data` with two parts:
|
||||
|
||||
| Part | Type | Description |
|
||||
| --------- | ------- | ---------------------- |
|
||||
| `payload` | JSON | Document configuration |
|
||||
| `files` | File(s) | One or more PDF files |
|
||||
|
||||
### Payload Schema
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------ | ------ | -------- | ------------------------------------------- |
|
||||
| `type` | string | Yes | Must be `DOCUMENT` |
|
||||
| `title` | string | Yes | Document title |
|
||||
| `externalId` | string | No | Your custom identifier |
|
||||
| `visibility` | string | No | `EVERYONE`, `ADMIN`, or `MANAGER_AND_ABOVE` |
|
||||
| `folderId` | string | No | Folder ID to create the document in |
|
||||
| `recipients` | array | No | Recipients with optional fields |
|
||||
| `meta` | object | No | Email subject, message, redirect URL, etc. |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/create" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F 'payload={
|
||||
"type": "DOCUMENT",
|
||||
"title": "Service Agreement",
|
||||
"externalId": "contract-2025-001",
|
||||
"recipients": [
|
||||
{
|
||||
"email": "signer@example.com",
|
||||
"name": "John Smith",
|
||||
"role": "SIGNER",
|
||||
"fields": [
|
||||
{
|
||||
"identifier": 0,
|
||||
"type": "SIGNATURE",
|
||||
"page": 1,
|
||||
"positionX": 10,
|
||||
"positionY": 80,
|
||||
"width": 30,
|
||||
"height": 5
|
||||
},
|
||||
{
|
||||
"identifier": 0,
|
||||
"type": "DATE",
|
||||
"page": 1,
|
||||
"positionX": 50,
|
||||
"positionY": 80,
|
||||
"width": 20,
|
||||
"height": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"subject": "Please sign this agreement",
|
||||
"message": "Hi John, please review and sign the attached agreement.",
|
||||
"redirectUrl": "https://example.com/thank-you"
|
||||
}
|
||||
}' \
|
||||
-F "files=@./contract.pdf;type=application/pdf"
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
import fs from 'fs';
|
||||
import FormData from 'form-data';
|
||||
|
||||
const form = new FormData();
|
||||
|
||||
const payload = {
|
||||
type: 'DOCUMENT',
|
||||
title: 'Service Agreement',
|
||||
externalId: 'contract-2025-001',
|
||||
recipients: [
|
||||
{
|
||||
email: 'signer@example.com',
|
||||
name: 'John Smith',
|
||||
role: 'SIGNER',
|
||||
fields: [
|
||||
{
|
||||
identifier: 0,
|
||||
type: 'SIGNATURE',
|
||||
page: 1,
|
||||
positionX: 10,
|
||||
positionY: 80,
|
||||
width: 30,
|
||||
height: 5,
|
||||
},
|
||||
{
|
||||
identifier: 0,
|
||||
type: 'DATE',
|
||||
page: 1,
|
||||
positionX: 50,
|
||||
positionY: 80,
|
||||
width: 20,
|
||||
height: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
meta: {
|
||||
subject: 'Please sign this agreement',
|
||||
message: 'Hi John, please review and sign the attached agreement.',
|
||||
redirectUrl: 'https://example.com/thank-you',
|
||||
},
|
||||
};
|
||||
|
||||
form.append('payload', JSON.stringify(payload));
|
||||
form.append('files', fs.createReadStream('./contract.pdf'), {
|
||||
contentType: 'application/pdf',
|
||||
});
|
||||
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
},
|
||||
body: form,
|
||||
});
|
||||
|
||||
const { id } = await response.json();
|
||||
console.log('Created document:', id);
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "envelope_abc123xyz"
|
||||
}
|
||||
````
|
||||
|
||||
### Field Positioning
|
||||
|
||||
Field positions use percentage values (0-100) relative to the PDF page:
|
||||
|
||||
| Parameter | Description |
|
||||
| ------------ | ---------------------------------------------------------- |
|
||||
| `positionX` | Horizontal position from left edge (0 = left, 100 = right) |
|
||||
| `positionY` | Vertical position from top edge (0 = top, 100 = bottom) |
|
||||
| `width` | Field width as percentage of page width |
|
||||
| `height` | Field height as percentage of page height |
|
||||
| `page` | Page number (1-indexed) |
|
||||
| `identifier` | File index (0 for first file) or filename |
|
||||
|
||||
### Field Types
|
||||
|
||||
| Type | Description |
|
||||
| ----------- | --------------------------- |
|
||||
| `SIGNATURE` | Signature field |
|
||||
| `INITIALS` | Initials field |
|
||||
| `NAME` | Auto-filled recipient name |
|
||||
| `EMAIL` | Auto-filled recipient email |
|
||||
| `DATE` | Signing date |
|
||||
| `TEXT` | Free text input |
|
||||
| `NUMBER` | Numeric input |
|
||||
| `CHECKBOX` | Checkbox selection |
|
||||
| `RADIO` | Radio button group |
|
||||
| `DROPDOWN` | Dropdown selection |
|
||||
|
||||
### Recipient Roles
|
||||
|
||||
| Role | Description |
|
||||
| ---------- | ----------------------------------------- |
|
||||
| `SIGNER` | Must sign the document |
|
||||
| `APPROVER` | Must approve before signers can sign |
|
||||
| `CC` | Receives a copy but doesn't sign |
|
||||
| `VIEWER` | Can view the document but takes no action |
|
||||
|
||||
---
|
||||
|
||||
## Update Document
|
||||
|
||||
Update a document's properties. Only works on documents in `DRAFT` status.
|
||||
|
||||
```
|
||||
POST /envelope/update
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------ | ------ | -------- | ------------------------------------ |
|
||||
| `envelopeId` | string | Yes | Document ID |
|
||||
| `data` | object | No | Document properties to update |
|
||||
| `meta` | object | No | Email and signing settings to update |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/update" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"envelopeId": "envelope_abc123",
|
||||
"data": {
|
||||
"title": "Updated Service Agreement",
|
||||
"externalId": "contract-2025-001-v2"
|
||||
},
|
||||
"meta": {
|
||||
"subject": "Updated: Please sign this agreement",
|
||||
"redirectUrl": "https://example.com/signed"
|
||||
}
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
envelopeId: 'envelope_abc123',
|
||||
data: {
|
||||
title: 'Updated Service Agreement',
|
||||
externalId: 'contract-2025-001-v2',
|
||||
},
|
||||
meta: {
|
||||
subject: 'Updated: Please sign this agreement',
|
||||
redirectUrl: 'https://example.com/signed',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const document = await response.json();
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Send Document
|
||||
|
||||
Send a document to recipients for signing. This changes the status from `DRAFT` to `PENDING`.
|
||||
|
||||
```
|
||||
|
||||
POST /envelope/distribute
|
||||
|
||||
````
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `envelopeId` | string | Yes | Document ID |
|
||||
| `meta` | object | No | Override email settings for this send |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
# Basic send
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/distribute" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"envelopeId": "envelope_abc123"
|
||||
}'
|
||||
|
||||
# Send with custom email settings
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/distribute" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"envelopeId": "envelope_abc123",
|
||||
"meta": {
|
||||
"subject": "Action Required: Sign Agreement",
|
||||
"message": "Please sign this document by end of day.",
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
}'
|
||||
````
|
||||
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/distribute', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
envelopeId: 'envelope_abc123',
|
||||
meta: {
|
||||
subject: 'Action Required: Sign Agreement',
|
||||
message: 'Please sign this document by end of day.',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const { id, recipients } = await response.json();
|
||||
|
||||
// Recipients now include signing URLs
|
||||
recipients.forEach((r) => {
|
||||
console.log(`${r.email}: ${r.signingUrl}`);
|
||||
});
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
The response includes signing URLs for each recipient:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"id": "envelope_abc123",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "John Smith",
|
||||
"email": "signer@example.com",
|
||||
"token": "abc123xyz",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": 1,
|
||||
"signingUrl": "https://app.documenso.com/sign/abc123xyz"
|
||||
}
|
||||
]
|
||||
}
|
||||
````
|
||||
|
||||
<Callout type="info">
|
||||
Use the `signingUrl` to redirect recipients directly to the signing page, or let them use the
|
||||
email link.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Delete Document
|
||||
|
||||
Delete a document. Completed documents cannot be deleted.
|
||||
|
||||
```
|
||||
POST /envelope/delete
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------ | ------ | -------- | ----------- |
|
||||
| `envelopeId` | string | Yes | Document ID |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/delete" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"envelopeId": "envelope_abc123"
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/delete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
envelopeId: 'envelope_abc123',
|
||||
}),
|
||||
});
|
||||
|
||||
const { success } = await response.json();
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## Get Multiple Documents
|
||||
|
||||
Retrieve multiple documents by their IDs in a single request.
|
||||
|
||||
```
|
||||
POST /envelope/get-many
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------- | ----- | -------- | --------------------- |
|
||||
| `envelopeIds` | array | Yes | Array of document IDs |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/get-many" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"envelopeIds": ["envelope_abc123", "envelope_def456", "envelope_ghi789"]
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/get-many', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
envelopeIds: ['envelope_abc123', 'envelope_def456', 'envelope_ghi789'],
|
||||
}),
|
||||
});
|
||||
|
||||
const documents = await response.json();
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Document Statuses
|
||||
|
||||
| Status | Description |
|
||||
| --- | --- |
|
||||
| `DRAFT` | Document is being prepared. Recipients have not been notified. |
|
||||
| `PENDING` | Document has been sent. Waiting for recipients to sign. |
|
||||
| `COMPLETED` | All recipients have signed. Document is sealed. |
|
||||
| `REJECTED` | A recipient rejected the document. |
|
||||
|
||||
### Status Transitions
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
DRAFT --> PENDING --> COMPLETED
|
||||
PENDING --> REJECTED
|
||||
```
|
||||
|
||||
- **DRAFT to PENDING**: Call the distribute endpoint
|
||||
- **PENDING to COMPLETED**: All recipients complete their signing
|
||||
- **PENDING to REJECTED**: A recipient rejects the document
|
||||
|
||||
<Callout type="warn">
|
||||
You cannot modify recipients or fields after a document moves to `PENDING` status.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Filtering and Pagination
|
||||
|
||||
### Pagination Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| --------- | ------- | ------- | --------------------------- |
|
||||
| `page` | integer | 1 | Page number |
|
||||
| `perPage` | integer | 10 | Results per page (max: 100) |
|
||||
|
||||
### Filter Parameters
|
||||
|
||||
| Parameter | Values | Description |
|
||||
| ---------- | ------------------------------------------- | ------------------------- |
|
||||
| `type` | `DOCUMENT`, `TEMPLATE` | Filter by envelope type |
|
||||
| `status` | `DRAFT`, `PENDING`, `COMPLETED`, `REJECTED` | Filter by status |
|
||||
| `source` | `DOCUMENT`, `TEMPLATE`, `API` | Filter by creation source |
|
||||
| `folderId` | string | Filter by folder |
|
||||
|
||||
### Sorting
|
||||
|
||||
| Parameter | Values | Description |
|
||||
| ------------------ | ------------- | -------------------------------- |
|
||||
| `orderByColumn` | `createdAt` | Field to sort by |
|
||||
| `orderByDirection` | `asc`, `desc` | Sort direction (default: `desc`) |
|
||||
|
||||
### Example: Fetch All Pending Documents
|
||||
|
||||
```typescript
|
||||
async function getAllPendingDocuments() {
|
||||
const documents = [];
|
||||
let page = 1;
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await fetch(
|
||||
`https://app.documenso.com/api/v2/envelope?status=PENDING&page=${page}&perPage=100`,
|
||||
{
|
||||
headers: { Authorization: 'api_xxxxxxxxxxxxxxxx' },
|
||||
},
|
||||
);
|
||||
|
||||
const { data, pagination } = await response.json();
|
||||
documents.push(...data);
|
||||
|
||||
hasMore = page < pagination.totalPages;
|
||||
page++;
|
||||
}
|
||||
|
||||
return documents;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Recipients API](/docs/developers/api/recipients) - Add and manage document recipients
|
||||
- [Fields API](/docs/developers/api/fields) - Add signature and form fields
|
||||
- [Templates API](/docs/developers/api/templates) - Create reusable document templates
|
||||
- [Webhooks](/docs/developers/webhooks) - Get notified when documents are signed
|
||||
@@ -0,0 +1,738 @@
|
||||
---
|
||||
title: Fields API
|
||||
description: Add signature and form fields to documents via API.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
<Callout type="warn">
|
||||
This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference,
|
||||
see the [OpenAPI Reference](https://openapi.documenso.com).
|
||||
</Callout>
|
||||
|
||||
## Field Object
|
||||
|
||||
| Property | Type | Description |
|
||||
| ---------------- | -------------- | -------------------------------------------- |
|
||||
| `id` | number | Unique field identifier |
|
||||
| `secondaryId` | string | Secondary identifier for audit logs |
|
||||
| `type` | string | Field type (see [Field Types](#field-types)) |
|
||||
| `recipientId` | number | ID of the recipient assigned to this field |
|
||||
| `envelopeId` | number | ID of the parent envelope |
|
||||
| `envelopeItemId` | string | ID of the PDF item the field is placed on |
|
||||
| `page` | number | Page number (1-indexed) |
|
||||
| `positionX` | number | X coordinate as percentage (0-100) |
|
||||
| `positionY` | number | Y coordinate as percentage (0-100) |
|
||||
| `width` | number | Width as percentage of page (0-100) |
|
||||
| `height` | number | Height as percentage of page (0-100) |
|
||||
| `customText` | string | Value entered by the recipient |
|
||||
| `inserted` | boolean | Whether the field has been completed |
|
||||
| `fieldMeta` | object \| null | Type-specific configuration options |
|
||||
|
||||
### Example Field Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 456,
|
||||
"secondaryId": "field_abc123",
|
||||
"type": "SIGNATURE",
|
||||
"recipientId": 123,
|
||||
"envelopeId": 789,
|
||||
"envelopeItemId": "envelope_item_xyz",
|
||||
"page": 1,
|
||||
"positionX": 10,
|
||||
"positionY": 80,
|
||||
"width": 30,
|
||||
"height": 5,
|
||||
"customText": "",
|
||||
"inserted": false,
|
||||
"fieldMeta": {
|
||||
"type": "signature",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Field Types
|
||||
|
||||
| Type | Description | Auto-filled |
|
||||
| ---------------- | ----------------------------------------- | ----------- |
|
||||
| `SIGNATURE` | Drawn, typed, or uploaded signature | No |
|
||||
| `FREE_SIGNATURE` | Unrestricted signature without validation | No |
|
||||
| `INITIALS` | Recipient's initials | No |
|
||||
| `NAME` | Recipient's full name | Yes |
|
||||
| `EMAIL` | Recipient's email address | Yes |
|
||||
| `DATE` | Date the field was completed | Yes |
|
||||
| `TEXT` | Free-form text input | No |
|
||||
| `NUMBER` | Numeric input with optional validation | No |
|
||||
| `RADIO` | Single selection from options | No |
|
||||
| `CHECKBOX` | Multiple selections from options | No |
|
||||
| `DROPDOWN` | Single selection from a dropdown menu | No |
|
||||
|
||||
---
|
||||
|
||||
## Get Field
|
||||
|
||||
Retrieve a single field by ID.
|
||||
|
||||
```
|
||||
GET /envelope/field/{fieldId}
|
||||
```
|
||||
|
||||
### Path Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ------ | ------------ |
|
||||
| `fieldId` | number | The field ID |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X GET "https://app.documenso.com/api/v2/envelope/field/456" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx"
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch(
|
||||
'https://app.documenso.com/api/v2/envelope/field/456',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const field = await response.json();
|
||||
console.log(field.type, field.page);
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
Returns the field object.
|
||||
|
||||
---
|
||||
|
||||
## Create Fields
|
||||
|
||||
Add one or more fields to a document.
|
||||
|
||||
```
|
||||
|
||||
POST /envelope/field/create-many
|
||||
|
||||
````
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ----------- | ------ | -------- | ------------------------------- |
|
||||
| `documentId`| number | Yes | The document ID |
|
||||
| `fields` | array | Yes | Array of field configurations |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/field/create-many" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"documentId": 123,
|
||||
"fields": [
|
||||
{
|
||||
"type": "SIGNATURE",
|
||||
"recipientId": 456,
|
||||
"pageNumber": 1,
|
||||
"pageX": 10,
|
||||
"pageY": 80,
|
||||
"width": 30,
|
||||
"height": 5
|
||||
},
|
||||
{
|
||||
"type": "DATE",
|
||||
"recipientId": 456,
|
||||
"pageNumber": 1,
|
||||
"pageX": 50,
|
||||
"pageY": 80,
|
||||
"width": 20,
|
||||
"height": 3
|
||||
},
|
||||
{
|
||||
"type": "TEXT",
|
||||
"recipientId": 456,
|
||||
"pageNumber": 1,
|
||||
"pageX": 10,
|
||||
"pageY": 70,
|
||||
"width": 40,
|
||||
"height": 4,
|
||||
"fieldMeta": {
|
||||
"type": "text",
|
||||
"label": "Job Title",
|
||||
"placeholder": "Enter your job title",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}'
|
||||
````
|
||||
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch(
|
||||
'https://app.documenso.com/api/v2/envelope/field/create-many',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
documentId: 123,
|
||||
fields: [
|
||||
{
|
||||
type: 'SIGNATURE',
|
||||
recipientId: 456,
|
||||
pageNumber: 1,
|
||||
pageX: 10,
|
||||
pageY: 80,
|
||||
width: 30,
|
||||
height: 5,
|
||||
},
|
||||
{
|
||||
type: 'DATE',
|
||||
recipientId: 456,
|
||||
pageNumber: 1,
|
||||
pageX: 50,
|
||||
pageY: 80,
|
||||
width: 20,
|
||||
height: 3,
|
||||
},
|
||||
{
|
||||
type: 'TEXT',
|
||||
recipientId: 456,
|
||||
pageNumber: 1,
|
||||
pageX: 10,
|
||||
pageY: 70,
|
||||
width: 40,
|
||||
height: 4,
|
||||
fieldMeta: {
|
||||
type: 'text',
|
||||
label: 'Job Title',
|
||||
placeholder: 'Enter your job title',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const { fields } = await response.json();
|
||||
console.log(`Created ${fields.length} fields`);
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"id": 101,
|
||||
"type": "SIGNATURE",
|
||||
"recipientId": 456,
|
||||
"page": 1,
|
||||
"positionX": 10,
|
||||
"positionY": 80,
|
||||
"width": 30,
|
||||
"height": 5
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"type": "DATE",
|
||||
"recipientId": 456,
|
||||
"page": 1,
|
||||
"positionX": 50,
|
||||
"positionY": 80,
|
||||
"width": 20,
|
||||
"height": 3
|
||||
},
|
||||
{
|
||||
"id": 103,
|
||||
"type": "TEXT",
|
||||
"recipientId": 456,
|
||||
"page": 1,
|
||||
"positionX": 10,
|
||||
"positionY": 70,
|
||||
"width": 40,
|
||||
"height": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## Update Fields
|
||||
|
||||
Update one or more fields in a single request.
|
||||
|
||||
```
|
||||
POST /envelope/field/update-many
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------ | ------ | -------- | ----------------------------- |
|
||||
| `documentId` | number | Yes | The document ID |
|
||||
| `fields` | array | Yes | Array of field update objects |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/field/update-many" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"documentId": 123,
|
||||
"fields": [
|
||||
{
|
||||
"id": 101,
|
||||
"type": "SIGNATURE",
|
||||
"pageY": 85
|
||||
},
|
||||
{
|
||||
"id": 102,
|
||||
"type": "DATE",
|
||||
"pageY": 85
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch(
|
||||
'https://app.documenso.com/api/v2/envelope/field/update-many',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
documentId: 123,
|
||||
fields: [
|
||||
{ id: 101, type: 'SIGNATURE', pageY: 85 },
|
||||
{ id: 102, type: 'DATE', pageY: 85 },
|
||||
],
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const { fields } = await response.json();
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"fields": [
|
||||
{ "id": 101, "type": "SIGNATURE", "positionY": 85 },
|
||||
{ "id": 102, "type": "DATE", "positionY": 85 }
|
||||
]
|
||||
}
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## Delete Field
|
||||
|
||||
Remove a field from a document.
|
||||
|
||||
```
|
||||
POST /envelope/field/delete
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| --------- | ------ | -------- | ------------ |
|
||||
| `fieldId` | number | Yes | The field ID |
|
||||
|
||||
### Code Examples
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/field/delete" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"fieldId": 456
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch(
|
||||
'https://app.documenso.com/api/v2/envelope/field/delete',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
fieldId: 456,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
const { success } = await response.json();
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## Field Positioning
|
||||
|
||||
Fields use percentage-based coordinates relative to the PDF page dimensions.
|
||||
|
||||
| Property | Range | Description |
|
||||
| ----------- | ----- | -------------------------------------------------- |
|
||||
| `positionX` | 0-100 | Horizontal position from left edge (0 = left edge) |
|
||||
| `positionY` | 0-100 | Vertical position from top edge (0 = top edge) |
|
||||
| `width` | 0-100 | Field width as percentage of page width |
|
||||
| `height` | 0-100 | Field height as percentage of page height |
|
||||
| `page` | 1+ | Page number (1-indexed) |
|
||||
|
||||
### Coordinate System
|
||||
|
||||
```
|
||||
(0,0) ─────────────────────────── (100,0)
|
||||
│ │
|
||||
│ ┌─────────┐ │
|
||||
│ │ Field │ (pageX: 10, │
|
||||
│ │ │ pageY: 20, │
|
||||
│ └─────────┘ width: 30, │
|
||||
│ height: 5) │
|
||||
│ │
|
||||
(0,100) ─────────────────────────(100,100)
|
||||
```
|
||||
|
||||
### Example: Position a Signature at Bottom Right
|
||||
|
||||
```typescript
|
||||
const field = {
|
||||
type: 'SIGNATURE',
|
||||
recipientId: 123,
|
||||
pageNumber: 1,
|
||||
pageX: 60, // 60% from left
|
||||
pageY: 85, // 85% from top (near bottom)
|
||||
width: 30, // 30% of page width
|
||||
height: 8, // 8% of page height
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Placeholder-Based Field Positioning
|
||||
|
||||
Instead of specifying exact coordinates, you can position fields using placeholder text embedded in your PDF. Include placeholder markers such as `{{signature, r1}}` in your document, and Documenso will create fields at those locations when the document is uploaded.
|
||||
|
||||
This approach is useful when generating PDFs programmatically or using templates with consistent layouts.
|
||||
|
||||
See the [PDF Placeholders](/docs/users/documents/advanced/pdf-placeholders) guide for the full placeholder format reference, including supported field types, recipient identifiers, and field options.
|
||||
|
||||
---
|
||||
|
||||
## Field Meta Options
|
||||
|
||||
Each field type supports specific configuration through `fieldMeta`.
|
||||
|
||||
### Common Options
|
||||
|
||||
All field types support these base options:
|
||||
|
||||
| Option | Type | Description |
|
||||
| ------------- | ------- | --------------------------------------- |
|
||||
| `label` | string | Display text shown near the field |
|
||||
| `placeholder` | string | Hint text when field is empty |
|
||||
| `required` | boolean | Whether field must be completed |
|
||||
| `readOnly` | boolean | Lock field with a pre-filled value |
|
||||
| `fontSize` | number | Text size in pixels (8-96, default: 12) |
|
||||
|
||||
### Signature Field
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "SIGNATURE",
|
||||
"fieldMeta": {
|
||||
"type": "signature",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Text Field
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "TEXT",
|
||||
"fieldMeta": {
|
||||
"type": "text",
|
||||
"label": "Company Name",
|
||||
"placeholder": "Enter company name",
|
||||
"text": "Default value",
|
||||
"characterLimit": 100,
|
||||
"textAlign": "left",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Description |
|
||||
| ---------------- | ------ | ---------------------------------- |
|
||||
| `text` | string | Default value |
|
||||
| `characterLimit` | number | Maximum characters allowed |
|
||||
| `textAlign` | string | `left`, `center`, or `right` |
|
||||
| `lineHeight` | number | Spacing between lines (1-10) |
|
||||
| `letterSpacing` | number | Spacing between characters (0-100) |
|
||||
|
||||
### Number Field
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "NUMBER",
|
||||
"fieldMeta": {
|
||||
"type": "number",
|
||||
"label": "Quantity",
|
||||
"minValue": 1,
|
||||
"maxValue": 100,
|
||||
"value": "10",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Description |
|
||||
| -------------- | ------ | --------------------- |
|
||||
| `value` | string | Default value |
|
||||
| `minValue` | number | Minimum allowed value |
|
||||
| `maxValue` | number | Maximum allowed value |
|
||||
| `numberFormat` | string | Display format |
|
||||
|
||||
### Date Field
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "DATE",
|
||||
"fieldMeta": {
|
||||
"type": "date",
|
||||
"textAlign": "left",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Checkbox Field
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "CHECKBOX",
|
||||
"fieldMeta": {
|
||||
"type": "checkbox",
|
||||
"label": "Agreements",
|
||||
"values": [
|
||||
{ "id": 1, "value": "Terms of Service", "checked": false },
|
||||
{ "id": 2, "value": "Privacy Policy", "checked": false }
|
||||
],
|
||||
"validationRule": "min",
|
||||
"validationLength": 1,
|
||||
"direction": "vertical",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Description |
|
||||
| ------------------ | ------ | --------------------------------- |
|
||||
| `values` | array | List of checkbox options |
|
||||
| `validationRule` | string | Validation type for selections |
|
||||
| `validationLength` | number | Number for validation rule |
|
||||
| `direction` | string | `vertical` or `horizontal` layout |
|
||||
|
||||
### Radio Field
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "RADIO",
|
||||
"fieldMeta": {
|
||||
"type": "radio",
|
||||
"label": "Payment Method",
|
||||
"values": [
|
||||
{ "id": 1, "value": "Credit Card", "checked": false },
|
||||
{ "id": 2, "value": "Bank Transfer", "checked": true },
|
||||
{ "id": 3, "value": "Check", "checked": false }
|
||||
],
|
||||
"direction": "vertical",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Dropdown Field
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "DROPDOWN",
|
||||
"fieldMeta": {
|
||||
"type": "dropdown",
|
||||
"label": "Country",
|
||||
"values": [{ "value": "United States" }, { "value": "Canada" }, { "value": "United Kingdom" }],
|
||||
"defaultValue": "United States",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Type | Description |
|
||||
| -------------- | ------ | ------------------------ |
|
||||
| `values` | array | List of dropdown options |
|
||||
| `defaultValue` | string | Pre-selected option |
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
Create a document with a signature block containing multiple field types:
|
||||
|
||||
```typescript
|
||||
async function addSignatureBlock(documentId: number, recipientId: number) {
|
||||
const fields = [
|
||||
// Signature
|
||||
{
|
||||
type: 'SIGNATURE',
|
||||
recipientId,
|
||||
pageNumber: 1,
|
||||
pageX: 10,
|
||||
pageY: 80,
|
||||
width: 30,
|
||||
height: 8,
|
||||
fieldMeta: {
|
||||
type: 'signature',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
// Printed name
|
||||
{
|
||||
type: 'NAME',
|
||||
recipientId,
|
||||
pageNumber: 1,
|
||||
pageX: 10,
|
||||
pageY: 90,
|
||||
width: 30,
|
||||
height: 4,
|
||||
fieldMeta: {
|
||||
type: 'name',
|
||||
label: 'Printed Name',
|
||||
},
|
||||
},
|
||||
// Date
|
||||
{
|
||||
type: 'DATE',
|
||||
recipientId,
|
||||
pageNumber: 1,
|
||||
pageX: 50,
|
||||
pageY: 80,
|
||||
width: 20,
|
||||
height: 4,
|
||||
fieldMeta: {
|
||||
type: 'date',
|
||||
label: 'Date',
|
||||
},
|
||||
},
|
||||
// Job title
|
||||
{
|
||||
type: 'TEXT',
|
||||
recipientId,
|
||||
pageNumber: 1,
|
||||
pageX: 50,
|
||||
pageY: 90,
|
||||
width: 30,
|
||||
height: 4,
|
||||
fieldMeta: {
|
||||
type: 'text',
|
||||
label: 'Title',
|
||||
placeholder: 'Enter your job title',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/field/create-many', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ documentId, fields }),
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Status | Description |
|
||||
| ------ | ---------------------------------------------------- |
|
||||
| `400` | Invalid field configuration or document already sent |
|
||||
| `401` | Invalid or missing API key |
|
||||
| `404` | Document, recipient, or field not found |
|
||||
| `500` | Server error |
|
||||
|
||||
<Callout type="warn">
|
||||
Fields cannot be modified after a document is sent for signing. Make all field changes while the
|
||||
document is in `DRAFT` status.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Documents API](/docs/developers/api/documents) - Create and manage documents
|
||||
- [Recipients API](/docs/developers/api/recipients) - Add signers to documents
|
||||
- [Field Types](/docs/concepts/field-types) - Detailed field type reference
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: API Reference
|
||||
description: Complete reference for the Documenso REST API.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
<Callout type="warn">
|
||||
The guides below cover common API patterns but may not reflect the latest endpoints or parameters.
|
||||
For an always up-to-date reference, see the [OpenAPI Reference](https://openapi.documenso.com).
|
||||
</Callout>
|
||||
|
||||
## Base URL
|
||||
|
||||
```
|
||||
https://app.documenso.com/api/v2
|
||||
```
|
||||
|
||||
For self-hosted instances, replace with your instance URL.
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
All requests require an API key in the `Authorization` header:
|
||||
|
||||
```
|
||||
Authorization: api_xxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
See [Authentication](/docs/developers/getting-started/authentication) for details.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="Documents"
|
||||
description="Create, retrieve, update, and delete documents."
|
||||
href="/docs/developers/api/documents"
|
||||
/>
|
||||
<Card
|
||||
title="Recipients"
|
||||
description="Manage document recipients and signers."
|
||||
href="/docs/developers/api/recipients"
|
||||
/>
|
||||
<Card
|
||||
title="Fields"
|
||||
description="Add and configure signature fields."
|
||||
href="/docs/developers/api/fields"
|
||||
/>
|
||||
<Card
|
||||
title="Templates"
|
||||
description="Work with document templates."
|
||||
href="/docs/developers/api/templates"
|
||||
/>
|
||||
<Card
|
||||
title="Teams"
|
||||
description="Manage teams and team members."
|
||||
href="/docs/developers/api/teams"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [First API Call](/docs/developers/getting-started/first-api-call) - Quick start example
|
||||
- [Webhooks](/docs/developers/webhooks) - Get notified about document events
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"title": "API Reference",
|
||||
"pages": [
|
||||
"documents",
|
||||
"recipients",
|
||||
"fields",
|
||||
"templates",
|
||||
"teams",
|
||||
"rate-limits",
|
||||
"versioning",
|
||||
"developer-mode"
|
||||
]
|
||||
}
|
||||
+16
-3
@@ -1,6 +1,11 @@
|
||||
import { Callout } from 'nextra/components';
|
||||
---
|
||||
title: Rate Limits
|
||||
description: Learn about the rate limits for the Documenso Public API.
|
||||
---
|
||||
|
||||
# Rate Limits
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
## Overview
|
||||
|
||||
Documenso enforces rate limits on all API endpoints to ensure service stability.
|
||||
|
||||
@@ -17,7 +22,7 @@ Documenso enforces rate limits on all API endpoints to ensure service stability.
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="warning">
|
||||
<Callout type="warn">
|
||||
No rate limit headers are currently provided. When you receive a 429 response, wait at least 60
|
||||
seconds before retrying.
|
||||
</Callout>
|
||||
@@ -52,3 +57,11 @@ When you exceed a resource limit:
|
||||
| ------------------- | ------ | ----------------------------- |
|
||||
| `TOO_MANY_REQUESTS` | 429 | HTTP rate limit exceeded |
|
||||
| `LIMIT_EXCEEDED` | 400 | Resource usage limit exceeded |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Authentication](/docs/developers/getting-started/authentication) - API authentication guide
|
||||
- [API Versioning](/docs/developers/api/versioning) - API version management
|
||||
- [First API Call](/docs/developers/getting-started/first-api-call) - Getting started with the API
|
||||
@@ -0,0 +1,504 @@
|
||||
---
|
||||
title: Recipients API
|
||||
description: Add and manage envelope recipients via API.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
<Callout type="warn">
|
||||
This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference,
|
||||
see the [OpenAPI Reference](https://openapi.documenso.com).
|
||||
</Callout>
|
||||
|
||||
## Recipient Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"envelopeId": "clu1abc2def3ghi4jkl",
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": 1,
|
||||
"token": "abc123...",
|
||||
"signedAt": "2024-01-15T10:30:00Z",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
| --------------- | -------------- | ------------------------------------- |
|
||||
| `id` | number | Unique recipient identifier |
|
||||
| `envelopeId` | string | ID of the associated envelope |
|
||||
| `email` | string | Recipient's email address |
|
||||
| `name` | string | Recipient's display name |
|
||||
| `role` | string | Recipient role (see below) |
|
||||
| `signingOrder` | number \| null | Order in sequential signing |
|
||||
| `token` | string | Unique token for signing URL |
|
||||
| `signedAt` | string \| null | ISO timestamp when signed |
|
||||
| `readStatus` | string | `NOT_OPENED` or `OPENED` |
|
||||
| `signingStatus` | string | `NOT_SIGNED`, `SIGNED`, or `REJECTED` |
|
||||
| `sendStatus` | string | `NOT_SENT` or `SENT` |
|
||||
|
||||
---
|
||||
|
||||
## Recipient Roles
|
||||
|
||||
| Role | Description |
|
||||
| ----------- | -------------------------------------------------------------- |
|
||||
| `SIGNER` | Must sign the document. Required fields must be completed. |
|
||||
| `APPROVER` | Must approve the document before signers can proceed. |
|
||||
| `VIEWER` | Can view the document but takes no action. |
|
||||
| `CC` | Receives a copy of the completed document. No action required. |
|
||||
| `ASSISTANT` | Can fill in fields on behalf of another recipient. |
|
||||
|
||||
---
|
||||
|
||||
## Get Recipient
|
||||
|
||||
Retrieve a single recipient by ID.
|
||||
|
||||
```
|
||||
GET /api/v2/envelope/recipient/{recipientId}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl "https://app.documenso.com/api/v2/envelope/recipient/789" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx"
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch(
|
||||
'https://app.documenso.com/api/v2/envelope/recipient/789',
|
||||
{
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const recipient = await response.json();
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
Returns the full recipient object including fields.
|
||||
|
||||
---
|
||||
|
||||
## Create Recipients
|
||||
|
||||
Add one or more recipients to an envelope.
|
||||
|
||||
```
|
||||
|
||||
POST /api/v2/envelope/recipient/create-many
|
||||
|
||||
````
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------ | ------ | -------- | --------------------------------------- |
|
||||
| `envelopeId` | string | Yes | ID of the envelope to add recipients to |
|
||||
| `data` | array | Yes | Array of recipient objects |
|
||||
|
||||
Each item in the `data` array:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| -------------- | -------- | -------- | ------------------------------------------ |
|
||||
| `email` | string | Yes | Recipient's email address |
|
||||
| `name` | string | Yes | Recipient's display name (max 255 chars) |
|
||||
| `role` | string | Yes | Recipient role (see Recipient Roles above) |
|
||||
| `signingOrder` | number | No | Position in sequential signing |
|
||||
| `accessAuth` | string[] | No | Access authentication types |
|
||||
| `actionAuth` | string[] | No | Action authentication types |
|
||||
|
||||
### Example
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/recipient/create-many" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"envelopeId": "clu1abc2def3ghi4jkl",
|
||||
"data": [
|
||||
{
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": 1
|
||||
},
|
||||
{
|
||||
"email": "approver@example.com",
|
||||
"name": "Jane Smith",
|
||||
"role": "APPROVER",
|
||||
"signingOrder": 0
|
||||
}
|
||||
]
|
||||
}'
|
||||
````
|
||||
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch(
|
||||
'https://app.documenso.com/api/v2/envelope/recipient/create-many',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
envelopeId: 'clu1abc2def3ghi4jkl',
|
||||
data: [
|
||||
{
|
||||
email: 'signer@example.com',
|
||||
name: 'John Doe',
|
||||
role: 'SIGNER',
|
||||
signingOrder: 1,
|
||||
},
|
||||
{
|
||||
email: 'approver@example.com',
|
||||
name: 'Jane Smith',
|
||||
role: 'APPROVER',
|
||||
signingOrder: 0,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const { data: recipients } = await response.json();
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 789,
|
||||
"envelopeId": "clu1abc2def3ghi4jkl",
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
"signingOrder": 1,
|
||||
"token": "abc123def456",
|
||||
"signedAt": null,
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "NOT_SENT"
|
||||
},
|
||||
{
|
||||
"id": 790,
|
||||
"envelopeId": "clu1abc2def3ghi4jkl",
|
||||
"email": "approver@example.com",
|
||||
"name": "Jane Smith",
|
||||
"role": "APPROVER",
|
||||
"signingOrder": 0,
|
||||
"token": "def456ghi789",
|
||||
"signedAt": null,
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "NOT_SENT"
|
||||
}
|
||||
]
|
||||
}
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## Update Recipients
|
||||
|
||||
Update one or more recipients on an envelope. Only available for envelopes that are not yet completed.
|
||||
|
||||
```
|
||||
POST /api/v2/envelope/recipient/update-many
|
||||
```
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------ | ------ | -------- | -------------------------------------------- |
|
||||
| `envelopeId` | string | Yes | ID of the envelope containing the recipients |
|
||||
| `data` | array | Yes | Array of recipient update objects |
|
||||
|
||||
Each item in the `data` array:
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| -------------- | -------- | -------- | ---------------------------------- |
|
||||
| `id` | number | Yes | ID of the recipient to update |
|
||||
| `email` | string | No | New email address |
|
||||
| `name` | string | No | New display name (max 255 chars) |
|
||||
| `role` | string | No | New recipient role |
|
||||
| `signingOrder` | number | No | New position in sequential signing |
|
||||
| `accessAuth` | string[] | No | Access authentication types |
|
||||
| `actionAuth` | string[] | No | Action authentication types |
|
||||
|
||||
### Example
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/recipient/update-many" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"envelopeId": "clu1abc2def3ghi4jkl",
|
||||
"data": [
|
||||
{
|
||||
"id": 789,
|
||||
"name": "Jane Doe",
|
||||
"signingOrder": 2
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch(
|
||||
'https://app.documenso.com/api/v2/envelope/recipient/update-many',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
envelopeId: 'clu1abc2def3ghi4jkl',
|
||||
data: [
|
||||
{
|
||||
id: 789,
|
||||
name: 'Jane Doe',
|
||||
signingOrder: 2,
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const { data: updatedRecipients } = await response.json();
|
||||
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
Returns the updated recipient objects in a `data` array.
|
||||
|
||||
---
|
||||
|
||||
## Delete Recipient
|
||||
|
||||
Remove a recipient from an envelope. Only available for envelopes that are not yet completed.
|
||||
|
||||
```
|
||||
|
||||
POST /api/v2/envelope/recipient/delete
|
||||
|
||||
````
|
||||
|
||||
### Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ------------- | ------ | -------- | ------------------------------- |
|
||||
| `recipientId` | number | Yes | ID of the recipient to remove |
|
||||
|
||||
### Example
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/recipient/delete" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"recipientId": 789
|
||||
}'
|
||||
````
|
||||
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch(
|
||||
'https://app.documenso.com/api/v2/envelope/recipient/delete',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
recipientId: 789,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
const result = await response.json();
|
||||
// { "success": true }
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## Signing Order
|
||||
|
||||
When an envelope uses sequential signing, recipients sign in a specific order defined by `signingOrder`.
|
||||
|
||||
### Setting Signing Order
|
||||
|
||||
When creating recipients, assign `signingOrder` values to control the sequence:
|
||||
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/recipient/create-many', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
envelopeId: 'clu1abc2def3ghi4jkl',
|
||||
data: [
|
||||
{
|
||||
email: 'approver@example.com',
|
||||
name: 'Approver',
|
||||
role: 'APPROVER',
|
||||
signingOrder: 0, // Approvers typically go first
|
||||
},
|
||||
{
|
||||
email: 'first@example.com',
|
||||
name: 'First Signer',
|
||||
role: 'SIGNER',
|
||||
signingOrder: 1,
|
||||
},
|
||||
{
|
||||
email: 'second@example.com',
|
||||
name: 'Second Signer',
|
||||
role: 'SIGNER',
|
||||
signingOrder: 2,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
To enable sequential signing, set `signingOrder` to `SEQUENTIAL` in the envelope metadata when
|
||||
creating or updating the envelope. See the [Documents API](/docs/developers/api/documents) for
|
||||
details.
|
||||
</Callout>
|
||||
|
||||
### Signing Order Behavior
|
||||
|
||||
- Recipients with lower `signingOrder` values sign first
|
||||
- Recipients with the same `signingOrder` can sign simultaneously
|
||||
- `CC` recipients receive the document after all signing is complete
|
||||
- `APPROVER` recipients must approve before signers with higher order values
|
||||
|
||||
---
|
||||
|
||||
## Authentication Options
|
||||
|
||||
For enhanced security, you can require additional authentication when recipients access or sign a document.
|
||||
|
||||
### Access Authentication
|
||||
|
||||
Controls who can view the document:
|
||||
|
||||
| Type | Description |
|
||||
| ----------------- | ------------------------------ |
|
||||
| `ACCOUNT` | Recipient must be logged in |
|
||||
| `TWO_FACTOR_AUTH` | Recipient must verify with 2FA |
|
||||
|
||||
### Action Authentication
|
||||
|
||||
Controls who can sign the document:
|
||||
|
||||
| Type | Description |
|
||||
| ----------------- | ---------------------------------------- |
|
||||
| `ACCOUNT` | Recipient must be logged in |
|
||||
| `PASSKEY` | Require passkey authentication |
|
||||
| `TWO_FACTOR_AUTH` | Require 2FA code |
|
||||
| `PASSWORD` | Require password verification |
|
||||
| `EXPLICIT_NONE` | Explicitly disable action authentication |
|
||||
|
||||
### Example
|
||||
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/recipient/create-many', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
envelopeId: 'clu1abc2def3ghi4jkl',
|
||||
data: [
|
||||
{
|
||||
email: 'signer@example.com',
|
||||
name: 'John Doe',
|
||||
role: 'SIGNER',
|
||||
accessAuth: ['ACCOUNT'],
|
||||
actionAuth: ['PASSKEY', 'TWO_FACTOR_AUTH'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Status | Description |
|
||||
| ------ | ------------------------------------------------ |
|
||||
| `400` | Invalid request body or recipient already exists |
|
||||
| `400` | Envelope is already completed |
|
||||
| `401` | Invalid or missing API key |
|
||||
| `404` | Envelope or recipient not found |
|
||||
| `500` | Server error |
|
||||
|
||||
### Example Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Recipient already exists"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Documents API](/docs/developers/api/documents) - Create and manage envelopes
|
||||
- [Fields API](/docs/developers/api/fields) - Add signature fields for recipients
|
||||
@@ -0,0 +1,373 @@
|
||||
---
|
||||
title: Teams API
|
||||
description: Manage team resources, documents, and templates with team-scoped API tokens.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
<Callout type="warn">
|
||||
This guide may not reflect the latest endpoints or parameters. For an always up-to-date reference,
|
||||
see the [OpenAPI Reference](https://openapi.documenso.com).
|
||||
</Callout>
|
||||
|
||||
## Team Object
|
||||
|
||||
A team object contains the following properties:
|
||||
|
||||
| Property | Type | Description |
|
||||
| ----------------- | -------------- | --------------------------------------------------- |
|
||||
| `id` | number | Unique team identifier |
|
||||
| `name` | string | Team display name |
|
||||
| `url` | string | Unique team URL slug |
|
||||
| `createdAt` | string | ISO 8601 timestamp |
|
||||
| `avatarImageId` | string \| null | ID of the team's avatar image |
|
||||
| `organisationId` | string | ID of the parent organisation |
|
||||
| `currentTeamRole` | string | Your role in the team: `ADMIN`, `MANAGER`, `MEMBER` |
|
||||
|
||||
### Example Team Object
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"name": "Engineering",
|
||||
"url": "engineering",
|
||||
"createdAt": "2025-01-15T10:30:00.000Z",
|
||||
"avatarImageId": null,
|
||||
"organisationId": "org_abc123",
|
||||
"currentTeamRole": "ADMIN"
|
||||
}
|
||||
```
|
||||
|
||||
## Team-Scoped API Tokens
|
||||
|
||||
API tokens in Documenso are always scoped to a specific team. When you create an API token, it is associated with the team you're currently working in.
|
||||
|
||||
### How Team Scoping Works
|
||||
|
||||
- Each API token belongs to exactly one team
|
||||
- All API operations using that token automatically access that team's resources
|
||||
- Documents, templates, and other resources created via the API belong to the token's team
|
||||
- You cannot access resources from other teams with a single token
|
||||
|
||||
### Creating Team-Scoped Tokens
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
Navigate to your team's settings
|
||||
</Step>
|
||||
<Step>
|
||||
Go to **API Tokens**
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Create Token**
|
||||
</Step>
|
||||
<Step>
|
||||
The token will be scoped to the current team
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
To work with multiple teams via API, create separate tokens for each team.
|
||||
</Callout>
|
||||
|
||||
### Token Permissions
|
||||
|
||||
Your API token inherits permissions based on your role in the team:
|
||||
|
||||
| Role | Permissions |
|
||||
| --------- | ------------------------------------------------ |
|
||||
| `ADMIN` | Full access to all team resources and settings |
|
||||
| `MANAGER` | Create, edit, and delete documents and templates |
|
||||
| `MEMBER` | Create and manage own documents |
|
||||
|
||||
## Working with Team Documents
|
||||
|
||||
When you use a team-scoped API token, all document operations are automatically scoped to that team.
|
||||
|
||||
### Create a Team Document
|
||||
|
||||
Documents created with a team token belong to that team:
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/create" \
|
||||
-H "Authorization: api_team_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F 'payload={
|
||||
"type": "DOCUMENT",
|
||||
"title": "Team Contract",
|
||||
"recipients": [
|
||||
{
|
||||
"email": "signer@example.com",
|
||||
"name": "John Smith",
|
||||
"role": "SIGNER"
|
||||
}
|
||||
]
|
||||
}' \
|
||||
-F "files=@./contract.pdf;type=application/pdf"
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const TEAM_API_TOKEN = process.env.DOCUMENSO_TEAM_API_TOKEN;
|
||||
|
||||
const form = new FormData();
|
||||
|
||||
const payload = {
|
||||
type: 'DOCUMENT',
|
||||
title: 'Team Contract',
|
||||
recipients: [
|
||||
{
|
||||
email: 'signer@example.com',
|
||||
name: 'John Smith',
|
||||
role: 'SIGNER',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
form.append('payload', JSON.stringify(payload));
|
||||
form.append('files', fs.createReadStream('./contract.pdf'), {
|
||||
contentType: 'application/pdf',
|
||||
});
|
||||
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: TEAM_API_TOKEN,
|
||||
},
|
||||
body: form,
|
||||
});
|
||||
|
||||
const { id } = await response.json();
|
||||
console.log('Created team document:', id);
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### List Team Documents
|
||||
|
||||
Retrieve all documents belonging to the team:
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
# List all team documents
|
||||
curl -X GET "https://app.documenso.com/api/v2/envelope" \
|
||||
-H "Authorization: api_team_xxxxxxxxxxxxxxxx"
|
||||
|
||||
# Filter by status
|
||||
curl -X GET "https://app.documenso.com/api/v2/envelope?status=PENDING" \
|
||||
-H "Authorization: api_team_xxxxxxxxxxxxxxxx"
|
||||
````
|
||||
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: TEAM_API_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
const { data, pagination } = await response.json();
|
||||
console.log(`Found ${pagination.totalItems} team documents`);
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Working with Team Templates
|
||||
|
||||
Templates created with a team token are shared across the team.
|
||||
|
||||
### Create a Team Template
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/template/create" \
|
||||
-H "Authorization: api_team_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F 'payload={
|
||||
"title": "NDA Template",
|
||||
"recipients": [
|
||||
{
|
||||
"email": "placeholder@example.com",
|
||||
"name": "Signer",
|
||||
"role": "SIGNER",
|
||||
"fields": [
|
||||
{
|
||||
"identifier": 0,
|
||||
"type": "SIGNATURE",
|
||||
"page": 1,
|
||||
"positionX": 10,
|
||||
"positionY": 80,
|
||||
"width": 30,
|
||||
"height": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}' \
|
||||
-F "files=@./nda-template.pdf;type=application/pdf"
|
||||
````
|
||||
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const form = new FormData();
|
||||
|
||||
const payload = {
|
||||
title: 'NDA Template',
|
||||
recipients: [
|
||||
{
|
||||
email: 'placeholder@example.com',
|
||||
name: 'Signer',
|
||||
role: 'SIGNER',
|
||||
fields: [
|
||||
{
|
||||
identifier: 0,
|
||||
type: 'SIGNATURE',
|
||||
page: 1,
|
||||
positionX: 10,
|
||||
positionY: 80,
|
||||
width: 30,
|
||||
height: 5,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
form.append('payload', JSON.stringify(payload));
|
||||
form.append('files', fs.createReadStream('./nda-template.pdf'), {
|
||||
contentType: 'application/pdf',
|
||||
});
|
||||
|
||||
const response = await fetch('https://app.documenso.com/api/v2/template/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: TEAM_API_TOKEN,
|
||||
},
|
||||
body: form,
|
||||
});
|
||||
|
||||
const template = await response.json();
|
||||
console.log('Created team template:', template.id);
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### List Team Templates
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X GET "https://app.documenso.com/api/v2/template" \
|
||||
-H "Authorization: api_team_xxxxxxxxxxxxxxxx"
|
||||
````
|
||||
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/template', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: TEAM_API_TOKEN,
|
||||
},
|
||||
});
|
||||
|
||||
const { data } = await response.json();
|
||||
console.log('Team templates:', data);
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Team Member Roles
|
||||
|
||||
| Role | Description |
|
||||
| --------- | ------------------------------------------------------------------- |
|
||||
| `ADMIN` | Full control over team settings, members, and all resources |
|
||||
| `MANAGER` | Can manage documents, templates, and view team resources |
|
||||
| `MEMBER` | Can create and manage their own documents within the team |
|
||||
|
||||
### Document Visibility
|
||||
|
||||
Team documents have visibility settings that control who can access them:
|
||||
|
||||
| Visibility | Description |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| `EVERYONE` | All team members can view the document |
|
||||
| `MANAGER_AND_ABOVE`| Only managers and admins can view |
|
||||
| `ADMIN` | Only admins can view |
|
||||
|
||||
Set visibility when creating a document:
|
||||
|
||||
```typescript
|
||||
const payload = {
|
||||
type: 'DOCUMENT',
|
||||
title: 'Confidential Agreement',
|
||||
visibility: 'ADMIN', // Only team admins can view
|
||||
recipients: [...],
|
||||
};
|
||||
````
|
||||
|
||||
## Multi-Team Workflow
|
||||
|
||||
To work with multiple teams, create and manage separate API tokens for each team.
|
||||
|
||||
### Example: Sync Documents Across Teams
|
||||
|
||||
```typescript
|
||||
// Tokens for different teams
|
||||
const SALES_TEAM_TOKEN = process.env.SALES_TEAM_API_TOKEN;
|
||||
const LEGAL_TEAM_TOKEN = process.env.LEGAL_TEAM_API_TOKEN;
|
||||
|
||||
// Get pending documents from sales team
|
||||
const salesResponse = await fetch('https://app.documenso.com/api/v2/envelope?status=PENDING', {
|
||||
headers: { Authorization: SALES_TEAM_TOKEN },
|
||||
});
|
||||
const salesDocs = await salesResponse.json();
|
||||
|
||||
// Get completed documents from legal team
|
||||
const legalResponse = await fetch('https://app.documenso.com/api/v2/envelope?status=COMPLETED', {
|
||||
headers: { Authorization: LEGAL_TEAM_TOKEN },
|
||||
});
|
||||
const legalDocs = await legalResponse.json();
|
||||
|
||||
console.log(`Sales team: ${salesDocs.pagination.totalItems} pending`);
|
||||
console.log(`Legal team: ${legalDocs.pagination.totalItems} completed`);
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Status | Description |
|
||||
| ------ | ------------------------------------------------- |
|
||||
| `401` | Invalid or expired API token |
|
||||
| `403` | Token doesn't have permission for this operation |
|
||||
| `404` | Resource not found or not accessible by this team |
|
||||
|
||||
### Example Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "You do not have permission to access this resource"
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
API tokens can only access resources belonging to their associated team. Attempting to access
|
||||
resources from another team returns a 403 or 404 error.
|
||||
</Callout>
|
||||
|
||||
## See Also
|
||||
|
||||
- [Documents API](/docs/developers/api/documents) - Create and manage documents
|
||||
- [Templates API](/docs/developers/api/templates) - Work with document templates
|
||||
- [Authentication](/docs/developers/getting-started/authentication) - Create and manage API tokens
|
||||
File diff suppressed because it is too large
Load Diff
+11
-4
@@ -3,16 +3,23 @@ title: API Versioning
|
||||
description: Versioning information for the Documenso public API.
|
||||
---
|
||||
|
||||
import { Callout } from 'nextra/components';
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
# API Versioning
|
||||
## Overview
|
||||
|
||||
Documenso uses API versioning to manage changes to the public API. This allows us to introduce new features, fix bugs, and make other changes without breaking existing integrations.
|
||||
|
||||
<Callout type="info">The current version of the API is `v1`.</Callout>
|
||||
<Callout type="info">The current version of the API is `v2`.</Callout>
|
||||
|
||||
The API version is specified in the URL. For example, the base URL for the `v1` API is `https://app.documenso.com/api/v1`.
|
||||
The API version is specified in the URL. For example, the base URL for the `v2` API is `https://app.documenso.com/api/v2`.
|
||||
|
||||
We may make changes to the API without incrementing the version number. We will always try to avoid breaking changes, but in some cases, it may be necessary to make changes that are not backward compatible. In these cases, we will increment the version number and provide information about the changes in the release notes.
|
||||
|
||||
Also, we may deprecate certain features or endpoints in the API. When we deprecate a feature or endpoint, we will provide information about the deprecation in the release notes and give a timeline for when the feature or endpoint will be removed.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Authentication](/docs/developers/getting-started/authentication) - API authentication guide
|
||||
- [Rate Limits](/docs/developers/api/rate-limits) - API rate limit details
|
||||
+41
-14
@@ -3,13 +3,12 @@ title: Contributing Translations
|
||||
description: Learn how to contribute translations to Documenso and become part of our community.
|
||||
---
|
||||
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
# Contributing Translations
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
We are always open for help with translations! Currently we utilise AI to generate the initial translations for new languages, which are then improved over time by our awesome community.
|
||||
|
||||
If you are looking for development notes on translations, you can find them [here](/developers/local-development/translations).
|
||||
If you are looking for development notes on translations, you can find them [here](/docs/developers/local-development/translations).
|
||||
|
||||
<Callout type="info">
|
||||
Contributions are made through GitHub Pull Requests, so you will need a GitHub account to
|
||||
@@ -25,7 +24,7 @@ The translation files are organized into folders represented by their respective
|
||||
Each PO file contains translations which look like this:
|
||||
|
||||
```po
|
||||
#: apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx:61
|
||||
#: apps/remix/app/(signing)/sign/[token]/no-longer-available.tsx:61
|
||||
msgid "Want to send slick signing links like this one? <0>Check out Documenso.</0>"
|
||||
msgstr "Möchten Sie auffällige Signatur-Links wie diesen senden? <0>Überprüfen Sie Documenso.</0>"
|
||||
```
|
||||
@@ -33,7 +32,7 @@ msgstr "Möchten Sie auffällige Signatur-Links wie diesen senden? <0>Überprüf
|
||||
- `msgid`: The original text in English (never edit this manually)
|
||||
- `msgstr`: The translated text in the target language
|
||||
|
||||
<Callout type="warning">
|
||||
<Callout type="warn">
|
||||
Notice the `<0>` tags? These represent HTML elements and must remain in both the `msgid` and `msgstr`. Make sure to translate the content between these tags while keeping the tags intact.
|
||||
</Callout>
|
||||
|
||||
@@ -41,19 +40,42 @@ msgstr "Möchten Sie auffällige Signatur-Links wie diesen senden? <0>Überprüf
|
||||
|
||||
### Updating Existing Translations
|
||||
|
||||
1. Fork the repository.
|
||||
2. Navigate to the appropriate language folder and open the PO file you want to update.
|
||||
3. Make your changes, ensuring you follow the PO file format.
|
||||
4. Commit your changes with a message such as `chore: update German translations`
|
||||
5. Create a Pull Request.
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
Fork the repository
|
||||
</Step>
|
||||
<Step>
|
||||
Navigate to the appropriate language folder and open the PO file you want to update
|
||||
</Step>
|
||||
<Step>
|
||||
Make your changes, ensuring you follow the PO file format
|
||||
</Step>
|
||||
<Step>
|
||||
Commit your changes with a message such as <code>chore: update German translations</code>
|
||||
</Step>
|
||||
<Step>
|
||||
Create a Pull Request
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Adding a New Language
|
||||
|
||||
If you want to add translations for a language that doesn't exist yet:
|
||||
|
||||
1. Create an issue in our GitHub repository requesting the addition of the new language.
|
||||
2. Wait for our team to review and approve the request.
|
||||
3. Once approved, we will set up the necessary files and kickstart the translations with AI to provide initial coverage.
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
Create an issue in our GitHub repository requesting the addition of the new language
|
||||
</Step>
|
||||
<Step>
|
||||
Wait for our team to review and approve the request
|
||||
</Step>
|
||||
<Step>
|
||||
Once approved, we will set up the necessary files and kickstart the translations with AI to
|
||||
provide initial coverage
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Need Help?
|
||||
|
||||
@@ -62,3 +84,8 @@ If you want to add translations for a language that doesn't exist yet:
|
||||
</Callout>
|
||||
|
||||
Thank you for helping make Documenso more accessible to users around the world!
|
||||
|
||||
## See Also
|
||||
|
||||
- [Translations (Development)](/docs/developers/local-development/translations) - Technical guide to translations in code
|
||||
- [Contributing Guide](/docs/developers/contributing) - General contributing guidelines
|
||||
+48
-13
@@ -1,11 +1,12 @@
|
||||
---
|
||||
title: Getting started
|
||||
title: Contributing to Documenso
|
||||
description: Learn how to contribute to Documenso and become part of our community.
|
||||
---
|
||||
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
# Contributing to Documenso
|
||||
## Overview
|
||||
|
||||
If you plan to contribute to Documenso, please take a moment to feel awesome. People like you are what open source is about. Any contributions, no matter how big or small, are highly appreciated.
|
||||
|
||||
@@ -13,24 +14,37 @@ This guide will help you get started with contributing to Documenso.
|
||||
|
||||
## Before Getting Started
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
|
||||
### Check the Existing Issues and Pull Requests
|
||||
<Step>
|
||||
### Check the existing issues and pull requests
|
||||
|
||||
Search the existing [issues](https://github.com/documenso/documenso/issues) to see if someone else reported the same issue. Or, check the [existing PRs](https://github.com/documenso/documenso/pulls) to see if someone else is already working on the same thing.
|
||||
|
||||
### Creating a New Issue
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Creating a new issue
|
||||
|
||||
If there is no issue or PR for the problem you are facing, feel free to create a new issue. Make sure to provide as much detail as possible, including the steps to reproduce the issue.
|
||||
|
||||
### Picking an Existing Issue
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Picking an existing issue
|
||||
|
||||
If you pick an existing issue, take into consideration the discussion on the issue.
|
||||
|
||||
### Contributor License Agreement
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Contributor license agreement
|
||||
|
||||
Accept the [Contributor License Agreement](https://documen.so/cla) to ensure we can accept your contributions.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Taking Issues
|
||||
@@ -53,13 +67,18 @@ Feel free to ask for help, clarification or guidance if needed. We are here to h
|
||||
|
||||
The development branch is `main`, and all pull requests should be made against this branch. Here's how you can get started with developing:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
|
||||
### Set Up Documenso Locally
|
||||
<Step>
|
||||
### Set up Documenso locally
|
||||
|
||||
To set up your local environment, check out the [local development](/developers/local-development) guide.
|
||||
To set up your local environment, check out the [local development](/docs/developers/local-development) guide.
|
||||
|
||||
### Pick a Task
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Pick a task
|
||||
|
||||
Find an issue to work on or create a new one.
|
||||
|
||||
@@ -67,7 +86,10 @@ Find an issue to work on or create a new one.
|
||||
|
||||
Before creating a new issue, check the existing issues to see if someone else has already reported it.
|
||||
|
||||
### Create a New Branch
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Create a new branch
|
||||
|
||||
After you're assigned an issue, you can start working on it. Create a new branch for your feature or bug fix.
|
||||
|
||||
@@ -86,14 +108,22 @@ git checkout -b feat/1234-add-share-button-to-articles
|
||||
|
||||
In the pull request description, include `references #yyyy` or `fixes #yyyy` to link it to the issue you are working on.
|
||||
|
||||
### Implement Your Changes
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Implement your changes
|
||||
|
||||
Start working on the issue you picked up and implement the changes. Make sure to test your changes locally and ensure that they work as expected.
|
||||
|
||||
### Open a Pull Request
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Open a pull request
|
||||
|
||||
After implementing your changes, open a pull request against the `main` branch.
|
||||
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
@@ -115,3 +145,8 @@ Once the project builds successfully, you can push your code changes or create a
|
||||
result, we can collaborate more effectively and maintain a high standard of code quality in our
|
||||
project.
|
||||
</Callout>
|
||||
|
||||
## See Also
|
||||
|
||||
- [Local Development](/docs/developers/local-development) - Set up your development environment
|
||||
- [Contributing Translations](/docs/developers/contributing/contributing-translations) - Help translate Documenso
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Contributing",
|
||||
"pages": ["contributing-translations"]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Demo Environment
|
||||
description: Use the demo environment to try out the Documenso platform and its features.
|
||||
---
|
||||
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
## Overview
|
||||
|
||||
The demo (staging) environment is a sandbox environment that replicates the production environment. It has the same features and capabilities as the production environment, but is intended for development and testing purposes.
|
||||
|
||||
You can use it to try out the Documenso platform and its features before committing to a paid plan.
|
||||
|
||||
## How to Use the Demo Environment
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Navigate to the staging environment
|
||||
|
||||
Go to the [staging environment](https://stg-app.documenso.com).
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Create an account
|
||||
|
||||
You need to create a new account for the demo environment. You can't use your production account.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Pick a paid plan
|
||||
|
||||
Choose the appropriate plan for your needs.
|
||||
|
||||
You can also use the free plan but it's limited to 5 documents per month and up to 10 recipients per document.
|
||||
|
||||
Whatever plan you choose, you can upgrade later.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Use a test card
|
||||
|
||||
To upgrade to a paid plan, you can use a test card. Example:
|
||||
|
||||
```
|
||||
Card number: 4242 4242 4242 4242
|
||||
Expiry date: 02/2030 (or any valid future date)
|
||||
CVV: 123
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Use the platform
|
||||
|
||||
You can then try out the platform and its features.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Issues, questions and feedback
|
||||
|
||||
If you have any issues, questions or feedback, please reach out to us on the [Documenso Discord](https://documen.so/discord) or [GitHub](https://github.com/documenso/documenso/issues).
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -0,0 +1,306 @@
|
||||
---
|
||||
title: Authoring
|
||||
description: Embed document and template creation directly in your application.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
In addition to embedding signing, Documenso supports embedded authoring. It allows your users to create and edit documents and templates without leaving your application.
|
||||
|
||||
<Callout type="warn">
|
||||
Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
|
||||
also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing).
|
||||
Contact sales for access.
|
||||
</Callout>
|
||||
|
||||
## Components
|
||||
|
||||
The SDK provides four authoring components:
|
||||
|
||||
| Component | Purpose |
|
||||
| ----------------------- | ----------------------- |
|
||||
| `EmbedCreateDocumentV1` | Create new documents |
|
||||
| `EmbedCreateTemplateV1` | Create new templates |
|
||||
| `EmbedUpdateDocumentV1` | Edit existing documents |
|
||||
| `EmbedUpdateTemplateV1` | Edit existing templates |
|
||||
|
||||
All authoring components require a **presign token** for authentication instead of a regular token.
|
||||
|
||||
---
|
||||
|
||||
## Presign Tokens
|
||||
|
||||
Before using any authoring component, obtain a presign token from your backend:
|
||||
|
||||
```
|
||||
POST /api/v2/embedding/create-presign-token
|
||||
```
|
||||
|
||||
This endpoint requires your Documenso API key. The token has a default expiration of 1 hour.
|
||||
|
||||
See the [API documentation](https://openapi.documenso.com/reference#tag/embedding) for full details.
|
||||
|
||||
<Callout type="warn">
|
||||
Presign tokens should be created server-side. Never expose your API key in client-side code.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Creating Documents
|
||||
|
||||
```jsx
|
||||
import { EmbedCreateDocumentV1 } from '@documenso/embed-react';
|
||||
|
||||
const DocumentCreator = ({ presignToken }) => {
|
||||
return (
|
||||
<div style={{ height: '800px', width: '100%' }}>
|
||||
<EmbedCreateDocumentV1
|
||||
presignToken={presignToken}
|
||||
externalId="order-12345"
|
||||
onDocumentCreated={(data) => {
|
||||
console.log('Document created:', data.documentId);
|
||||
console.log('External ID:', data.externalId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Creating Templates
|
||||
|
||||
```jsx
|
||||
import { EmbedCreateTemplateV1 } from '@documenso/embed-react';
|
||||
|
||||
const TemplateCreator = ({ presignToken }) => {
|
||||
return (
|
||||
<div style={{ height: '800px', width: '100%' }}>
|
||||
<EmbedCreateTemplateV1
|
||||
presignToken={presignToken}
|
||||
externalId="template-12345"
|
||||
onTemplateCreated={(data) => {
|
||||
console.log('Template created:', data.templateId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Editing Documents
|
||||
|
||||
```jsx
|
||||
import { EmbedUpdateDocumentV1 } from '@documenso/embed-react';
|
||||
|
||||
const DocumentEditor = ({ presignToken, documentId }) => {
|
||||
return (
|
||||
<div style={{ height: '800px', width: '100%' }}>
|
||||
<EmbedUpdateDocumentV1
|
||||
presignToken={presignToken}
|
||||
documentId={documentId}
|
||||
externalId="order-12345"
|
||||
onDocumentUpdated={(data) => {
|
||||
console.log('Document updated:', data.documentId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Editing Templates
|
||||
|
||||
```jsx
|
||||
import { EmbedUpdateTemplateV1 } from '@documenso/embed-react';
|
||||
|
||||
const TemplateEditor = ({ presignToken, templateId }) => {
|
||||
return (
|
||||
<div style={{ height: '800px', width: '100%' }}>
|
||||
<EmbedUpdateTemplateV1
|
||||
presignToken={presignToken}
|
||||
templateId={templateId}
|
||||
externalId="template-12345"
|
||||
onTemplateUpdated={(data) => {
|
||||
console.log('Template updated:', data.templateId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Props
|
||||
|
||||
### All Authoring Components
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| ------------------ | --------- | -------- | -------------------------------------------------------- |
|
||||
| `presignToken` | `string` | Yes | Authentication token from your backend |
|
||||
| `externalId` | `string` | No | Your reference ID to link with the document or template |
|
||||
| `host` | `string` | No | Custom host URL. Defaults to `https://app.documenso.com` |
|
||||
| `css` | `string` | No | Custom CSS string (Platform Plan) |
|
||||
| `cssVars` | `object` | No | CSS variable overrides (Platform Plan) |
|
||||
| `darkModeDisabled` | `boolean` | No | Disable dark mode (Platform Plan) |
|
||||
| `className` | `string` | No | CSS class for the iframe |
|
||||
| `features` | `object` | No | Feature toggles for the authoring experience |
|
||||
|
||||
### Update Components Only
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| ---------------- | --------- | -------- | ---------------------------------------------------------- |
|
||||
| `documentId` | `number` | Yes | The document ID to edit (for document update component) |
|
||||
| `templateId` | `number` | Yes | The template ID to edit (for template update component) |
|
||||
| `onlyEditFields` | `boolean` | No | Restrict editing to fields only, skipping recipient config |
|
||||
|
||||
---
|
||||
|
||||
## Feature Toggles
|
||||
|
||||
Customize what options are available in the authoring experience:
|
||||
|
||||
```jsx
|
||||
<EmbedCreateDocumentV1
|
||||
presignToken={presignToken}
|
||||
features={{
|
||||
allowConfigureSignatureTypes: true,
|
||||
allowConfigureLanguage: true,
|
||||
allowConfigureDateFormat: true,
|
||||
allowConfigureTimezone: true,
|
||||
allowConfigureRedirectUrl: true,
|
||||
allowConfigureCommunication: true,
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Callbacks
|
||||
|
||||
All creation callbacks receive:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------------------------- | -------- | --------------------------------------- |
|
||||
| `documentId` or `templateId` | `number` | The ID of the created or updated item |
|
||||
| `externalId` | `string` | Your external reference ID, if provided |
|
||||
|
||||
---
|
||||
|
||||
## Field-Only Editing
|
||||
|
||||
Restrict users to editing fields only, skipping recipient configuration:
|
||||
|
||||
```jsx
|
||||
<EmbedUpdateDocumentV1
|
||||
presignToken={presignToken}
|
||||
documentId={123}
|
||||
onlyEditFields={true}
|
||||
onDocumentUpdated={(data) => {
|
||||
console.log('Fields updated:', data.documentId);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Integration Example
|
||||
|
||||
This example shows a full workflow where users create a document and then edit it:
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
|
||||
import { EmbedCreateDocumentV1, EmbedUpdateDocumentV1 } from '@documenso/embed-react';
|
||||
|
||||
const DocumentManager = ({ presignToken }) => {
|
||||
const [documentId, setDocumentId] = useState(null);
|
||||
const [mode, setMode] = useState('create');
|
||||
|
||||
if (mode === 'success') {
|
||||
return (
|
||||
<div>
|
||||
<h2>Document updated successfully</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setDocumentId(null);
|
||||
setMode('create');
|
||||
}}
|
||||
>
|
||||
Create Another Document
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'edit' && documentId) {
|
||||
return (
|
||||
<div style={{ height: '800px', width: '100%' }}>
|
||||
<button onClick={() => setMode('create')}>Back to Create</button>
|
||||
<EmbedUpdateDocumentV1
|
||||
presignToken={presignToken}
|
||||
documentId={documentId}
|
||||
onDocumentUpdated={(data) => {
|
||||
console.log('Document updated:', data.documentId);
|
||||
setMode('success');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ height: '800px', width: '100%' }}>
|
||||
<EmbedCreateDocumentV1
|
||||
presignToken={presignToken}
|
||||
features={{
|
||||
allowConfigureSignatureTypes: true,
|
||||
allowConfigureLanguage: true,
|
||||
}}
|
||||
onDocumentCreated={(data) => {
|
||||
console.log('Document created:', data.documentId);
|
||||
setDocumentId(data.documentId);
|
||||
setMode('edit');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Additional Props
|
||||
|
||||
Pass extra props to the iframe for testing experimental features:
|
||||
|
||||
```jsx
|
||||
<EmbedCreateDocumentV1
|
||||
presignToken="YOUR_PRESIGN_TOKEN"
|
||||
additionalProps={{
|
||||
experimentalFeature: true,
|
||||
customSetting: 'value',
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
Presign tokens expire after 1 hour by default. You can customize this duration based on your
|
||||
security requirements. Generate fresh tokens for each session and avoid caching them on the client
|
||||
side.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Documents API](/docs/developers/api/documents) - Create documents via API
|
||||
- [Templates API](/docs/developers/api/templates) - Create templates via API
|
||||
@@ -0,0 +1,215 @@
|
||||
---
|
||||
title: CSS Variables
|
||||
description: Customize the appearance of embedded signing experiences with CSS variables and class targets.
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
<Callout type="info">
|
||||
Custom CSS and CSS variables are available on the [Platform
|
||||
Plan](https://documen.so/platform-cta-pricing).
|
||||
</Callout>
|
||||
|
||||
## CSS Variables
|
||||
|
||||
Use the `cssVars` prop on any embed component to override default colors, spacing, and more.
|
||||
|
||||
```jsx
|
||||
<EmbedDirectTemplate
|
||||
token="your-token"
|
||||
cssVars={{
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
primary: '#0000ff',
|
||||
primaryForeground: '#ffffff',
|
||||
radius: '0.5rem',
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Colors
|
||||
|
||||
| Variable | Description |
|
||||
| ----------------------- | --------------------------------- |
|
||||
| `background` | Base background color |
|
||||
| `foreground` | Base text color |
|
||||
| `muted` | Muted/subtle background color |
|
||||
| `mutedForeground` | Muted/subtle text color |
|
||||
| `popover` | Popover/dropdown background color |
|
||||
| `popoverForeground` | Popover/dropdown text color |
|
||||
| `card` | Card background color |
|
||||
| `cardBorder` | Card border color |
|
||||
| `cardBorderTint` | Card border tint/highlight color |
|
||||
| `cardForeground` | Card text color |
|
||||
| `fieldCard` | Field card background color |
|
||||
| `fieldCardBorder` | Field card border color |
|
||||
| `fieldCardForeground` | Field card text color |
|
||||
| `widget` | Widget background color |
|
||||
| `widgetForeground` | Widget text color |
|
||||
| `border` | Default border color |
|
||||
| `input` | Input field border color |
|
||||
| `primary` | Primary action/button color |
|
||||
| `primaryForeground` | Primary action/button text color |
|
||||
| `secondary` | Secondary action/button color |
|
||||
| `secondaryForeground` | Secondary button text color |
|
||||
| `accent` | Accent/highlight color |
|
||||
| `accentForeground` | Accent/highlight text color |
|
||||
| `destructive` | Destructive/danger action color |
|
||||
| `destructiveForeground` | Destructive/danger text color |
|
||||
| `ring` | Focus ring color |
|
||||
| `warning` | Warning/alert color |
|
||||
|
||||
### Spacing
|
||||
|
||||
| Variable | Description |
|
||||
| -------- | ---------------------------------- |
|
||||
| `radius` | Border radius size (e.g. `0.5rem`) |
|
||||
|
||||
### Framework Usage
|
||||
|
||||
Pass `cssVars` to any embed component. The syntax varies by framework:
|
||||
|
||||
```jsx
|
||||
// React / Preact
|
||||
<EmbedDirectTemplate token={token} cssVars={cssVars} />
|
||||
|
||||
// Vue
|
||||
<EmbedDirectTemplate :token="token" :cssVars="cssVars" />
|
||||
|
||||
// Svelte
|
||||
<EmbedDirectTemplate {token} cssVars={cssVars} />
|
||||
|
||||
// Solid
|
||||
<EmbedDirectTemplate token={token} cssVars={cssVars} />
|
||||
```
|
||||
|
||||
### Color Formats
|
||||
|
||||
Colors can be specified in any valid CSS format:
|
||||
|
||||
- Hex: `#ff0000`
|
||||
- RGB: `rgb(255, 0, 0)`
|
||||
- HSL: `hsl(0, 100%, 50%)`
|
||||
- Named: `red`
|
||||
|
||||
---
|
||||
|
||||
## Custom CSS
|
||||
|
||||
Use the `css` prop to inject a CSS string for more targeted control:
|
||||
|
||||
```jsx
|
||||
<EmbedDirectTemplate
|
||||
token="your-token"
|
||||
css={`
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CSS Class Targets
|
||||
|
||||
Specific parts of the embed can be targeted with CSS classes for granular styling.
|
||||
|
||||
### Component Classes
|
||||
|
||||
| Class | Description |
|
||||
| --------------------------------- | --------------------------------------------- |
|
||||
| `.embed--Root` | Main container for the embedded experience |
|
||||
| `.embed--DocumentContainer` | Container for the document and signing widget |
|
||||
| `.embed--DocumentViewer` | Container for the document viewer |
|
||||
| `.embed--DocumentWidget` | The signing widget container |
|
||||
| `.embed--DocumentWidgetContainer` | Outer container for the signing widget |
|
||||
| `.embed--DocumentWidgetHeader` | Header section of the signing widget |
|
||||
| `.embed--DocumentWidgetContent` | Main content area of the signing widget |
|
||||
| `.embed--DocumentWidgetForm` | Form section within the signing widget |
|
||||
| `.embed--DocumentWidgetFooter` | Footer section of the signing widget |
|
||||
| `.embed--WaitingForTurn` | Waiting screen when it is not the user's turn |
|
||||
| `.embed--DocumentCompleted` | Completion screen after signing |
|
||||
| `.field--FieldRootContainer` | Base container for document fields |
|
||||
|
||||
### Field Data Attributes
|
||||
|
||||
Fields expose data attributes for state-based styling:
|
||||
|
||||
| Attribute | Values | Description |
|
||||
| ------------------- | ---------------------------------------------- | ------------------------------------ |
|
||||
| `[data-field-type]` | `SIGNATURE`, `TEXT`, `CHECKBOX`, `RADIO`, etc. | The type of field |
|
||||
| `[data-inserted]` | `true`, `false` | Whether the field has been filled |
|
||||
| `[data-validate]` | `true`, `false` | Whether the field is being validated |
|
||||
|
||||
### Example
|
||||
|
||||
```css
|
||||
/* Style signature fields */
|
||||
.field--FieldRootContainer[data-field-type='SIGNATURE'] {
|
||||
background-color: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
/* Style filled fields */
|
||||
.field--FieldRootContainer[data-inserted='true'] {
|
||||
background-color: var(--primary);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
/* Custom widget styling */
|
||||
.embed--DocumentWidget {
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
```
|
||||
|
||||
### Additional Examples
|
||||
|
||||
```css
|
||||
/* Style all field containers with transitions */
|
||||
.field--FieldRootContainer {
|
||||
transition: all 200ms ease;
|
||||
}
|
||||
|
||||
/* Custom styles for the waiting screen */
|
||||
.embed--WaitingForTurn {
|
||||
background-color: #f9fafb;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for the document container */
|
||||
@media (min-width: 768px) {
|
||||
.embed--DocumentContainer {
|
||||
gap: 2rem;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
<Accordions type="multiple">
|
||||
<Accordion title="Maintain contrast">
|
||||
Ensure sufficient contrast between background and foreground colors for accessibility.
|
||||
</Accordion>
|
||||
<Accordion title="Test dark mode">
|
||||
If dark mode is not disabled, verify your variables work in both modes.
|
||||
</Accordion>
|
||||
<Accordion title="Match your brand">
|
||||
Align `primary` and `accent` colors with your brand for a cohesive look.
|
||||
</Accordion>
|
||||
<Accordion title="Consistent radius">
|
||||
Use a border radius that matches your application's design system.
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [React](/docs/developers/embedding/sdks/react) - React SDK usage
|
||||
- [Vue](/docs/developers/embedding/sdks/vue) - Vue SDK usage
|
||||
@@ -0,0 +1,514 @@
|
||||
---
|
||||
title: Direct Links
|
||||
description: Share a URL or embed an iframe to let users sign documents without email invitations.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## What Are Direct Links?
|
||||
|
||||
Direct links are unique URLs tied to a template that allow anyone to:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
View and sign a document without receiving an email invitation
|
||||
</Step>
|
||||
<Step>
|
||||
Enter their own name and email address
|
||||
</Step>
|
||||
<Step>
|
||||
Complete signature fields and submit the document
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
When someone uses a direct link, Documenso creates a new document from the template with that person as the signer.
|
||||
|
||||
### When to Use Direct Links
|
||||
|
||||
- Collecting signatures from unknown recipients (forms, waivers, petitions)
|
||||
- Embedding signing in your website or application
|
||||
- Self-service contracts where customers initiate signing
|
||||
- Public-facing agreements that anyone can sign
|
||||
|
||||
### Limitations
|
||||
|
||||
- Only work with templates, not individual documents
|
||||
- Each link is tied to one recipient role in the template
|
||||
- Recipients enter their own information (you cannot prefill recipient details)
|
||||
|
||||
## Creating Direct Link Templates
|
||||
|
||||
Before embedding, you need a template with direct links enabled.
|
||||
|
||||
### Via the Dashboard
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
Go to **Templates** and create or select a template
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
Click the three-dot menu and select **Direct link**
|
||||
</Step>
|
||||
<Step>
|
||||
Click **Enable direct link signing**
|
||||
</Step>
|
||||
<Step>
|
||||
Choose which recipient in your template will use the direct link
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
Copy the generated URL
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Via the API
|
||||
|
||||
Create a direct link for an existing template:
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
# Create direct link for template
|
||||
curl -X POST "https://app.documenso.com/api/v2/template/direct/create" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"templateId": 123,
|
||||
"directRecipientId": 1
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const API_TOKEN = process.env.DOCUMENSO_API_TOKEN;
|
||||
const BASE_URL = 'https://app.documenso.com/api/v2';
|
||||
|
||||
const response = await fetch(`${BASE_URL}/template/direct/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: API_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateId: 123,
|
||||
directRecipientId: 1, // Optional: specific recipient to use
|
||||
}),
|
||||
});
|
||||
|
||||
const directLink = await response.json();
|
||||
console.log('Direct link token:', directLink.token);
|
||||
// URL: https://app.documenso.com/d/{token}
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 456,
|
||||
"token": "abc123xyz",
|
||||
"templateId": 123,
|
||||
"directTemplateRecipientId": 1,
|
||||
"enabled": true,
|
||||
"createdAt": "2025-01-15T10:00:00.000Z"
|
||||
}
|
||||
````
|
||||
|
||||
The direct link URL format is:
|
||||
|
||||
```
|
||||
https://app.documenso.com/d/{token}
|
||||
```
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Embedding in an iframe
|
||||
|
||||
Embed the signing experience directly in your application using an iframe.
|
||||
|
||||
### Basic iframe Embedding
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://app.documenso.com/embed/direct/abc123xyz"
|
||||
width="100%"
|
||||
height="800"
|
||||
frameborder="0"
|
||||
allow="clipboard-write"
|
||||
></iframe>
|
||||
```
|
||||
|
||||
### Responsive iframe
|
||||
|
||||
```html
|
||||
<div style="position: relative; width: 100%; padding-bottom: 75%; height: 0; overflow: hidden;">
|
||||
<iframe
|
||||
src="https://app.documenso.com/embed/direct/abc123xyz"
|
||||
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;"
|
||||
allow="clipboard-write"
|
||||
></iframe>
|
||||
</div>
|
||||
```
|
||||
|
||||
### React Component Example
|
||||
|
||||
```tsx
|
||||
function DocumentSigner({ token }: { token: string }) {
|
||||
return (
|
||||
<div className="h-[800px] w-full">
|
||||
<iframe
|
||||
src={`https://app.documenso.com/embed/direct/${token}`}
|
||||
className="h-full w-full border-0"
|
||||
allow="clipboard-write"
|
||||
title="Sign Document"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The embed URL uses `/embed/direct/{token}` instead of `/d/{token}`. The embed version is optimized
|
||||
for iframe embedding with reduced navigation.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Embedding with Redirect
|
||||
|
||||
Instead of an iframe, redirect users to the signing page and bring them back after completion.
|
||||
|
||||
### Simple Redirect
|
||||
|
||||
```typescript
|
||||
function redirectToSigning(token: string) {
|
||||
window.location.href = `https://app.documenso.com/d/${token}`;
|
||||
}
|
||||
```
|
||||
|
||||
### With Return URL
|
||||
|
||||
Configure a redirect URL in your template settings to return users to your application after signing:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
Edit your template
|
||||
</Step>
|
||||
<Step>
|
||||
Go to **Advanced Settings**
|
||||
</Step>
|
||||
<Step>
|
||||
Set **Redirect URL** to your desired return URL (e.g., `https://yourapp.com/signed`)
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
Or set it via API when creating the template:
|
||||
|
||||
```typescript
|
||||
const response = await fetch(`${BASE_URL}/template/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: API_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateId: 123,
|
||||
meta: {
|
||||
redirectUrl: 'https://yourapp.com/signing-complete',
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
After signing completes, users are automatically redirected to the specified URL.
|
||||
|
||||
---
|
||||
|
||||
## URL Parameters
|
||||
|
||||
Pass additional data through the direct link URL using query parameters.
|
||||
|
||||
### External ID
|
||||
|
||||
Track which document belongs to which transaction in your system:
|
||||
|
||||
```
|
||||
https://app.documenso.com/d/abc123xyz?externalId=order-12345
|
||||
```
|
||||
|
||||
The external ID is stored with the created document and included in webhook payloads.
|
||||
|
||||
### Example: Dynamic External ID
|
||||
|
||||
```typescript
|
||||
function getSigningUrl(token: string, orderId: string) {
|
||||
const params = new URLSearchParams({
|
||||
externalId: orderId,
|
||||
});
|
||||
|
||||
return `https://app.documenso.com/d/${token}?${params.toString()}`;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const url = getSigningUrl('abc123xyz', 'order-12345');
|
||||
// https://app.documenso.com/d/abc123xyz?externalId=order-12345
|
||||
```
|
||||
|
||||
### Embed URL Parameters
|
||||
|
||||
The embed URL supports the same parameters:
|
||||
|
||||
```
|
||||
https://app.documenso.com/embed/direct/abc123xyz?externalId=order-12345
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Access Authentication
|
||||
|
||||
Direct links can require authentication before signing. Configure this in your template settings:
|
||||
|
||||
| Auth Type | Description |
|
||||
| --------- | ---------------------------------------------- |
|
||||
| None | Anyone with the link can sign |
|
||||
| Account | Signer must be logged into a Documenso account |
|
||||
|
||||
To require authentication, set `globalAccessAuth` when creating or updating the template:
|
||||
|
||||
```typescript
|
||||
const response = await fetch(`${BASE_URL}/template/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: API_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateId: 123,
|
||||
data: {
|
||||
globalAccessAuth: ['ACCOUNT'], // Require login
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### Disabling Direct Links
|
||||
|
||||
Temporarily disable a direct link without deleting it:
|
||||
|
||||
<Tabs items={['curl', 'TypeScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/template/direct/toggle" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"templateId": 123,
|
||||
"enabled": false
|
||||
}'
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```typescript
|
||||
const response = await fetch(`${BASE_URL}/template/direct/toggle`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: API_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateId: 123,
|
||||
enabled: false,
|
||||
}),
|
||||
});
|
||||
```
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
Re-enable later by setting `enabled: true`. The URL remains the same.
|
||||
|
||||
### Deleting Direct Links
|
||||
|
||||
Permanently remove a direct link:
|
||||
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/template/direct/delete" \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"templateId": 123
|
||||
}'
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
Deleting a direct link invalidates the URL permanently. If you enable direct links again, a new
|
||||
URL will be generated.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Styling and Customization
|
||||
|
||||
### Branding
|
||||
|
||||
The signing experience uses your organisation's branding settings:
|
||||
|
||||
- Logo
|
||||
- Primary color
|
||||
- Email customization
|
||||
|
||||
Configure branding in **Settings** > **Branding** or via team settings.
|
||||
|
||||
### Signature Options
|
||||
|
||||
Control which signature input methods are available:
|
||||
|
||||
```typescript
|
||||
const response = await fetch(`${BASE_URL}/template/update`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: API_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateId: 123,
|
||||
meta: {
|
||||
typedSignatureEnabled: true, // Allow typed signatures
|
||||
drawSignatureEnabled: true, // Allow drawn signatures
|
||||
uploadSignatureEnabled: false, // Disable uploaded signatures
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### White-labeling
|
||||
|
||||
Enterprise plans support white-label embedding with the "Powered by Documenso" badge removed. [Contact sales](mailto:support@documenso.com) for details.
|
||||
|
||||
---
|
||||
|
||||
## Handling Completion
|
||||
|
||||
### Webhook Notifications
|
||||
|
||||
Set up webhooks to receive notifications when documents are signed:
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_SIGNED",
|
||||
"payload": {
|
||||
"id": 123,
|
||||
"externalId": "order-12345",
|
||||
"title": "contract.pdf",
|
||||
"status": "COMPLETED",
|
||||
"Recipient": [
|
||||
{
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"signingStatus": "SIGNED",
|
||||
"signedAt": "2024-01-15T10:30:00.000Z"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-01-15T10:30:00.000Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
See [Webhooks](/docs/developers/webhooks) for setup instructions.
|
||||
|
||||
### Polling for Status
|
||||
|
||||
If webhooks aren't suitable, poll the document status:
|
||||
|
||||
```typescript
|
||||
async function checkDocumentStatus(externalId: string) {
|
||||
const response = await fetch(`${BASE_URL}/envelope?source=TEMPLATE_DIRECT_LINK`, {
|
||||
headers: { Authorization: API_TOKEN },
|
||||
});
|
||||
|
||||
const { data } = await response.json();
|
||||
|
||||
return data.find((doc) => doc.externalId === externalId);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
This example shows a complete workflow for embedding direct link signing:
|
||||
|
||||
```typescript
|
||||
const API_TOKEN = process.env.DOCUMENSO_API_TOKEN;
|
||||
const BASE_URL = 'https://app.documenso.com/api/v2';
|
||||
|
||||
// 1. Get template with direct link
|
||||
async function getDirectLinkTemplate(templateId: number) {
|
||||
const response = await fetch(`${BASE_URL}/template/${templateId}`, {
|
||||
headers: { Authorization: API_TOKEN },
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 2. Create direct link if not exists
|
||||
async function ensureDirectLink(templateId: number, recipientId: number) {
|
||||
const template = await getDirectLinkTemplate(templateId);
|
||||
|
||||
if (template.directLink?.enabled) {
|
||||
return template.directLink.token;
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/template/direct/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: API_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
templateId,
|
||||
directRecipientId: recipientId,
|
||||
}),
|
||||
});
|
||||
|
||||
const directLink = await response.json();
|
||||
return directLink.token;
|
||||
}
|
||||
|
||||
// 3. Generate signing URL with tracking
|
||||
function getEmbedUrl(token: string, orderId: string) {
|
||||
const params = new URLSearchParams({ externalId: orderId });
|
||||
return `https://app.documenso.com/embed/direct/${token}?${params}`;
|
||||
}
|
||||
|
||||
// Usage
|
||||
const token = await ensureDirectLink(123, 1);
|
||||
const embedUrl = getEmbedUrl(token, 'order-12345');
|
||||
|
||||
// Embed in your page
|
||||
document.getElementById('signer-frame').src = embedUrl;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [SDKs](/docs/developers/embedding/sdks) - Framework SDK integration
|
||||
- [Templates API](/docs/developers/api/templates) - Template management
|
||||
- [Webhooks](/docs/developers/webhooks) - Event notifications
|
||||
- [Direct Links (User Guide)](/docs/users/documents/direct-links) - End-user documentation
|
||||
@@ -0,0 +1,242 @@
|
||||
---
|
||||
title: Embedding
|
||||
description: Embed document signing experiences directly in your application using official SDKs.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
## Embedded Signing vs Embedded Authoring
|
||||
|
||||
Documenso offers two types of embedding:
|
||||
|
||||
- **Embedded Signing** lets you embed the signing experience in your application. Your users sign documents without leaving your site. Available on Teams Plan and above.
|
||||
- **Embedded Authoring** lets you embed document and template _creation and editing_ in your application. This is an [Enterprise](/docs/policies/enterprise-edition) feature (also available as a Platform Plan add-on). See the [Authoring](/docs/developers/embedding/authoring) guide.
|
||||
|
||||
This page covers **embedded signing**. If you need your users to create or edit documents inside your app, see [Authoring](/docs/developers/embedding/authoring).
|
||||
|
||||
---
|
||||
|
||||
## Availability
|
||||
|
||||
Embedding is available on **Teams Plan** and above, as well as for **Early Adopters** within a team (Early Adopters can create a team for free).
|
||||
|
||||
The [Platform Plan](https://documen.so/platform-cta-pricing) adds enhanced customization:
|
||||
|
||||
- Custom CSS and styling variables
|
||||
- Dark mode controls
|
||||
- Removal of Documenso branding
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
There are two ways to embed signing, each using a different component and token type.
|
||||
|
||||
### Direct Templates
|
||||
|
||||
Direct templates are evergreen - each time a user completes signing, a new document is created from the template. This is the recommended approach for most use cases.
|
||||
|
||||
Use the `EmbedDirectTemplate` component with a template token:
|
||||
|
||||
```jsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-react';
|
||||
|
||||
<EmbedDirectTemplate
|
||||
token="your-template-token"
|
||||
onDocumentCompleted={(data) => {
|
||||
console.log('Signed:', data.documentId);
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
### Signing Tokens
|
||||
|
||||
For advanced integrations where you create documents via the API, you can embed the signing experience for a specific recipient using their signing token.
|
||||
|
||||
Use the `EmbedSignDocument` component with the recipient's token:
|
||||
|
||||
```jsx
|
||||
import { EmbedSignDocument } from '@documenso/embed-react';
|
||||
|
||||
<EmbedSignDocument
|
||||
token="recipient-signing-token"
|
||||
onDocumentCompleted={(data) => {
|
||||
console.log('Signed:', data.documentId);
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
For most use cases, direct templates are the way to go. Use signing tokens when you need
|
||||
programmatic control over document creation via the API.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Getting Your Token
|
||||
|
||||
### Direct Template Token
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
Navigate to your team's templates in Documenso
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
Click on a direct link template to copy its URL. The token is the last segment of the URL.
|
||||
|
||||
For example, `https://app.documenso.com/d/-WoSwWVT-fYOERS2MI37k` has the token `-WoSwWVT-fYOERS2MI37k`.
|
||||
|
||||
If your template is not a direct link template yet, select **Direct Link** from the three-dot menu on the templates table to enable it.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Signing Token
|
||||
|
||||
Signing tokens are returned in API responses when distributing a document. You can also get one manually by hovering over a recipient's avatar on a document you own and clicking their email.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Framework SDKs
|
||||
|
||||
Pick your framework to get started:
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="React"
|
||||
description="@documenso/embed-react"
|
||||
href="/docs/developers/embedding/sdks/react"
|
||||
/>
|
||||
<Card title="Vue" description="@documenso/embed-vue" href="/docs/developers/embedding/sdks/vue" />
|
||||
<Card
|
||||
title="Svelte"
|
||||
description="@documenso/embed-svelte"
|
||||
href="/docs/developers/embedding/sdks/svelte"
|
||||
/>
|
||||
<Card
|
||||
title="Angular"
|
||||
description="@documenso/embed-angular"
|
||||
href="/docs/developers/embedding/sdks/angular"
|
||||
/>
|
||||
<Card
|
||||
title="Solid"
|
||||
description="@documenso/embed-solid"
|
||||
href="/docs/developers/embedding/sdks/solid"
|
||||
/>
|
||||
<Card
|
||||
title="Preact"
|
||||
description="@documenso/embed-preact"
|
||||
href="/docs/developers/embedding/sdks/preact"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
A [Web Components](https://developer.mozilla.org/en-US/docs/Web/API/Web_components) SDK (`@documenso/embed-webcomponent`) is also available for use outside of JavaScript frameworks. It works in any environment that supports custom elements.
|
||||
|
||||
If you prefer not to use any SDK, you can embed signing using [Direct Links](/docs/developers/embedding/direct-links) with a plain iframe or redirect.
|
||||
|
||||
---
|
||||
|
||||
## Props
|
||||
|
||||
### EmbedDirectTemplate
|
||||
|
||||
| Prop | Type | Description |
|
||||
| --------------------- | ---------- | ---------------------------------------------------------------- |
|
||||
| `token` | `string` | **Required.** The direct template token. |
|
||||
| `host` | `string` | Documenso instance URL. Defaults to `https://app.documenso.com`. |
|
||||
| `name` | `string` | Pre-fill the signer's name. |
|
||||
| `lockName` | `boolean` | Prevent the signer from changing their name. |
|
||||
| `email` | `string` | Pre-fill the signer's email. |
|
||||
| `lockEmail` | `boolean` | Prevent the signer from changing their email. |
|
||||
| `externalId` | `string` | Your reference ID, stored with the created document. |
|
||||
| `css` | `string` | Custom CSS string (Platform Plan). |
|
||||
| `cssVars` | `object` | CSS variable overrides for theming (Platform Plan). |
|
||||
| `darkModeDisabled` | `boolean` | Disable dark mode in the embed (Platform Plan). |
|
||||
| `onDocumentReady` | `function` | Called when the document is loaded and ready. |
|
||||
| `onDocumentCompleted` | `function` | Called when signing is completed. |
|
||||
| `onDocumentError` | `function` | Called when an error occurs. |
|
||||
| `onFieldSigned` | `function` | Called when a field is signed. |
|
||||
| `onFieldUnsigned` | `function` | Called when a field value is cleared. |
|
||||
|
||||
### EmbedSignDocument
|
||||
|
||||
| Prop | Type | Description |
|
||||
| --------------------- | ---------- | ---------------------------------------------------------------- |
|
||||
| `token` | `string` | **Required.** The recipient's signing token. |
|
||||
| `host` | `string` | Documenso instance URL. Defaults to `https://app.documenso.com`. |
|
||||
| `name` | `string` | Pre-fill the signer's name. |
|
||||
| `lockName` | `boolean` | Prevent the signer from changing their name. |
|
||||
| `onDocumentReady` | `function` | Called when the document is loaded and ready. |
|
||||
| `onDocumentCompleted` | `function` | Called when signing is completed. |
|
||||
| `onDocumentError` | `function` | Called when an error occurs. |
|
||||
|
||||
---
|
||||
|
||||
## Event Callbacks
|
||||
|
||||
### onDocumentCompleted
|
||||
|
||||
Receives an object with:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------- | -------- | ------------------------------ |
|
||||
| `token` | `string` | The token used for signing. |
|
||||
| `documentId` | `number` | The ID of the signed document. |
|
||||
| `recipientId` | `number` | The ID of the recipient. |
|
||||
|
||||
### onFieldSigned
|
||||
|
||||
Receives an object with:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------- | --------- | -------------------------------------------- |
|
||||
| `fieldId` | `number` | The ID of the field. |
|
||||
| `value` | `string` | The field value. |
|
||||
| `isBase64` | `boolean` | Whether the value is a base64 encoded image. |
|
||||
|
||||
### onFieldUnsigned
|
||||
|
||||
Receives an object with:
|
||||
|
||||
| Field | Type | Description |
|
||||
| --------- | -------- | -------------------- |
|
||||
| `fieldId` | `number` | The ID of the field. |
|
||||
|
||||
---
|
||||
|
||||
## More
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="Direct Links"
|
||||
description="Embed with iframes or redirects, no SDK required."
|
||||
href="/docs/developers/embedding/direct-links"
|
||||
/>
|
||||
<Card
|
||||
title="CSS Variables"
|
||||
description="Customize colors, spacing, and theming."
|
||||
href="/docs/developers/embedding/css-variables"
|
||||
/>
|
||||
<Card
|
||||
title="Authoring"
|
||||
description="Embed document and template creation."
|
||||
href="/docs/developers/embedding/authoring"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Documents API](/docs/developers/api/documents) - Create documents programmatically
|
||||
- [Templates API](/docs/developers/api/templates) - Manage templates via API
|
||||
- [Webhooks](/docs/developers/webhooks) - Receive server-side signing notifications
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Embedding",
|
||||
"pages": ["sdks", "direct-links", "css-variables", "authoring"]
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
title: Angular
|
||||
description: Embed Documenso signing in your Angular application using the official SDK.
|
||||
---
|
||||
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs items={['npm', 'yarn', 'pnpm']}>
|
||||
<Tab value="npm">``` npm install @documenso/embed-angular ```</Tab>
|
||||
<Tab value="yarn">``` yarn add @documenso/embed-angular ```</Tab>
|
||||
<Tab value="pnpm">``` pnpm add @documenso/embed-angular ```</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Direct Template
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-signing',
|
||||
standalone: true,
|
||||
imports: [EmbedDirectTemplate],
|
||||
template: `
|
||||
<embed-direct-template
|
||||
[token]="token"
|
||||
[name]="'John Doe'"
|
||||
[email]="'john@example.com'"
|
||||
[lockEmail]="true"
|
||||
[externalId]="'order-12345'"
|
||||
(documentReady)="onReady()"
|
||||
(documentCompleted)="onCompleted($event)"
|
||||
(documentError)="onError()"
|
||||
/>
|
||||
`,
|
||||
})
|
||||
export class SigningComponent {
|
||||
token = 'your-template-token';
|
||||
|
||||
onReady() {
|
||||
console.log('Ready');
|
||||
}
|
||||
|
||||
onCompleted(data: { documentId: number }) {
|
||||
console.log('Signed:', data.documentId);
|
||||
}
|
||||
|
||||
onError() {
|
||||
console.error('Error');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signing Token
|
||||
|
||||
```typescript
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { EmbedSignDocument } from '@documenso/embed-angular';
|
||||
|
||||
@Component({
|
||||
selector: 'app-signing',
|
||||
standalone: true,
|
||||
imports: [EmbedSignDocument],
|
||||
template: `
|
||||
<embed-sign-document
|
||||
[token]="token"
|
||||
(documentCompleted)="onCompleted($event)"
|
||||
/>
|
||||
`,
|
||||
})
|
||||
export class SigningComponent {
|
||||
@Input() token = '';
|
||||
|
||||
onCompleted(data: { documentId: number }) {
|
||||
console.log('Signed:', data.documentId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: SDKs
|
||||
description: Official embedding SDKs for React, Vue, Svelte, Angular, Solid, and Preact.
|
||||
---
|
||||
|
||||
Install the SDK for your framework and embed document signing with a few lines of code.
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="React"
|
||||
description="@documenso/embed-react"
|
||||
href="/docs/developers/embedding/sdks/react"
|
||||
/>
|
||||
<Card title="Vue" description="@documenso/embed-vue" href="/docs/developers/embedding/sdks/vue" />
|
||||
<Card
|
||||
title="Svelte"
|
||||
description="@documenso/embed-svelte"
|
||||
href="/docs/developers/embedding/sdks/svelte"
|
||||
/>
|
||||
<Card
|
||||
title="Angular"
|
||||
description="@documenso/embed-angular"
|
||||
href="/docs/developers/embedding/sdks/angular"
|
||||
/>
|
||||
<Card
|
||||
title="Solid"
|
||||
description="@documenso/embed-solid"
|
||||
href="/docs/developers/embedding/sdks/solid"
|
||||
/>
|
||||
<Card
|
||||
title="Preact"
|
||||
description="@documenso/embed-preact"
|
||||
href="/docs/developers/embedding/sdks/preact"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
If you are not using a JavaScript framework, you can embed signing using [Direct Links](/docs/developers/embedding/direct-links) with a plain iframe or redirect.
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "SDKs",
|
||||
"pages": ["react", "vue", "svelte", "angular", "solid", "preact"]
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Preact
|
||||
description: Embed Documenso signing in your Preact application using the official SDK.
|
||||
---
|
||||
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs items={['npm', 'yarn', 'pnpm']}>
|
||||
<Tab value="npm">``` npm install @documenso/embed-preact ```</Tab>
|
||||
<Tab value="yarn">``` yarn add @documenso/embed-preact ```</Tab>
|
||||
<Tab value="pnpm">``` pnpm add @documenso/embed-preact ```</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Direct Template
|
||||
|
||||
```tsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-preact';
|
||||
|
||||
const SigningPage = () => {
|
||||
return (
|
||||
<EmbedDirectTemplate
|
||||
token="your-template-token"
|
||||
name="John Doe"
|
||||
email="john@example.com"
|
||||
lockEmail={true}
|
||||
externalId="order-12345"
|
||||
onDocumentReady={() => console.log('Ready')}
|
||||
onDocumentCompleted={(data) => {
|
||||
console.log('Signed:', data.documentId);
|
||||
}}
|
||||
onDocumentError={() => console.error('Error')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signing Token
|
||||
|
||||
```tsx
|
||||
import { EmbedSignDocument } from '@documenso/embed-preact';
|
||||
|
||||
const SigningPage = ({ token }: { token: string }) => {
|
||||
return (
|
||||
<EmbedSignDocument
|
||||
token={token}
|
||||
onDocumentCompleted={(data) => {
|
||||
console.log('Signed:', data.documentId);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Styling (Platform Plan)
|
||||
|
||||
```tsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-preact';
|
||||
|
||||
const StyledEmbed = () => {
|
||||
return (
|
||||
<EmbedDirectTemplate
|
||||
token="your-token"
|
||||
css={`
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`}
|
||||
cssVars={{
|
||||
primary: '#0000FF',
|
||||
background: '#F5F5F5',
|
||||
radius: '8px',
|
||||
}}
|
||||
darkModeDisabled={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
See [CSS Variables](/docs/developers/embedding/css-variables) for all available variables.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: React
|
||||
description: Embed Documenso signing in your React application using the official SDK.
|
||||
---
|
||||
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs items={['npm', 'yarn', 'pnpm']}>
|
||||
<Tab value="npm">``` npm install @documenso/embed-react ```</Tab>
|
||||
<Tab value="yarn">``` yarn add @documenso/embed-react ```</Tab>
|
||||
<Tab value="pnpm">``` pnpm add @documenso/embed-react ```</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Direct Template
|
||||
|
||||
```tsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-react';
|
||||
|
||||
const SigningPage = () => {
|
||||
return (
|
||||
<EmbedDirectTemplate
|
||||
token="your-template-token"
|
||||
name="John Doe"
|
||||
email="john@example.com"
|
||||
lockEmail={true}
|
||||
externalId="order-12345"
|
||||
onDocumentReady={() => console.log('Ready')}
|
||||
onDocumentCompleted={(data) => {
|
||||
console.log('Signed:', data.documentId);
|
||||
}}
|
||||
onDocumentError={() => console.error('Error')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signing Token
|
||||
|
||||
```tsx
|
||||
import { EmbedSignDocument } from '@documenso/embed-react';
|
||||
|
||||
const SigningPage = ({ token }: { token: string }) => {
|
||||
return (
|
||||
<EmbedSignDocument
|
||||
token={token}
|
||||
onDocumentCompleted={(data) => {
|
||||
console.log('Signed:', data.documentId);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Styling (Platform Plan)
|
||||
|
||||
```tsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-react';
|
||||
|
||||
const StyledEmbed = () => {
|
||||
return (
|
||||
<EmbedDirectTemplate
|
||||
token="your-token"
|
||||
css={`
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`}
|
||||
cssVars={{
|
||||
primary: '#0000FF',
|
||||
background: '#F5F5F5',
|
||||
radius: '8px',
|
||||
}}
|
||||
darkModeDisabled={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
See [CSS Variables](/docs/developers/embedding/css-variables) for all available variables.
|
||||
|
||||
---
|
||||
|
||||
## Complete Example
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-react';
|
||||
|
||||
type Status = 'loading' | 'ready' | 'completed' | 'error';
|
||||
|
||||
const DocumentSigning = ({ token }: { token: string }) => {
|
||||
const [status, setStatus] = useState<Status>('loading');
|
||||
|
||||
if (status === 'completed') {
|
||||
return <p>Thank you for signing the document.</p>;
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return <p>An error occurred. Please try again.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', height: '100vh' }}>
|
||||
{status === 'loading' && (
|
||||
<div style={{ position: 'absolute', inset: 0, display: 'grid', placeItems: 'center' }}>
|
||||
Loading...
|
||||
</div>
|
||||
)}
|
||||
<EmbedDirectTemplate
|
||||
token={token}
|
||||
onDocumentReady={() => setStatus('ready')}
|
||||
onDocumentCompleted={() => setStatus('completed')}
|
||||
onDocumentError={() => setStatus('error')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
title: Solid
|
||||
description: Embed Documenso signing in your Solid application using the official SDK.
|
||||
---
|
||||
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs items={['npm', 'yarn', 'pnpm']}>
|
||||
<Tab value="npm">``` npm install @documenso/embed-solid ```</Tab>
|
||||
<Tab value="yarn">``` yarn add @documenso/embed-solid ```</Tab>
|
||||
<Tab value="pnpm">``` pnpm add @documenso/embed-solid ```</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Direct Template
|
||||
|
||||
```tsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-solid';
|
||||
|
||||
const SigningPage = () => {
|
||||
return (
|
||||
<EmbedDirectTemplate
|
||||
token="your-template-token"
|
||||
name="John Doe"
|
||||
email="john@example.com"
|
||||
lockEmail={true}
|
||||
externalId="order-12345"
|
||||
onDocumentReady={() => console.log('Ready')}
|
||||
onDocumentCompleted={(data) => {
|
||||
console.log('Signed:', data.documentId);
|
||||
}}
|
||||
onDocumentError={() => console.error('Error')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signing Token
|
||||
|
||||
```tsx
|
||||
import { EmbedSignDocument } from '@documenso/embed-solid';
|
||||
|
||||
const SigningPage = (props: { token: string }) => {
|
||||
return (
|
||||
<EmbedSignDocument
|
||||
token={props.token}
|
||||
onDocumentCompleted={(data) => {
|
||||
console.log('Signed:', data.documentId);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Styling (Platform Plan)
|
||||
|
||||
```tsx
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-solid';
|
||||
|
||||
const StyledEmbed = () => {
|
||||
return (
|
||||
<EmbedDirectTemplate
|
||||
token="your-token"
|
||||
css={`
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`}
|
||||
cssVars={{
|
||||
primary: '#0000FF',
|
||||
background: '#F5F5F5',
|
||||
radius: '8px',
|
||||
}}
|
||||
darkModeDisabled={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
See [CSS Variables](/docs/developers/embedding/css-variables) for all available variables.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Svelte
|
||||
description: Embed Documenso signing in your Svelte application using the official SDK.
|
||||
---
|
||||
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs items={['npm', 'yarn', 'pnpm']}>
|
||||
<Tab value="npm">``` npm install @documenso/embed-svelte ```</Tab>
|
||||
<Tab value="yarn">``` yarn add @documenso/embed-svelte ```</Tab>
|
||||
<Tab value="pnpm">``` pnpm add @documenso/embed-svelte ```</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Direct Template
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-svelte';
|
||||
|
||||
const token = 'your-template-token';
|
||||
|
||||
function onCompleted(data: { documentId: number }) {
|
||||
console.log('Signed:', data.documentId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<EmbedDirectTemplate
|
||||
{token}
|
||||
name="John Doe"
|
||||
email="john@example.com"
|
||||
lockEmail={true}
|
||||
externalId="order-12345"
|
||||
onDocumentReady={() => console.log('Ready')}
|
||||
onDocumentCompleted={onCompleted}
|
||||
onDocumentError={() => console.error('Error')}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signing Token
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { EmbedSignDocument } from '@documenso/embed-svelte';
|
||||
|
||||
export let token: string;
|
||||
|
||||
function onCompleted(data: { documentId: number }) {
|
||||
console.log('Signed:', data.documentId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<EmbedSignDocument
|
||||
{token}
|
||||
onDocumentCompleted={onCompleted}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Styling (Platform Plan)
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-svelte';
|
||||
|
||||
const token = 'your-token';
|
||||
|
||||
const customCss = `
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`;
|
||||
|
||||
const cssVars = {
|
||||
primary: '#0000FF',
|
||||
background: '#F5F5F5',
|
||||
radius: '8px',
|
||||
};
|
||||
</script>
|
||||
|
||||
<EmbedDirectTemplate
|
||||
{token}
|
||||
css={customCss}
|
||||
cssVars={cssVars}
|
||||
darkModeDisabled={true}
|
||||
/>
|
||||
```
|
||||
|
||||
See [CSS Variables](/docs/developers/embedding/css-variables) for all available variables.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
@@ -0,0 +1,107 @@
|
||||
---
|
||||
title: Vue
|
||||
description: Embed Documenso signing in your Vue application using the official SDK.
|
||||
---
|
||||
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Installation
|
||||
|
||||
<Tabs items={['npm', 'yarn', 'pnpm']}>
|
||||
<Tab value="npm">``` npm install @documenso/embed-vue ```</Tab>
|
||||
<Tab value="yarn">``` yarn add @documenso/embed-vue ```</Tab>
|
||||
<Tab value="pnpm">``` pnpm add @documenso/embed-vue ```</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Direct Template
|
||||
|
||||
```html
|
||||
<script setup lang="ts">
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-vue';
|
||||
|
||||
const token = 'your-template-token';
|
||||
|
||||
function onCompleted(data: { documentId: number }) {
|
||||
console.log('Signed:', data.documentId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmbedDirectTemplate
|
||||
:token="token"
|
||||
name="John Doe"
|
||||
email="john@example.com"
|
||||
:lockEmail="true"
|
||||
externalId="order-12345"
|
||||
@document-completed="onCompleted"
|
||||
@document-ready="() => console.log('Ready')"
|
||||
@document-error="() => console.error('Error')"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Signing Token
|
||||
|
||||
```html
|
||||
<script setup lang="ts">
|
||||
import { EmbedSignDocument } from '@documenso/embed-vue';
|
||||
|
||||
const props = defineProps<{ token: string }>();
|
||||
|
||||
function onCompleted(data: { documentId: number }) {
|
||||
console.log('Signed:', data.documentId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmbedSignDocument :token="props.token" @document-completed="onCompleted" />
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Styling (Platform Plan)
|
||||
|
||||
```html
|
||||
<script setup lang="ts">
|
||||
import { EmbedDirectTemplate } from '@documenso/embed-vue';
|
||||
|
||||
const token = 'your-token';
|
||||
|
||||
const customCss = `
|
||||
.documenso-embed {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
`;
|
||||
|
||||
const cssVars = {
|
||||
primary: '#0000FF',
|
||||
background: '#F5F5F5',
|
||||
radius: '8px',
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EmbedDirectTemplate
|
||||
:token="token"
|
||||
:css="customCss"
|
||||
:cssVars="cssVars"
|
||||
:darkModeDisabled="true"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
See [CSS Variables](/docs/developers/embedding/css-variables) for all available variables.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
---
|
||||
title: Examples
|
||||
description: Common integration patterns and end-to-end workflows.
|
||||
---
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="Common Workflows"
|
||||
description="End-to-end examples for typical use cases."
|
||||
href="/docs/developers/examples/common-workflows"
|
||||
/>
|
||||
</Cards>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Examples",
|
||||
"pages": ["common-workflows"]
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
---
|
||||
title: Authentication
|
||||
description: Generate an API key and authenticate your requests.
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Documenso account (cloud or self-hosted)
|
||||
- A Documenso account on any plan (Free, Individual, Team, or Enterprise)
|
||||
|
||||
<Callout type="info">
|
||||
Free accounts include API access with a limit of 5 documents per month. [Upgrade to a paid
|
||||
plan](https://documen.so/pricing) for higher limits.
|
||||
</Callout>
|
||||
|
||||
## Create an API Token
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Open settings
|
||||
|
||||
- Log in to your Documenso account
|
||||
- Click your avatar in the top right corner
|
||||
- Select **Settings** from the dropdown menu
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Navigate to the API Tokens tab
|
||||
|
||||
Go to **Settings** and open the **API Tokens** tab.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Generate a new token
|
||||
|
||||
- Click **Create Token**
|
||||
- Enter a descriptive name (e.g., `production-backend`, `zapier-integration`)
|
||||
- Select an expiration period: never expires, 7 days, 1 month, 3 months, 6 months, or 1 year
|
||||
- Click **Create Token**
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Copy your token
|
||||
|
||||
Your token is displayed once after creation. Copy it immediately and store it securely.
|
||||
|
||||

|
||||
|
||||
<Callout type="warn">
|
||||
You cannot view the token again after leaving this page. If you lose it, you must create a new
|
||||
token.
|
||||
</Callout>
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Using Your Token
|
||||
|
||||
Include the token in the `Authorization` header of your HTTP requests.
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
curl https://app.documenso.com/api/v2/documents \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx"
|
||||
```
|
||||
|
||||
### JavaScript / TypeScript
|
||||
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/documents', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
},
|
||||
});
|
||||
|
||||
const documents = await response.json();
|
||||
```
|
||||
|
||||
### Using the TypeScript SDK
|
||||
|
||||
Documenso provides official SDKs that handle authentication for you:
|
||||
|
||||
```typescript
|
||||
import { Documenso } from '@documenso/sdk-typescript';
|
||||
|
||||
const client = new Documenso({
|
||||
apiKey: 'api_xxxxxxxxxxxxxxxx',
|
||||
});
|
||||
|
||||
const documents = await client.documents.find();
|
||||
```
|
||||
|
||||
SDKs are available for [TypeScript](https://github.com/documenso/sdk-typescript), [Python](https://github.com/documenso/sdk-python), and [Go](https://github.com/documenso/sdk-go).
|
||||
|
||||
## API Base URLs
|
||||
|
||||
| Environment | Base URL |
|
||||
| ----------- | -------------------------------------- |
|
||||
| Production | `https://app.documenso.com/api/v2` |
|
||||
| Staging | `https://stg-app.documenso.com/api/v2` |
|
||||
| Self-hosted | `https://your-domain.com/api/v2` |
|
||||
|
||||
<Callout type="info">
|
||||
API V1 is deprecated. Use V2 for all new integrations. V1 only works with legacy documents created
|
||||
before the envelope system. If you need V1 documentation for migration purposes, see the [V1
|
||||
OpenAPI reference](https://app.documenso.com/api/v1/openapi).
|
||||
</Callout>
|
||||
|
||||
<Callout type="info">
|
||||
The API is available on all plans, including Free (5 documents per month). [Fair
|
||||
Use](/docs/policies/fair-use) applies to all API usage.
|
||||
</Callout>
|
||||
|
||||
## Token Security
|
||||
|
||||
API tokens grant full access to your account. Follow these practices to keep them secure:
|
||||
|
||||
- **Never commit tokens to version control.** Use environment variables instead.
|
||||
- **Use descriptive names.** Names like `zapier-prod` or `backend-staging` help you identify token usage.
|
||||
- **Set expiration dates.** Shorter expiration periods reduce risk if a token is compromised.
|
||||
- **Rotate tokens regularly.** Create new tokens and revoke old ones periodically.
|
||||
- **Use separate tokens per integration.** If one is compromised, you only need to revoke that specific token.
|
||||
- **Revoke unused tokens.** Delete tokens you no longer need from the API Tokens settings page.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Store your token in an environment variable rather than hardcoding it:
|
||||
|
||||
```bash
|
||||
# .env (do not commit this file)
|
||||
DOCUMENSO_API_KEY=api_xxxxxxxxxxxxxxxx
|
||||
```
|
||||
|
||||
```typescript
|
||||
const client = new Documenso({
|
||||
apiKey: process.env.DOCUMENSO_API_KEY,
|
||||
});
|
||||
```
|
||||
|
||||
## Token Scope
|
||||
|
||||
API tokens have full access to your account, including:
|
||||
|
||||
- Creating, reading, updating, and deleting documents
|
||||
- Managing recipients and fields
|
||||
- Accessing templates
|
||||
- Managing team resources (if the token owner has team access)
|
||||
|
||||
There is currently no way to create tokens with limited scopes or permissions.
|
||||
|
||||
## Revoking a Token
|
||||
|
||||
To revoke a token:
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
Go to **Settings** > **API Tokens**
|
||||
</Step>
|
||||
<Step>
|
||||
Find the token you want to revoke
|
||||
</Step>
|
||||
<Step>
|
||||
Click the delete icon next to the token
|
||||
</Step>
|
||||
<Step>
|
||||
Confirm the deletion
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
Revoked tokens stop working immediately. Any integrations using that token will receive `401 Unauthorized` errors.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<Accordions type="multiple">
|
||||
<Accordion title="401 Unauthorized — Missing or invalid token">
|
||||
Check that you included the token in the `Authorization` header.
|
||||
</Accordion>
|
||||
<Accordion title="401 Unauthorized — Expired token">Create a new token in settings.</Accordion>
|
||||
<Accordion title="403 Forbidden — Token doesn't have access to the resource">
|
||||
Ensure you're accessing resources owned by the token's account.
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Make your first API call](/docs/developers/getting-started/first-api-call) - Create a document via the API
|
||||
- [API Reference](/docs/developers/api) - Explore available endpoints
|
||||
@@ -0,0 +1,519 @@
|
||||
---
|
||||
title: First API Call
|
||||
description: Create and send a document for signing using the Documenso API, from uploading a PDF to adding recipients and distributing for signature.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting, you need:
|
||||
|
||||
- A Documenso account (cloud or self-hosted)
|
||||
- An API token ([create one in team settings](/docs/developers/getting-started/authentication))
|
||||
- A PDF file to send for signing
|
||||
|
||||
<Callout type="warn">
|
||||
API tokens have full access to your account. Store them securely and never commit them to version
|
||||
control.
|
||||
</Callout>
|
||||
|
||||
## Limitations
|
||||
|
||||
The API cannot:
|
||||
|
||||
- Sign documents on behalf of recipients (recipients must sign themselves)
|
||||
- Convert non-PDF files to PDF (you must upload PDFs)
|
||||
- Retrieve the signed PDF until all recipients have completed signing
|
||||
|
||||
## Base URL
|
||||
|
||||
All API requests use the following base URLs:
|
||||
|
||||
| Environment | Base URL |
|
||||
| ----------- | -------------------------------------- |
|
||||
| Production | `https://app.documenso.com/api/v2` |
|
||||
| Staging | `https://stg-app.documenso.com/api/v2` |
|
||||
|
||||
## Example 1: List Your Documents
|
||||
|
||||
Start with a simple GET request to verify your API token works.
|
||||
|
||||
<Tabs items={['curl', 'JavaScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X GET "https://app.documenso.com/api/v2/envelope" \
|
||||
-H "Authorization: YOUR_API_TOKEN"
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="JavaScript">
|
||||
```javascript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': 'YOUR_API_TOKEN',
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
A successful response returns a list of your documents (envelopes):
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "envelope_abc123",
|
||||
"status": "DRAFT",
|
||||
"title": "Contract Agreement",
|
||||
"createdAt": "2025-01-15T10:30:00.000Z"
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"perPage": 10,
|
||||
"totalPages": 1,
|
||||
"totalItems": 1
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
If you receive a `401 Unauthorized` error, verify your API token is correct and includes the `api_` prefix.
|
||||
|
||||
## Example 2: Create a Document with Recipient and Signature Field
|
||||
|
||||
The V2 API uses a single endpoint to create a document with recipients and fields in one request. This is the most common pattern for sending documents.
|
||||
|
||||
<Tabs items={['curl', 'JavaScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/create" \
|
||||
-H "Authorization: YOUR_API_TOKEN" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F 'payload={
|
||||
"type": "DOCUMENT",
|
||||
"title": "Service Agreement",
|
||||
"recipients": [
|
||||
{
|
||||
"email": "signer@example.com",
|
||||
"name": "John Smith",
|
||||
"role": "SIGNER",
|
||||
"fields": [
|
||||
{
|
||||
"identifier": 0,
|
||||
"type": "SIGNATURE",
|
||||
"page": 1,
|
||||
"positionX": 10,
|
||||
"positionY": 80,
|
||||
"width": 30,
|
||||
"height": 5
|
||||
},
|
||||
{
|
||||
"identifier": 0,
|
||||
"type": "DATE",
|
||||
"page": 1,
|
||||
"positionX": 50,
|
||||
"positionY": 80,
|
||||
"width": 20,
|
||||
"height": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}' \
|
||||
-F "files=@./contract.pdf;type=application/pdf"
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="JavaScript">
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
const FormData = require('form-data');
|
||||
|
||||
const form = new FormData();
|
||||
|
||||
const payload = {
|
||||
type: 'DOCUMENT',
|
||||
title: 'Service Agreement',
|
||||
recipients: [
|
||||
{
|
||||
email: 'signer@example.com',
|
||||
name: 'John Smith',
|
||||
role: 'SIGNER',
|
||||
fields: [
|
||||
{
|
||||
identifier: 0,
|
||||
type: 'SIGNATURE',
|
||||
page: 1,
|
||||
positionX: 10,
|
||||
positionY: 80,
|
||||
width: 30,
|
||||
height: 5,
|
||||
},
|
||||
{
|
||||
identifier: 0,
|
||||
type: 'DATE',
|
||||
page: 1,
|
||||
positionX: 50,
|
||||
positionY: 80,
|
||||
width: 20,
|
||||
height: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
form.append('payload', JSON.stringify(payload));
|
||||
form.append('files', fs.createReadStream('./contract.pdf'), {
|
||||
contentType: 'application/pdf',
|
||||
});
|
||||
|
||||
const response = await fetch('https://app.documenso.com/api/v2/envelope/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'YOUR_API_TOKEN',
|
||||
},
|
||||
body: form,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Created envelope:', data.id);
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Understanding Field Positioning
|
||||
|
||||
Field positions use percentage values (0-100) relative to the PDF page dimensions:
|
||||
|
||||
| Parameter | Description |
|
||||
| --------- | ----------- |
|
||||
| `positionX` | Horizontal position from left edge (0 = left, 100 = right) |
|
||||
| `positionY` | Vertical position from top edge (0 = top, 100 = bottom) |
|
||||
| `width` | Field width as percentage of page width |
|
||||
| `height` | Field height as percentage of page height |
|
||||
| `page` | Page number (1-indexed) |
|
||||
| `identifier` | Index of the file (0 for first file, 1 for second, etc.) |
|
||||
|
||||
<Callout type="info">
|
||||
To place a signature near the bottom-left of the page, use `positionX: 10` and `positionY: 80`.
|
||||
</Callout>
|
||||
|
||||
### Recipient Roles
|
||||
|
||||
| Role | Description |
|
||||
| ---- | ----------- |
|
||||
| `SIGNER` | Must sign the document |
|
||||
| `APPROVER` | Must approve before signers can sign |
|
||||
| `CC` | Receives a copy but doesn't sign |
|
||||
| `VIEWER` | Can view the document but takes no action |
|
||||
|
||||
<Callout type="info">
|
||||
See the [recipient roles](/docs/concepts/recipient-roles) page for more information.
|
||||
</Callout>
|
||||
|
||||
## Example 3: Send the Document for Signing
|
||||
|
||||
After creating a document, it's in `DRAFT` status. To send it to recipients, use the distribute endpoint:
|
||||
|
||||
<Tabs items={['curl', 'JavaScript']}>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
curl -X POST "https://app.documenso.com/api/v2/envelope/envelope_abc123/distribute" \
|
||||
-H "Authorization: YOUR_API_TOKEN" \
|
||||
-H "Content-Type: application/json"
|
||||
````
|
||||
|
||||
</Tab>
|
||||
<Tab value="JavaScript">
|
||||
```javascript
|
||||
const envelopeId = 'envelope_abc123';
|
||||
|
||||
const response = await fetch(
|
||||
`https://app.documenso.com/api/v2/envelope/${envelopeId}/distribute`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'YOUR_API_TOKEN',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Document sent:', data);
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
After distribution, recipients receive an email with a link to sign the document. The document status changes from `DRAFT` to `PENDING`.
|
||||
|
||||
## Full Workflow Example
|
||||
|
||||
Here's a complete script that creates and sends a document:
|
||||
|
||||
<Tabs items={['JavaScript', 'curl']}>
|
||||
<Tab value="JavaScript">
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
const FormData = require('form-data');
|
||||
|
||||
const API_TOKEN = process.env.DOCUMENSO_API_TOKEN;
|
||||
const BASE_URL = 'https://app.documenso.com/api/v2';
|
||||
|
||||
async function createAndSendDocument(pdfPath, recipientEmail, recipientName) {
|
||||
// Step 1: Create the envelope with recipient and fields
|
||||
const form = new FormData();
|
||||
|
||||
const payload = {
|
||||
type: 'DOCUMENT',
|
||||
title: 'Service Agreement',
|
||||
recipients: [
|
||||
{
|
||||
email: recipientEmail,
|
||||
name: recipientName,
|
||||
role: 'SIGNER',
|
||||
fields: [
|
||||
{
|
||||
identifier: 0,
|
||||
type: 'SIGNATURE',
|
||||
page: 1,
|
||||
positionX: 10,
|
||||
positionY: 80,
|
||||
width: 30,
|
||||
height: 5,
|
||||
},
|
||||
{
|
||||
identifier: 0,
|
||||
type: 'NAME',
|
||||
page: 1,
|
||||
positionX: 10,
|
||||
positionY: 75,
|
||||
width: 30,
|
||||
height: 3,
|
||||
},
|
||||
{
|
||||
identifier: 0,
|
||||
type: 'DATE',
|
||||
page: 1,
|
||||
positionX: 50,
|
||||
positionY: 80,
|
||||
width: 20,
|
||||
height: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
form.append('payload', JSON.stringify(payload));
|
||||
form.append('files', fs.createReadStream(pdfPath), {
|
||||
contentType: 'application/pdf',
|
||||
});
|
||||
|
||||
const createResponse = await fetch(`${BASE_URL}/envelope/create`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': API_TOKEN,
|
||||
},
|
||||
body: form,
|
||||
});
|
||||
|
||||
if (!createResponse.ok) {
|
||||
const error = await createResponse.json();
|
||||
throw new Error(`Failed to create envelope: ${JSON.stringify(error)}`);
|
||||
}
|
||||
|
||||
const envelope = await createResponse.json();
|
||||
console.log('Created envelope:', envelope.id);
|
||||
|
||||
// Step 2: Send the document for signing
|
||||
const distributeResponse = await fetch(
|
||||
`${BASE_URL}/envelope/${envelope.id}/distribute`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': API_TOKEN,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (!distributeResponse.ok) {
|
||||
const error = await distributeResponse.json();
|
||||
throw new Error(`Failed to distribute envelope: ${JSON.stringify(error)}`);
|
||||
}
|
||||
|
||||
console.log('Document sent for signing!');
|
||||
return envelope.id;
|
||||
}
|
||||
|
||||
// Usage
|
||||
createAndSendDocument(
|
||||
'./contract.pdf',
|
||||
'signer@example.com',
|
||||
'John Smith'
|
||||
).catch(console.error);
|
||||
````
|
||||
|
||||
</Tab>
|
||||
<Tab value="curl">
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
API_TOKEN="YOUR_API_TOKEN"
|
||||
BASE_URL="https://app.documenso.com/api/v2"
|
||||
PDF_FILE="./contract.pdf"
|
||||
RECIPIENT_EMAIL="signer@example.com"
|
||||
RECIPIENT_NAME="John Smith"
|
||||
|
||||
# Step 1: Create the envelope with recipient and fields
|
||||
|
||||
echo "Creating envelope..."
|
||||
ENVELOPE_RESPONSE=$(curl -s -X POST "${BASE_URL}/envelope/create" \
|
||||
-H "Authorization: ${API_TOKEN}" \
|
||||
-H "Content-Type: multipart/form-data" \
|
||||
-F "payload={
|
||||
\"type\": \"DOCUMENT\",
|
||||
\"title\": \"Service Agreement\",
|
||||
\"recipients\": [
|
||||
{
|
||||
\"email\": \"${RECIPIENT_EMAIL}\",
|
||||
\"name\": \"${RECIPIENT_NAME}\",
|
||||
\"role\": \"SIGNER\",
|
||||
\"fields\": [
|
||||
{
|
||||
\"identifier\": 0,
|
||||
\"type\": \"SIGNATURE\",
|
||||
\"page\": 1,
|
||||
\"positionX\": 10,
|
||||
\"positionY\": 80,
|
||||
\"width\": 30,
|
||||
\"height\": 5
|
||||
},
|
||||
{
|
||||
\"identifier\": 0,
|
||||
\"type\": \"DATE\",
|
||||
\"page\": 1,
|
||||
\"positionX\": 50,
|
||||
\"positionY\": 80,
|
||||
\"width\": 20,
|
||||
\"height\": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}" \
|
||||
-F "files=@${PDF_FILE};type=application/pdf")
|
||||
|
||||
ENVELOPE_ID=$(echo $ENVELOPE_RESPONSE | jq -r '.id')
|
||||
echo "Created envelope: ${ENVELOPE_ID}"
|
||||
|
||||
# Step 2: Send the document for signing
|
||||
|
||||
echo "Sending document..."
|
||||
curl -s -X POST "${BASE_URL}/envelope/${ENVELOPE_ID}/distribute" \
|
||||
-H "Authorization: ${API_TOKEN}" \
|
||||
-H "Content-Type: application/json"
|
||||
|
||||
echo "Document sent for signing!"
|
||||
|
||||
````
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Error Handling
|
||||
|
||||
The API returns standard HTTP status codes and JSON error responses:
|
||||
|
||||
| Status Code | Meaning |
|
||||
| ----------- | ------- |
|
||||
| `400` | Bad request - check your request payload |
|
||||
| `401` | Unauthorized - invalid or missing API token |
|
||||
| `404` | Not found - resource doesn't exist |
|
||||
| `429` | Rate limited - wait 60 seconds and retry |
|
||||
| `500` | Server error - retry or contact support |
|
||||
|
||||
### Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Description of what went wrong",
|
||||
"code": "ERROR_CODE",
|
||||
"statusCode": 400
|
||||
}
|
||||
````
|
||||
|
||||
### Common Errors
|
||||
|
||||
**Invalid file type:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Invalid file type. Only PDF files are supported.",
|
||||
"statusCode": 400
|
||||
}
|
||||
```
|
||||
|
||||
**Missing required field:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Recipient email is required",
|
||||
"statusCode": 400
|
||||
}
|
||||
```
|
||||
|
||||
**Envelope not found:**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Envelope not found",
|
||||
"statusCode": 404
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Rate Limits
|
||||
|
||||
The API allows 100 requests per minute per IP address. When rate limited, wait at least 60 seconds before retrying:
|
||||
|
||||
```javascript
|
||||
async function fetchWithRetry(url, options, maxRetries = 3) {
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
if (response.status === 429) {
|
||||
console.log('Rate limited, waiting 60 seconds...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 60000));
|
||||
continue;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
throw new Error('Max retries exceeded');
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [API Reference](https://openapi.documenso.com/) - Full endpoint documentation with request/response schemas
|
||||
- [Webhooks](/docs/developers/webhooks/setup) - Get notified when documents are signed
|
||||
- [Templates](/docs/developers/api/templates) - Create reusable document templates
|
||||
- [SDKs](#official-sdks) - Use typed client libraries
|
||||
|
||||
### Official SDKs
|
||||
|
||||
For production applications, consider using the official SDKs:
|
||||
|
||||
- [TypeScript SDK](https://github.com/documenso/sdk-typescript)
|
||||
- [Python SDK](https://github.com/documenso/sdk-python)
|
||||
- [Go SDK](https://github.com/documenso/sdk-go)
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
title: Getting Started
|
||||
description: Get your API key and make your first API call.
|
||||
---
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="Authentication"
|
||||
description="Generate an API key and authenticate your requests."
|
||||
href="/docs/developers/getting-started/authentication"
|
||||
/>
|
||||
<Card
|
||||
title="First API Call"
|
||||
description="Create your first document via the API."
|
||||
href="/docs/developers/getting-started/first-api-call"
|
||||
/>
|
||||
</Cards>
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Getting Started",
|
||||
"pages": ["authentication", "first-api-call"]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user