Compare commits

...

787 Commits

Author SHA1 Message Date
ephraimduncan 5a0f438ee6 Merge branch 'main' into feat/prefetch-intent-navigation-links
# Conflicts:
#	apps/remix/app/components/general/app-header.tsx
#	apps/remix/app/components/general/app-nav-desktop.tsx
#	apps/remix/app/components/general/app-nav-mobile.tsx
#	apps/remix/app/components/general/folder/folder-card.tsx
#	apps/remix/app/components/general/folder/folder-grid.tsx
#	apps/remix/app/components/general/menu-switcher.tsx
#	apps/remix/app/components/general/org-menu-switcher.tsx
#	apps/remix/app/components/general/settings-nav-mobile.tsx
#	apps/remix/app/components/tables/admin-claims-table.tsx
#	apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#	apps/remix/app/components/tables/admin-organisations-table.tsx
#	apps/remix/app/components/tables/documents-table-action-button.tsx
#	apps/remix/app/components/tables/organisation-email-domains-table.tsx
#	apps/remix/app/components/tables/templates-table-action-dropdown.tsx
#	apps/remix/app/components/tables/user-billing-organisations-table.tsx
#	apps/remix/app/routes/_authenticated+/admin+/documents._index.tsx
#	apps/remix/app/routes/_authenticated+/admin+/email-domains._index.tsx
#	apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
#	apps/remix/app/routes/_authenticated+/dashboard.tsx
#	apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
#	apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
#	packages/email/template-components/template-document-reminder.tsx
#	packages/ui/primitives/tooltip.tsx
2026-05-08 11:17:51 +00:00
Lucas Smith ec8728b33e chore: add translations (#2774) 2026-05-08 16:22:32 +10:00
github-actions[bot] 22122f51da chore: extract translations (#2772) 2026-05-08 16:22:08 +10:00
David Nguyen 8671f269e8 fix: lint project (#2693) 2026-05-08 16:04:22 +10:00
David Nguyen edbf65969b fix: replace linter with biome (#2645) 2026-05-08 15:40:31 +10:00
David Nguyen 207135d6f3 feat: add new field overflow methods (#2715) 2026-05-08 15:14:27 +10:00
Lucas Smith 4877d1964a chore: add translations (#2771) 2026-05-07 15:32:14 +10:00
Lucas Smith f66751668a fix: paginate and search member/group pickers (#2768) 2026-05-07 15:03:38 +10:00
github-actions[bot] bc3aa9c858 chore: extract translations (#2737) 2026-05-07 11:39:39 +10:00
Catalin Pit b79b4bd111 feat: add DD-MM-YYYY date format variants (#2767) 2026-05-06 23:34:05 +10:00
Lucas Smith 36c10d1a92 v2.10.1 2026-05-05 21:02:28 +10:00
Ephraim Duncan 8c0e029b1b feat: add pending signed PDF downloads (#2730) 2026-05-05 17:25:24 +10:00
David Nguyen f10d3284ba feat: remove default personal orgs from custom sso (#2741) 2026-05-05 14:50:07 +10:00
David Nguyen 6a6ef8d2ad feat: allow add myself feature for embeds (#2754) 2026-05-04 15:05:13 +10:00
Lucas Smith 690491c3b1 fix: prevent 2fa users from being flagged as bots (#2748) 2026-05-04 12:45:43 +10:00
Lucas Smith 6243a514af fix: csp frame-ancestors on signing routes 2026-05-02 09:55:51 +10:00
Lucas Smith a697832b43 v2.10.0 2026-05-01 21:58:05 +10:00
Lucas Smith aebb5e2067 fix: assistant signing auth (#2753) 2026-05-01 15:51:58 +10:00
David Nguyen e19b1d00d0 fix: improve embed error messages (#2752) 2026-05-01 14:24:42 +10:00
David Nguyen c428170b5c fix: allow users to download templates (#2746) 2026-04-30 16:50:07 +10:00
David Nguyen 84fc866cfb fix: improve signature rendering quality with high-resolution caching (#2745) 2026-04-30 15:21:09 +10:00
David Nguyen 5d92aaf20a feat: render signatures on pending envelopes (#2743) 2026-04-30 14:43:48 +10:00
Catalin Pit ae497092d7 fix: security improvements (#2593) 2026-04-30 14:43:20 +10:00
David Nguyen 2f4c3893a3 fix: remove envelope title cropping (#2739) 2026-04-28 16:01:19 +10:00
Lucas Smith 61338af216 chore: add translations (#2735) 2026-04-28 14:55:25 +10:00
David Nguyen 2c7a1be051 feat: add envelope ids to certs (#2733) 2026-04-28 14:54:47 +10:00
github-actions[bot] 8bad62cc92 chore: extract translations (#2734) 2026-04-27 10:57:19 +10:00
Lucas Smith 19c2f7b4a1 docs: add signing reminders guide (#2716) 2026-04-27 10:51:14 +10:00
Lucas Smith 135b676cd4 chore: add translations (#2689) 2026-04-27 10:49:09 +10:00
Lucas Smith 8f3e1893c7 v2.9.1 2026-04-23 14:03:52 +10:00
Catalin Pit e063af628f feat: allow admins to remove organisation and team members (#2705) 2026-04-22 23:08:16 +10:00
Lucas Smith dc575f5c80 fix: don't block organisation member removal on billing checks (#2706) 2026-04-22 21:59:22 +10:00
Ephraim Duncan e5da5bca38 fix: unwrap webhook payload before test and resend (#2710) 2026-04-22 15:42:16 +10:00
Catalin Pit d38d703fd3 fix: error message (update title) (#2691) 2026-04-22 15:42:07 +10:00
Lucas Smith 3249f855fb fix: show captcha on challenge for sign in (#2713) 2026-04-22 14:26:15 +10:00
Lucas Smith 34b31c0d80 chore: deps upgrades (#2712) 2026-04-21 14:43:49 +10:00
ephraimduncan e0cdddc59c chore: restore translation files from main
These .po files were accidentally reverted during the merge commit.
Restoring them to match main since translations are autogenerated.
2026-04-20 00:40:29 +00:00
ephraimduncan 862b2a78ea Merge branch 'main' into feat/prefetch-intent-navigation-links 2026-04-20 00:20:51 +00:00
Lucas Smith 198dafc8ec v2.9.0 2026-04-18 22:04:26 +10:00
armorbreak001 2f1aaa2b5d fix: prevent TooltipTrigger from submitting parent forms (fixes #2684) (#2701) 2026-04-16 14:29:35 +10:00
Lucas Smith f54a8ed72f feat: add turnstile captcha to auth flow (#2703) 2026-04-16 14:29:07 +10:00
David Nguyen 5082226e08 fix: brand logo caching (#2699) 2026-04-14 21:18:17 +10:00
David Nguyen bc82b2e70e fix: admin org sorting (#2694) 2026-04-14 21:17:16 +10:00
Ephraim Duncan 4935f387bf feat: signing reminders (#1749) 2026-04-14 21:01:53 +10:00
David Nguyen 6d7bd212bf fix: clean up duplicate dialogs (#2686) 2026-04-09 14:37:49 +10:00
David Nguyen 283334921b fix: update team member invitation ux (#2687) 2026-04-09 14:32:29 +10:00
Lucas Smith 1af83ea854 chore: add translations (#2683) 2026-04-09 14:08:44 +10:00
Lucas Smith 7cb64c3d04 fix: allow nullable document audit logs (#2682) 2026-04-08 16:23:43 +10:00
github-actions[bot] 4c69cb9c66 chore: extract translations (#2631) 2026-04-08 15:37:18 +10:00
David Nguyen 14b0b4805d feat: auto insert email and date fields (#2639) 2026-04-08 15:35:08 +10:00
Ephraim Duncan 9bfaa08d38 fix: documents table team email recipient lookup (#2578) 2026-04-07 20:10:38 +00:00
chaoliang yan 229cd2f7e9 fix: validate Resend API key before creating mail transport (#2672) 2026-04-07 12:08:29 +10:00
Swalih kolakkadan 6f650e1c2f feat: add document rename feature (#2542) (#2595) 2026-04-02 19:07:52 +11:00
Lucas Smith 0b9a23c550 fix: handle malformed pdf cropbox/mediabox entries (#2668)
Some PDFs have CropBox or MediaBox entries stored as a PDFDict
instead of the expected PDFArray, causing pdf-lib to throw during
lookup.

Wrap both box lookups in try-catch and fall back to A4 dimensions
when neither can be parsed
2026-04-02 18:58:13 +11:00
David Nguyen 3cca8cdae8 fix: labeler typo (#2670) 2026-04-02 18:57:43 +11:00
David Nguyen b13ec8909c fix: resolve incorrect recipient comparision check (#2646)
## Description

Resolve issues with comparison checks.

The `envelope-editor-provider.tsx` should be low impact since it's embed
only which will only cause the non relevant attributes (such as sent at)
to be incorrectly mapped

The `auth-provider.tsx` one should have no impact
2026-04-01 16:04:14 +11:00
David Nguyen e3b7a9e7cb feat: add ability to save documents as template (#2661) 2026-04-01 16:03:26 +11:00
Timur Ercan 74d79dc6b2 chore: update labeler.yml (#2653) 2026-04-01 15:26:45 +11:00
jpsimonsen 1c82595c12 feat: webhook allow private hosts (#2654) 2026-04-01 15:22:07 +11:00
Lucas Smith ad559f72dd feat: add BullMQ background job provider with Bull Board dashboard (#2657)
Add a new BullMQ/Redis-backed job provider as an alternative to the
existing Inngest and Local providers. Includes Bull Board UI for job
monitoring at /api/jobs/board (admin-only in production, open in dev).
2026-04-01 13:07:47 +11:00
Lucas Smith 025a27d385 docs: add user-facing documentation for recipient expiration (#2659) 2026-03-30 12:24:18 +11:00
Catalin Pit a71c44570b feat: admin panel org improvements (#2548)
## Description

- Add a new team page showing team details, global settings, members,
and pending invites
- Update the organisation page to display organisation usage and global
settings
- Show the role and ID of each organisation member, with navigation to
their teams

## Checklist

<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->

- [ ] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these
changes.
- [ ] I have updated the documentation to reflect these changes, if
applicable.
- [ ] I have followed the project's coding style guidelines.
- [ ] I have addressed the code review feedback from the previous
submission, if applicable.
2026-03-27 11:55:33 +02:00
Catalin Pit f5b3babcbb feat: display the field id in dev mode (#2658) 2026-03-27 00:40:29 +11:00
Lucas Smith 2346de83a6 fix: replace z.string().email() with RFC 5322 compliant zEmail() (#2656) 2026-03-26 16:31:21 +11:00
Lucas Smith 814f6e62de fix: replace z.string().email() with RFC 5322 compliant ZEmail/zEmail (#2655) 2026-03-26 13:31:26 +11:00
Lucas Smith 0434bdfacf fix: require billing address on checkout (#2647) 2026-03-25 15:07:27 +11:00
David Nguyen 53b6078fa9 fix: missing embed direct template email validation (#2635) 2026-03-23 15:12:42 +11:00
Catalin Pit 5be71cca21 feat: add option to disable Document created from template (#2609) 2026-03-23 15:11:42 +11:00
David Nguyen ace472c294 fix: prevent managers from deleting admin invitations (#2636) 2026-03-20 22:26:59 +11:00
David Nguyen b2d395e00b fix: stale envelope editor query (#2633) 2026-03-19 17:22:07 +11:00
Lucas Smith dd1b6d7dfe chore: add translations (#2632) 2026-03-19 16:02:09 +11:00
Lucas Smith bef3ea483d chore: add translations (#2630) 2026-03-19 15:57:31 +11:00
David Nguyen e87aa29823 feat: add page title translations (#2629) 2026-03-19 15:44:53 +11:00
Niels Kaspers 4f8132be61 fix(ui): add scroll to date format dropdown (#2626) 2026-03-19 14:47:38 +11:00
David Nguyen 9cf8ed1d00 fix: resolve envelope editor settings ccer logic (#2628)
## Description

Fix issue where having a CCer for a draft document would prevent
changing the date/timezone and some other settings.
2026-03-19 14:21:28 +11:00
github-actions[bot] 108d422a2e chore: extract translations (#2613) 2026-03-19 14:18:42 +11:00
David Nguyen 48fb066b9a feat: allow editing pending envelope titles (#2604) 2026-03-19 14:03:30 +11:00
David Nguyen 0b605d61c6 feat: add envelope pdf replacement (#2602) 2026-03-18 22:53:28 +11:00
Ted Liang 5dcdac7ecd feat: support language in embedding (#2364) 2026-03-18 16:17:23 +11:00
Abdul Alim f48aa84c9e fix(recipient): filter invalid emails in suggestions (#2510) 2026-03-18 14:43:44 +11:00
Catalin Pit 455fef70bd fix: folder view all page nested navigation and search filtering (#2450)
Add parentId query param support to documents/templates folder index
pages so View All correctly shows subfolders. Fix search not filtering
unpinned folders on documents page and broken mt- Tailwind class on
templates page.
2026-03-17 12:02:32 +02:00
Konrad 647dc5fc2d fix(i18n): mark billing messages for translation (#2525) 2026-03-17 12:05:27 +11:00
Lucas Smith de134afba1 v2.8.1 2026-03-17 01:30:28 +11:00
Ephraim Duncan 36bbd97514 feat: add organisation template type (#2611) 2026-03-17 01:29:34 +11:00
ephraimduncan 807ad95354 perf: add prefetch="intent" to navigation Link components
Enables React Router's intent-based prefetching on Link components
across the app, preloading route data and modules on hover/focus
for faster perceived navigation.
2026-03-16 10:02:32 +00:00
Ephraim Duncan 943a0b50e3 perf: parallelize async operations in duplicateEnvelope (#2619) 2026-03-16 02:34:08 +00:00
Ephraim Duncan 6ef501c9f2 perf: parallelize getTeamSettings and getEditorEnvelopeById (#2617) 2026-03-16 11:13:39 +11:00
Ephraim Duncan ac09a48eaa perf: parallelize independent async operations in createEnvelope (#2618) 2026-03-16 11:13:36 +11:00
Ephraim Duncan 70fb834a6a feat: add more webhook events (#2125) 2026-03-15 19:47:52 +11:00
Ephraim Duncan 66e357c9b3 feat: add email domain restriction for signups (#2266)
Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2026-03-14 16:32:34 +11:00
Ted Liang 3106fd7483 fix: exclude native modules from Vite dependency optimization (#2615) 2026-03-14 11:51:00 +11:00
Catalin Pit 32c54e1245 fix: hide name/email in embed signing when provided via prop (#2600)
## Description

When signing via embed, recipient name and email provided through the
embed context were ignored if the DB recipient record had empty values.

This fix adds:
- the signing context's fullName and email as fallbacks in the recipient
payload
- keeps the form in sync with values instead of defaultValues
- ensures the override payload is sent even when the form is hidden
2026-03-13 21:59:10 +11:00
Ted Liang 83fbc70a1c refactor: avoid recipient color duplication (#2355) 2026-03-13 15:52:15 +11:00
Lucas Smith 1ee6ec87a2 chore: add translations (#2614) 2026-03-13 15:22:20 +11:00
Lucas Smith 6b1b1d0417 fix: improve webhook execution (#2608)
Webhook URLs were being fetched without validating whether they
resolved to private/loopback addresses, exposing the server to SSRF.

Current SSRF is best effort and fail open, you should never host
services that
you cant risk exposure of.

This extracts webhook execution into a shared module that validates
URLs against private IP ranges (including DNS resolution), enforces
timeouts, and disables redirect following. The resend route now
queues through the job system instead of calling fetch inline.
2026-03-13 15:02:09 +11:00
Lucas Smith 9f680c7a61 perf: set global prisma transaction timeouts and reduce transaction scope (#2607)
Configure default transaction options (5s maxWait, 10s timeout) on the
PrismaClient instead of per-transaction overrides. Move side effects
like email sending, webhook triggers, and job dispatches out of
$transaction blocks to avoid holding database connections open during
network I/O.

Also extracts the direct template email into a background job and fixes
a bug where prisma was used instead of tx inside a transaction.
2026-03-13 14:51:53 +11:00
github-actions[bot] 76d96d2f65 chore: extract translations (#2583) 2026-03-13 14:50:48 +11:00
David Nguyen 2f2b5dd232 feat: allow creating embeds in folder (#2612)
## Description

Allow passing in a `folderId` when creating an embedded envelope 

## Embed repo changes here

https://github.com/documenso/embeds/pull/69/changes
2026-03-13 14:50:14 +11:00
David Nguyen 8d97f1dcfa fix: resolve error flash on page refresh (#2606) 2026-03-13 12:37:30 +11:00
David Nguyen e67e19358a fix: add hipaa flag (#2603) 2026-03-13 12:06:10 +11:00
Timur Ercan 364537e8fe chore: update hipaa status in docs (#2599) 2026-03-13 12:00:05 +11:00
Joshua Sharp 4751c9cecc fix: template description overflow (#2605) 2026-03-12 18:15:21 +11:00
VIVEK TIWARI a5fd814fbc fix: handle invalid qr share tokens without 500 (#2597) 2026-03-12 13:46:17 +11:00
Ephraim Duncan 1d2c781a6d docs: add organisation ownership transfer guide (#2601) 2026-03-12 13:39:37 +11:00
Lucas Smith 03ca3971a0 perf: upgrade @libpdf/core to 0.3.3 and deduplicate font registration (#2598)
Upgrade @libpdf/core from 0.2.12 to 0.3.3, which includes:
- WebCrypto SHA-256 replacing pure-JS @noble/hashes (10x signing
speedup)
- Iterative collectReachableRefs (fixes stack overflow on large PDFs)
- Iterative Math.max helpers in xref writer (fixes remaining stack
overflow)

Extract duplicated FontLibrary.use() calls from render-certificate,
render-audit-logs, and insert-field-in-pdf-v2 into a shared
ensureFontLibrary() helper with has() guards so fonts are only
registered once per process.
2026-03-11 20:23:18 +11:00
Lucas Smith 5ea4060fd7 v2.8.0 2026-03-10 21:43:01 +11:00
Lucas Smith af346b179c feat: add recipient role editing and audit log PDF download in admin (#2594)
- Allow admins to update recipient role from document detail page
- Add download button to export audit logs as PDF
- Display recipient status details in accordion
- Add LocalTime component with hover popover for timestamps
2026-03-10 21:41:46 +11:00
Catalin Pit ab69ee627b fix: include extra recipient info in missing fields error msg (#2590) 2026-03-10 12:17:24 +11:00
Lucas Smith 4daec44550 fix: move window.__ENV__ script before client bundle to prevent stale fallback (#2592) 2026-03-10 12:15:15 +11:00
Ted Liang 11eb4dd2cd fix: security CVE-2026-29045 (#2589) 2026-03-09 16:46:11 +11:00
Lucas Smith cc71c7d9ba fix: add cmaps (#2588) 2026-03-09 14:07:13 +11:00
Lucas Smith f82bf97480 fix: only use embed hash name/email as fallback when recipient values are blank (#2586)
For document signing embeds, the hash-provided name and email should
only
be used when the recipient doesn't already have values set. For template
signing, the hash values are always allowed.

Also makes the email input editable in V1 embeds when the recipient has
no email, matching V2 behavior.

Ref: documenso/embeds#53
2026-03-09 13:30:27 +11:00
Lucas Smith 0e20d364ef fix: opt findDocumentsInternal query out of batch fetching (#2585) 2026-03-09 12:47:59 +11:00
David Nguyen ef57c8448a fix: dropdown fields (#2584) 2026-03-09 12:19:20 +11:00
Lucas Smith eaaf8f9e63 chore: add translations (#2582) 2026-03-09 11:56:17 +11:00
David Nguyen 58f0c98038 chore: add embed envelope docs (#2576) 2026-03-09 11:50:13 +11:00
Catalin Pit da7b5d12f8 fix: make signing page left-hand sidebar collapsible (#2541) 2026-03-09 11:45:28 +11:00
github-actions[bot] 7cfe876762 chore: extract translations (#2577) 2026-03-09 11:39:37 +11:00
Ephraim Duncan 15399cbe8e feat: auto-disable telemetry when license key is configured (#2562) 2026-03-09 11:24:24 +11:00
Catalin Pit c4754553c9 feat: implement template search functionality (#2376)
- Added  function to handle template searches based on user input
- Introduced in the TRPC router to facilitate authenticated template
searches
- Updated to include template search results alongside document search
results
- Enhanced query handling by enabling searches only when the input is
valid
- Created corresponding Zod schemas for request and response validation
in
2026-03-09 10:44:51 +11:00
David Nguyen 6c8726b58c fix: performance improvements (#2581) 2026-03-09 10:22:57 +11:00
Lucas Smith abd031b58b chore: add translations (#2575) 2026-03-06 16:10:54 +11:00
github-actions[bot] 1ff8680c32 chore: extract translations (#2566) 2026-03-06 14:15:37 +11:00
David Nguyen 7ea664214a feat: add embedded envelopes (#2564)
## Description

Add envelopes V2 embedded support

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-03-06 14:11:27 +11:00
Ephraim Duncan 7e2cbe46c0 fix: show current month data and add caching (#2573)
### Summary

- Add Cache-Control headers to all route responses (1h s-maxage, 2h
stale-while-revalidate)
- Append current month to chart data so graphs stay up-to-date
(cumulative carries forward, else zero)
- Remove `.limit(12)` from growth queries for full history
- Pass isCumulative flag through addZeroMonth
- Deduplicate TransformedData type, remove transformRepoStats
2026-03-06 13:30:31 +11:00
Konrad c63b4ca3cc fix(i18n): mark dropdown and radio placeholder for translation (#2537) 2026-03-06 13:05:03 +11:00
David Nguyen 6faa01d384 feat: add pdf image renderer (#2554)
## Description

Replace the PDF renderer with an custom image renderer.

This allows us to remove the "react-pdf" dependency and allows us to use
a virtual list to improve performance.
2026-03-06 12:39:03 +11:00
Lucas Smith 0ce909a298 refactor: find envelopes (#2557) 2026-03-06 12:38:40 +11:00
Lucas Smith 7f271379b9 fix: upgrade @libpdf/core (#2572) 2026-03-06 10:08:58 +11:00
Lucas Smith 406e77e4be chore: add translations (#2570) 2026-03-05 17:33:36 +11:00
Lucas Smith bff360b084 fix: upgrade @libpdf/core (#2569) 2026-03-05 15:34:40 +11:00
Lucas Smith db1087d76d v2.7.1 2026-03-05 15:16:45 +11:00
Lucas Smith ef0a5b54ba fix: verify before re-registering in email sync (#2568) 2026-03-05 15:12:20 +11:00
David Nguyen 1f985e2cd3 fix: invalid po translations (#2567) 2026-03-05 14:54:36 +11:00
Konrad 525dd92a56 fix(i18n): mark SUBSCRIPTION_STATUS_MAP for translation (#2515) 2026-03-05 14:42:40 +11:00
Konrad d21b99825d fix(i18n): add pluralization to expiration period picker (#2535) 2026-03-05 14:32:12 +11:00
Konrad dfbf68e4cd fix(i18n): mark editor field number form placeholder for translation (#2536) 2026-03-05 14:31:24 +11:00
github-actions[bot] 8b0231825f chore: extract translations (#2539) 2026-03-05 14:11:53 +11:00
Ephraim Duncan 03e2e4f171 docs: clarify placeholder support is envelope.* only (#2560) 2026-03-05 13:58:29 +11:00
Lucas Smith 7f5f2b22ed feat: add seal-document sweep job and admin unsealed documents page (#2563) 2026-03-05 13:56:40 +11:00
Lucas Smith 7d3a56a006 feat: add admin ability to move subscription between orgs (#2558)
## Summary

- Adds a new admin action to move a subscription (and Stripe customerId)
from one organisation to another owned by the same user
- The target organisation must be on the free plan (no active
subscription) — enforces paid → free only
- The source organisation's claim is reset to the free plan after the
move

## How it works

A "Move Subscription" option appears in the actions dropdown of the
organisations table (on the admin user detail page) for any org with an
active or past-due subscription. Clicking it opens a dialog where the
admin selects a target org from a filtered list of eligible (free-plan)
orgs owned by the same user.

The backend performs the swap atomically in a single Prisma transaction:
1. Deletes any stale inactive subscription on the target org
2. Moves the `customerId` from source to target org
3. Reassigns the `Subscription` record to the target org
4. Copies claim entitlements to the target org
5. Resets the source org's claim to FREE

No Stripe API calls are made — the Stripe subscription and customer
remain unchanged; only the DB-level org association is updated.

## Files changed

- **New:**
`packages/trpc/server/admin-router/swap-organisation-subscription.types.ts`
— Zod schemas
- **New:**
`packages/trpc/server/admin-router/swap-organisation-subscription.ts` —
Admin mutation
- **New:**
`apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx` —
Dialog component
- **Modified:** `packages/trpc/server/admin-router/router.ts` — Register
route
- **Modified:**
`apps/remix/app/components/tables/admin-organisations-table.tsx` — Add
action menu item
2026-03-04 22:34:53 +11:00
Catalin Pit f1323679aa fix: use default field meta for embedding template fields (#2556) 2026-03-03 22:24:57 +11:00
Lucas Smith a05251d5ee v2.7.0 2026-03-03 16:19:38 +11:00
Lucas Smith 454f73f2a9 chore: remove old docs (#2550) 2026-03-02 13:47:40 +11:00
Lucas Smith 24a5c85b6c fix(docs): rewrite mermaid component to avoid async client component error (#2549)
Replace use() with useEffect/useState pattern to prevent 'async Client
Component' errors in Next.js 16 / React 19. Also strip colons from
useId() output which broke mermaid's render().
2026-02-28 16:55:33 +11:00
Lucas Smith b92c53dbb2 feat: docs v2 (#2460)
Co-authored-by: Catalin Pit <catalinpit@gmail.com>
2026-02-27 22:05:27 +11:00
Lucas Smith f8ac782f2e deps: 2026-02-26 upgrades (#2545) 2026-02-26 14:17:08 +11:00
Ephraim Duncan 194660d847 fix: return all documents from V1 API regardless of folder (#2471) 2026-02-26 13:55:14 +11:00
Lucas Smith 0d3bd59ec6 fix: handle cjk and special chars when slugifying (#2544) 2026-02-26 13:54:35 +11:00
Konrad 92d82c0423 fix(i18n): mark supported languages for translation (#2377) 2026-02-26 12:06:18 +11:00
Lucas Smith 484e1c20d0 chore: add translations (#2533) 2026-02-26 11:50:08 +11:00
Lucas Smith 6f5014a561 feat: support optional read replicas (#2540) 2026-02-25 19:07:02 +11:00
Lucas Smith c112392da9 feat: add admin email domain management and sync job (#2538) 2026-02-25 15:14:18 +11:00
github-actions[bot] bc72d9cb17 chore: extract translations (#2505) 2026-02-24 22:07:03 +11:00
Karlo 3ad3216c4c fix: update button width to fit content in public profile page (#2506)
Co-authored-by: Catalin Pit <catalinpit@gmail.com>
2026-02-23 12:46:58 +02:00
Lucas Smith 36eef79b1a fix: omit fieldId from embed create endpoints (#2523) 2026-02-21 21:14:51 +11:00
Lucas Smith 6fb88fede5 chore: upgrade libpdf (#2522) 2026-02-21 20:54:33 +11:00
Lucas Smith 653ab3678a feat: better ratelimiting (#2520)
Replace hono-rate-limiter with a Prisma/PostgreSQL bucketed counter
approach that works correctly across multiple instances without sticky
sessions.

- Add RateLimit model with composite PK (key, action, bucket) and atomic
upsert
- Create rate limit factory with window parsing, bucket computation, and
fail-open
- Define auth-tier and API-tier rate limit instances
- Add Hono middleware, rateLimitResponse helper, and tRPC
assertRateLimit helper
- Wire rate limit headers through AppError constructor (was declared but
never assigned)
- Apply rate limits to auth routes (email-password, passkey), tRPC
routes
  (2FA email, link org account), API routes, and file upload endpoints
- Add cleanup cron job for expired rate limit rows (batched delete every
15 min)
- Remove hono-rate-limiter dependency
2026-02-20 12:23:02 +11:00
Lucas Smith 006b1d0a57 feat: per-recipient envelope expiration (#2519) 2026-02-20 11:36:20 +11:00
Lucas Smith f3ec8ddc57 v2.6.1 2026-02-18 21:57:10 +11:00
Lucas Smith 9a66d0ebf6 fix: simplify openapi field schemas to fix SDK generation (#2503) 2026-02-18 17:07:46 +11:00
Konrad 29622d3151 fix(i18n): mark strings inside div for translation (#2514) 2026-02-18 13:50:42 +11:00
Lucas Smith 5de2527e54 fix: v2 embed direct templates not reading email/lockEmail from hash params (#2509) 2026-02-18 13:35:04 +11:00
Lucas Smith 6fcf0a638c chore: add translations (#2507) 2026-02-17 11:31:37 +11:00
Louis Liu ff9e6acb7a fix(ui): clarify email settings labels (#2448) 2026-02-16 17:00:24 +11:00
Lucas Smith a60c6a90ab chore: add translations (#2504) 2026-02-16 16:10:43 +11:00
github-actions[bot] f35c19d098 chore: extract translations (#2458) 2026-02-16 14:34:33 +11:00
McMek590 cf8e21bf35 fix: create full sentences for document-signing-auth files (#2451) 2026-02-16 13:30:36 +11:00
Jahangir Babar 3f7c4df1b1 fix: strip diacritics from team URL slug generation (#2489) 2026-02-16 12:36:14 +11:00
Konrad ca199e7885 fix(i18n): mark span strings for translation (#2494) 2026-02-16 12:07:53 +11:00
Konrad 435d61ea57 fix(i18n): mark badge string for translation (#2495) 2026-02-16 11:58:03 +11:00
Konrad 34f14ba69a fix(i18n): mark tabs trigger strings for translation (#2496) 2026-02-16 11:57:44 +11:00
Konrad 51916cd3f0 fix(i18n): mark DialogTitle string for translation (#2497) 2026-02-16 11:57:23 +11:00
Konrad f158305499 fix(i18n): mark paragraph strings for translation (#2498) 2026-02-16 11:57:03 +11:00
Lucas Smith 2e3d22c856 fix: use instance-specific emails for service accounts (#2502) 2026-02-16 11:52:19 +11:00
Ephraim Duncan d66c330d46 fix: match cert and audit log page dimensions to source document (#2473) 2026-02-12 18:25:11 +11:00
David Nguyen 9bcb240895 fix: revert canceled individual subscriptions to free claim (#2483)
## Description

Resolves an issue where individual plan customers who cancel are not
correctly put down to the free plan.

To resolve this, we delete the subscription on the stripe subscription
delete webhook. Since the customerId is stored on the organisation they
can still access their old invoices.
2026-02-12 17:44:33 +11:00
David Nguyen 066e6bc847 fix: envelope editor flush race condition (#2482)
## Description

Fixes a race condition in the envelope editor when opening "Send
Document" immediately after moving/resizing a selected field

Replication
1. Move or resize a field (do not blur the selector/quickbar that
appears when a field is selected)
2. Directly click the "Send Document" dialog
3. Error appears

Note: Step 2 needs to happen relatively fast after step 1 since this is
a race against the flush debouncer

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-12 16:32:26 +11:00
David Nguyen 0d65693d55 fix: highlight rejected certificate text (#2478)
## Description

- Update the rejected certificate so that is it more clear on who
rejected the document.
- Updated the audit log generation so that the completed audit log is
included

### Before

<img width="681" height="597" alt="image"
src="https://github.com/user-attachments/assets/3dab41c1-c86f-4555-8d50-3d9245be65d5"
/>

### After

Note that the order of the recipient is different in this case

<img width="818" height="769" alt="image"
src="https://github.com/user-attachments/assets/71f0ac12-5859-47b4-8980-2420ef949d18"
/>

---------

Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2026-02-12 16:06:43 +11:00
Lucas Smith e3dee5e565 fix: auto placement field meta (#2480) 2026-02-12 14:20:52 +11:00
Catalin Pit f1c91c4951 fix: bulk actions improvements (#2440) 2026-02-10 20:13:03 +11:00
Lucas Smith a5ef1d23e6 feat: add team memberships section to admin user detail page (#2457) 2026-02-09 17:35:22 +11:00
github-actions[bot] d91414697d chore: extract translations (#2429) 2026-02-09 17:30:46 +11:00
Konrad e222a872d2 fix(i18n): rewrite audit log messages to support correct grammar (#2455) 2026-02-09 13:20:12 +11:00
Ephraim Duncan e3b0087be6 feat: create plain customer (#2442)
Co-authored-by: Catalin Pit <catalinpit@gmail.com>
2026-02-09 11:24:45 +11:00
Konrad da89ce7c9a fix(i18n): add localization context to dialog messages (#2452) 2026-02-09 10:52:50 +11:00
Konrad b762561f11 chore(i18n): add context to ambiguous message (#2454) 2026-02-09 10:52:00 +11:00
Catalin Pit 9b190ef582 docs: add info callout for enterprise-only embedded authoring (#2443) 2026-02-04 12:41:46 +11:00
Lucas Smith 1669216a91 fix: flatten pdf-lib form fields before sealing document (#2441)
- Fixes checkbox fields not displaying correctly in sealed documents by
calling `flatten()` on the pdf-lib form before saving
2026-02-03 14:24:23 +11:00
Lucas Smith 594a0f0c3f fix: store formValues in database when creating document from template (#2437) 2026-02-02 11:36:06 +11:00
Konrad 39ebc8184a fix(i18n): add pluralization to envelopes-bulk-delete-dialog.tsx (#2428) 2026-01-30 12:43:27 +11:00
Konrad 2df41b9f01 feat(ui): rename sign up button for better clarity (#2427) 2026-01-30 12:30:33 +11:00
Lucas Smith 8704c731c0 chore: upgrade libpdf (#2435) 2026-01-29 23:34:46 +11:00
Lucas Smith eaee0d4bc6 v2.6.0 2026-01-29 18:44:58 +11:00
Lucas Smith 0f8b7670f4 fix: correct path prefix check for static assets caching (#2433) 2026-01-29 16:05:08 +11:00
Catalin Pit 25e148d459 feat: update team member creation dialog with invite functionality (#2366) 2026-01-29 15:15:06 +11:00
David Nguyen 97ceb317a8 fix: license banner not correctly showing (#2432) 2026-01-29 15:09:23 +11:00
David Nguyen c83109628d fix: add license logging (#2431) 2026-01-29 14:08:36 +11:00
David Nguyen a4d0e3e873 fix: resolve safari cert download issues (#2430) 2026-01-29 14:08:07 +11:00
Catalin Pit 59a514c238 feat: allow non-team members as default recipients (#2404) 2026-01-29 13:32:18 +11:00
David Nguyen 1b0df2d082 feat: add license integration (#2346)
Changes:
- Adds integration for the license server.
- Prevent adding flags that the instance is not allowed to add
2026-01-29 13:30:48 +11:00
Catalin Pit d18dcb4d60 feat: autoplace fields from placeholders (#2111)
This PR introduces automatic detection and placement of fields and
recipients based on PDF placeholders.

The placeholders have the following structure:
- `{{fieldType,recipientPosition,fieldMeta}}` 
- `{{text,r1,required=true,textAlign=right,fontSize=50}}`

When the user uploads a PDF document containing such placeholders, they
get converted automatically to Documenso fields and assigned to
recipients.
2026-01-29 13:13:45 +11:00
Konrad d77f81163b fix(i18n): mark missing strings for translation in card components (#2308) 2026-01-29 12:22:07 +11:00
Lahiru Dahampath 62fb9e5248 fix: correct webhook event name in documentation (#2424) 2026-01-29 11:52:36 +11:00
github-actions[bot] 53b0131740 chore: extract translations (#2418) 2026-01-28 21:25:23 +11:00
Catalin Pit 155310b028 feat: add bulk document selection and move functionality (#2387)
This PR introduces bulk actions for documents, allowing users to select
multiple envelopes and perform actions such as moving or deleting 1 or
more documents simultaneously.
2026-01-28 18:27:32 +11:00
Catalin Pit 28bc2dc975 fix: send organisation member removal email to correct user (#2405) 2026-01-28 09:18:58 +02:00
David Nguyen eb3b3b18ce chore: add v1 deprecated docs (#2423) 2026-01-28 14:09:13 +11:00
misha 8bc4f1a713 fix: exclude soft-deleted documents from folder count (#2410) 2026-01-28 13:07:57 +11:00
Timur Ercan d3c898e317 chore: update fair policy with support (#2422)
updated fair policy and added fair self-host support
2026-01-27 17:34:07 +01:00
Lucas Smith d08049ed3b v2.5.1 2026-01-27 20:25:31 +11:00
Lucas Smith 7a583aa7af fix: preserve prompt parameter in OAuth authorize URL builder (#2421)
The prompt option was being discarded for OAuth authorize URLs after
adding support for the NEXT_PRIVATE_OIDC_PROMPT env var. This meant
select_account (used elsewhere) was not being passed through.

Now defaults prompt to the provided option (or 'login'), and only
overwrites it when a valid OIDC prompt env var is set. Also adds a
type guard to validate the env var value.
2026-01-27 20:25:16 +11:00
David Nguyen b590076d85 fix: allow past due subscriptions (#2420)
Allow plans with past_due subscriptions to continue to use the platform
until the subscription becomes inactive.
2026-01-27 18:45:58 +11:00
Lucas Smith 65e30b88be fix: persist formValues in document creation endpoints (#2419) 2026-01-27 16:21:09 +11:00
Ted Liang 9c6ee88cc4 fix: security CVE-2026-23527 (#2399) 2026-01-27 15:52:34 +11:00
Lucas Smith 6028ad9158 chore: add translations (#2412)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2026-01-27 15:44:01 +11:00
Lucas Smith 7fc6f5bb6e fix: make teamId optional in support form validation (#2417)
The contact form accepts teamId as an optional param based on
where the user clicks "Support" from. Previously, when opened
from a non-team context, the null teamId would be parsed to NaN
and fail validation, causing the form to error out. Now the
validation only runs when a teamId is actually provided.
2026-01-27 15:00:53 +11:00
Jorge Ramirez 17b261df1f fix(api): add take parameter to template search query for pagination (#2396)
This PR fixes a bug in the `/api/v2/template` endpoint where the
pagination parameter `perPage` was being ignored. Previously, the
endpoint would return all matching templates regardless of the requested
limit, which could lead to performance issues and incorrect API
behavior.
2026-01-27 15:00:37 +11:00
Lucas Smith c732c85082 chore: add manual dispatch to publish workflow and remove chromium builds (#2415) 2026-01-27 14:15:04 +11:00
Lucas Smith 7d38e18f93 v2.5.0 2026-01-26 15:59:30 +11:00
Lucas Smith 0a3e0b8727 feat: validate signers have signature fields before distribution (#2411)
API users were inadvertently sending documents without signature fields,
causing confusion for recipients and breaking their signing flows.

- Add getRecipientsWithMissingFields helper in recipients.ts
- Add server-side validation in sendDocument to block distribution
- Fix v1 API to return 400 instead of 500 for validation errors
- Consolidate UI signature field checks to use isSignatureFieldType
- Add E2E tests for both v1 and v2 APIs
2026-01-26 15:22:12 +11:00
github-actions[bot] b538580a1e chore: extract translations (#2380) 2026-01-26 12:21:02 +11:00
Lucas Smith 42d6e1cbbd chore: upgrade libpdf (#2409) 2026-01-26 12:20:33 +11:00
Lucas Smith 67da488f63 chore: upgrade libpdf (#2408) 2026-01-23 21:38:48 +11:00
Lucas Smith fd3ebc08ec chore: upgrade libpdf (#2406) 2026-01-22 12:45:20 +11:00
Catalin Pit a7963b385a docs: add default recipients section (#2400) 2026-01-21 09:45:34 +02:00
Lucas Smith 9035240b4d refactor: replace pdf-sign with libpdf/core for PDF operations (#2403)
Migrate from @documenso/pdf-sign and @cantoo/pdf-lib to @libpdf/core
for all PDF manipulation and signing operations. This includes:

- New signing transports for Google Cloud KMS and local certificates
- Consolidated PDF operations using libpdf API
- Added TSA (timestamp authority) helper for digital signatures
- Removed deprecated flatten and insert utilities
- Updated tests to use new PDF library
2026-01-21 15:16:23 +11:00
Ephraim Duncan ed7a0011c7 fix: sync envelope state after direct link changes (#2257) 2026-01-21 14:43:24 +11:00
Ted Liang 158b36a9b7 fix: security CVE-2026-22817 CVE-2026-22818 (#2390) 2026-01-15 18:27:04 +11:00
Lucas Smith fabd69bd62 build: upgrade simplewebauthn packages from v9 to v13 (#2389)
The v9 packages are deprecated. This updates to v13 which includes
breaking API changes: optionsJSON wrapper for auth functions,
renamed properties (authenticator→credential), and base64 encoding
for credential IDs via isoBase64URL helper.
2026-01-15 14:22:37 +11:00
Lucas Smith c976e747e3 fix: dont flatten forms for templates (#2386)
Templates shouldn't have their form flattened until they're
converted to a document.
2026-01-14 12:06:28 +11:00
Lucas Smith 34f512bd55 docs: add OpenCode AI-assisted development guide (#2384)
Adds OpenCode support for AI-assisted development, including custom
commands and skills to help contributors maintain consistency and
streamline common workflows.

#### Changes
- Added "AI-Assisted Development with OpenCode" section to
CONTRIBUTING.md with:
  - Installation instructions and provider configuration
- Documentation for 8 custom commands (/implement, /continue,
/interview, /document, /commit, /create-plan, /create-scratch,
/create-justification)
  - Typical workflow guide
- Clear policy that AI-generated code must be reviewed before submission
- Added .agents/ directory for plans, scratches, and justifications
- Added .opencode/ commands and skills for the agent
- Added helper scripts for creating agent files
2026-01-14 10:10:20 +11:00
Karlo db913e95b6 fix: downgrade pdfjs-dist to version 5.4.296 and update react-pdf to version 10.3.0 (#2383) 2026-01-13 21:01:29 +11:00
Catalin Pit bb3e9583e4 feat: add default recipients for teams and orgs (#2248) 2026-01-13 20:32:00 +11:00
Lucas Smith 5bc73a7471 chore: npm audit fix (#2367) 2026-01-13 16:39:10 +11:00
Lucas Smith 06d7849146 chore: add translations (#2373) 2026-01-13 14:34:26 +11:00
Lucas Smith cef7987a72 feat: add audit logs to document details page (#2379)
- Add collapsible audit logs section with paginated table
- Add View JSON button to inspect raw audit log entries
- Display legacy document ID and recipient roles
- Add admin TRPC endpoint for fetching audit logs
- Add database index on envelopeId for DocumentAuditLog table

<img width="887" height="724" alt="image"
src="https://github.com/user-attachments/assets/aeb904c9-515f-49e1-9f8f-513aef455678"
/>
2026-01-13 14:18:10 +11:00
github-actions[bot] cf6f6bcea0 chore: extract translations (#2363) 2026-01-13 12:49:05 +11:00
Catalin Pit 2f27304750 refactor: simplify field dialog component (#2369) 2026-01-13 12:38:10 +11:00
Konrad 912530ca17 fix: mark document visibility options for translation (#2330) 2026-01-12 10:17:03 +11:00
Konrad a995961c4e fix: mark document auth types for translation (#2331) 2026-01-12 09:28:16 +11:00
Lucas Smith 6b041c23b4 v2.4.0 2026-01-08 15:16:57 +11:00
Ted Liang 7b6e948aa2 refactor: reuse svgToPng function (#2365) 2026-01-08 11:30:45 +11:00
Catalin Pit f6d81b22bd docs: update field coordinates documentation and improve devmode (#2359) 2026-01-06 10:29:21 +02:00
Lucas Smith c861dd2ee2 chore: add translations (#2362) 2026-01-06 15:54:54 +11:00
github-actions[bot] 7eabae4b4b chore: extract translations (#2351) 2026-01-06 15:36:46 +11:00
Lucas Smith ae4272a6b6 fix: remove logo from embedded signing v2 page (#2361) 2026-01-06 15:10:58 +11:00
Dylan Tarre fd672943d1 fix: replace hardcoded #7AC455 with text-documenso-700 token (#2358)
Standardizes navigation link colors by replacing hardcoded `#7AC455` hex
values with the existing `text-documenso-700` design token.
2026-01-06 14:58:45 +11:00
David Nguyen c2ea5e5859 fix: migrate certificate generation (#2251)
Generate certificates and audit logs using Konva instead of browserless.

This should:
- Reduce the changes of generations failing
- Improve sealing speed
2026-01-06 14:26:19 +11:00
Grégoire Bécue c1217c5a58 docs: ensure cert directory exists before generating PKCS12 (#2354) 2026-01-03 11:43:55 +11:00
Ted Liang 27eb2d65d4 feat: upgrade alpine and support chromium path (#2353)
Upgrade alpine to 3.22
Support chromium executable path
2026-01-03 11:31:56 +11:00
Catalin Pit ef407cb0b4 refactor: simplify form validation and enhance recipient handling (#2317) 2026-01-02 13:16:45 +11:00
Lucas Smith 1e20561e91 v2.3.2 2025-12-24 16:20:23 +11:00
Lucas Smith a2ec5f0fa1 fix: cleanup konva stages during field insertion (#2347) 2025-12-24 16:09:09 +11:00
Ted Liang de8d13a4c1 fix: hide branding logo in audit log (#2342) 2025-12-24 15:10:13 +11:00
github-actions[bot] 495d61a11d chore: extract translations (#2327) 2025-12-24 13:51:40 +11:00
Catalin Pit 90fdba8000 feat: get many endpoints (#2226)
Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2025-12-24 11:02:02 +11:00
Ephraim Duncan aa1cada79b feat: add find envelopes endpoint (#2244) 2025-12-23 22:51:51 +11:00
Lucas Smith 790b385849 chore: add bundled chromium docker container (#2344)
We use playwright + chromium for certificate generation
and other things.

Self-hosters often have an issue with generating certificates
due to the base image not coming with chromium for size purposes.

This adds a new `-chromium` tag to our docker images for downloading
the larger bundled chromium containers.
2025-12-23 22:09:12 +11:00
Catalin Pit baa2c51123 feat: add delegate document ownership option (#2272)
When using an API key created in a team context, the
documents/templates’ owner always defaults to the team API token
creator, rather than the actual uploader.

For example, John creates the API key for the team "Lawyers". Tom and
Maria use the API key to upload documents. All the uploaded documents
are attributed to John.

This makes it impossible to see who actually uploaded a document.

The new feature allows users to enable document ownership delegation
from the organization/team settings.
2025-12-23 22:08:54 +11:00
Catalin Pit 1e585e06e6 docs: update documentation (#2339) 2025-12-22 15:07:28 +02:00
Ted Liang 5624484631 fix: security CVE-2025-68130 (#2343)
## Description

Fix security
[CVE-2025-68130](https://github.com/advisories/GHSA-43p4-m455-4f4j)
2025-12-22 21:53:49 +11:00
Catalin Pit 810e00da03 feat: add new features to the FEATURES list (#2338) 2025-12-19 10:38:56 +11:00
Lucas Smith eeeee2fa0e v2.3.1 2025-12-18 12:02:04 +11:00
Lucas Smith c50a31a503 fix: use cpu for field rendering (#2337) 2025-12-18 10:48:46 +11:00
Lucas Smith 7360709795 fix: use gemimi 3 flash preview (#2336) 2025-12-18 10:48:16 +11:00
Lucas Smith df678d7d69 v2.3.0 2025-12-17 22:10:47 +11:00
Lucas Smith 6739242554 fix: use cpu for skia-canvas rendering (#2334)
Seems there's a memory leak in gpu rendering with skia canvas
where contexts can live for much longer than expected escaping gc
cleanup

CPU rendering seems better albeit a bit slower.

Synthetic tests were ran with `--expose-gc` to simulate load over time.
2025-12-17 14:48:21 +11:00
Konrad a5e5eecf8b fix: mark links for translation (#2333) 2025-12-17 12:02:12 +11:00
Lucas Smith b0248c20eb v2.2.8 2025-12-16 16:04:07 +11:00
Lucas Smith f129968968 fix: ensure PDF form appearance streams have required /Subtype /Form entry (#2328)
When flattening PDF forms, some appearance streams lack the required
/Subtype /Form dictionary entry needed when used as XObjects. This
causes
corruption in Adobe Reader which fails to render these flattened fields.

Per PDF spec, Form XObject streams require:
- /Subtype /Form (required)
- /FormType 1 (optional)

The normalizeAppearanceStream function ensures these entries exist
before
adding appearance streams as XObjects to the page content stream.

Fixes rendering issues where flattened fields don't display in PDF
viewers.
2025-12-16 16:00:11 +11:00
Lucas Smith c5c87e3fd1 v2.2.7 2025-12-16 12:38:53 +11:00
Lucas Smith 24a74c7b57 chore: add translations (#2321)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-12-16 12:06:58 +11:00
Lucas Smith f0a5a7e816 feat: prefill typed signature with user's full name (#2324)
Add fullName prop to signature pad components to automatically populate
typed signature
field with signer's name. Updates signature dialog, type component, and
all signing forms
across embed, document, template, and envelope flows to pass through the
user's full
name for better user experience.
2025-12-16 12:06:04 +11:00
Catalin Pit 8462cd13fd fix: assignment operator for directRecipientName (#2323) 2025-12-16 12:04:19 +11:00
Lucas Smith 576846de32 fix: fallback for certficate sent date when using link distribution (#2316) 2025-12-16 11:40:16 +11:00
Lucas Smith 06071ea035 fix: memory leak in PDF to images conversion (#2325)
Add proper cleanup for PDF.js pages and loading task to prevent memory
leaks when
processing multiple PDF pages. Ensure page cleanup is called after each
page is
rendered and both PDF document and loading task are properly destroyed
with error
handling.
2025-12-16 11:34:30 +11:00
dzhou777 b45a2691ba fix: Unhide text field scrollbar (#2277) 2025-12-15 15:52:39 +11:00
Ted Liang f31cc575d0 fix: white-label for next-button, progress-bar, and steps (#2319) 2025-12-15 15:51:11 +11:00
github-actions[bot] 05d7015ef0 chore: extract translations (#2320) 2025-12-15 13:06:08 +11:00
Chenyang Gao 2ca5d6cfaa fix: local job retry loop for webhook calls (#2295) 2025-12-15 13:04:35 +11:00
Ryan Wagoner 04814ca14e fix: on error job should resubmit with isRetry (#2072) 2025-12-15 13:03:04 +11:00
Ryan Wagoner dd1dccdb6a fix: organisation invite member should be case insensitive (#2068) 2025-12-15 12:50:27 +11:00
Valentin Cocaud df4316ac5c fix: log unknown errors in the auth error handler (#2014) 2025-12-15 12:44:03 +11:00
Catalin Pit 02f1264eea feat: unlink documents from deleted organization (#2006) 2025-12-15 12:17:13 +11:00
github-actions[bot] 928edb8645 chore: extract translations (#2302) 2025-12-15 12:11:55 +11:00
Konrad 54b0e4964e chore(i18n): improve punctuation (#2307) 2025-12-15 12:00:51 +11:00
Konrad 68e6ccdd19 fix(i18n): mark sr-only strings for translation (#2309) 2025-12-15 11:51:02 +11:00
Konrad 09ab7e9a09 fix(i18n): mark "(Optional)" strings for translation (#2310) 2025-12-15 11:50:06 +11:00
Konrad 3bb0777914 fix(i18n): mark field content for translation (#2306) 2025-12-15 11:49:23 +11:00
Catalin Pit 4d6389e901 fix(api): replace generic errors with AppError in getApiTokenByToken (#2315) 2025-12-15 11:47:38 +11:00
Vincent Vu 51e3d5030d fix(security): CVE-2025-55184, CVE-2025-55183 (#2314) 2025-12-12 16:50:00 +11:00
David Nguyen 0cebdec637 fix: remove legacy envelope uploads (#2303) 2025-12-11 14:09:38 +11:00
Lucas Smith 43486d8448 v2.2.6 2025-12-09 21:11:01 +11:00
Lucas Smith 4d3d1b8d14 fix: make ai features more discoverable (#2305)
Previously you had to have explicit knowledge of the
feature and enable it in order to use AI assisted field
detection.

This surfaces it by having a secondary dialog prompting
for enablement.

Also includes a fix for CC recipients not getting marked
as signed in weird edge cases.
2025-12-09 15:30:48 +11:00
David Nguyen 0387f3c20a chore: add missing dropdown image (#2304)
## Description

Add missing dropdown image in the docs.
2025-12-09 12:37:45 +11:00
Ted Liang c5032d0c43 refactor: extract image-helpers (#2261) 2025-12-09 09:19:49 +11:00
Konrad 3bd34964cd fix(i18n): add pluralization to ai features (#2301) 2025-12-09 09:18:38 +11:00
Dailson Allves fe93b11a2c chore: update existing pt-BR translations after commit #2289 (#2300) 2025-12-09 09:17:22 +11:00
github-actions[bot] 7638faf27b chore: extract translations (#2289)
Automated translation extraction

Co-authored-by: github-actions <github-actions@documenso.com>
2025-12-08 19:20:21 +11:00
Ephraim Duncan 8fca029d96 fix: invalidate sessions on password reset and update (#2076) 2025-12-08 19:17:23 +11:00
Lucas Smith bac2bf11f4 v2.2.5 2025-12-08 14:33:00 +11:00
Lucas Smith d93b2a70a7 fix: upgrade react-email/render (#2297)
Upgrade the `@react-email/render` package to handle
suspense during renders.

We could have just swapped to `renderAsync` for the 0.0.x
version of the package but it's better to upgrade as part
of this change.

CI has been run locally and emails have been verified to
work and render as expected in our local mail trap.
2025-12-08 13:08:34 +11:00
Lucas Smith 5da915da38 fix: update server only urls to use private internal web app url (#2290)
Replaced instances of NEXT_PUBLIC_WEBAPP_URL with
NEXT_PRIVATE_INTERNAL_WEBAPP_URL
2025-12-08 12:56:41 +11:00
Ted Liang dcaecf1fc5 feat: resource restriction in presign token (#2150) 2025-12-08 12:55:54 +11:00
Ephraim Duncan f70b76d8b8 feat: add envelope audit logs endpoint (#2232) 2025-12-08 12:34:03 +11:00
David Nguyen 93137c6396 fix: translation extraction job (#2288)
## Description

Workaround until we can commit directly to main for translation
extractions

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-06 16:19:35 +11:00
Catalin Pit d058b7c705 feat: include CC role in removed recipient email check (#2285) 2025-12-06 14:20:25 +11:00
David Nguyen b51f562224 feat: add empty emails for envelopes (#2267) 2025-12-06 13:38:10 +11:00
David Nguyen f80aa4bf72 chore: optimize tests (#2280) 2025-12-06 12:59:53 +11:00
Lucas Smith 9238f759a6 v2.2.4 2025-12-05 12:23:23 +11:00
Lucas Smith 74ad6af47d chore: add docs for ai features (#2284)
Adds documentation for the recently added AI features

Includes details for how users can enable AI features for their team or
organisation

Also includes details for how self-hosters can setup their instance to
allow for AI features
2025-12-05 11:47:53 +11:00
Lucas Smith 18902ed59d fix: export loader for personal document preferences (#2283) 2025-12-05 11:22:29 +11:00
Lucas Smith 3f70082146 v2.2.3 2025-12-05 09:53:40 +11:00
Lucas Smith 31ba6d5f00 fix: polyfill promise.withResolvers (#2282)
Co-authored-by: Catalin Pit <catalinpit@gmail.com>
2025-12-04 23:33:31 +11:00
Lucas Smith c4f89a87a2 fix: use skia-canvas with pdfjs to avoid N-API errors (#2281)
Use custom CanvasFactory for pdfjs so we can continue to use
skia-canvas.
2025-12-04 23:26:08 +11:00
Ted Liang 89d6dd5b0e fix: embed authoring permission issue (#2279) 2025-12-04 15:02:50 +11:00
Lucas Smith 08a9ab3aaf v2.2.2 2025-12-04 14:50:09 +11:00
Lucas Smith e66bd422e3 chore: upgrade dependencies (#2278) 2025-12-04 14:31:30 +11:00
Lucas Smith 0f5814ff89 chore: add translations (#2259) 2025-12-04 14:01:35 +11:00
Konrad 1275a15571 fix(i18n): mark missing toast messages for translation (#2274) 2025-12-04 14:00:25 +11:00
Lucas Smith 22d99c7410 v2.2.1 2025-12-04 11:39:19 +11:00
Lucas Smith 26a36487d4 fix: pass canvas context to napi-rs/canvas (#2276) 2025-12-04 11:19:44 +11:00
Lucas Smith 2ee6b90c99 fix: add debug logging for ai streaming (#2275) 2025-12-04 10:03:29 +11:00
Lucas Smith f70e6ac50a v2.2.0 2025-12-04 00:31:11 +11:00
Lucas Smith 7a94ee3b83 feat: add ai detection for recipients and fields (#2271)
Use Gemini to handle detection of recipients and fields within
documents.

Opt in using organisation or team settings.

Replaces #2128 since the branch was cursed and would include
dependencies that weren't even in the lock file.



https://github.com/user-attachments/assets/e6cbb58f-62b9-4079-a9ae-7af5c4f2e4ec
2025-12-03 23:39:41 +11:00
Filbert Wijaya e39924714a fix: invalid email display bug when recipient suggestions on select (#2198) 2025-12-03 12:10:38 +11:00
Konrad c9604fee64 chore(i18n): change recipient invitation messages (#2172) 2025-12-03 11:55:53 +11:00
Konrad 90f8340af4 fix(i18n): add pluralization to envelope items (#2183) 2025-12-03 11:30:43 +11:00
Eesh Midha 28b8d2d415 fix: disable browser autocomplete in typed signature input (#2269) 2025-12-03 11:22:35 +11:00
Timur Ercan 978a2047d4 chore: update readme 2025-12-03 11:20:15 +11:00
Catalin Pit 0dfa953f54 feat: add external ID to use template (#2264) 2025-12-02 18:53:42 +11:00
David Nguyen 4774324e07 fix: prevent client side distribution when missing signatures (#2260) 2025-12-02 11:29:48 +11:00
David Nguyen bc19699a58 feat: add dutch language (#2255) 2025-12-02 11:28:04 +11:00
Harishraju04 55480826de docs: add missing translate:compile step to setup guid 2025-12-01 12:05:51 +11:00
Konrad 327b0eaf86 fix(i18n): add pluralization to pagination (#2217) 2025-12-01 11:38:57 +11:00
Konrad 2de5c1992f chore(i18n): add message context to subscription status (#2220) 2025-12-01 11:34:43 +11:00
Konrad df0c03816e chore(i18n): add message context for "Free" and "Paid" (#2222) 2025-12-01 11:30:07 +11:00
Konrad a610a06372 fix(i18n): mark table headers for translation (#2174) 2025-12-01 11:20:18 +11:00
Konrad d5e085d7ee fix(i18n): mark document visibility strings for translation (#2263) 2025-12-01 11:02:55 +11:00
Timur Ercan c322356654 chore: remove cummulative mau (#2250) 2025-11-28 18:07:30 +11:00
Lucas Smith b16862b480 chore: update embed authoring docs (#2254) 2025-11-27 23:29:06 +11:00
Lucas Smith 7065b0dd88 chore: add translations (#2253)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-11-27 21:36:48 +11:00
Lucas Smith dff9cfec05 chore: add translations (#2228) 2025-11-27 16:40:18 +11:00
David Nguyen d84cf0e58d chore: extract translations (#2252) 2025-11-27 16:04:22 +11:00
Ephraim Duncan 5d8b147199 fix: delay field tooltip scroll on envelope item switch (#2246) 2025-11-27 13:37:33 +11:00
Filbert Wijaya 7d28295d42 build: remove unsupported auto-install-peers from .npmrc (#2199) 2025-11-27 13:35:23 +11:00
Ephraim Duncan 94646cd48a perf: add database indexes for insights queries (#2211) 2025-11-26 21:21:01 +11:00
Ephraim Duncan 14db9b8203 feat: add navigation links between admin org pages (#2243) 2025-11-26 15:15:29 +11:00
Lucas Smith 6ae672c16b v2.1.0 2025-11-25 16:38:06 +11:00
Lucas Smith e9a9d65937 chore: telemetry (#2241) 2025-11-25 16:35:26 +11:00
David Nguyen d857dfdb38 feat: add webhook logs (#2237) 2025-11-25 16:03:52 +11:00
Lucas Smith 11a56f3228 chore: telemetry (#2240) 2025-11-25 16:01:31 +11:00
Lucas Smith 91642ddf0b fix: add missing properties for template/use (#2234)
Adds the `override` and `attachments` properties to the
`api/v2/templates/use` endpoint that were previously missing.
2025-11-25 11:44:47 +11:00
David Nguyen e364b08b6a fix: optimize webhook routing (#2236) 2025-11-25 11:43:23 +11:00
Catalin Pit 5df3932958 fix: update branding logic (#2238)
Update branding logic to ensure company details are displayed only when
branding is enabled
2025-11-24 21:45:31 +11:00
Lucas Smith ae31860b16 fix: USE_INTERNAL_URL_BROWSERLESS breaks builds (#2233) 2025-11-23 23:49:08 +11:00
Timur Ercan 16ee6b7a6d fix: give the possibility to use internal webapp url in browserless requests (get-certificate-pdf and get-audit-logs-pdf) (#2127) (#2230) 2025-11-22 20:49:34 +11:00
Matteo Sillitti 921c3d1ff3 fix: give the possibility to use internal webapp url in browserless requests (get-certificate-pdf and get-audit-logs-pdf) (#2127) 2025-11-22 20:36:24 +11:00
Ephraim Duncan 2d7a4d0dde docs: add missing environment variables to self-hosting guide (#2225) 2025-11-22 20:28:51 +11:00
Lucas Smith d2176627ca chore: dependency updates (#2229) 2025-11-22 20:28:20 +11:00
Lucas Smith 17c6098638 v2.0.14 2025-11-20 15:12:40 +11:00
Lucas Smith e5bde53ee4 chore: add translations (#2223) 2025-11-20 15:09:13 +11:00
Lucas Smith 0663605ffd fix: handle loading files in embedded authoring update flows (#2218) 2025-11-20 15:07:41 +11:00
Lucas Smith 1bbe561162 chore: add pending ui to signing completion page (#2224)
Adds a pending UI state to the signing completion page for when all
recipients have finished signing but the document hasn't completed the
sealing background job.

<img width="695" height="562" alt="image"
src="https://github.com/user-attachments/assets/b015bc38-9489-4baa-ac0a-07cb1ac24b25"
/>
2025-11-20 15:07:26 +11:00
Dailson Allves fbc156722a feat: add Portuguese (Brazil) translation support version 2.0.6 (#2165)
Portuguese (Brazil) Translation Support for Documenso
2025-11-20 14:14:47 +11:00
Karlo f5d63fb76c feat: add option to change or disable OIDC login prompt parameter (#2037) 2025-11-20 13:08:36 +11:00
Catalin Pit 374477e692 refactor: improve layout of completed signing page (#2209) 2025-11-20 11:04:41 +11:00
David Nguyen 11d9bde8f8 fix: improve sealing speed (#2210) 2025-11-19 14:15:12 +11:00
Lucas Smith fa1680aaf1 v2.0.13 2025-11-18 16:59:02 +11:00
David Nguyen 798b6bd750 feat: add japanese chinese and korean support (#2202)
## Description

Adds the following languages since we updated our PDF sealing to support
special characters
- Japanese
- Korean
- Chinese (Simplified)

## Tests

Ran through the signing process in these new languages.
2025-11-18 16:57:38 +11:00
Lucas Smith 8fbace0f61 fix: viewed webhook had stale data (#2208) 2025-11-18 16:57:14 +11:00
David Nguyen 1bbd04be9b feat: add field dev mode (#2203) 2025-11-18 16:57:06 +11:00
Lucas Smith 6aa56fe7e0 chore: add translations (#2182) 2025-11-17 15:48:02 +11:00
Lucas Smith a07f3090cf v2.0.12 2025-11-15 00:48:27 +11:00
Lucas Smith de3e6d2115 fix: embed editing updates (#2197)
Allows empty recipients for embed template authoring.

Also allows fixing the step to editing fields only for embedded
authoring updates.
2025-11-15 00:47:50 +11:00
David Nguyen dabd2564cd feat: add ability to change field recipient (#2194)
## Description

Add ability to change recipients for a given field.

<img width="336" height="224" alt="image"
src="https://github.com/user-attachments/assets/c122672d-68ab-4652-9c76-1a1bc874c16a"
/>

<img width="716" height="465" alt="image"
src="https://github.com/user-attachments/assets/a8e8636c-c780-4d56-910b-5522161b12d3"
/>
2025-11-15 00:46:33 +11:00
Tanushree Ahir 5811716d12 fix(email): preserve word wrap and line breaks in email body (#2139) (#2159) 2025-11-14 15:12:36 +11:00
David Nguyen 56526f9448 feat: extend use envelope response (#2192) 2025-11-14 13:52:19 +11:00
Lucas Smith 6ec1c3a3fb fix: hide overflow on envelope download dialog (#2193) 2025-11-14 13:17:55 +11:00
Timur Ercan 47466a2db9 chore: Update README.md (#2188)
We are launching Documenso 2.0 on Product Hunt, come say hi!
2025-11-13 20:52:34 +11:00
Ephraim Duncan e4e04cdddc feat: simplify billing ux (#2117) 2025-11-13 15:58:16 +11:00
samuel-cglg 74a03077b7 fix: placeholders translations (#2020) 2025-11-13 14:26:19 +11:00
samuel-cglg 58bff33275 fix: add context to signature pad translations (#2051) 2025-11-13 13:35:23 +11:00
Konrad 0cb23be27a chore: add question mark to message (#2176) 2025-11-13 12:08:39 +11:00
David Nguyen 4304fc1d35 chore: add envelope docs (#2186) 2025-11-13 11:50:06 +11:00
Lucas Smith ce53bcea8c v2.0.11 2025-11-13 10:56:58 +11:00
Lucas Smith 5a3d5b8b4a fix: persist missed meta values for envelopes (#2185) 2025-11-13 10:44:59 +11:00
Lucas Smith 3d1fe85d62 fix: add foreign key indexes (#2184)
Can't believe we missed some of these ☠️
2025-11-13 10:43:11 +11:00
Lucas Smith 1772c3ee36 v2.0.10 2025-11-12 18:57:24 +11:00
Lucas Smith f55b902a01 fix: openapi path for envelope item download 2025-11-12 18:57:03 +11:00
Lucas Smith 50db4e39be v2.0.9 2025-11-12 18:22:46 +11:00
David Nguyen 2802813c76 fix: add default values for envelope field meta (#2181)
🙏
2025-11-12 18:22:30 +11:00
Lucas Smith 29d40f1cca v2.0.8 2025-11-12 17:19:53 +11:00
Lucas Smith d67f32eae2 chore: add translations (#2166)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-11-12 17:17:44 +11:00
David Nguyen a33233443b chore: extract translations (#2179) 2025-11-12 17:09:50 +11:00
Lucas Smith 68a3608aee fix: handle bracket notation arrays (#2178)
Co-authored-by: David Nguyen <davidngu28@gmail.com>
2025-11-12 16:38:06 +11:00
Lucas Smith 378dd605b9 v2.0.7 2025-11-12 13:09:18 +11:00
Lucas Smith 211ae6c9e9 fix: prefill arcoforms with formdata endpoints (#2169) 2025-11-12 12:41:10 +11:00
David Nguyen f931885a95 fix: remove redundant endpoint (#2170)
Remove duplicate redundant endpoint
2025-11-12 11:01:15 +11:00
Lucas Smith 4ade408001 fix: include cmaps for pdf viewer (#2177)
We were previously omitting cmaps meaning that
when signing documents with certain UTF-8 characters or CJK characters
they would appear as outlined squares in the pdf viewer despite the
actual pdf looking as expected with the characters displaying correctly.
2025-11-12 11:00:44 +11:00
David Nguyen 3d0e3c6e8e fix: update dropzone to create envelopes (#2168) 2025-11-11 20:49:42 +11:00
David Nguyen 936d8d90b3 fix: envelope auth not being passed (#2167) 2025-11-11 13:05:05 +11:00
Ephraim Duncan c6b08d8594 feat: org insights (#1937) 2025-11-11 12:09:58 +11:00
Konrad 575634e326 fix: add pluralization to document details (#2130) 2025-11-11 10:54:12 +11:00
Konrad c66eda4aae chore: add message context in user organisation table (#2138) 2025-11-11 10:44:20 +11:00
Konrad ef52b35f79 chore: change message in dropzone wrappers (#2137) 2025-11-11 10:43:22 +11:00
Konrad 95a647034a chore: add message context in envelope download dialog (#2136) 2025-11-11 10:42:30 +11:00
Konrad 34dba0b6ff chore: fix message formatting (#2135) 2025-11-11 10:41:54 +11:00
Konrad fccd97e124 fix: add pluralization to 2fa code template (#2131) 2025-11-11 10:39:05 +11:00
Konrad 3dbbcefddf fix: add pluralization to passkey page (#2129) 2025-11-11 10:37:54 +11:00
David Nguyen 2aea3c4de0 fix: rename envelope buttons (#2161) 2025-11-10 22:21:34 +11:00
Lucas Smith ff44ffbc03 v2.0.6 2025-11-10 19:08:43 +11:00
Lucas Smith 441842d2bd fix: use correct token for embeded template files (#2160) 2025-11-10 19:08:08 +11:00
David Nguyen ca0b83579f fix: auto insert prefilled text and number fields (#2157) 2025-11-10 18:04:21 +11:00
Eesh Midha 6c0d1da91e fix(input): prevent mobile zoom on input focus (#2079) 2025-11-10 12:43:06 +11:00
David Nguyen 805982f3e8 fix: envelope cc issues (#2158) 2025-11-10 11:42:57 +11:00
David Nguyen e2f5e570cf fix: envelope direct template (#2156) 2025-11-09 22:23:13 +11:00
David Nguyen 9fd9613076 feat: add additional field options (#2154) 2025-11-08 23:40:03 +11:00
Lucas Smith 0977c16e33 v2.0.5 2025-11-08 16:03:59 +11:00
Lucas Smith 88d5a636c3 fix: show legacy ids on template and document view page (#2153)
<img width="557" height="455" alt="image"
src="https://github.com/user-attachments/assets/7b669f4a-c6c5-4fdc-bf10-da0def7b0b3f"
/>
2025-11-08 16:03:26 +11:00
Lucas Smith 1e6292b1d9 v2.0.4 2025-11-08 13:58:11 +11:00
Lucas Smith d65866156d fix: remove parallel steps (#2152) 2025-11-08 13:57:26 +11:00
Lucas Smith fe8915162f v2.0.3 2025-11-08 12:53:50 +11:00
Lucas Smith 37a2634aca feat: support optimizeParallelism for inngest jobs (#2151) 2025-11-08 12:53:13 +11:00
Lucas Smith eff7d90f43 v2.0.2 2025-11-08 00:48:31 +11:00
Lucas Smith db5524f8ce fix: resolve issue with sealing task on inngest (#2146)
Currently on inngest the sealing task fails during decoration stating
that it can not find the step "xxx"

My running theory is that this was due to it being a
Promise.all(map(...)) even though that isn't explicitly disallowed.

This change turns it into a for loop collecting promises to be awaited
after the fact.

Local inngest testing looks promising.
2025-11-08 00:48:13 +11:00
Lucas Smith 3d539b20ad v2.0.1 2025-11-07 23:42:03 +11:00
Lucas Smith 48626b9169 fix: support utf8 filenames download (#2145) 2025-11-07 23:41:31 +11:00
David Nguyen 88371b665a fix: set correct envelope item cache url (#2144) 2025-11-07 16:50:58 +11:00
Lucas Smith 1650c55b19 v2.0.0 2025-11-07 15:40:24 +11:00
Lucas Smith 60d73e0921 chore: add translations (#2143)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-11-07 14:45:21 +11:00
Lucas Smith 4a779ec81e fix: handle custom org limits with member invite 2025-11-07 14:24:05 +11:00
Lucas Smith 7f19ec1265 fix: embedded direct template recipient auth 2025-11-07 14:23:46 +11:00
Lucas Smith d6a2f5a4c9 chore: extract translations (#2070) 2025-11-07 14:20:53 +11:00
David Nguyen d05bfa9fed feat: add envelopes api (#2105) 2025-11-07 14:17:52 +11:00
David Nguyen d2a009d52e fix: allow direct template recipient dictation (#2108) 2025-11-01 12:44:34 +11:00
Lucas Smith 9350c53c7d chore: add code styleguide (#2089)
Co-authored-by: Ephraim Atta-Duncan <ephraimduncan68@gmail.com>
2025-10-28 22:25:27 +11:00
Ephraim Duncan ffce7a2c81 fix: filter document stats by folder (#2083)
This pull request refactors the filtering logic in the `getTeamCounts`
function within `get-stats.ts` to improve consistency and
maintainability. The main change is the consolidation of multiple filter
conditions into a single `AND` clause, which now includes search
filters, folder filters, and visibility filters. This ensures that all
relevant filters are applied in a unified way for document count
queries.
2025-10-28 21:16:12 +11:00
Ephraim Duncan 353bdce86b feat: admin member role updates (#2093) 2025-10-28 21:09:38 +11:00
David Nguyen e13b9f7c84 fix: hide banner for envelope editor (#2109) 2025-10-28 16:55:44 +11:00
David Nguyen 9908580bf1 fix: add envelopes flag (#2104)
## Description

Add a global flag override for envelopes
2025-10-28 11:42:03 +11:00
David Nguyen b0b07106b4 fix: envelope autosave (#2103) 2025-10-27 19:53:35 +11:00
Ephraim Duncan 35250fa308 feat: server port configurable via PORT env (#2097) 2025-10-27 17:24:24 +11:00
David Nguyen 5cdd7f8623 fix: envelope styling (#2102) 2025-10-27 16:11:10 +11:00
David Nguyen 47bdcd833f chore: extract translations (#2094) 2025-10-24 16:37:10 +11:00
David Nguyen 03eb6af69a feat: polish envelopes (#2090)
## Description

The rest of the owl
2025-10-24 16:22:06 +11:00
Lucas Smith 88836404d1 v1.13.1 2025-10-24 10:50:25 +11:00
Lucas Smith 2eebc0e439 feat: add attachments (#2091) 2025-10-23 23:07:10 +11:00
Ephraim Duncan 4a3859ec60 feat: signin with microsoft (#1998) 2025-10-22 12:05:11 +11:00
Ephraim Duncan 49b792503f fix: query envelope table for openpage stats (#2086) 2025-10-21 12:43:57 +00:00
Catalin Pit c3dc76b1b4 feat: add API support for folders (#1967) 2025-10-21 18:22:19 +11:00
David Nguyen daab8461c7 fix: email attachment names (#2085) 2025-10-21 12:59:40 +11:00
Lucas Smith 1ffc4bd703 v1.13.0 2025-10-21 11:21:04 +11:00
Lucas Smith f15c0778b5 fix: authoring token arg and null email settings 2025-10-21 10:42:44 +11:00
David Nguyen 06cb8b1f23 fix: email attachment formats (#2077) 2025-10-16 14:16:00 +11:00
David Nguyen 7f09ba72f4 feat: add envelopes (#2025)
This PR is handles the changes required to support envelopes. The new
envelope editor/signing page will be hidden during release.

The core changes here is to migrate the documents and templates model to
a centralized envelopes model.

Even though Documents and Templates are removed, from the user
perspective they will still exist as we remap envelopes to documents and
templates.
2025-10-14 21:56:36 +11:00
Lucas Smith 7b17156e56 v1.12.10 2025-10-09 15:32:35 +11:00
Lucas Smith 86e89e137e fix: bump search limit and path formatting (#2069) 2025-10-09 15:11:43 +11:00
Lucas Smith 26f65dbdd7 v1.12.9 2025-10-07 17:07:11 +11:00
Lucas Smith a902bec96d fix: use select account prompt for sso oidc (#2065)
Use the `select_account` prompt for SSO OIDC to avoid constantly asking
for credentials to be entered with a client has an existing session with
the SSO provider.
2025-10-07 17:06:28 +11:00
samuel-cglg 399f91de73 feat: improve invite email (#2030)
Improve the email sent to invite a user to approve/sign/assist/view a
document.
The current links "Reject Document" / "Sign Document" confuse some users
as they think the document will be rejected/signed just by clicking on
these buttons.
This change makes it more clear that they will have a chance to view the
document before they can reject/sign.
2025-10-06 16:19:16 +11:00
Lucas Smith 995bc9c362 feat: support 2fa for document completion (#2063)
Adds support for 2FA when completing a document, also adds support for
using email for 2FA when no authenticator has been associated with the
account.
2025-10-06 16:17:54 +11:00
David Nguyen 3467317271 chore: extract translations (#2056)
## Description

Extract translations to be translated
2025-10-02 13:20:30 +10:00
Lucas Smith a5eaa8ad47 v1.12.8 2025-09-29 23:22:59 +10:00
Lucas Smith 577691214b fix: add viewed call for embedded signing (#2055)
Adds the missing `viewedDocument` method call for embedded signing.

I believe this had previously been added but was lost as the result of a
merge conflict being resolved.
2025-09-29 23:22:36 +10:00
Catalin Pit c7d21c6587 fix: update personal organisation email settings (#2048) 2025-09-29 10:11:00 +03:00
Mythie 2aa391f917 v1.12.7 2025-09-26 09:57:34 +10:00
Lucas Smith 681540b501 fix: add removed date formats (#2049)
Add date formats that were removed in a prior pull request causing
issues with certain API requests.
2025-09-26 09:56:46 +10:00
Catalin Pit f3305ac306 feat: show branding logo on signing page (#2031)
If the team has the branding enabled & a logo uploaded, it'll show on
the document signing page view.
2025-09-25 22:41:17 +10:00
Catalin Pit 68b4305b6a feat: add max file size for uploaded documents (#2044) 2025-09-25 22:40:00 +10:00
Catalin Pit 3de1ea0a02 feat: resend dialog improvements (#2034)
The checkboxes were difficult to see and the "Send reminder" button
wasn't disabled when no recipients were selected. This PR disables the
sending button when there's no selected recipient and improves the
checkboxes visibility.
2025-09-25 22:23:07 +10:00
Lucas Smith b8fc47b719 v1.12.6 2025-09-25 22:10:20 +10:00
Lucas Smith cfceebd78f feat: change organisation owner in admin panel (#2047)
Allows changing the owner of an organisation within the admin panel,
useful for support requests to change ownership from a testing account
to the main admin account.

<img width="890" height="431" alt="image"
src="https://github.com/user-attachments/assets/475bbbdd-0f26-4f74-aacf-3e793366551d"
/>
2025-09-25 17:13:47 +10:00
Catalin Pit b9b3ddfb98 chore: update tests to use new date formats (#2045)
## Description

Update the tests to use the new date formats form this PR
https://github.com/documenso/documenso/pull/2038.
2025-09-25 16:55:31 +10:00
Catalin Pit 8590502338 fix: file upload error messages (#2041) 2025-09-24 16:06:41 +03:00
Catalin Pit 53f29daf50 fix: allow dates with and without time (#2038) 2025-09-24 14:46:04 +03:00
Lucas Smith 197d17ed7b v1.12.5 2025-09-23 21:00:48 +10:00
Lucas Smith 3c646d9475 feat: remove email requirement for recipients (#2040) 2025-09-23 17:13:52 +10:00
Lucas Smith ed4dfc9b55 v1.12.4 2025-09-13 18:08:55 +10:00
Lucas Smith 32ce573de4 fix: incorrect certificate health logic (#2028) 2025-09-13 18:07:39 +10:00
Lucas Smith 2ecfdbdde5 v1.12.3 2025-09-12 23:02:59 +10:00
Maximilian Schwerin a3005f8616 fix: Include NEXT_PRIVATE_SMTP_UNSAFE_IGNORE_TLS in docker compose file. (#2022) 2025-09-10 22:55:56 +10:00
Ephraim Duncan 2c0d4f8789 chore: self hosting docs update and certificate issues (#1847) 2025-09-09 21:26:42 +10:00
Catalin Pit 7c8e93b53e feat: implement recipients autosuggestions (#1923) 2025-09-09 20:57:26 +10:00
Ephraim Duncan 93a3809f6a fix: add maxLength limits to document input fields (#1988) 2025-09-09 17:52:03 +10:00
samuel-cglg 4550bca3d3 fix: signature pad translation (#2007) 2025-09-09 17:14:44 +10:00
David Nguyen 9ac7b94d9a feat: add organisation sso portal (#1946)
Allow organisations to manage an SSO OIDC compliant portal. This method
is intended to streamline the onboarding process and paves the way to
allow organisations to manage their members in a more strict way.
2025-09-09 17:14:07 +10:00
Timur Ercan 374f2c45b4 chore: add soc2 compliance (#2019)
added soc2 compliance to docs
2025-09-08 17:56:53 +02:00
Catalin Pit bb5c2edefd feat: implement auto-save functionality for signers in document edit form (#1792) 2025-09-02 21:01:16 +10:00
Ephraim Duncan 19565c1821 fix: access audit logs for documents in folder (#1989) 2025-08-31 12:17:31 +10:00
samuel-cglg 2603ae8b90 fix: send signing request email after the document status is updated (#1944)
When sending a document for signing, emails for recipients are sent
before the document status is updated.
In this case, the job "send.signing.requested.email" fails because it
cannot find the document with a PENDING status.
2025-08-31 11:37:49 +10:00
samuel-cglg 7d257236a6 fix: default pagination on documents list API (#1929) 2025-08-28 16:20:27 +10:00
Ephraim Duncan 31c1a9a783 fix: preserve existing recipient properties when adding new recipient (#1987) 2025-08-28 16:19:14 +10:00
Lucas Smith 657db3bc84 fix: improve mobile signing ux (#2003)
Improves the mobile signing UX making actions available via the floating
navbar more obvious.

Also adds an automatic switch to the complete button once all fields
have been signed.
2025-08-28 16:15:52 +10:00
Lucas Smith 184ebdedf1 v1.12.2-rc.6 2025-08-26 11:17:43 +10:00
Lucas Smith 4012022f55 fix: element visible race condition (#1996)
On larger documents we could accidentally start trying to render fields
while not all pages of the PDF have loaded due to us checking for a
single page existing. This would cause an error to be thrown, hard
locking those documents.

This change resolves this by grabbing the highest page number from the
given fields and using it for the visibility check instead.
2025-08-26 11:08:43 +10:00
David Nguyen 44f5da95b3 chore: refactor routes (#1992) 2025-08-25 21:00:35 +10:00
David Nguyen 7eb882aea8 fix: email domain sender logic (#1993) 2025-08-25 20:59:37 +10:00
David Nguyen dbf10e5b7b chore: add agents file (#1991) 2025-08-25 11:32:15 +10:00
Lucas Smith fe4d3ed1fd v1.12.2-rc.5 2025-08-25 09:48:04 +10:00
David Nguyen b8d07fd1a6 fix: refactor token router (#1981) 2025-08-25 08:25:01 +10:00
David Nguyen 49fabeb0ec fix: refactor auth router (#1983) 2025-08-25 08:24:32 +10:00
David Nguyen 5a5bfe6e34 fix: refactor admin router (#1982) 2025-08-25 08:23:48 +10:00
David Nguyen d7e5a9eec7 fix: refactor document router (#1990) 2025-08-25 08:23:12 +10:00
Ephraim Duncan adefac81e2 fix: outdated docs (#1985) 2025-08-24 16:48:30 +10:00
Catalin Pit 67501b45cf feat: create document in a specific folder (#1965) 2025-08-23 00:12:17 +10:00
Catalin Pit 17b36ac8e4 feat: sync organization name with stripe (#1974) 2025-08-22 23:28:04 +10:00
Lucas Smith 80e452afa2 fix: get accurate pdf page size (#1980)
Handles edge cases with PDF media boxes and crop boxes, deals with
certain documents that had been uploaded with weird combos of sizings.
2025-08-22 22:50:41 +10:00
Ephraim Duncan 1cb9de8083 chore: remove 'use client' directives (#1979) 2025-08-22 02:20:41 +00:00
Catalin Pit 231ef9c27e chore: add support option (#1853) 2025-08-19 20:59:03 +10:00
Catalin Pit 6f35342a83 feat: reset user 2fa from admin panel (#1943) 2025-08-19 13:09:05 +10:00
David Nguyen a51110d276 fix: prevent document unsigning on edit (#1963) 2025-08-18 13:48:51 +10:00
David Nguyen 7f81231467 fix: template e2e tests (#1969) 2025-08-18 12:42:36 +10:00
Lucas Smith 439262fd02 v1.12.2-rc.4 2025-08-16 19:16:29 +10:00
Lucas Smith 93a184355b chore: add translations (#1955) 2025-08-16 19:10:21 +10:00
David Nguyen 1dea0b8fab add dummy teamid (#1968) 2025-08-16 19:09:21 +10:00
David Nguyen ea7a2c2712 fix: create customer on signup (#1964) 2025-08-14 16:30:16 +10:00
Catalin Pit deb3a63fb8 feat: allow empty placeholder emails on templates (#1930)
Allow users to create template placeholders without the placeholder
emails.
2025-08-12 20:41:23 +10:00
Ephraim Duncan cc05af2062 feat: backport the embedded mobile signing ux to main application (#1919)
This PR improves the mobile experience of the document signing page by
implementing a collapsible widget design for the signing form. On mobile
devices, the form now appears as a fixed bottom sheet that can be
expanded/collapsed, while maintaining the sticky sidebar layout on
desktop.
2025-08-12 20:40:14 +10:00
David Nguyen 9026aabe3b fix: broken e2e tests (#1956) 2025-08-11 16:16:21 +10:00
David Nguyen b844e166a9 fix: build 2025-08-11 12:16:34 +10:00
David Nguyen 950951de75 fix: github actions 2025-08-11 12:05:41 +10:00
David Nguyen c37e10faab fix: add document page access logging (#1947)
Add logging when someone accesses a document page
2025-08-11 11:50:32 +10:00
David Nguyen fdf6efe94e chore: extract translations (#1949)
Extract translations
2025-08-11 11:49:30 +10:00
David Nguyen 4c1eb8f874 fix: translation extraction github action (#1950)
Fix checkout action for translation extraction
2025-08-11 11:48:19 +10:00
Konrad e547b0b410 fix: add special context to strings (#1954) 2025-08-11 11:47:21 +10:00
Catalin Pit 803edf5b16 feat: implement Drag-n-Drop for templates (#1791) 2025-08-07 15:37:55 +10:00
David Nguyen 86c133ae84 fix: remove field truncate logic (#1940)
Remove the truncation logic and render the text for preview/edit mode.

Text will now overflow, but it's up to the user to correct it
2025-08-07 11:55:25 +10:00
David Nguyen c28c5ab91d chore: correct the email domains documentation (#1941)
## Description

Update the documentation for email domains
2025-08-07 11:54:41 +10:00
Catalin Pit d1eb14ac16 feat: include audit trail log in the completed doc (#1916)
This change allows users to include the audit trail log in the completed
documents; similar to the signing certificate.


https://github.com/user-attachments/assets/d9ae236a-2584-4ad6-b7bc-27b3eb8c74d3

It also solves the issue with the text cutoff.
2025-08-07 11:44:59 +10:00
Novapixel1010 f24b71f559 feat: env for support email (#1945)
Add the ability to change the support email. For the signature
disclosure page.
2025-08-07 10:39:03 +10:00
David Nguyen 2ee0d77870 fix: correctly set stripe customer names (#1939)
Currently the Stripe customer name is set to the organisation name,
which in some cases is just the organisation name.

This update makes it so it uses the owner name instead.
2025-08-05 12:30:02 +10:00
Ephraim Duncan 9b01a2318f feat: download document via api v2 (#1918)
adds document download functionality to the API v2, returning
pre-signed S3 URLs that provide secure, time-limited access to document
files similar to what happens in the API v1 download document endpoint.
2025-08-05 12:29:21 +10:00
Catalin Pit 5689cd1538 feat: add tooltip to team member creation dialog for guidance (#1933) 2025-08-04 08:49:43 +03:00
Lucas Smith 9d5b573dda v1.12.2-rc.3 2025-08-02 00:46:22 +10:00
David Nguyen c48486472a fix: add missing email reply validation (#1934)
## Description

General fixes to the email domain features

Changes made:
- Add "email" validation for "Reply-To email" fields
- Fix issue where you can't remove the "Reply-To" email after it's set
- Fix issue where setting the "Sender email" back to Documenso would
still send using the org/team pref
2025-08-02 00:40:41 +10:00
Lucas Smith 1e2388519c hotfix: certificate pdfs are blank when using browserless (#1935)
Certificates have suddenly become blank when using browserless and
Chrome CDP.

This change introduces a workaround that involves reloading the
certificate pdf. Which is hacky but seems to work for now, a better
solution should be found in the future.
2025-08-02 00:39:48 +10:00
Ephraim Duncan 20198b5b6c feat: add european date format (#1925) 2025-07-28 12:32:10 +10:00
Tom 7cbf527eb3 chore: update French translations (#1762) 2025-07-25 10:52:18 +10:00
Lucas Smith 767b66672e chore: add translations (#1910) 2025-07-25 10:51:47 +10:00
David Nguyen 109a49826c chore: extract translations 2025-07-24 16:15:34 +10:00
David Nguyen 3409aae411 feat: add email domains (#1895)
Implemented Email Domains which allows Platform/Enterprise customers to
send emails to recipients using their custom emails.
2025-07-24 16:05:00 +10:00
David Nguyen 07119f0e8d fix: correctly render new lines in text fields (#1920)
Currently new lines are not rendered in text fields correctly on the
`/sign` page. This is an issue because when the field is inserted and
sealed we respect new lines.
2025-07-24 14:30:33 +10:00
Catalin Pit 7a5a9eefe8 feat: upload template via API (#1842)
Allow users to upload templates via both v1 and v2 APIs. Similar to
uploading documents.
2025-07-23 14:41:12 +10:00
samuel-cglg 5570690b3b fix: clicking on tooltip icon submit parent form (#1915) 2025-07-23 14:28:02 +10:00
Mythie 9ea56a77ff v1.12.2-rc.2 2025-07-20 17:05:19 +10:00
Mythie 32c94118ce fix: subscription update handler logic 2025-07-20 11:18:02 +10:00
Lucas Smith 512e3555b4 feat: horizontal checkboxes (#1911)
Adds the ability to have checkboxes align horizontally, wrapping when
they would go off the PDF
2025-07-19 22:06:50 +10:00
Catalin Pit c47dc8749a fix: handle unauthorized document move error (#1884) 2025-07-16 14:45:12 +10:00
David Nguyen 32a5d33a16 fix: invalid folder queries (#1898)
Currently the majority of folder mutations only work if the user is the
owner of the folder.
2025-07-16 14:37:55 +10:00
David Nguyen e5aaa17545 fix: restrict individual plans to upgrade only (#1900)
Prevent users from creating a separate organisation for individual
plans. Only applies to users who have 1 personal organisation and are
subscribing to the "Individual" plan.

The reason for this change is to keep the layout in the "Personal" mode
which means it doesn't show a bunch of unusable "organisation" related
UI.
2025-07-16 14:35:42 +10:00
Ephraim Duncan f9d7fd7d9a fix: resend document api v1 filtering logic (#1888)
The resend document API was not working correctly when filtering
recipients. The query was filtering recipients at the database level,
which could result in an empty recipients array being returned even when
the document had recipients. This prevented the API from properly
identifying which recipients needed reminder emails.
2025-07-16 14:31:40 +10:00
David Nguyen 5083ecb4b8 fix: allow resubscribing (#1901)
Currently users who cancel their plan are stuck without the ability to
resubscribe. This allows them to choose a plan to subscribe

This assumes that a Subscription in the "INACTIVE" state means that the
plan has been paid but canceled.

No tests have been done to determine the relation between "PAST_DUE" and
"INACTIVE" states within our context.
2025-07-16 14:26:21 +10:00
Catalin Pit 168648164b docs: add test webhook section (#1902) 2025-07-16 13:22:30 +10:00
Jan Piotrowski 202e9fedb9 fix: remove unsupported frontmatter from PULL_REQUEST_TEMPLATE.md (#1867) 2025-07-15 16:18:15 +10:00
Ephraim Duncan 939bbcdb33 docs: api rate limit (#1899) 2025-07-15 16:16:50 +10:00
Lucas Smith 70f6036525 chore: add translations (#1877) 2025-07-15 12:29:37 +10:00
Catalin Pit 122e25b491 feat: test webhook functionality (#1886) 2025-07-14 15:13:56 +10:00
Lucas Smith ca9a70ced5 fix: handle trials and resubscribing (#1897) 2025-07-14 12:31:06 +10:00
Ephraim Duncan 55abecc526 fix: isAssistantMode was incorrectly set to true for regular recipients (#1854) 2025-07-13 22:41:18 +10:00
Lucas Smith 49c70fc8a8 chore: update docs 2025-07-11 17:02:10 +10:00
Ephraim Duncan 4195a871ce chore: update gitginore (#1894) 2025-07-11 13:16:51 +10:00
Lucas Smith 37ed5ad222 v1.12.2-rc.1 2025-07-11 12:55:56 +10:00
Catalin Pit d6c11bd195 fix: sign-able readonly fields (#1885) 2025-07-10 16:47:36 +10:00
Catalin Pit cb73d21e05 chore: api tests (#1856) 2025-07-10 12:56:46 +10:00
David Nguyen 106f796fea fix: readonly field styling (#1887)
Changes:
- Updating styling of read only fields
- Removed truncation for fields and used overflow hidden instead
2025-07-10 12:35:18 +10:00
Lucas Smith 9917def0ca v1.12.2-rc.0 2025-07-03 10:31:22 +10:00
David Nguyen cdb9b9ee03 chore: add certificate error logs (#1875)
Add certificate logs
2025-07-03 10:13:12 +10:00
Lucas Smith 8d1d098e3a v1.12.1 2025-07-03 10:07:54 +10:00
Lucas Smith b682d2785f chore: add translations (#1835)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-07-03 10:07:11 +10:00
Lucas Smith 1a1a30791e v1.12.0 2025-07-03 10:01:03 +10:00
David Nguyen ea1cf481eb chore: extract translations 2025-07-01 21:27:35 +10:00
Ephraim Duncan eda0d5eeb6 fix: open advanced settings when fields are added to templates (#1855) 2025-07-01 21:21:13 +10:00
Jan Piotrowski 8da4ab533f fix(add-subject): remove superfluous word (#1866) 2025-07-01 12:34:14 +10:00
Lucas Smith 8695ef766e v1.12.0-rc.8 2025-06-30 19:47:37 +10:00
David Nguyen 7487399123 feat: add more api logs (#1870)
Adds more detailed API logging using Pino
2025-06-30 19:46:32 +10:00
David Nguyen 0cc729e9bd feat: add sequential document view logs (#1871)
## Description

Add a new document audit log to detect when the document is viewed. This
should only be visible in the document audit log page

Notes:
1. I wanted to reuse the `DOCUMENT_OPENED` event and add an additional
paramter to track sequential views, but it's not query-able
2. This will log "DOCUMENT_VIEWED" before "DOCUMENT_OPENED" but i don't
think it matters
2025-06-30 19:11:16 +10:00
Lucas Smith 58d97518c8 v1.12.0-rc.7 2025-06-27 22:17:45 +10:00
Lucas Smith 20c8969272 fix: get real ip for rate limit key 2025-06-27 22:17:02 +10:00
Lucas Smith 85ac65e405 v1.12.0-rc.6 2025-06-27 21:46:16 +10:00
David Nguyen e07a497b69 feat: api logging by pino (#1865)
experiemental
2025-06-27 21:44:51 +10:00
Lucas Smith 21dc4eee62 v1.12.0-rc.5 2025-06-27 18:53:45 +10:00
David Nguyen dc2042a1ee fix: rate limit api endpoints (#1863)
Rate limit API endpoint
2025-06-27 18:50:22 +10:00
Ephraim Duncan bb9ba80edb fix: duplicate fields and recipients when you duplicate a document (#1852) 2025-06-23 16:43:07 +10:00
Catalin Pit bfe8c674f2 fix: globalAccessAuth error (#1851) 2025-06-23 10:10:57 +10:00
David Nguyen ebe1baf0a0 chore: extract translations 2025-06-19 15:16:44 +10:00
Ephraim Duncan 2345de679b feat: admin monthly active users metric (#1724) 2025-06-19 15:12:17 +10:00
Catalin Pit 1be0e2842c fix: refactor folders UI/UX (#1770)
- Add folder search
- Used correct HTML elements
- Added missing translations
- Removed automatic folder redirects
- Removed duplicate code
- Added folder loading skeletons and empty states
2025-06-19 14:57:32 +10:00
Ephraim Duncan 29a03d4ec7 feat: add inbox counter (#1849) 2025-06-18 13:30:01 +10:00
Ephraim Duncan 039cd7d449 fix: remove preconnect font links (#1798) 2025-06-18 12:42:54 +10:00
Ephraim Duncan 484f6c8b85 fix: admin metrics broken (#1845) 2025-06-17 21:15:11 +10:00
Timur Ercan 4fd8a767b2 chore: Update README.md (#1840) 2025-06-13 22:42:38 +10:00
David Nguyen b8e08e88ac fix: api keys not showing (#1839) 2025-06-13 17:20:03 +10:00
David Nguyen 031a7b9e36 fix: visibility 2025-06-13 01:02:40 +10:00
David Nguyen 12fe045195 fix: visiblity 2025-06-13 00:05:08 +10:00
David Nguyen 614106a5e4 fix: rework documents limits logic (#1836) 2025-06-12 13:42:31 +10:00
Lucas Smith 8be7137b59 v1.12.0-rc.4 2025-06-12 10:27:41 +10:00
Lucas Smith 31e2a6443e fix: legacy authOptions support for api v1 2025-06-12 10:21:41 +10:00
Ephraim Duncan 400d2a2b1a feat: sign out of all sessions (#1797) 2025-06-11 17:57:38 +10:00
David Nguyen e3ce7f94e6 chore: update build 2025-06-11 14:52:23 +10:00
Shubham Palriwala cad04f26e7 feat: sitemap auto-generation for docs (#1822) 2025-06-11 14:09:45 +10:00
Ephraim Duncan d27f0ee0ef fix: duplicate field bugs (#1685) 2025-06-11 13:26:19 +10:00
Ephraim Duncan fd2b413ed9 chore: increase wait times for tests (#1778) 2025-06-11 13:25:21 +10:00
Catalin Pit d11ec8fa2a feat: show field coordinates in devmode (#1802)
Show the fields coordinates when the `devmode` search param is present.
It's meant to help API users understand where to position the fields.
2025-06-11 12:28:39 +10:00
Damien B. b1127b4f0d chore: update readme 2025-06-11 10:42:32 +10:00
Lucas Smith be4244fb62 chore: add translations (#1832)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-06-11 10:39:33 +10:00
David Nguyen 504a0893ab chore: add organisation docs (#1831) 2025-06-10 20:54:36 +10:00
Lucas Smith 22a37409c1 v1.12.0-rc.3 2025-06-10 12:49:37 +10:00
Lucas Smith 50605d5912 chore: add translations (#1830)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-06-10 12:30:36 +10:00
David Nguyen 4609fc852d chore: extract translations 2025-06-10 11:52:59 +10:00
David Nguyen e6dc237ad2 feat: add organisations (#1820) 2025-06-10 11:49:52 +10:00
Lucas Smith 0b37f19641 chore: add translations (#1774) 2025-06-09 16:00:03 +10:00
Lucas Smith 64c6a51e04 v1.12.0-rc.2 2025-06-07 02:25:14 +10:00
Lucas Smith d1eddb02c4 fix: add missing awaits for font normalization 2025-06-07 02:24:59 +10:00
Lucas Smith 60a623fafd v1.12.0-rc.1 2025-06-07 00:56:40 +10:00
Lucas Smith 6059b79a8e fix: type error 2025-06-07 00:56:33 +10:00
Lucas Smith c73d61955b v1.12.0-rc.0 2025-06-07 00:47:49 +10:00
Lucas Smith 7c3ca72359 fix: track uninserted fields for multisign 2025-06-07 00:44:41 +10:00
Lucas Smith 55c8632620 feat: password reauthentication for documents and recipients (#1827)
Adds password reauthentication to our existing reauth providers,
additionally swaps from an exclusive provider to an inclusive type where
multiple methods can be selected to offer a this or that experience.
2025-06-07 00:27:19 +10:00
Lucas Smith ce66da0055 feat: multisign embedding (#1823)
Adds the ability to use a multisign embedding for cases where multiple
documents need to be signed in a convenient manner.
2025-06-05 12:58:52 +10:00
Lucas Smith 695ed418e2 fix: documents failing to seal (#1821)
During our field rework that makes fields appear
more accurately between signing and the completed pdf we swapped to
using text fields. Unfortunately as part of that we dropped using the
Noto font for the text field causing ANSI encoding issues when
encountering certain characters.

This change restores the font and handles a nasty issue we had with our
form flattening reverting our selected font.
2025-06-04 23:29:36 +10:00
Lucas Smith 93aece9644 chore: dependency updates (#1808) 2025-05-22 14:30:22 +10:00
Timur Ercan abd4fddf31 chore: test reo integration (#1806)
---
name: Pull Request
about: Submit changes to the project for review and inclusion
---

## Description

Experimental Short-Term Reo Integration
2025-05-21 15:24:46 +02:00
Lucas Smith 44bc769e60 v1.11.1 2025-05-20 22:37:46 +10:00
Lucas Smith c8f80f7be0 fix: reverse original document logic for api endpoint 2025-05-20 22:37:17 +10:00
Lucas Smith 8540f24de0 v1.11.0 2025-05-19 15:44:10 +10:00
Lucas Smith 67203d4bd7 fix: show powered by logic (#1801)
Previous powered by display logic was incorrect, likely due to a merge
conflict.
2025-05-19 14:31:24 +10:00
Lucas Smith 9d1e638f0f fix: pending tooltip click triggers field (#1800)
Makes it so clicking on the pending field tooltip will trigger the
underlying field it refers to on click if the field can be found within
the DOM.
2025-05-19 10:27:13 +10:00
Lucas Smith bd64ad9fef fix: improve multiselect for webhook triggers (#1795)
Replaces https://github.com/documenso/documenso/pull/1660 with the same
code but targeting our main branch.

## Demo

![CleanShot 2025-02-18 at 18 01
05](https://github.com/user-attachments/assets/5afeab95-1a80-4d54-b845-b32cb2e33266)
2025-05-15 13:01:45 +10:00
Ephraim Duncan 99b0ad574e feat: bulk add fields (#1683)
## Demo

![CleanShot 2025-03-04 at 02 17
47](https://github.com/user-attachments/assets/2cffaee3-9933-49e9-bdab-eadfd4c35030)

---------

Co-authored-by: Lucas Smith <me@lucasjamessmith.me>
2025-05-14 19:35:32 +00:00
Ephraim Duncan 9594e1fee8 chore: minor ui fixes (#1793) 2025-05-14 20:08:03 +10:00
Lucas Smith 5e3a2b8f76 fix: allow prefilling date field (#1794)
Allows the prefilling of date fields when creating a document from a
template.

Current implementation is super dirty and should be replaced asap.
2025-05-14 20:06:53 +10:00
Ephraim Duncan f928503a33 chore: update dropdown icons (#1790)
### Before

![CleanShot 2025-05-12 at 11 11
05@2x](https://github.com/user-attachments/assets/af2a60bf-9676-405d-8c3d-e6b2256b53ae)

### After

![CleanShot 2025-05-12 at 11 10
25@2x](https://github.com/user-attachments/assets/aec67e9c-f0f2-4b0d-9baa-7aa327680cf1)
2025-05-14 16:44:13 +10:00
Ephraim Duncan c389670785 fix: trigger webhook for duplicated documents (#1789) 2025-05-14 16:43:31 +10:00
Lucas Smith 99ad2eb645 fix: allow download of original document via api (#1788) 2025-05-14 08:22:11 +10:00
Lucas Smith 2f48679b0b fix: make lang cookie httpOnly (#1783) 2025-05-08 15:59:43 +10:00
Mythie e40c5d9d24 v1.10.3 2025-05-03 09:23:25 +10:00
Mythie ab323f149f fix: resolve issue with uploading templates 2025-05-03 09:23:17 +10:00
Mythie bf1c1ff9dc v1.10.2 2025-05-03 08:11:27 +10:00
Mythie 516e237966 fix: resolve issue with uploading templates 2025-05-03 08:09:44 +10:00
Mythie ac7d24eb12 v1.10.1 2025-05-03 07:39:19 +10:00
Mythie 0931c472a7 fix: resolve issue with uploading templates 2025-05-03 07:38:48 +10:00
Lucas Smith 8c9dd5e372 v1.10.0 2025-05-02 12:03:08 +10:00
Lucas Smith e108da546d fix: incorrect data for postMessage 2025-05-02 10:50:13 +10:00
Catalin Pit 17370749b4 feat: add folders (#1711) 2025-05-02 02:46:59 +10:00
Lucas Smith 12ada567f5 feat: embed authoring part two (#1768) 2025-05-01 23:32:56 +10:00
Ephraim Duncan bdb0b0ea88 feat: certificate qrcode (#1755)
Adds document access tokens and QR code functionality to enable secure
document sharing via URLs. It includes a new document access page that
allows viewing and downloading documents through tokenized links.
2025-04-28 11:30:09 +10:00
Ephraim Duncan 6a41a37bd4 feat: download original documents (#1742)
## Preview
![CleanShot 2025-04-10 at 14 26
11@2x](https://github.com/user-attachments/assets/d4984d85-ab40-4d38-8d5c-a1085bde21a2)
2025-04-25 22:44:03 +10:00
David Nguyen d78cfec00e fix: branding logos (#1759) 2025-04-24 16:15:06 +10:00
Ephraim Duncan f0dcf7e9bf fix: signing volume query (#1753)
This pull request updates the implementation of the admin leaderboard,
enhancing data handling and improving type safety. It introduces clearer
differentiation between users and teams, adds additional fields to track
more relevant information, and refactors the querying logic to optimize
performance and maintainability.
2025-04-24 16:14:38 +10:00
Ephraim Duncan 6540291055 feat: migrate webhook execution to background jobs (#1694) 2025-04-24 06:00:53 +00:00
David Nguyen 193325717d fix: rework fields (#1697)
Rework:
- Field styling to improve visibility
- Field insertions, better alignment, centering and overflows

## Changes

General changes:

- Set default text alignment to left if no meta found
- Reduce borders and rings around fields to allow smaller fields
- Removed lots of redundant duplicated code surrounding field rendering
- Make fields more consistent across viewing, editing and signing
- Add more transparency to fields to allow users to see under fields
- No more optional/required/etc colors when signing, required fields
will be highlighted as orange when form is "validating"

Highlighted internal changes:

- Utilize native PDF fields to insert text, instead of drawing text 
- Change font auto scaling to only apply to when the height overflows
AND no custom font is set

⚠️ Multiline changes:

Multi line is enabled for a field under these conditions

1. Field content exceeds field width
2. Field includes a new line
3. Field type is TEXT

## [BEFORE] Field UI Signing 


![image](https://github.com/user-attachments/assets/ea002743-faeb-477c-a239-3ed240b54f55)

## [AFTER] Field UI Signing 


![image](https://github.com/user-attachments/assets/0f8eb252-4cf3-4d96-8d4f-cd085881b78c)

## [BEFORE] Signing a checkbox


![image](https://github.com/user-attachments/assets/4567d745-e1da-4202-a758-5d3c178c930e)

![image](https://github.com/user-attachments/assets/c25068e7-fe80-40f5-b63a-e8a0d4b38b6c)

## [AFTER] Signing a checkbox


![image](https://github.com/user-attachments/assets/effa5e3d-385a-4c35-bc8a-405386dd27d6)

![image](https://github.com/user-attachments/assets/64be34a9-0b32-424d-9264-15361c03eca5)

## [BEFORE] What a 2nd recipient sees once someone else signed a
document


![image](https://github.com/user-attachments/assets/21c21ae2-fc62-4ccc-880a-46aab012aa70)

## [AFTER] What a 2nd recipient sees once someone else signed a document


![image](https://github.com/user-attachments/assets/ae51677b-f1d5-4008-a7fd-756533166542)

## **[BEFORE]** Inserting fields


![image](https://github.com/user-attachments/assets/1a8bb8da-9a15-4deb-bc28-eb349414465c)

## **[AFTER]** Inserting fields


![image](https://github.com/user-attachments/assets/c52c5238-9836-45aa-b8a4-bc24a3462f40)

## Overflows, multilines and field alignments testing

Debugging borders:
- Red border = The original field placement without any modifications
- Blue border = The available space to overflow

### Single line overflows and field alignments 

This is left aligned fields, overflow will always go to the end of the
page and will not wrap


![image](https://github.com/user-attachments/assets/47003658-783e-4f9c-adbf-c4686804d98f)

This is center aligned fields, the max width is the closest edge to the
page * 2


![image](https://github.com/user-attachments/assets/05a38093-75d6-4600-bae2-21ecff63e115)

This is right aligned text, the width will extend all the way to the
left hand side of the page


![image](https://github.com/user-attachments/assets/6a9d84a8-4166-4626-9fb3-1577fac2571e)

### Multiline line overflows and field alignments 

These are text fields that can be overflowed


![image](https://github.com/user-attachments/assets/f7b5456e-2c49-48b2-8d4c-ab1dc3401644)

Another example of left aligned text overflows with more text


![image](https://github.com/user-attachments/assets/3db6b35e-4c8d-4ffe-8036-0da760d9c167)
2025-04-23 21:40:42 +10:00
Catalin Pit b94645a451 fix: optional fields being required in direct links (#1752) 2025-04-21 16:34:29 +10:00
Mythie 7e6704faae chore: update tests 2025-04-21 16:23:50 +10:00
Mythie cf17fc61bc chore: update tests 2025-04-21 16:07:19 +10:00
Mythie 6df8b3aac8 chore: update ci 2025-04-21 14:29:40 +10:00
Mythie fdb31772db chore: update tests 2025-04-21 14:13:12 +10:00
Mythie a3dfd81870 chore: update playwright config 2025-04-21 13:27:19 +10:00
Mythie 755ef697ba chore: update playwright config 2025-04-21 13:03:29 +10:00
Mythie 37cc41d713 fix: skip immediate expiration presign test 2025-04-21 12:41:38 +10:00
Mythie dd2ef3a657 v1.10.0-rc.5 2025-04-17 23:01:43 +10:00
David Nguyen 435b3ca4f8 chore: remove legacy document update route (#1751)
Remove deprecated route
2025-04-17 16:36:10 +10:00
Mythie 278cd8a9de fix: always show ip and useragent in certificate 2025-04-17 12:55:03 +10:00
Catalin Pit f1526315f5 feat: limit free teams platform plan (#1673)
This pull request removes the `id` field from
`IsDocumentPlatformOptions` in `is-document-platform.ts` and updates the
billing logic in `create-team.ts`: platform plan users create their
first team free, but pay for subsequent teams; non-platform users need
an active team subscription if billing is enabled.
2025-04-15 21:32:15 +10:00
RefRexi 353a7e8e0d fix: dynamic route for team transfer (#1730)
fix: dynamic route handling for /team/verify/transfer/:token
2025-04-15 21:30:44 +10:00
Ephraim Duncan 34b2504268 chore: husky (#1706) 2025-04-15 21:29:03 +10:00
Catalin Pit 566abda36b chore: update render build command (#1748) 2025-04-15 19:06:06 +10:00
Mythie 9121a062b3 chore: add docs for authoring 2025-04-14 11:31:54 +10:00
Lucas Smith e613e0e347 feat: support embedded authoring for creation (#1741)
Adds support for creating documents and templates
using our embed components.

Support is super primitive at the moment and is being polished.
2025-04-11 00:20:39 +10:00
Lucas Smith 95aae52fa4 chore: add translations (#1715)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2025-04-10 12:24:07 +10:00
Ephraim Duncan 5958f38719 chore: set the default value on the top (#1734) 2025-04-08 23:35:32 +10:00
Catalin Pit 419bc02171 docs: prefill fields (#1688) 2025-04-04 00:03:37 +11:00
Ephraim Duncan 5e4956f3a2 fix: zero month addition (#1733)
- Add zero month at the begining of each metric on the open page
2025-04-01 11:12:41 +00:00
Mythie da71613c9f v1.10.0-rc.4 2025-03-31 20:02:22 +11:00
Lucas Smith 4d6efe091e fix: pass document meta to readonly field component (#1737)
## Description

Previously we weren't passing the DocumentMeta to our readonly field
component which is used for displaying completed fields by other
recipients.

Due to this dates that were not using the default format were displaying
as invalid date adding confusion to the signing process.

## Related Issue

Reported via support email.

## Changes Made

- Pass the document meta to the readonly field component.
- Support showing completed fields within the embedding UI.

## Testing Performed

- Manual testing
2025-03-31 17:14:56 +11:00
David Nguyen 7e6ac4db40 fix: direct template redirects (#1727) 2025-03-28 14:45:54 +11:00
Mythie a87af910c7 v1.10.0-rc.2 2025-03-28 01:50:59 +11:00
Mythie e37b005d7f chore: update dockerfile 2025-03-28 01:28:49 +11:00
Mythie 73f8518b47 chore: update tests 2025-03-28 01:21:48 +11:00
Mythie ac3deb113e chore: update ci 2025-03-27 22:49:59 +11:00
Catalin Pit c82388c40a fix: remove console.log embed document completed (#1723) 2025-03-25 16:36:52 +02:00
David Nguyen 31be548939 fix: duplicate webhook calls on document complete (#1721)
Fix webhooks being sent twice due to duplicate frontend calls

Updated the assistant confirmation dialog so the next signer is always
visible (if dictate is enabled). Because if the form is invalid (due to
no name) there is no visual queue that the form is invalid (since it's
hidden)

## Notes

Didn't bother to remove the weird assistants form since it currently
works for now


![image](https://github.com/user-attachments/assets/47910fec-e05e-486a-a61d-16078d948893)

## Tests

- Currently running locally
- Tested webhooks via network tab and via webhook.site
2025-03-25 21:59:13 +11:00
David Nguyen 063fd32f18 feat: add signature configurations (#1710)
Add ability to enable or disable allowed signature types:
- Drawn
- Typed
- Uploaded

**Tabbed style signature dialog**

![image](https://github.com/user-attachments/assets/a816fab6-b071-42a5-bb5c-6d4a2572431e)

**Document settings**

![image](https://github.com/user-attachments/assets/f0c1bff1-6be1-4c87-b384-1666fa25d7a6)

**Team preferences**

![image](https://github.com/user-attachments/assets/8767b05e-1463-4087-8672-f3f43d8b0f2c)

- Add multiselect to select allowed signatures in document and templates
settings tab
- Add multiselect to select allowed signatures in teams preferences
- Removed "Enable typed signatures" from document/template edit page
- Refactored signature pad to use tabs instead of an all in one
signature pad

Added E2E tests to check settings are applied correctly for documents
and templates
2025-03-24 17:13:11 +11:00
Mythie 231f51bd1f v1.10.0-rc.1 2025-03-22 17:34:33 +11:00
Mythie a8de8368a2 fix: hide powered by on certificate for platform documents 2025-03-22 12:04:08 +11:00
Mythie 7dd331addf fix: allow blank rejection reasons 2025-03-22 12:01:18 +11:00
Mythie c6743a7cec v1.10.0-rc.0 2025-03-22 03:23:23 +11:00
Mythie efbc097191 fix: unblock last signer when using dictation 2025-03-22 02:34:12 +11:00
Lucas Smith f1525991dc feat: dictate next signer (#1719)
Adds next recipient dictation functionality to document signing flow,
allowing assistants and signers to update the next recipient's
information during the signing process.

## Related Issue

N/A

## Changes Made

- Added form handling for next recipient dictation in signing dialogs
- Implemented UI for updating next recipient information
- Added e2e tests covering dictation scenarios:
  - Regular signing with dictation enabled
  - Assistant role with dictation
  - Parallel signing flow
  - Disabled dictation state

## Testing Performed

- Added comprehensive e2e tests covering:
  - Sequential signing with dictation
  - Assistant role dictation
  - Parallel signing without dictation
  - Form validation and state management
- Tested on Chrome and Firefox
- Verified recipient state updates in database
2025-03-21 13:27:04 +11:00
Mythie fb173e4d0e chore: update docker build scripts 2025-03-20 10:52:33 +11:00
Catalin Pit d422ffa873 chore: add terms and privacy policy link (#1707) 2025-03-19 19:29:09 +11:00
Catalin Pit 55b7697316 chore: update d script in package.json (#1703)
Add the `translate:compile` command to the `d` script.
2025-03-14 16:15:57 +11:00
Catalin Pit 67bbb6c6f4 fix: autosigning fields with direct links (#1696)
## Description

The changes in `sign-direct-template.tsx` automatically fill in field
values for text, number, and dropdown fields when default values are
present or if the fields are read-only. In `checkbox-field.tsx`, the
changes fix the checkbox signing by checking if the validation is met
and improving how it saves or removes checkbox choices.

## Testing Performed

I tested the code locally with a variety of documents/fields.

## Checklist

<!--- Please check the boxes that apply to this pull request. -->
<!--- You can add or remove items as needed. -->

- [x] I have tested these changes locally and they work as expected.
- [ ] I have added/updated tests that prove the effectiveness of these
changes.
- [ ] I have updated the documentation to reflect these changes, if
applicable.
- [x] I have followed the project's coding style guidelines.
- [ ] I have addressed the code review feedback from the previous
submission, if applicable.
2025-03-13 11:18:01 +02:00
Lucas Smith 63a4bab0fe feat: better document rejection (#1702)
Improves the existing document rejection process by actually marking a
document as completed cancelling further actions.

## Related Issue

N/A

## Changes Made

- Added a new rejection status for documents
- Updated a million areas that check for document completion
- Updated email sending, so rejection is confirmed for the rejecting
recipient while other recipients are notified that the document is now
cancelled.

## Testing Performed

- Ran the testing suite to ensure there are no regressions.
- Performed manual testing of current core flows.
2025-03-13 15:08:57 +11:00
Jenil Savani 9f17c1e48e fix: adjust desktop nav search button width and spacing (#1699) 2025-03-13 10:52:01 +11:00
Catalin Pit 91ae818213 fix: missing prefillfields property from the api v2 documentation (#1700) 2025-03-12 22:54:58 +11:00
David Nguyen a0ace803cf fix: admin signing page crash 2025-03-12 16:53:09 +11:00
Ephraim Duncan b3db3be8e9 fix: signing field disabled when pointer is out of canvas (#1652) 2025-03-12 16:44:21 +11:00
Jenil Savani 44cdbeecb4 fix: improve layout and truncate document information in logs page (#1656) 2025-03-12 16:31:03 +11:00
Tom 7214965c0c chore: update French translations (#1679) 2025-03-12 16:22:06 +11:00
Ephraim Duncan 8d6bf91d12 fix: persist theme cookie for a much longer time (#1693) 2025-03-12 16:09:37 +11:00
Ephraim Duncan fec078081b fix: correct signer deletion (#1596) 2025-03-12 16:05:45 +11:00
David Nguyen c646afcd97 fix: tests 2025-03-09 15:10:19 +11:00
Catalin Pit 63d990ce8d fix: optional fields in embeds (#1691) 2025-03-09 14:41:17 +11:00
eddielu aa7d6b28a4 docs: Update documentation to match reality. colorPrimary, colorBackground,… (#1666)
Update documentation to match reality. colorPrimary, colorBackground,
and borderRadius do not exist according to the schema:
https://github.com/documenso/embeds/blob/280251cfddb56a6aeeb0d767098915ae7058c376/packages/react/src/css-vars.ts
2025-03-09 14:38:51 +11:00
David Nguyen b990532633 fix: remove refresh on focus 2025-03-08 15:30:13 +11:00
Catalin Pit 65be37514f fix: prefill fields (#1689)
Users can now selectively choose which properties to pre-fill for each
field - from just a label to all available properties.
2025-03-07 09:09:15 +11:00
Catalin Pit 0df29fce36 fix: invalid request body (#1686)
Fix the invalid request body so the webhooks work again.
2025-03-06 19:47:24 +11:00
Ephraim Duncan ba5b7ce480 feat: hide signature ui when theres no signature field (#1676) 2025-03-06 19:47:02 +11:00
Catalin Pit 422770a8c7 feat: allow fields prefill when generating a document from a template (#1615)
This change allows API users to pre-fill fields with values by
passing the data in the request body. Example body for V2 API endpoint
`/api/v2-beta/template/use`:

```json
{
    "templateId": 1,
    "recipients": [
        {
            "id": 1,
            "email": "signer1@mail.com",
            "name": "Signer 1"
        },
        {
            "id": 2,
            "email": "signer2@mail.com",
            "name": "Signer 2"
        }
    ],
    "prefillValues": [
        {
            "id": 14,
            "fieldMeta": {
                "type": "text",
                "label": "my label",
                "placeholder": "text placeholder test",
                "text": "auto-sign value",
                "characterLimit": 25,
                "textAlign": "right",
                "fontSize": 94,
                "required": true
            }
        },
        {
            "id": 15,
            "fieldMeta": {
                "type": "radio",
                "label": "radio label",
                "placeholder": "new radio placeholder",
                "required": false,
                "readOnly": true,
                "values": [
                    {
                        "id": 2,
                        "checked": true,
                        "value": "radio val 1"
                    },
                    {
                        "id": 3,
                        "checked": false,
                        "value": "radio val 2"
                    }
                ]
            }
        },
        {
            "id": 16,
            "fieldMeta": {
                "type": "dropdown",
                "label": "dropdown label",
                "placeholder": "DD placeholder",
                "required": false,
                "readOnly": false,
                "values": [
                    {
                        "value": "option 1"
                    },
                    {
                        "value": "option 2"
                    },
                    {
                        "value": "option 3"
                    }
                ],
                "defaultValue": "option 2"
            }
        }
    ],
    "distributeDocument": false,
    "customDocumentDataId": ""
}
```
2025-03-06 19:45:33 +11:00
David Nguyen 083a706373 fix: duplex and 2fa refresh 2025-03-04 11:41:38 +11:00
David Nguyen db326cb4a9 fix: posthog reverse proxy 2025-03-04 10:48:19 +11:00
David Nguyen d664f571d6 fix: posthog reverse proxy 2025-03-04 10:46:59 +11:00
David Nguyen 7c38970ee8 fix: update error logging 2025-03-04 01:41:39 +11:00
David Nguyen e08d62c844 fix: remove invalid prisma zod schemas 2025-03-04 01:20:13 +11:00
David Nguyen 25bb6ffe77 fix: imports 2025-03-03 14:49:28 +11:00
Catalin Pit e79d762710 chore: add label for checkbox and radio fields (#1607) 2025-03-03 13:46:29 +11:00
Mythie d970976299 fix: remove auto-expand in embeddding 2025-02-28 14:46:15 +11:00
David Nguyen 3dce814ab2 fix: stripe price fetch (#1677)
Currently Stripe prices search is omitting a price for an unknown
reason.

Changed our fetch logic to use `list` instead of `search` allows us to
work around the issue.

It's unknown on the performance impact of using `list` vs `search`
2025-02-28 14:44:06 +11:00
David Nguyen ad520bb032 fix: remove oauth from embeds 2025-02-27 14:08:59 +11:00
David Nguyen 596d30e2e5 fix: remove lazy pdf loader 2025-02-26 21:48:06 +11:00
David Nguyen 6474b4a524 fix: add preferred team middleware 2025-02-26 19:42:42 +11:00
David Nguyen 5b4db51051 fix: react-pdf canvas build 2025-02-26 18:39:21 +11:00
Mythie cf58c80e31 fix: handle empty field meta for checkboxes 2025-02-26 15:30:51 +11:00
Catalin Pit 11dbb8873e docs: add the v2 api staging base url (#1671) 2025-02-26 15:30:32 +11:00
Mythie bc7907271b fix: unbreak build for docker 2025-02-25 21:46:51 +11:00
David Nguyen 9b376d34cf feat: add stripe dev cli 2025-02-25 21:22:28 +11:00
Mythie deea99d865 feat: search by externalId 2025-02-25 20:07:47 +11:00
Mythie 3328074f51 fix: early adopters can use platform features 2025-02-25 20:07:40 +11:00
David Nguyen 5e69665e00 fix: rr7 github build 2025-02-25 16:52:10 +11:00
David Nguyen c1c7cfaf8b chore: cleanup 2025-02-25 16:37:36 +11:00
David Nguyen 7e8955b89c fix: add posthog error monitor 2025-02-25 15:14:45 +11:00
David Nguyen cedd5e87b1 chore: update API documentation 2025-02-25 02:36:08 +11:00
David Nguyen 5255e8671f chore: refactor pdf worker loader 2025-02-24 21:47:06 +11:00
David Nguyen d4c1bad407 fix: add default oauth user url 2025-02-23 18:49:22 +11:00
David Nguyen 01dccb7916 chore: flattern routes 2025-02-21 15:53:23 +11:00
Lucas Smith 483d7caef7 feat: allow document rejection in embeds (#1662) 2025-02-21 01:27:03 +11:00
David Nguyen 139bc265c7 fix: migrate billing to RR7 2025-02-21 01:16:23 +11:00
David Nguyen 991ce5ff46 fix: update teams API tokens logic 2025-02-21 00:34:50 +11:00
David Nguyen 7728c8641c fix: share opengraph 2025-02-20 15:38:06 +11:00
David Nguyen 50a41d0799 fix: pdf viewer and embeds 2025-02-20 15:06:36 +11:00
David Nguyen 250381fec8 fix: billing 2025-02-20 12:17:55 +11:00
David Nguyen d2f3d24542 chore: update docs 2025-02-19 22:36:17 +11:00
David Nguyen ec07092bf6 fix: session refresh 2025-02-19 22:29:30 +11:00
David Nguyen 63e2ef0abf fix: static caching 2025-02-19 21:35:35 +11:00
David Nguyen 90ce52164c chore: add password tests 2025-02-19 18:41:53 +11:00
David Nguyen ac30654913 fix: add auth session lifetime 2025-02-19 18:04:36 +11:00
David Nguyen 24f3ecd94f fix: remove marketing url 2025-02-19 16:45:54 +11:00
David Nguyen a319ea0f5e fix: add public profiles tests 2025-02-19 16:07:04 +11:00
David Nguyen 5ce2bae39d fix: resolve internal pdf translations 2025-02-19 14:43:35 +11:00
David Nguyen 5d86e84217 fix: prepare auth migration (#1648)
Add schema session migration in preparation for auth migration.
2025-02-18 15:19:42 +11:00
David Nguyen 79e26a9a46 fix: remove session migration 2025-02-18 15:19:39 +11:00
David Nguyen dd602a7e1c fix: themes 2025-02-18 15:17:13 +11:00
Ephraim Duncan fb16214dc5 chore: add asssitant role to the docs (#1638) 2025-02-17 23:28:00 +11:00
David Nguyen 5fc724b247 fix: rework sessions 2025-02-17 22:46:36 +11:00
David Nguyen 1ed1cb0773 chore: refactor sessions 2025-02-16 00:44:01 +11:00
David Nguyen 8d5fafec27 feat: add static cache 2025-02-15 00:08:36 +11:00
David Nguyen 0f6f236e0c fix: firefox fouc 2025-02-14 23:02:45 +11:00
David Nguyen e518985833 fix: migrate 2fa to custom auth 2025-02-14 22:00:55 +11:00
David Nguyen 595e901bc2 fix: make auth migration more flexible 2025-02-14 19:22:11 +11:00
David Nguyen df8ea09021 fix: add oidc env variables 2025-02-14 18:11:54 +11:00
David Nguyen 180656978b feat: add themes 2025-02-14 17:50:23 +11:00
David Nguyen 28f5177064 fix: dialogs with search params 2025-02-14 16:14:02 +11:00
David Nguyen 31de86e425 feat: add oidc 2025-02-14 16:01:16 +11:00
Mythie 113ab293bb chore: make all the docker stuff work 2025-02-14 14:53:01 +11:00
David Nguyen 1c4878e526 fix: documentation build 2025-02-13 21:21:51 +11:00
David Nguyen 92db4d68db fix: cleanup env variables 2025-02-13 20:56:44 +11:00
David Nguyen 7379391f92 fix: migrate translations 2025-02-13 20:24:27 +11:00
David Nguyen ebc2b00067 fix: add sign up hook 2025-02-13 20:21:23 +11:00
Ephraim Duncan 87dcdd44cd chore: update local seed data (#1622)
Add multiple example documents, pending documents, and templates for
both admin and example users
2025-02-13 19:50:05 +11:00
Catalin Pit b205f7e5f3 fix: typed signature not working (#1635)
The `typedSignatureEnabled` prop was removed from the `SignatureField`
component, which broke the typed signature meaning that nobody could
sign documents by typing their signature.
2025-02-13 19:47:38 +11:00
Mythie c5d5355cf7 fix: assistant mode breaks for number fields 2025-02-13 19:46:14 +11:00
Mythie 5fac29a07f fix: add css targets for embeds 2025-02-13 19:45:54 +11:00
Catalin Pit 1aaacab6ca fix: temp field label/text truncation (#1565)
TEMP: Fix the truncation of the field label/text.
2025-02-13 19:43:35 +11:00
Ephraim Duncan 06076c1809 fix: unable to check on the checkbox field (#1593)
This change prevents race conditions between state updates and API
operations by updating local state immediately before making async
calls.
2025-02-13 19:42:08 +11:00
Ephraim Duncan c0ae68c28b feat: assistant role (#1588)
Introduces the ability for users with the **Assistant** role to prefill
fields on behalf of other signers. Assistants can fill in various field
types such as text, checkboxes, dates, and more, streamlining the
document preparation process before it reaches the final signers.
2025-02-13 19:37:34 +11:00
Timur Ercan 3e106c1a2d chore: api v2 docs (#1620)
chore update docs for api v2 announce
2025-02-13 18:49:37 +11:00
Mythie 741639ee78 fix: improve move to team display logic 2025-02-13 18:49:03 +11:00
David Nguyen 0b3638c42c feat: add Polish and Italian (#1618) 2025-02-13 18:48:37 +11:00
Catalin Pit 0f50110853 fix: create global settings on team creation (#1601)
The global team settings weren't created when creating a new team.

## Changes Made

The global team settings are now created when a new team is created.
2025-02-13 18:47:59 +11:00
Catalin Pit b0f8c83134 chore: add cancelled webhook event (#1608) 2025-02-13 18:47:43 +11:00
Lucas Smith c9e8a32471 feat: bulk send templates via csv (#1578)
Implements a bulk send feature allowing users to upload a CSV file to
create multiple documents from a template. Includes CSV template
generation, background processing, and email notifications.
2025-02-13 18:44:29 +11:00
Lucas Smith 84b193d99c fix: tidy document invite email render logic (#1597)
Updates one of our confusing ternaries to use `ts-pattern` for rendering
the conditional blocks making it easy to follow the logic occurring.
2025-02-13 18:34:38 +11:00
Ephraim Duncan b03c5ab1a7 fix: admin leaderboard query sorting (#1548) 2025-02-13 18:32:38 +11:00
Lucas Smith 9db42accf3 feat: add text align option to fields (#1610)
Adds the ability to align text to the left, center or right for relevant
fields.

Previously text was always centered which can be less desirable.

See attached debug document which has left, center and right text
alignments set for fields.

<img width="614" alt="image"
src="https://github.com/user-attachments/assets/361a030e-813d-458b-9c7a-ff4c9fa5e33c"
/>

N/A

- Added text align option
- Update the insert in pdf method to support different alignments
- Added a debug mode to field insertion

- Ran manual tests using the debug mode
2025-02-13 18:28:33 +11:00
David Nguyen 383b5f78f0 feat: migrate nextjs to rr7 2025-02-13 14:10:38 +11:00
2911 changed files with 394176 additions and 170261 deletions
View File
View File
@@ -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,239 @@
---
date: 2026-03-26
title: Bullmq Background Jobs
---
## Context
The codebase has a well-designed background job provider abstraction (`BaseJobProvider`) with two existing implementations:
- **InngestJobProvider** — cloud/SaaS provider, externally hosted
- **LocalJobProvider** — database-backed (Postgres via Prisma), uses HTTP self-calls to dispatch
The goal is to add a third provider backed by a proper job queue library for self-hosted deployments that need more reliability than the Local provider offers.
### Current Architecture
All code lives in `packages/lib/jobs/`:
- `client/base.ts` — Abstract `BaseJobProvider` with 4 methods: `defineJob()`, `triggerJob()`, `getApiHandler()`, `startCron()`
- `client/client.ts``JobClient` facade, selects provider via `NEXT_PRIVATE_JOBS_PROVIDER` env var
- `client/inngest.ts` — Inngest implementation
- `client/local.ts` — Local/Postgres implementation
- `client/_internal/job.ts` — Core types: `JobDefinition`, `JobRunIO`, `SimpleTriggerJobOptions`
- `definitions/` — 19 job definitions (15 event-triggered, 4 cron)
The `JobRunIO` interface provided to handlers includes:
- `runTask(cacheKey, callback)` — idempotent task execution (cached via `BackgroundJobTask` table)
- `triggerJob(cacheKey, options)` — chain jobs from within handlers
- `wait(cacheKey, ms)` — delay/sleep (not implemented in Local provider)
- `logger` — structured logging
### Local Provider Limitations
The current Local provider has several issues that motivate this work:
1. `io.wait()` throws "Not implemented"
2. HTTP self-call with 150ms fire-and-forget `Promise.race` is fragile
3. No concurrency control — jobs run in the web server process
4. No real retry backoff (immediate re-dispatch)
5. No monitoring/visibility into job status
6. Jobs compete for resources with HTTP request handling
---
## Provider Evaluation
Three alternatives were evaluated against the existing provider interface and project requirements.
### BullMQ (Redis-backed) — Recommended
| Attribute | Detail |
| ------------------- | -------------------------- |
| Backend | Redis 7.x |
| npm downloads/month | ~15M |
| TypeScript | Native |
| Delayed jobs | Yes (ms precision) |
| Cron/repeatable | Yes (`upsertJobScheduler`) |
| Retries + backoff | Yes (exponential, custom) |
| Concurrency control | Yes (per-worker) |
| Rate limiting | Yes (per-queue, per-group) |
| Dashboard | Bull Board (mature) |
| New infrastructure | Yes — Redis required |
**Why BullMQ**: Most mature and widely-adopted Node.js queue. Native delayed jobs solve the `io.wait()` gap. Redis is purpose-built for queue workloads and keeps Postgres clean for application data. Bull Board gives immediate operational visibility. The provider abstraction already exists so wrapping BullMQ is straightforward.
**Trade-off**: Requires Redis, which is additional infrastructure. However, Redis is a single Docker Compose service or a free Upstash tier, and the operational benefit is significant.
### pg-boss (PostgreSQL-backed) — Strong Alternative
| Attribute | Detail |
| ------------------- | ----------------------------- |
| Backend | PostgreSQL (existing) |
| npm downloads/month | ~1.4M |
| TypeScript | Native |
| Delayed jobs | Yes (`startAfter`) |
| Cron/repeatable | Yes (`schedule()`) |
| New infrastructure | No — reuses existing Postgres |
**Why it could work**: Zero new infrastructure since the project already uses Postgres. API maps well to existing patterns.
**Why it's second choice**: Polling-based (no LISTEN/NOTIFY), adds write amplification to the primary database, smaller ecosystem, no dashboard. At scale, queue operations on the primary database become a concern.
### Graphile Worker (PostgreSQL-backed) — Less Suitable
Uses LISTEN/NOTIFY for instant pickup but has a file-based task convention and separate schema that don't mesh well with the existing Prisma-centric architecture. Would require more adapter work.
### Improving the Local Provider — Not Recommended
Fixing the Local provider's issues (wait support, replacing HTTP self-calls, adding concurrency control, backoff) essentially means rebuilding a queue library from scratch with less robustness and no community maintenance.
---
## Recommendation
**Proceed with BullMQ.** It's the most capable option, maps cleanly to the existing provider interface, and is the standard choice for production Node.js applications. Redis is lightweight infrastructure with managed options available at every cloud provider.
**If Redis is a hard blocker**, pg-boss is the clear fallback — but the plan below assumes BullMQ.
---
## Implementation Plan
### Phase 1: BullMQ Provider Core
**File: `packages/lib/jobs/client/bullmq.ts`**
Create `BullMQJobProvider extends BaseJobProvider` with singleton pattern matching the existing providers.
Key implementation details:
1. **Constructor / `getInstance()`**
- Initialize a Redis `IORedis` connection using new env var: `NEXT_PRIVATE_REDIS_URL`
- Create a single `Queue` instance for dispatching jobs, using `NEXT_PRIVATE_REDIS_PREFIX` as the BullMQ `prefix` option (defaults to `documenso` if unset). This namespaces all Redis keys so multiple environments (worktrees, branches, developers) sharing the same Redis instance don't collide.
- Create a single `Worker` instance for processing jobs (in-process, same prefix)
- Store job definitions in a `_jobDefinitions` record (same pattern as Local provider)
2. **`defineJob()`**
- Store definition in `_jobDefinitions` keyed by ID
- If the definition has a `trigger.cron`, register it via `queue.upsertJobScheduler()` with the cron expression
3. **`triggerJob(options)`**
- Find eligible definitions by `trigger.name` (same lookup as Local provider)
- For each, call `queue.add(jobDefinitionId, payload)` with appropriate options
- Support `options.id` for deduplication via BullMQ's `jobId` option
4. **`getApiHandler()`**
- Return a minimal health-check / queue-status handler. Unlike the Local provider, BullMQ workers don't need an HTTP endpoint to receive jobs — they pull from Redis directly. The API handler can return queue metrics for monitoring.
5. **`startCron()`**
- No-op — cron is handled by BullMQ's `upsertJobScheduler` registered during `defineJob()`
6. **Worker setup**
- Single worker processes all job types by dispatching to the correct handler from `_jobDefinitions`
- Configure concurrency with a default of 10 (overridable via `NEXT_PRIVATE_BULLMQ_CONCURRENCY` env var for those who need to tune it)
- Configure retry with exponential backoff: `backoff: { type: 'exponential', delay: 1000 }`
- Default 3 retries (matching current Local provider behavior)
7. **`createJobRunIO(jobId)`** — Implement `JobRunIO`:
- `runTask()`: Reuse the existing `BackgroundJobTask` Prisma table for idempotent task tracking (same pattern as Local provider)
- `triggerJob()`: Delegate to `this.triggerJob()`
- `wait()`: Throw "Not implemented" (same as Local provider). No handler uses `io.wait()` so this has zero impact
- `logger`: Same console-based logger pattern as Local provider
### Phase 2: Provider Registration
**File: `packages/lib/jobs/client/client.ts`**
Add `'bullmq'` case to the provider match:
```typescript
this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
.with('inngest', () => InngestJobProvider.getInstance())
.with('bullmq', () => BullMQJobProvider.getInstance())
.otherwise(() => LocalJobProvider.getInstance());
```
**File: `packages/tsconfig/process-env.d.ts`**
Add `'bullmq'` to the `NEXT_PRIVATE_JOBS_PROVIDER` type union and add Redis env var types:
```typescript
NEXT_PRIVATE_JOBS_PROVIDER?: 'inngest' | 'local' | 'bullmq';
NEXT_PRIVATE_REDIS_URL?: string;
NEXT_PRIVATE_REDIS_PREFIX?: string;
NEXT_PRIVATE_BULLMQ_CONCURRENCY?: string;
```
**File: `.env.example`**
Add Redis configuration examples:
```env
NEXT_PRIVATE_JOBS_PROVIDER="local" # Options: local, inngest, bullmq
NEXT_PRIVATE_REDIS_URL="redis://localhost:63790"
NEXT_PRIVATE_REDIS_PREFIX="documenso" # Namespace for Redis keys (useful when sharing a Redis instance)
```
**File: `turbo.json`**
Add `NEXT_PRIVATE_REDIS_URL`, `NEXT_PRIVATE_REDIS_PREFIX`, and `NEXT_PRIVATE_BULLMQ_CONCURRENCY` to the env vars list for cache invalidation.
### Phase 3: Infrastructure & Dependencies
**File: `packages/lib/package.json`**
Add dependencies:
- `bullmq` — the queue library
- `ioredis` — Redis client (peer dependency of BullMQ, but explicit is better)
**File: `docker-compose.yml` (or equivalent)**
Add Redis service for local development:
```yaml
redis:
image: redis:7-alpine
ports:
- '6379:6379'
```
### Phase 4: Optional Enhancements
These are not required for the initial implementation but worth considering for follow-up:
1. **Bull Board integration** — Add a `/api/jobs/dashboard` route that serves Bull Board UI for monitoring. Gate behind an admin auth check.
2. **Separate worker process** — Add an `apps/worker` entry point that runs BullMQ workers without the web server, for deployments that want to isolate job processing from request handling.
3. **Graceful shutdown** — Register `SIGTERM`/`SIGINT` handlers to call `worker.close()` and `queue.close()` for clean shutdown.
4. **BackgroundJob table integration** — Optionally continue writing to the `BackgroundJob` Prisma table for audit/history, using BullMQ events (`completed`, `failed`) to update status. This preserves the existing database-level visibility.
---
## Files to Create/Modify
| File | Action | Description |
| ------------------------------------ | ---------- | ---------------------------------------- |
| `packages/lib/jobs/client/bullmq.ts` | **Create** | BullMQ provider implementation |
| `packages/lib/jobs/client/client.ts` | Modify | Add `'bullmq'` provider case |
| `packages/tsconfig/process-env.d.ts` | Modify | Add type for `'bullmq'` + Redis env vars |
| `.env.example` | Modify | Add Redis config example |
| `turbo.json` | Modify | Add Redis env var to cache keys |
| `packages/lib/package.json` | Modify | Add `bullmq` + `ioredis` dependencies |
| `docker-compose.yml` | Modify | Add Redis service |
---
## Open Questions
1. **Should the BullMQ provider also write to the `BackgroundJob` Prisma table?** This would maintain audit history and allow existing admin tooling to query job status. Trade-off is dual-write complexity.
2. **Redis connection resilience**: Should the provider gracefully degrade if Redis is unavailable (e.g., fall back to Local provider), or fail hard? Failing hard is simpler and more predictable.
## Resolved Questions
- **`io.wait()`**: Not a concern. Only Inngest implements it (via `step.sleep`), the Local provider throws "Not implemented", and no job handler calls `io.wait()`. The BullMQ provider can throw "Not implemented" identically to the Local provider.
@@ -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,151 @@
---
date: 2026-03-04
title: Swap Subscription Between Orgs
---
## Overview
Add the ability for admins to move a subscription (and its associated Stripe customerId) from one organisation to another, when viewing a user in the admin panel. The target org must be owned by the same user and must be on the free plan (no existing active subscription).
## Context & Data Model
- `Organisation` has a 1:1 optional `Subscription` and a `customerId` (Stripe customer ID, `@unique`)
- `Organisation` has a 1:1 `OrganisationClaim` that tracks entitlements (team count, member count, feature flags)
- `Subscription` also stores a redundant `customerId` and has `organisationId` (`@unique`)
- When a subscription is removed from an org, its `OrganisationClaim` should be reset to the FREE claim
- Relationship chain: `User --owns--> Organisation --has--> Subscription + OrganisationClaim`
## Constraints
- **paid → free only**: The target org must NOT have an active subscription (status ACTIVE or PAST_DUE). It must be on the free plan.
- **same owner**: Both source and target orgs must be owned by the same user (the user being viewed).
- The `customerId` must move with the subscription to the target org (cleared from source, set on target).
- The Stripe subscription object itself is NOT modified — only the DB-level mapping changes. The Stripe customer stays the same; we just reassociate it to a different org.
## Implementation Plan
### 1. Backend: TRPC Admin Route
**Files to create:**
- `packages/trpc/server/admin-router/swap-organisation-subscription.types.ts`
- `packages/trpc/server/admin-router/swap-organisation-subscription.ts`
**Request schema (`ZSwapOrganisationSubscriptionRequestSchema`):**
```ts
z.object({
sourceOrganisationId: z.string(),
targetOrganisationId: z.string(),
});
```
**Response schema:** `z.void()`
**Route logic (in a single `prisma.$transaction`):**
1. Fetch source org with `subscription` + `organisationClaim`
2. Fetch target org with `subscription` + `organisationClaim`
3. Validate:
- Source org has an active subscription (status `ACTIVE` or `PAST_DUE`)
- Target org does NOT have an active subscription (no subscription record, or status `INACTIVE`)
- Both orgs have the same `ownerUserId`
4. In a transaction:
a. Clear `customerId` on source org (set to `null`)
b. Set `customerId` on target org to the source's `customerId`
c. Move the `Subscription` record: update `organisationId` to target org ID
d. Copy the source org's `OrganisationClaim` entitlements to the target org's `OrganisationClaim` (`originalSubscriptionClaimId`, `teamCount`, `memberCount`, `envelopeItemCount`, `flags`)
e. Reset the source org's `OrganisationClaim` to the FREE claim (using `createOrganisationClaimUpsertData(internalClaims[INTERNAL_CLAIM_ID.FREE])` pattern from `on-subscription-deleted.ts`)
**Note on ordering:** Because `Organisation.customerId` is `@unique`, we must clear the source first, then set the target — or do both in a transaction that handles the constraint. Prisma transactions handle this correctly as they apply all writes atomically.
**Register the route:**
- Import in `packages/trpc/server/admin-router/router.ts`
- Add under `organisation` as `swapSubscription`
- Call path: `trpc.admin.organisation.swapSubscription`
### 2. Frontend: Dialog Component
**File to create:**
- `apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx`
**Props:**
```ts
type AdminSwapSubscriptionDialogProps = {
trigger?: React.ReactNode;
sourceOrganisationId: string;
sourceOrganisationName: string;
userId: number;
} & Omit<DialogPrimitive.DialogProps, 'children'>;
```
**Dialog behavior:**
1. Opens when the trigger is clicked (from the organisations table actions dropdown)
2. Fetches the user's owned orgs via `trpc.admin.organisation.find.useQuery({ ownerUserId: userId })`
3. Filters to only show orgs that are on the free plan (no active subscription) and excludes the source org
4. Displays a select dropdown to pick the target org
5. Shows a warning alert: "This will move the subscription from {source} to {target}. The source organisation will be reset to the free plan."
6. On submit, calls `trpc.admin.organisation.swapSubscription.useMutation()`
7. On success, shows a toast, invalidates relevant queries, and closes the dialog
**UI layout (following existing dialog patterns like `admin-organisation-create-dialog.tsx`):**
- `DialogHeader` with title "Move Subscription" and description
- A select dropdown listing eligible target orgs (name + url)
- An `Alert` explaining what will happen
- `DialogFooter` with Cancel + "Move Subscription" buttons (submit button uses `loading` prop)
### 3. Frontend: Wire into the Organisations Table
**File to modify:**
- `apps/remix/app/components/tables/admin-organisations-table.tsx`
**Changes:**
- Import the `AdminSwapSubscriptionDialog`
- Add a new prop `ownerUserId?: number` to `AdminOrganisationsTableOptions` (needed so the dialog can query other owned orgs)
- Add a new dropdown menu item in the actions column: "Move Subscription" with `ArrowRightLeftIcon` from lucide
- Only render this item when the org row has an active subscription (`subscription?.status === 'ACTIVE' || subscription?.status === 'PAST_DUE'`)
- The menu item renders inside `AdminSwapSubscriptionDialog` with `trigger` prop as the menu item
### 4. Frontend: Pass userId from User Detail Page
**File to modify:**
- `apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx`
**Changes:**
- Pass `ownerUserId={user.id}` to `<AdminOrganisationsTable>` so it can forward this to the swap dialog
## File Change Summary
| File | Action | Description |
| --------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------- |
| `packages/trpc/server/admin-router/swap-organisation-subscription.types.ts` | **Create** | Request/response Zod schemas + TS types |
| `packages/trpc/server/admin-router/swap-organisation-subscription.ts` | **Create** | Admin mutation with prisma transaction |
| `packages/trpc/server/admin-router/router.ts` | **Modify** | Register route at `organisation.swapSubscription` |
| `apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx` | **Create** | Dialog for selecting target org |
| `apps/remix/app/components/tables/admin-organisations-table.tsx` | **Modify** | Add "Move Subscription" action + accept `ownerUserId` prop |
| `apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx` | **Modify** | Pass `ownerUserId={user.id}` to table |
## Edge Cases & Considerations
1. **Stripe customer stays the same**: The Stripe subscription is tied to a Stripe customer. We move the `customerId` to the target org, so webhook lookups (`findFirst where customerId`) will correctly resolve to the target org going forward.
2. **`@unique` constraint on `Organisation.customerId`**: Must clear source before setting target within the transaction. Prisma interactive transactions handle this correctly.
3. **`@unique` constraint on `Subscription.organisationId`**: Since the target org should not have a subscription record, updating the existing subscription's `organisationId` to the target should work. If the target has an INACTIVE subscription record, we need to delete it first.
4. **Target org has INACTIVE subscription**: The target org might have a stale INACTIVE subscription from a previous cancellation. In this case, delete the target's old subscription record before moving the source's subscription over.
5. **Seat-based plans**: If the subscription is seat-based, the Stripe quantity may not match the target org's member count. Consider calling `syncMemberCountWithStripeSeatPlan` after the swap as a post-transaction step.
6. **OrganisationClaim transfer**: Copy `originalSubscriptionClaimId`, `teamCount`, `memberCount`, `envelopeItemCount`, and `flags` from source claim to target claim. Reset source claim to FREE.
7. **No Stripe API calls needed**: This is purely a DB-level reassociation. The Stripe subscription, customer, and payment method all remain unchanged.
@@ -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,94 @@
---
date: 2026-04-22
title: Partial Signed Pdf Download
---
## Summary
Let team members fetch a PDF with all currently-inserted fields burned in while the envelope is still in `PENDING` status. Today the only available bytes for a pending envelope are the original (no fields) - the sealed PDF only materialises after the last recipient signs and the `seal-document` job runs.
Exposed in two places:
- v2 API: `GET /api/v2/envelope/item/{envelopeItemId}/download?version=pending` (API-token auth)
- UI: a `Partial` button in the existing `EnvelopeDownloadDialog`, alongside `Original`. Replaces the `Signed` slot when the envelope is `PENDING`. Backed by the existing session-authed file route `GET /api/files/envelope/{envelopeId}/envelopeItem/{id}/download/pending`.
## Scope
- v2 API only (no v1).
- `internalVersion === 2` envelopes only. Legacy v1 returns 400 `ENVELOPE_LEGACY`.
- Team-side / owner only. No recipient-token download path - recipients have the in-app overlay viewer for verification, and a downloadable half-signed PDF is a leak vector for partially-executed contracts. Enforced both at the server (the recipient-token file route does not accept `pending`) and at the UI (the dialog hides the Partial button when a recipient token is set).
- No PKI signature, no certificate page, no audit log appendix - the response is explicitly not a final executed document.
- No watermark or banner text. The filename suffix (`_pending.pdf`), the `Cache-Control: no-store, private` header, and the absence of a PKI signature are sufficient to signal draft status.
## Behaviour
API response matrix (both `/api/v2/envelope/item/{id}/download?version=pending` and the UI-facing `/api/files/envelope/{envelopeId}/envelopeItem/{id}/download/pending`):
| Envelope status | Response |
|---|---|
| `PENDING` (v2) | 200, PDF with currently-inserted fields burned in |
| `PENDING` (v1) | 400 `ENVELOPE_LEGACY` |
| `DRAFT` | 400 `ENVELOPE_DRAFT` |
| `COMPLETED` | 400 `ENVELOPE_COMPLETED` |
| `REJECTED` | 400 `ENVELOPE_REJECTED` |
All v1-vs-v2 / status-mismatch errors are 4xx so callers can cleanly separate them from real server failures (5xx). Specifically v1 PENDING returns 400 not 501: 5xx is reserved for actual server problems, while "this envelope can't satisfy this request shape" is a client-addressable condition.
Filename: `{title}_pending.pdf`.
ETag is content-addressed over `sha256(envelope.status + sorted((field.id, field.customText, field.signature?.id, field.signature?.created) for inserted===true fields))`. Returns 304 on `If-None-Match` match.
No persistent caching. Generated on-demand per request when ETag misses.
Error response shape (envelope item v2 download route and the team-side file route): preserves the existing `{ error: <message> }` field for backwards compatibility and adds `code: <APP_ERROR_CODE>` as a new field for callers that want to branch on it. The document download route (`/document/{documentId}/download`) is untouched.
## UI
`apps/remix/app/components/dialogs/envelope-download-dialog.tsx`:
- The dialog shows `Original` plus one of:
- `Signed` when status is `COMPLETED` (existing behaviour)
- `Partial` when status is `PENDING`, there is no recipient token, and the envelope is not legacy (`!isLegacy`)
- nothing otherwise
- New optional prop `isLegacy?: boolean`. Only consulted to gate the `Partial` button, so callers whose status can never be `PENDING` (DRAFT/COMPLETED/REJECTED hardcoded, or `isComplete: true` matchers) and callers that always set a recipient token can omit it. Three call sites pass it (`isLegacy={envelope.internalVersion === 1}`): `documents-table-action-dropdown.tsx`, `envelope-editor.tsx`, `document-page-view-dropdown.tsx`. The other eight callers were left alone.
Trade-off: a future team-side dialog usage where status could be PENDING but the dev forgets `isLegacy` will silently not render the Partial button. The status gate prevents an actively broken click; missing button is discoverable in testing. Required-prop alternative was rejected because eight of eleven call sites would carry a meaningless value.
## Files
Server:
- `apps/remix/server/api/download/download.types.ts` - added `'pending'` to the `version` enum; split the validator into `param` (envelopeItemId) + `query` (version). The original wiring as a path-param validator was a pre-existing bug: requests like `?version=original` were silently returning the signed PDF since `version` actually arrives as a query string. Fixed as a side effect.
- `packages/trpc/server/envelope-router/download-envelope-item.types.ts` - mirrored the enum change in the OpenAPI schema.
- `apps/remix/server/api/download/download.ts` - the envelope item v2 route now fetches envelope recipients alongside the envelope, branches on `version` when calling the helper, and emits AppError responses as `{ error, code }` consistently across all status codes.
- `apps/remix/server/api/files/files.types.ts` - added `'pending'` to the team-side download schema only. The recipient-token download schema is untouched, so `/api/files/token/.../download/pending` is rejected by the schema validator.
- `apps/remix/server/api/files/files.ts` - the team-side download handler fetches envelope recipients and dispatches the `pending` branch through the same `handleEnvelopeItemFileRequest` helper. Wrapped in a try/catch that returns `{ error, code }` for AppErrors.
- `apps/remix/server/api/files/files.helpers.ts` - `handleEnvelopeItemFileRequest` is now a single entry point taking a discriminated-union options type. The static-file flow (`signed`/`original`) and the on-demand pending flow are private helpers in the same module.
- `packages/lib/server-only/pdf/generate-partial-signed-pdf.ts` (new) - small orchestrator that loads the original PDF, groups inserted fields by page, calls the existing `insertFieldInPDFV2` overlay helper for each page, flattens, and saves.
- `packages/lib/errors/app-error.ts` - added `ENVELOPE_DRAFT`, `ENVELOPE_COMPLETED`, `ENVELOPE_REJECTED`, `ENVELOPE_LEGACY` codes, all mapped to 400. The legacy-envelope case deliberately returns 4xx rather than 501 to keep "this resource can't satisfy this operation" distinct from real 5xx server failures in caller logs/metrics.
Client:
- `packages/lib/utils/envelope-download.ts` - `EnvelopeItemPdfUrlOptions` download variant now allows `'pending'` as a version. The recipient-token URL builder will produce a URL the server rejects, but the dialog gates on no-token at the call site.
- `packages/lib/client-only/download-pdf.ts` - `DocumentVersion` extended; filename suffix logic moved into a small switch (`_signed.pdf`, `_pending.pdf`, `.pdf`).
- `apps/remix/app/components/dialogs/envelope-download-dialog.tsx` - secondary download derivation with the new `Partial` branch, optional `isLegacy` prop.
- `apps/remix/app/components/tables/documents-table-action-dropdown.tsx`, `apps/remix/app/components/general/envelope-editor/envelope-editor.tsx`, `apps/remix/app/components/general/document/document-page-view-dropdown.tsx` - pass `isLegacy={envelope.internalVersion === 1}` (or `row.internalVersion === 1`) to the dialog.
## Verification
1. E2E (`packages/app-tests/e2e/api/v2/partial-signed-pdf-download.spec.ts`):
- Pending envelope, recipient 1 signs, API token download with `?version=pending` returns 200 + PDF; subsequent call with `If-None-Match: <etag>` returns 304; after recipient 2 completes the envelope flips to `COMPLETED` and the same call returns 400 `ENVELOPE_COMPLETED`; `?version=signed` then succeeds.
- Draft envelope returns 400 `ENVELOPE_DRAFT`.
- `internalVersion === 1` pending envelope returns 400 `ENVELOPE_LEGACY`.
2. `npx tsc --noEmit -p apps/remix/tsconfig.json` and `npm run lint`.
3. Manual: open the Documents table or envelope editor on a PENDING envelope (v2), open the download dialog, confirm `Partial` appears alongside `Original` and produces a `_pending.pdf` with current fields burned in. Same dialog on a COMPLETED envelope shows `Signed`. Same dialog on a v1 PENDING envelope shows neither (status gate would show Partial, but the `isLegacy` flag filters it out).
## Out of Scope / Follow-ups
- Recipient-token download path (API and UI) - decided against. Revisit if there is concrete demand and a story for limiting the leak vector.
- v1 API parity / v1 partial rendering - not building. Implementing partial for v1 would require porting `legacy_insertFieldInPDF` / `insertFieldInPDFV1` into a partial-only flow, which is code with no long-term home as v1 is being phased out.
- Document download route (`/document/{documentId}/download`) - untouched. Same error shape and validator wiring as before. Consider normalising to the same `{ error, code }` shape in a follow-up if any caller wants to branch on `code` from that route.
- Persistent caching layer / job-queue generation - revisit if p95 latency on large PDFs becomes an issue.
- Specific toast for `ENVELOPE_LEGACY` in the dialog - currently the catch-all "Something went wrong" handles it. Worth a polish if v1 PENDING envelopes are common in your data and we see complaints. (Note: with the `isLegacy` gate at the UI, the error is unreachable from the dialog itself; the API can still surface it for direct callers.)
@@ -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.
View File
@@ -0,0 +1,113 @@
---
date: 2026-03-07
title: Search Query Optimization
---
## Problem
The `searchDocumentsWithKeyword` and `searchTemplatesWithKeyword` functions generate a single massive Prisma `findMany` with 7 OR branches. This produces a SQL query that:
- Joins `Team` twice (aliased j3 and j8) for the two team-access branches
- Embeds 4-level deep EXISTS subqueries (`TeamGroup -> OrganisationGroup -> OrganisationGroupMember -> OrganisationMember`) for each team branch
- Uses `ILIKE` across multiple columns with no way for Postgres to use indexes effectively across the OR
- Includes `recipients: true` on the result even though only a small subset of fields are needed
- Fetches all matching rows then filters visibility **in application code**
With 1,000 documents seeded under `medium-account@documenso.com`, this query is noticeably slow.
---
## Option A: Pre-resolve team IDs, keep Prisma
**Change:** Before the envelope query, resolve the user's accessible team IDs in a single query:
```ts
const teamIds = await prisma.teamGroup
.findMany({
where: {
organisationGroup: {
organisationGroupMembers: {
some: { organisationMember: { userId } },
},
},
},
select: { teamId: true },
})
.then((rows) => [...new Set(rows.map((r) => r.teamId))]);
```
Then replace `team: buildTeamWhereQuery(...)` with `teamId: { in: teamIds }` in the envelope query.
**Benefits:**
- Eliminates the duplicated 4-level deep join chain from the envelope query
- The team ID resolution is a simple indexed lookup (runs once, not twice)
- Minimal code change -- still Prisma, same structure
- Can also pre-resolve team roles to move visibility filtering into the WHERE clause
**Drawbacks:**
- Still a single large OR query with ILIKE branches
- Prisma still generates suboptimal SQL for the remaining OR conditions
---
## Option B: Kysely rewrite with pre-resolved teams
**Change:** Rewrite using Kysely (already set up in codebase as `kyselyPrisma.$kysely`). Follow the pattern in `find-envelopes.ts` -- use Kysely for filtering/ID fetching, then Prisma for hydration.
Structure as a UNION of targeted queries instead of a single OR:
```
Query 1: owned docs matching title/externalId (simple indexed lookup)
Query 2: docs where user is recipient matching title (EXISTS on Recipient)
Query 3: team docs matching title/externalId (using pre-resolved teamIds)
UNION ALL -> deduplicate -> ORDER BY createdAt DESC -> LIMIT 20
```
Then hydrate the 20 IDs with Prisma for the include data.
**Benefits:**
- Each sub-query is simple and independently optimizable by Postgres
- UNION eliminates the massive OR which forces bad query plans
- Kysely gives control over exact SQL structure
- Only hydrate the final 20 results (not all matches)
- Follows existing `find-envelopes.ts` pattern -- not a new paradigm
**Drawbacks:**
- More code than Option A
- Two query layers (Kysely for IDs, Prisma for hydration)
---
## Option C: Hybrid -- pre-resolve teams + simplify Prisma OR
**Change:** Pre-resolve team IDs (like Option A), but also restructure the Prisma query to reduce OR branches:
- Merge "owned + title" and "owned + externalId" and "owned + recipient email" into a single owned-docs branch with nested OR
- Merge "team + title" and "team + externalId" into a single team-docs branch
- Keep "recipient inbox" branches separate
This reduces from 7 OR branches to ~3-4, with simpler conditions in each.
**Benefits:**
- Simpler than Kysely rewrite
- Fewer OR branches = better query plan
- Pre-resolved team IDs eliminate the deep joins
- Still pure Prisma
**Drawbacks:**
- Postgres still has to handle OR across different access patterns in one query
- Less control over SQL than Kysely
---
## Recommendation
**Option B (Kysely)** is the strongest choice. The codebase already uses this exact pattern for `find-envelopes.ts` which solves the same class of problem. The UNION approach gives Postgres the best chance at using indexes per sub-query. Pre-resolving team IDs is a prerequisite for all options and is trivially cheap.
The template search query has the same structure and should get the same treatment.
+207
View File
@@ -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
+69
View File
@@ -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"
+62
View File
@@ -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"
@@ -0,0 +1,337 @@
---
name: create-documentation
description: Generate markdown documentation for a module or feature
---
You are creating proper markdown documentation for a feature or guide in the Documenso documentation site.
**Read [WRITING_STYLE.md](../../../WRITING_STYLE.md) first** for tone, formatting conventions, and anti-patterns to avoid.
## Your Task
1. **Identify the scope** - Based on the conversation context, determine what feature or topic needs documentation. Ask the user if unclear.
2. **Identify the audience** - Is this for Users, Developers, or Self-Hosters?
3. **Read the source code** - Understand the feature, API, or configuration being documented.
4. **Read existing docs** - Check `apps/docs/content/docs/` for documentation to update.
5. **Write comprehensive documentation** - Create or update MDX docs following the patterns below.
6. **Update navigation** - Add to the relevant `meta.json` if creating a new page.
## Documentation Framework
This project uses [Fumadocs](https://fumadocs.dev). All documentation lives in `apps/docs/content/docs/` as MDX files. The docs app is a Next.js app at `apps/docs/`.
## Documentation Structure
```
apps/docs/content/docs/
├── index.mdx # Landing page with audience navigation
├── meta.json # Root navigation: guides + resources
├── users/ # Application usage guides
│ ├── meta.json # { "root": true, "pages": [...] }
│ ├── getting-started/ # Account creation, first document
│ ├── documents/ # Upload, recipients, fields, send
│ │ └── advanced/ # AI detection, visibility, placeholders
│ ├── templates/ # Create and use templates
│ ├── organisations/ # Overview, members, groups, SSO, billing
│ │ ├── single-sign-on/
│ │ └── preferences/
│ └── settings/ # Profile, security, API tokens
├── developers/ # API and integration docs
│ ├── meta.json # { "root": true, "pages": [...] }
│ ├── getting-started/ # Authentication, first API call
│ ├── api/ # Documents, recipients, fields, templates, teams
│ ├── webhooks/ # Setup, events, verification
│ ├── embedding/ # Authoring, direct links, CSS vars, SDKs
│ │ └── sdks/ # React, Vue, Svelte, Solid, Preact, Angular
│ ├── examples/ # Common workflows
│ ├── local-development/ # Quickstart, manual, translations
│ └── contributing/ # Contributing translations
├── self-hosting/ # Self-hosting documentation
│ ├── meta.json # { "root": true, "pages": [...] }
│ ├── getting-started/ # Quick start, requirements, tips
│ ├── deployment/ # Docker, docker-compose, Kubernetes, Railway
│ ├── configuration/ # Environment, database, email, storage
│ │ ├── signing-certificate/ # Local, Google Cloud HSM, timestamp
│ │ └── advanced/ # OAuth providers, AI features
│ └── maintenance/ # Upgrades, backups, troubleshooting
├── concepts/ # Shared across audiences
│ └── ... # Document lifecycle, field types, signing
├── compliance/ # eSign, GDPR, standards, certifications
└── policies/ # Terms, privacy, security, licenses
```
### Where to Put Documentation
| Type | Location | When to use |
| ------------------- | ------------------------------------------------ | -------------------------------------------------- |
| **User Guide** | `apps/docs/content/docs/users/<section>/` | UI workflows for using the Documenso web app |
| **Developer Guide** | `apps/docs/content/docs/developers/<section>/` | API reference, SDK guides, webhooks, embedding |
| **Self-Hosting** | `apps/docs/content/docs/self-hosting/<section>/` | Deployment, configuration, environment variables |
| **Concept** | `apps/docs/content/docs/concepts/` | Cross-audience concepts (document lifecycle, etc.) |
| **Compliance** | `apps/docs/content/docs/compliance/` | Legal and regulatory documentation |
### Navigation (meta.json)
Each directory has a `meta.json` controlling navigation order:
```json
{
"title": "Section Title",
"pages": ["getting-started", "documents", "templates"]
}
```
Top-level audience sections use `"root": true`:
```json
{
"title": "Users",
"description": "Send and sign documents",
"root": true,
"pages": ["getting-started", "documents", "templates", "organisations", "settings"]
}
```
Root `meta.json` uses `---Label---` for section dividers:
```json
{
"title": "Documentation",
"pages": [
"---Guides---",
"users",
"developers",
"self-hosting",
"---Resources---",
"concepts",
"compliance",
"policies"
]
}
```
## MDX File Format
### Frontmatter
Every page needs frontmatter:
```yaml
---
title: Upload Documents
description: Upload documents to Documenso to prepare them for signing. Covers supported formats, file size limits, and upload methods.
---
```
### Fumadocs Components
Import components at the top of the file (after frontmatter):
```mdx
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
;
```
Callouts (use sparingly for warnings, beta features, security):
```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>
```
Steps (for sequential UI instructions):
```mdx
{/* prettier-ignore */}
<Steps>
<Step>
### Step title
Step description.
</Step>
<Step>
### Next step
Next description.
</Step>
</Steps>
```
Tabs (for multiple approaches or platforms):
````mdx
<Tabs items={['cURL', 'JavaScript', 'Python']}>
<Tab value="cURL">```bash curl -X POST ... ```</Tab>
<Tab value="JavaScript">```typescript const response = await fetch(...) ```</Tab>
</Tabs>
````
## Page Structure by Audience
### User Documentation
```mdx
---
title: Feature Name
description: Brief description for SEO and previews.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
## Limitations
| Limitation | Value |
| ----------------- | -------- |
| Supported format | PDF only |
| Maximum file size | 50MB |
## How to Do the Thing
{/* prettier-ignore */}
<Steps>
<Step>
### Navigate to the page
Open **Settings > Feature**.
</Step>
<Step>
### Configure the setting
Fill in the required fields and click **Save**.
</Step>
</Steps>
---
## See Also
- [Related Guide](/docs/users/related)
```
### Developer Documentation
````mdx
---
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. For an always up-to-date reference, see the
[OpenAPI Reference](https://openapi.documenso.com).
</Callout>
## Overview
Brief description of the resource and what you can do with it.
## Resource Object
| Property | Type | Description |
| -------- | ------ | ----------------- |
| `id` | string | Unique identifier |
| `status` | string | Current status |
## Create a Resource
```typescript
const response = await fetch('https://app.documenso.com/api/v2/document', {
method: 'POST',
headers: {
Authorization: 'Bearer YOUR_API_TOKEN',
'Content-Type': 'application/json',
},
body: JSON.stringify({
title: 'Service Agreement',
}),
});
```
````
---
## See Also
- [Related Guide](/docs/developers/related)
````
### Self-Hosting Documentation
```mdx
---
title: Environment Variables
description: Complete reference for all environment variables used to configure Documenso.
---
## Required Variables
| Variable | Description |
| ------------------ | ------------------------------------------------ |
| `NEXTAUTH_SECRET` | Secret key for session encryption (min 32 chars) |
| `DATABASE_URL` | PostgreSQL connection URL |
---
## Optional Variables
| Variable | Default | Description |
| -------------- | ------- | ---------------------- |
| `PORT` | `3000` | Port the server runs on |
---
## See Also
- [Database Configuration](/docs/self-hosting/configuration/database)
````
## Documentation Audiences
Tailor content to the audience:
- **User docs**: Focus on UI workflows, bold UI elements (**Settings**, **Save**), use `>` for navigation paths (**Settings > Team > Members**), number sequential steps, no code required
- **Developer docs**: API/SDK examples, authentication, webhooks, code samples in TypeScript, link to OpenAPI reference
- **Self-hosting docs**: Deployment guides, environment variables, Docker/non-Docker approaches, system requirements, troubleshooting
## Guidelines
See [WRITING_STYLE.md](../../../WRITING_STYLE.md) for complete guidelines. Key points:
- **Tone**: Direct, second-person, no emojis, no excessive personality
- **Examples**: Progressive complexity, all must be valid TypeScript
- **Tables**: Use Sharp-style nested parameter tables for API docs
- **Callouts**: Use sparingly for warnings, beta features, security
- **Cross-references**: Link related docs, add "See Also" sections
- **Navigation**: Update `meta.json` when adding new pages
- **Limitations**: Explicitly list what is NOT supported
- **Images**: Use `.webp` format, store in `apps/docs/public/`
## Process
1. **Identify the audience** - Users, Developers, or Self-Hosters?
2. **Explore the code** - Read source files to understand the feature
3. **Check existing docs** - Look in `apps/docs/content/docs/` for related pages
4. **Draft the structure** - Outline sections before writing
5. **Write content** - Fill in each section following audience-specific patterns
6. **Update navigation** - Add to relevant `meta.json` if creating a new page
7. **Add cross-references** - Link from related docs, add "See Also" section
## Begin
Analyze the conversation context to determine the documentation scope, read the relevant source code, and create comprehensive MDX documentation in `apps/docs/content/docs/`.
@@ -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.
+56
View File
@@ -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.
+56
View File
@@ -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.
@@ -0,0 +1,371 @@
---
name: envelope-editor-v2-e2e
description: Writing and maintaining Playwright E2E tests for the Envelope Editor V2. Use when the user needs to create, modify, debug, or extend E2E tests in packages/app-tests/e2e/envelope-editor-v2/. Triggers include requests to "write an e2e test", "add a test for the envelope editor", "test envelope settings/recipients/fields/items/attachments", "fix a failing envelope test", or any task involving Playwright tests for the envelope editor feature.
---
# Envelope Editor V2 E2E Tests
## Overview
The Envelope Editor V2 E2E test suite lives in `packages/app-tests/e2e/envelope-editor-v2/`. Each test file covers a distinct feature area of the envelope editor and follows a strict architectural pattern that tests the **same flow** across four surfaces:
1. **Document** (`documents/<id>`) - Native document editor
2. **Template** (`templates/<id>`) - Native template editor
3. **Embedded Create** (`/embed/v2/authoring/envelope/create`) - Embedded editor creating a new envelope
4. **Embedded Edit** (`/embed/v2/authoring/envelope/edit/<id>`) - Embedded editor updating an existing envelope
## Project Structure
```
packages/app-tests/
e2e/
envelope-editor-v2/
envelope-attachments.spec.ts # Attachment CRUD
envelope-fields.spec.ts # Field placement on PDF canvas
envelope-items.spec.ts # PDF document item CRUD
envelope-recipients.spec.ts # Recipient management
envelope-settings.spec.ts # Settings dialog
fixtures/
authentication.ts # apiSignin, apiSignout
documents.ts # Document tab helpers
envelope-editor.ts # Core fixture: surface openers + locator/action helpers
generic.ts # Toast assertions, text visibility
signature.ts # Signature pad helpers
playwright.config.ts # Test configuration
```
## Core Abstraction: `TEnvelopeEditorSurface`
Every test revolves around the `TEnvelopeEditorSurface` type from `fixtures/envelope-editor.ts`. This is the central abstraction that normalizes differences between the four surfaces:
```typescript
type TEnvelopeEditorSurface = {
root: Page; // The Playwright page
isEmbedded: boolean; // true for embed surfaces
envelopeId?: string; // Set for document/template/embed-edit, undefined for embed-create
envelopeType: 'DOCUMENT' | 'TEMPLATE';
userId: number; // Seeded user ID
userEmail: string; // Seeded user email
userName: string; // Seeded user name
teamId: number; // Seeded team ID
};
```
### Surface Openers (from `fixtures/envelope-editor.ts`)
```typescript
// Native surfaces - seed user + document/template, sign in, navigate
const surface = await openDocumentEnvelopeEditor(page);
const surface = await openTemplateEnvelopeEditor(page);
// Embedded surfaces - seed user, create API token, get presign token, navigate
const surface = await openEmbeddedEnvelopeEditor(page, {
envelopeType: 'DOCUMENT' | 'TEMPLATE',
mode?: 'create' | 'edit', // default: 'create'
tokenNamePrefix?: string, // for unique API token names
externalId?: string, // optional external ID in hash
features?: EmbeddedEditorConfig, // feature flags
});
```
## Test Architecture Pattern
Every test file follows this structure, with four `test.describe` blocks grouping tests by editor surface:
### 1. Imports
```typescript
import { type Page, expect, test } from '@playwright/test';
// Prisma enums if needed for DB assertions
import { SomePrismaEnum } from '@prisma/client';
import { nanoid } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import {
type TEnvelopeEditorSurface, // Import needed helpers from the fixture
openDocumentEnvelopeEditor,
openEmbeddedEnvelopeEditor,
openTemplateEnvelopeEditor,
persistEmbeddedEnvelope, // ... other helpers
} from '../fixtures/envelope-editor';
import { expectToastTextToBeVisible } from '../fixtures/generic';
```
### 2. Type definitions and constants
```typescript
type FlowResult = {
externalId: string;
// ... other data needed for DB assertions
};
const TEST_VALUES = {
// Centralized test data constants
};
```
### 3. Local helper functions
```typescript
// Common: open settings and set external ID for DB lookup
const openSettingsDialog = async (root: Page) => {
await getEnvelopeEditorSettingsTrigger(root).click();
await expect(root.getByRole('heading', { name: 'Document Settings' })).toBeVisible();
};
const updateExternalId = async (surface: TEnvelopeEditorSurface, externalId: string) => {
await openSettingsDialog(surface.root);
await surface.root.locator('input[name="externalId"]').fill(externalId);
await surface.root.getByRole('button', { name: 'Update' }).click();
if (!surface.isEmbedded) {
await expectToastTextToBeVisible(surface.root, 'Envelope updated');
}
};
```
### 4. The flow function
A single `runXxxFlow` function that works across ALL surfaces. It handles embedded vs non-embedded differences internally:
```typescript
const runMyFeatureFlow = async (surface: TEnvelopeEditorSurface): Promise<FlowResult> => {
const externalId = `e2e-feature-${nanoid()}`;
// For embedded create, may need to add a PDF first
if (surface.isEmbedded && !surface.envelopeId) {
await addEnvelopeItemPdf(surface.root, 'embedded-feature.pdf');
}
await updateExternalId(surface, externalId);
// Handle embedded vs native differences
if (surface.isEmbedded) {
// No "Add Myself" button in embedded mode
await setRecipientEmail(surface.root, 0, 'embedded@example.com');
} else {
await clickAddMyselfButton(surface.root);
}
// ... perform feature-specific actions ...
// Navigate away and back to verify UI persistence
await clickEnvelopeEditorStep(surface.root, 'addFields');
await clickEnvelopeEditorStep(surface.root, 'upload');
// ... assert UI state after navigation ...
return { externalId /* ... */ };
};
```
### 5. Database assertion function
Uses Prisma directly to verify data was persisted correctly:
```typescript
const assertFeaturePersistedInDatabase = async ({
surface,
externalId,
// ... expected values
}: {
surface: TEnvelopeEditorSurface;
externalId: string;
// ...
}) => {
const envelope = await prisma.envelope.findFirstOrThrow({
where: {
externalId,
userId: surface.userId,
teamId: surface.teamId,
type: surface.envelopeType,
},
include: {
// Include related data as needed
documentMeta: true,
recipients: true,
fields: true,
envelopeAttachments: true,
},
orderBy: { createdAt: 'desc' },
});
// Assert expected values
expect(envelope.someField).toBe(expectedValue);
};
```
### 6. The four `test.describe` blocks
Tests are organized into four `test.describe` blocks, one per editor surface. Each describe block contains the tests relevant to that surface. This structure allows adding multiple tests per surface while keeping them grouped:
```typescript
test.describe('document editor', () => {
test('description of what is tested', async ({ page }) => {
const surface = await openDocumentEnvelopeEditor(page);
const result = await runMyFeatureFlow(surface);
await assertFeaturePersistedInDatabase({
surface,
...result,
});
});
// Additional document-editor-specific tests here...
});
test.describe('template editor', () => {
test('description of what is tested', async ({ page }) => {
const surface = await openTemplateEnvelopeEditor(page);
const result = await runMyFeatureFlow(surface);
await assertFeaturePersistedInDatabase({
surface,
...result,
});
});
// Additional template-editor-specific tests here...
});
test.describe('embedded create', () => {
test('description of what is tested', async ({ page }) => {
const surface = await openEmbeddedEnvelopeEditor(page, {
envelopeType: 'DOCUMENT',
tokenNamePrefix: 'e2e-embed-feature',
});
const result = await runMyFeatureFlow(surface);
// IMPORTANT: Must persist before DB assertions for embedded
await persistEmbeddedEnvelope(surface);
await assertFeaturePersistedInDatabase({
surface,
...result,
});
});
// Additional embedded-create-specific tests here...
});
test.describe('embedded edit', () => {
test('description of what is tested', async ({ page }) => {
const surface = await openEmbeddedEnvelopeEditor(page, {
envelopeType: 'TEMPLATE',
mode: 'edit',
tokenNamePrefix: 'e2e-embed-feature',
});
const result = await runMyFeatureFlow(surface);
// IMPORTANT: Must persist before DB assertions for embedded
await persistEmbeddedEnvelope(surface);
await assertFeaturePersistedInDatabase({
surface,
...result,
});
});
// Additional embedded-edit-specific tests here...
});
```
When a test only applies to specific surfaces (e.g., a document-only action like "send document"), only include it in the relevant describe block(s). Not every describe block needs the same tests -- the structure groups tests by surface, not by requiring symmetry.
## Key Differences Between Surfaces
| Behavior | Document/Template | Embedded Create | Embedded Edit |
| -------------------------- | -------------------------- | ----------------------------------------- | ----------------------------------------- |
| User seeding | Seed + sign in | Seed + API token | Seed + API token + seed envelope |
| "Add Myself" button | Available | Not available | Not available |
| Toast on settings update | Yes (`'Envelope updated'`) | No | No |
| PDF already attached | Yes (1 item) | No (0 items, must upload) | Yes (1 item) |
| Delete confirmation dialog | Yes (`'Delete'` button) | No (immediate) | No (immediate) |
| DB persistence timing | Immediate (autosaved) | After `persistEmbeddedEnvelope()` | After `persistEmbeddedEnvelope()` |
| Persist button label | N/A | `'Create Document'` / `'Create Template'` | `'Update Document'` / `'Update Template'` |
## Available Fixture Helpers
### From `fixtures/envelope-editor.ts`
**Locator helpers** (return Playwright Locators):
- `getEnvelopeEditorSettingsTrigger(root)` - Settings gear button
- `getEnvelopeItemTitleInputs(root)` - Title inputs for envelope items
- `getEnvelopeItemDragHandles(root)` - Drag handles for reordering items
- `getEnvelopeItemRemoveButtons(root)` - Remove buttons for items
- `getEnvelopeItemDropzoneInput(root)` - File input for PDF upload
- `getRecipientEmailInputs(root)` - Email inputs for recipients
- `getRecipientNameInputs(root)` - Name inputs for recipients
- `getRecipientRows(root)` - Full recipient row fieldsets
- `getRecipientRemoveButtons(root)` - Remove buttons for recipients
- `getSigningOrderInputs(root)` - Signing order number inputs
**Action helpers**:
- `addEnvelopeItemPdf(root, fileName?)` - Upload a PDF to the dropzone
- `clickEnvelopeEditorStep(root, stepId)` - Navigate to a step: `'upload'`, `'addFields'`, `'preview'`
- `clickAddMyselfButton(root)` - Click "Add Myself" (native only)
- `clickAddSignerButton(root)` - Click "Add Signer"
- `setRecipientEmail(root, index, email)` - Fill recipient email
- `setRecipientName(root, index, name)` - Fill recipient name
- `setRecipientRole(root, index, roleLabel)` - Set role via combobox
- `assertRecipientRole(root, index, roleLabel)` - Assert role value
- `toggleSigningOrder(root, enabled)` - Toggle signing order switch
- `toggleAllowDictateSigners(root, enabled)` - Toggle dictate signers switch
- `setSigningOrderValue(root, index, value)` - Set signing order number
- `persistEmbeddedEnvelope(surface)` - Click Create/Update button for embedded flows
### From `fixtures/generic.ts`
- `expectTextToBeVisible(page, text)` - Assert text visible on page
- `expectTextToNotBeVisible(page, text)` - Assert text not visible
- `expectToastTextToBeVisible(page, text)` - Assert toast message visible
## External ID Pattern
Every test uses an `externalId` (e.g., `e2e-feature-${nanoid()}`) set via the settings dialog. This unique ID is then used in Prisma queries to reliably locate the envelope in the database for assertions. This is critical because multiple tests run in parallel.
## Running Tests
```bash
# Run all envelope editor tests
npm run test:dev -w @documenso/app-tests -- --grep "Envelope Editor V2"
# Run a specific test file
npm run test:dev -w @documenso/app-tests -- e2e/envelope-editor-v2/envelope-recipients.spec.ts
# Run with UI
npm run test-ui:dev -w @documenso/app-tests -- e2e/envelope-editor-v2/
# Run specific test by name
npm run test:dev -w @documenso/app-tests -- --grep "documents/<id>: add myself"
```
## Checklist When Writing a New Test
1. Create the spec file in `packages/app-tests/e2e/envelope-editor-v2/`
2. Import `TEnvelopeEditorSurface` and the three opener functions
3. Import `persistEmbeddedEnvelope` if you need DB assertions for embedded flows
4. Define a `FlowResult` type for data passed between flow and assertion
5. Define `TEST_VALUES` constants for test data
6. Write `updateExternalId` helper (or reuse the pattern)
7. Write the `runXxxFlow` function handling embedded vs native differences
8. Write the `assertXxxPersistedInDatabase` function using Prisma
9. Create four `test.describe` blocks: `'document editor'`, `'template editor'`, `'embedded create'`, `'embedded edit'`
10. Place tests inside the appropriate describe block for each surface
11. For embedded create tests, add a PDF via `addEnvelopeItemPdf` before the flow
12. For embedded tests, call `persistEmbeddedEnvelope(surface)` before DB assertions
13. Use `surface.isEmbedded` to branch on behavioral differences (toasts, "Add Myself", etc.)
## Common Pitfalls
- **Missing `persistEmbeddedEnvelope`**: Embedded flows don't autosave. You MUST call this before any DB assertions.
- **PDF required for embedded create**: Embedded create starts with 0 items. Upload a PDF before navigating to fields.
- **Toast assertions in embedded**: Don't assert toasts for settings updates in embedded mode (they don't appear).
- **Parallel test isolation**: Always use a unique `externalId` via `nanoid()` so parallel tests don't collide.
- **Navigation verification**: Navigate away from and back to the current step to verify UI state persistence (the editor may re-render).
- **Delete confirmation**: Native surfaces show a confirmation dialog for item deletion; embedded surfaces delete immediately.
+43 -16
View File
@@ -1,4 +1,7 @@
You are an expert in TypeScript, Node.js, Remix, React, Shadcn UI and Tailwind.
Code Style and Structure:
- Write concise, technical TypeScript code with accurate examples
- Use functional and declarative programming patterns; avoid classes
- Prefer iteration and modularization over code duplication
@@ -6,20 +9,25 @@ Code Style and Structure:
- Structure files: exported component, subcomponents, helpers, static content, types
Naming Conventions:
- Use lowercase with dashes for directories (e.g., components/auth-wizard)
- Favor named exports for components
TypeScript Usage:
- Use TypeScript for all code; prefer interfaces over types
- Avoid enums; use maps instead
- Use TypeScript for all code; prefer types over interfaces
- Use functional components with TypeScript interfaces
Syntax and Formatting:
- Use the "function" keyword for pure functions
- Create functions using `const fn = () => {}`
- Avoid unnecessary curly braces in conditionals; use concise syntax for simple statements
- Use declarative JSX
- Never use 'use client'
- Never use 1 line if statements
Error Handling and Validation:
- Prioritize error handling: handle errors and edge cases early
- Use early returns and guard clauses
- Implement proper error logging and user-friendly messages
@@ -28,21 +36,40 @@ Error Handling and Validation:
- Use error boundaries for unexpected errors
UI and Styling:
- Use Shadcn UI, Radix, and Tailwind Aria for components and styling
- Implement responsive design with Tailwind CSS; use a mobile-first approach
- When using Lucide icons, prefer the longhand names, for example HomeIcon instead of Home
Performance Optimization:
- Minimize 'use client', 'useEffect', and 'setState'; favor React Server Components (RSC)
- Wrap client components in Suspense with fallback
- Use dynamic loading for non-critical components
- Optimize images: use WebP format, include size data, implement lazy loading
React forms
Key Conventions:
- Use 'nuqs' for URL search parameter state management
- Optimize Web Vitals (LCP, CLS, FID)
- Limit 'use client':
- Favor server components and Next.js SSR
- Use only for Web API access in small components
- Avoid for data fetching or state management
- Use zod for form validation react-hook-form for forms
- Look at TeamCreateDialog.tsx as an example of form usage
- Use <Form> <FormItem> elements, and also wrap the contents of form in a fieldset which should have the :disabled attribute when the form is loading
Follow Next.js docs for Data Fetching, Rendering, and Routing
TRPC Specifics
- Every route should be in it's own file, example routers/teams/create-team.ts
- Every route should have a types file associated with it, example routers/teams/create-team.types.ts. These files should have the OpenAPI meta, and request/response zod schemas
- The request/response schemas should be named like Z[RouteName]RequestSchema and Z[RouteName]ResponseSchema
- Use create-team.ts and create-team.types.ts as an example when creating new routes.
- When creating the OpenAPI meta, only use GET and POST requests, do not use any other REST methods
- Deconstruct the input argument on it's one line of code.
Toast usage
- Use the t`string` macro from @lingui/react/macro to display toast messages
Remix/ReactRouter Usage
- Use (params: Route.Params) to get the params from the route
- Use (loaderData: Route.LoaderData) to get the loader data from the route
- When using loaderdata, deconstruct the data you need from the loader data inside the function body
- Do not use json() to return data, directly return the data
Translations
- Use <Trans>string</Trans> to display translations in jsx code, this should be imported from @lingui/react/macro
- Use the t`string` macro from @lingui/react/macro to display translations in typescript code
- t should be imported as const { t } = useLingui() where useLingui is imported from @lingui/react/macro
- String in constants should be using the t`string` macro
+76 -15
View File
@@ -1,5 +1,7 @@
# The license key to enable enterprise features for self hosters
NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=
# [[AUTH]]
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="secret"
# [[CRYPTO]]
@@ -14,21 +16,31 @@ 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"
# This can be used to still allow signups for OIDC connections
# when signup is disabled via `NEXT_PUBLIC_DISABLE_SIGNUP`
NEXT_PRIVATE_OIDC_ALLOW_SIGNUP=""
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"
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"
# URL used by the web app to request itself (e.g. local background jobs)
NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000"
# OPTIONAL: Comma-separated hostnames or IPs whose webhooks are allowed to resolve to private/loopback addresses. (e.g., internal.example.com,192.168.1.5).
NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS=
# [[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"
@@ -52,6 +64,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
@@ -110,17 +134,26 @@ NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY=
# OPTIONAL: Displays the maximum document upload limit to the user in MBs
NEXT_PUBLIC_DOCUMENT_SIZE_UPLOAD_LIMIT=5
# [[EE ONLY]]
# OPTIONAL: The AWS SES API KEY to verify email domains with.
NEXT_PRIVATE_SES_ACCESS_KEY_ID=
NEXT_PRIVATE_SES_SECRET_ACCESS_KEY=
NEXT_PRIVATE_SES_REGION=
# [[STRIPE]]
NEXT_PRIVATE_STRIPE_API_KEY=
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID=
NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID=
# [[BACKGROUND JOBS]]
# Available options: local (default) | inngest | bullmq
NEXT_PRIVATE_JOBS_PROVIDER="local"
NEXT_PRIVATE_TRIGGER_API_KEY=
NEXT_PRIVATE_TRIGGER_API_URL=
NEXT_PRIVATE_INNGEST_EVENT_KEY=
# OPTIONAL: Redis URL for the BullMQ jobs provider.
NEXT_PRIVATE_REDIS_URL="redis://localhost:63790"
# OPTIONAL: Key prefix for Redis to namespace queues (useful when sharing a Redis instance).
NEXT_PRIVATE_REDIS_PREFIX="documenso"
# OPTIONAL: Number of concurrent jobs to process. Defaults to 10.
# NEXT_PRIVATE_BULLMQ_CONCURRENCY=10
# [[FEATURES]]
# OPTIONAL: Leave blank to disable PostHog and feature flags.
@@ -129,16 +162,44 @@ 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: Comma-separated list of email domains allowed to sign up (e.g., example.com,acme.org).
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=
# 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=""
# [[CLOUDFLARE TURNSTILE]]
# OPTIONAL: Cloudflare Turnstile site key (public). When configured, Turnstile challenges
# will be shown on sign-up (visible) and sign-in (invisible) pages.
# See: https://developers.cloudflare.com/turnstile/
NEXT_PUBLIC_TURNSTILE_SITE_KEY=
# OPTIONAL: Cloudflare Turnstile secret key (server-side verification).
NEXT_PRIVATE_TURNSTILE_SECRET_KEY=
# [[E2E Tests]]
E2E_TEST_AUTHENTICATE_USERNAME="Test User"
E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
E2E_TEST_AUTHENTICATE_USER_PASSWORD="test_Password123"
# This is only required for the marketing site
# [[REDIS]]
NEXT_PRIVATE_REDIS_URL=
NEXT_PRIVATE_REDIS_TOKEN=
# OPTIONAL: Set to "true" to disable all rate limiting. Only use for E2E tests.
DANGEROUS_BYPASS_RATE_LIMITS=
# [[LOGGER]]
NEXT_PRIVATE_LOGGER_HONEY_BADGER_API_KEY=
# OPTIONAL: The file to save the logger output to. Will disable stdout if provided.
NEXT_PRIVATE_LOGGER_FILE_PATH=
# [[PLAIN SUPPORT]]
NEXT_PRIVATE_PLAIN_API_KEY=
-8
View File
@@ -1,8 +0,0 @@
# Config files
*.config.js
*.config.cjs
# Statically hosted javascript files
apps/*/public/*.js
apps/*/public/*.cjs
scripts/
-15
View File
@@ -1,15 +0,0 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
root: true,
extends: ['@documenso/eslint-config'],
rules: {
'@next/next/no-img-element': 'off',
'no-unreachable': 'error',
},
settings: {
next: {
rootDir: ['apps/*/'],
},
},
ignorePatterns: ['lingui.config.ts', 'packages/lib/translations/**/*.js'],
};
-5
View File
@@ -1,8 +1,3 @@
---
name: Pull Request
about: Submit changes to the project for review and inclusion
---
## Description
<!--- Describe the changes introduced by this pull request. -->
-23
View File
@@ -1,23 +0,0 @@
name: Cache production build binaries
description: 'Cache or restore if necessary'
inputs:
node_version:
required: false
default: v20.x
runs:
using: 'composite'
steps:
- name: Cache production build
uses: actions/cache@v3
id: production-build-cache
with:
path: |
${{ github.workspace }}/apps/web/.next
**/.turbo/**
**/dist/**
key: prod-build-${{ github.run_id }}-${{ hashFiles('package-lock.json') }}
restore-keys: prod-build-
- run: npm run build
shell: bash
+5 -1
View File
@@ -2,7 +2,7 @@ name: 'Setup node and cache node_modules'
inputs:
node_version:
required: false
default: v20.x
default: v22.x
runs:
using: 'composite'
@@ -12,6 +12,10 @@ runs:
with:
node-version: ${{ inputs.node_version }}
- name: Enable corepack
shell: bash
run: corepack enable npm
- name: Cache npm
uses: actions/cache@v3
with:
+4 -1
View File
@@ -1,5 +1,8 @@
'apps: web':
- apps/web/**
- apps/remix/**
'type: documentation':
- apps/docs/**
'version bump 👀':
- '**/package.json'
+2 -1
View File
@@ -26,7 +26,8 @@ jobs:
- name: Copy env
run: cp .env.example .env
- uses: ./.github/actions/cache-build
- name: Build app
run: npm run build
build_docker:
name: Build Docker Image
-29
View File
@@ -1,29 +0,0 @@
name: cleanup caches by a branch
on:
pull_request:
types:
- closed
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Cleanup
run: |
gh extension install actions/gh-actions-cache
echo "Fetching list of cache key"
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 )
## Setting this to not fail the workflow while deleting cache keys.
set +e
echo "Deleting caches..."
for cacheKey in $cacheKeysForPR
do
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
done
echo "Done"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge
+3 -2
View File
@@ -10,7 +10,7 @@ on:
jobs:
analyze:
name: Analyze
runs-on: ubuntu-22.04
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
@@ -30,7 +30,8 @@ jobs:
- uses: ./.github/actions/node-install
- uses: ./.github/actions/cache-build
- name: Build app
run: npm run build
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
+10 -3
View File
@@ -4,11 +4,16 @@ on:
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,20 +33,22 @@ jobs:
- name: Seed the database
run: npm run prisma:seed
- uses: ./.github/actions/cache-build
- name: Install playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npm run ci
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 }}
+26 -3
View File
@@ -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)
+45 -4
View File
@@ -17,23 +17,64 @@ jobs:
environment: Translations
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_PAT }}
- uses: ./.github/actions/node-install
- 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
+24
View File
@@ -50,3 +50,27 @@ yarn-error.log*
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# logs
logs.json
# claude
.claude
CLAUDE.md
# agents
.specs
# scripts
scripts/output*
scripts/bench-*
# license
.documenso-license.json
.documenso-license-backup.json
# tmp
tmp/
# opencode
.opencode/package-lock.json
-2
View File
@@ -4,9 +4,7 @@ tasks:
npm run dx:up &&
cp .env.example .env &&
set -a; source .env &&
export NEXTAUTH_URL="$(gp url 3000)" &&
export NEXT_PUBLIC_WEBAPP_URL="$(gp url 3000)" &&
export NEXT_PUBLIC_MARKETING_URL="$(gp url 3001)"
command: npm run d
ports:
-3
View File
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run commitlint -- $1
+1 -7
View File
@@ -1,15 +1,9 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
echo "Copying pdf.js"
npm run copy:pdfjs --workspace apps/**
echo "Copying .well-known/ contents"
node "$MONOREPO_ROOT/scripts/copy-wellknown.cjs"
git add "$MONOREPO_ROOT/apps/web/public/"
git add "$MONOREPO_ROOT/apps/remix/public/"
npx lint-staged
+3 -1
View File
@@ -1 +1,3 @@
auto-install-peers = true
legacy-peer-deps = true
prefer-dedupe = true
min-release-age = 7
+80
View File
@@ -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.
+112
View File
@@ -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,27 @@
---
description: Generate markdown documentation for a module or feature
argument-hint: <topic-or-feature>
---
You are generating documentation for the Documenso project.
## Your Task
Load and follow the skill at `.agents/skills/create-documentation/SKILL.md`. It contains the complete instructions for writing documentation including:
- Documentation structure and file locations
- MDX format and Fumadocs components
- Audience-specific patterns (Users, Developers, Self-Hosters)
- Navigation (`meta.json`) updates
- Writing style guidelines
## Context
The topic or feature to document is: `$ARGUMENTS`
## Begin
1. **Read the skill** at `.agents/skills/create-documentation/SKILL.md`
2. **Read WRITING_STYLE.md** for tone and formatting conventions
3. **Follow the skill instructions** to create comprehensive documentation
4. **Use TodoWrite** to track your progress throughout
@@ -0,0 +1,23 @@
---
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
Load and follow the skill at `.agents/skills/create-justification/SKILL.md`. It contains the complete instructions for creating justification files including:
- Unique three-word ID generation
- Frontmatter format with date and title
- Script usage (`scripts/create-justification.ts`)
## Context
The justification slug and optional content: `$ARGUMENTS`
## Begin
1. **Read the skill** at `.agents/skills/create-justification/SKILL.md`
2. **Create the justification file** using the slug from `$ARGUMENTS` and appropriate content documenting the reasoning or justification
+23
View File
@@ -0,0 +1,23 @@
---
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
Load and follow the skill at `.agents/skills/create-plan/SKILL.md`. It contains the complete instructions for creating plan files including:
- Unique three-word ID generation
- Frontmatter format with date and title
- Script usage (`scripts/create-plan.ts`)
## Context
The plan slug and optional content: `$ARGUMENTS`
## Begin
1. **Read the skill** at `.agents/skills/create-plan/SKILL.md`
2. **Create the plan file** using the slug from `$ARGUMENTS` and appropriate content
+23
View File
@@ -0,0 +1,23 @@
---
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
Load and follow the skill at `.agents/skills/create-scratch/SKILL.md`. It contains the complete instructions for creating scratch files including:
- Unique three-word ID generation
- Frontmatter format with date and title
- Script usage (`scripts/create-scratch.ts`)
## Context
The scratch slug and optional content: `$ARGUMENTS`
## Begin
1. **Read the skill** at `.agents/skills/create-scratch/SKILL.md`
2. **Create the scratch file** using the slug from `$ARGUMENTS` and appropriate content for notes or exploration
+100
View File
@@ -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.
+57
View File
@@ -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.
-17
View File
@@ -1,17 +0,0 @@
node_modules
.next
public
**/**/node_modules
**/**/.next
**/**/public
packages/lib/translations/**/*.js
*.lock
*.log
*.test.ts
.gitignore
.npmignore
.prettierignore
.DS_Store
.eslintignore
+17 -4
View File
@@ -1,11 +1,11 @@
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"],
"javascript.preferences.importModuleSpecifier": "non-relative",
"javascript.preferences.useAliasesForRenames": false,
"typescript.enablePromptUseWorkspaceTsdk": true,
@@ -15,7 +15,20 @@
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
},
"[html]": {
"editor.defaultFormatter": "vscode.html-language-features"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "biomejs.biome"
},
"prisma.pinToPrisma6": true,
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
+59
View File
@@ -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 Biome
- `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
+358
View File
@@ -0,0 +1,358 @@
# 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/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) |
| BullMQ | Redis-backed queue | `bullmq` |
| 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
View File
@@ -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
+50
View File
@@ -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.
+17 -27
View File
@@ -1,7 +1,3 @@
> 🚨 We are live on Product Hunt 🎉 Check out our latest launch: <a href="documen.so/sign-everywhere">The Platform Plan</a>!
<a href="https://www.producthunt.com/posts/documenso-platform-plan?embed=true&utm_source=badge-featured&utm_medium=badge&utm_souce=badge-documenso&#0045;platform&#0045;plan" target="_blank"><img src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=670576&theme=light" alt="Documenso&#0032;Platform&#0032;Plan - Whitelabeled&#0032;signing&#0032;flows&#0032;in&#0032;your&#0032;product | Product Hunt" style="width: 250px; height: 54px;" width="250" height="54" /></a>
<img src="https://github.com/documenso/documenso/assets/13398220/a643571f-0239-46a6-a73e-6bef38d1228b" alt="Documenso Logo">
<p align="center" style="margin-top: 20px">
@@ -53,8 +49,6 @@ Join us in creating the next generation of open trust infrastructure.
## Community and Next Steps 🎯
We're currently working on a redesign of the application, including a revamp of the codebase, so Documenso can be more intuitive to use and robust to develop upon.
- Check out the first source code release in this repository and test it.
- Tell us what you think in the [Discussions](https://github.com/documenso/documenso/discussions).
- Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members.
@@ -73,9 +67,9 @@ Contact us if you are interested in our Enterprise plan for large organizations
<a href="https://cal.com/timurercan/enterprise-customers?utm_source=banner&utm_campaign=oss"><img alt="Book us with Cal.com" src="https://cal.com/book-with-cal-dark.svg" /></a>
## Tech Stack
<p align="left">
<a href="https://www.typescriptlang.org"><img src="https://shields.io/badge/TypeScript-3178C6?logo=TypeScript&logoColor=FFF&style=flat-square" alt="TypeScript"></a>
<a href="https://nextjs.org/"><img src="https://img.shields.io/badge/next.js-000000?style=flat-square&logo=nextdotjs&logoColor=white" alt="NextJS"></a>
<a href="https://prisma.io"><img width="122" height="20" src="http://made-with.prisma.io/indigo.svg" alt="Made with Prisma" /></a>
<a href="https://tailwindcss.com/"><img src="https://img.shields.io/badge/tailwindcss-0F172A?&logo=tailwindcss" alt="Tailwind CSS"></a>
<a href=""><img src="" alt=""></a>
@@ -85,20 +79,17 @@ Contact us if you are interested in our Enterprise plan for large organizations
<a href=""><img src="" alt=""></a>
</p>
- [Typescript](https://www.typescriptlang.org/) - Language
- [Next.js](https://nextjs.org/) - Framework
- [Prisma](https://www.prisma.io/) - ORM
- [ReactRouter](https://reactrouter.com/) - Framework
- [Prisma](https://www.prisma.io/) - ORM
- [Tailwind](https://tailwindcss.com/) - CSS
- [shadcn/ui](https://ui.shadcn.com/) - Component Library
- [NextAuth.js](https://next-auth.js.org/) - Authentication
- [react-email](https://react.email/) - Email Templates
- [tRPC](https://trpc.io/) - API
- [@documenso/pdf-sign](https://www.npmjs.com/package/@documenso/pdf-sign) - PDF Signatures (launching soon)
- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs
- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation
- [Stripe](https://stripe.com/) - Payments
- [Vercel](https://vercel.com) - Hosting
<!-- - Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned. -->
@@ -108,7 +99,7 @@ Contact us if you are interested in our Enterprise plan for large organizations
To run Documenso locally, you will need
- Node.js (v18 or above)
- Node.js (v22 or above)
- Postgres SQL Database
- Docker (optional)
@@ -171,10 +162,8 @@ git clone https://github.com/<your-username>/documenso
4. Set the following environment variables:
- NEXTAUTH_URL
- NEXTAUTH_SECRET
- NEXT_PUBLIC_WEBAPP_URL
- NEXT_PUBLIC_MARKETING_URL
- NEXT_PRIVATE_DATABASE_URL
- NEXT_PRIVATE_DIRECT_DATABASE_URL
- NEXT_PRIVATE_SMTP_FROM_NAME
@@ -182,15 +171,20 @@ 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
---
- Optional: Seed the database using `npm run prisma:seed -w @documenso/prisma` to create a test user and document.
- Optional: Create your own signing certificate.
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL), see **[Create your own signing certificate](./SIGNING.md)**.
- Optional: Configure job provider for document reminders.
- The default local job provider does not support scheduled jobs required for document reminders.
- To enable reminders, set `NEXT_PRIVATE_JOBS_PROVIDER=inngest` and provide `NEXT_PRIVATE_INNGEST_EVENT_KEY` in your `.env` file.
### Run in Gitpod
@@ -225,8 +219,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:
@@ -243,35 +235,33 @@ cp .env.example .env
The following environment variables must be set:
- `NEXTAUTH_URL`
- `NEXTAUTH_SECRET`
- `NEXT_PUBLIC_WEBAPP_URL`
- `NEXT_PUBLIC_MARKETING_URL`
- `NEXT_PRIVATE_DATABASE_URL`
- `NEXT_PRIVATE_DIRECT_DATABASE_URL`
- `NEXT_PRIVATE_SMTP_FROM_NAME`
- `NEXT_PRIVATE_SMTP_FROM_ADDRESS`
> If you are using a reverse proxy in front of Documenso, don't forget to provide the public URL for both `NEXTAUTH_URL` and `NEXT_PUBLIC_WEBAPP_URL` variables!
> If you are using a reverse proxy in front of Documenso, don't forget to provide the public URL for the `NEXT_PUBLIC_WEBAPP_URL` variable!
Now you can install the dependencies and build it:
```
npm i
npm run build:web
npm run build
npm run prisma:migrate-deploy
```
Finally, you can start it with:
```
cd apps/web
cd apps/remix
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
@@ -286,7 +276,7 @@ After=network.target
Environment=PATH=/path/to/your/node/binaries
Type=simple
User=www-data
WorkingDirectory=/var/www/documenso/apps/web
WorkingDirectory=/var/www/documenso/apps/remix
ExecStart=/usr/bin/next start -p 3500
TimeoutSec=15
Restart=always
@@ -321,7 +311,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
View File
@@ -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
+343
View File
@@ -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
+26
View File
@@ -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
+45
View File
@@ -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
+13
View File
@@ -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="info">Status: [Compliant](https://documen.so/trust)</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
+187
View File
@@ -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
+174
View File
@@ -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
![Signature field in the document editor](/document-signing/signature-field-document-editor-view.webp)
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
![Text field in the document editor](/document-signing/text-field-document-editor-view.webp)
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
![Checkbox field in the document editor](/document-signing/checkbox-field-document-editor-view.webp)
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
![Dropdown field in the document editor](/document-signing/dropdown-field-document-editor-view.webp)
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
+70
View File
@@ -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,4 @@
{
"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,42 @@
---
title: Developer Mode
description: Advanced development tools for debugging field IDs, recipient IDs, coordinates and integrating with the Documenso API.
---
## Overview
Developer mode provides additional tools and features to help you integrate and debug Documenso.
## Field Information
When enabled, developer mode displays the following information for each field:
- **Field ID** - The unique identifier of the field
- **Recipient ID** - The ID of the recipient assigned to the field
- **Pos X / Pos Y** - The position of the field on the page
- **Width / Height** - The dimensions of the field
To enable developer mode, 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
```
![Field Coordinates Legacy Editor](/developer-mode/field-coordinates-legacy-editor.webp)
```bash
# New editor
https://app.documenso.com/t/<team-url>/documents/<envelope-id>/edit?step=addFields&devmode=true
```
![Field Coordinates New Editor](/developer-mode/field-coordinates-new-editor.webp)
---
## 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,819 @@
---
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.
<Callout type="info">
This endpoint automatically scans uploaded PDFs for [placeholder patterns](/docs/users/documents/advanced/pdf-placeholders) like `{"{{signature, r1}}"}` and creates fields at those locations.
</Callout>
```
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,742 @@
---
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.
<Callout type="info">
Placeholder support is only available in `envelope.*` endpoints. `POST /template/use` does not support placeholder parsing.
</Callout>
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,4 @@
{
"title": "API Reference",
"pages": ["documents", "recipients", "fields", "templates", "teams", "rate-limits", "versioning", "developer-mode"]
}
@@ -0,0 +1,67 @@
---
title: Rate Limits
description: Learn about the rate limits for the Documenso Public API.
---
import { Callout } from 'fumadocs-ui/components/callout';
## Overview
Documenso enforces rate limits on all API endpoints to ensure service stability.
## HTTP Rate Limits
**Limit:** 100 requests per minute per IP address
**Response:** 429 Too Many Requests
### Rate Limit Response
```json
{
"error": "Too many requests, please try again later."
}
```
<Callout type="warn">
No rate limit headers are currently provided. When you receive a 429 response, wait at least 60
seconds before retrying.
</Callout>
## Resource Limits
Beyond HTTP rate limits, your account has usage limits based on your subscription plan.
### Plan Limits
| Resource | Free | Paid | Self-hosted | Enterprise |
| ---------------- | ---- | --------- | ----------- | ---------- |
| Documents/month | 5 | Unlimited | Unlimited | Unlimited |
| Total Recipients | 10 | Unlimited | Unlimited | Unlimited |
| Direct Templates | 3 | Unlimited | Unlimited | Unlimited |
### Error Response
When you exceed a resource limit:
```json
{
"error": "You have reached your document limit for this month. Please upgrade your plan.",
"code": "LIMIT_EXCEEDED",
"statusCode": 400
}
```
## Error Codes
| Code | Status | Description |
| ------------------- | ------ | ----------------------------- |
| `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
@@ -0,0 +1,25 @@
---
title: API Versioning
description: Versioning information for the Documenso public API.
---
import { Callout } from 'fumadocs-ui/components/callout';
## 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 `v2`.</Callout>
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
@@ -0,0 +1,91 @@
---
title: Contributing Translations
description: Learn how to contribute translations to Documenso and become part of our community.
---
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](/docs/developers/local-development/translations).
<Callout type="info">
Contributions are made through GitHub Pull Requests, so you will need a GitHub account to
contribute.
</Callout>
## Overview
We store our translations in PO files, which are located in our GitHub repository [here](https://github.com/documenso/documenso/tree/main/packages/lib/translations).
The translation files are organized into folders represented by their respective language codes (`en` for English, `de` for German, etc).
Each PO file contains translations which look like this:
```po
#: 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>"
```
- `msgid`: The original text in English (never edit this manually)
- `msgstr`: The translated text in the target language
<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>
## How to Contribute
### Updating Existing Translations
{/* 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:
{/* 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?
<Callout type="info">
If you have any questions, hop into our [Discord](https://documen.so/discord) and ask us directly!
</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
@@ -0,0 +1,152 @@
---
title: Contributing to Documenso
description: Learn how to contribute to Documenso and become part of our community.
---
import { Callout } from 'fumadocs-ui/components/callout';
import { Step, Steps } from 'fumadocs-ui/components/steps';
## 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.
This guide will help you get started with contributing to Documenso.
## Before Getting Started
{/* prettier-ignore */}
<Steps>
<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.
</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.
</Step>
<Step>
### Picking an existing issue
If you pick an existing issue, take into consideration the discussion on the issue.
</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
Before taking an issue, ensure that:
- The issue has been assigned the public label.
- The issue is clearly defined and understood.
- No one has been assigned to the issue.
- No one has expressed the intention to work on it.
After that:
1. Comment on the issue with your intention to work on it.
2. Start working on the issue.
Feel free to ask for help, clarification or guidance if needed. We are here to help you.
## Developing
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>
<Step>
### Set up Documenso locally
To set up your local environment, check out the [local development](/docs/developers/local-development) guide.
</Step>
<Step>
### Pick a task
Find an issue to work on or create a new one.
> Before working on an issue, ensure that no one else is working on it. If no one is assigned to the issue, you can pick it up by leaving a comment and asking to assign it to you.
Before creating a new issue, check the existing issues to see if someone else has already reported it.
</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.
When creating a branch, make sure that the branch name:
- starts with the correct prefix: `feat/` for new features, `fix/` for bug fixes, etc.
- includes the issue ID you are working on (if applicable).
- is descriptive.
```sh
git checkout -b feat/issue-id-your-branch-name
## Example
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.
</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.
</Step>
<Step>
### Open a pull request
After implementing your changes, open a pull request against the `main` branch.
</Step>
</Steps>
<Callout type="info">
If you need help getting started, [join us on Discord](https://documen.so/discord).
</Callout>
## Building
Before pushing code or creating pull requests, please ensure you can successfully create a successful production build. You can build the project by running the following command in your terminal:
```bash
npm run build
```
Once the project builds successfully, you can push your code changes or create a pull request.
<Callout type="info">
Remember to run tests and perform any necessary checks before finalizing your changes. As 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,56 @@
---
title: Authoring
description: Embed document, template, and envelope 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, templates, and envelopes 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>
## Versions
Embedded authoring is available in two versions:
- **[V1 Authoring](/docs/developers/embedding/authoring/v1)** — Works with V1 Documents and Templates.
- **[V2 Authoring](/docs/developers/embedding/authoring/v2)** — Works with Envelopes, which are the unified model for documents and templates.
### Comparison
| Aspect | V1 | V2 |
| --- | --- | --- |
| Entity model | Documents and Templates (separate) | Envelopes (unified, can be documents or templates) |
| API compatibility | V1 Documents/Templates API | V2 Envelopes API |
| Customization | 6 simple boolean flags | Rich structured settings with sections (general, settings, actions, envelope items, recipients) |
---
## 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>
---
## Next Steps
- [V1 Authoring](/docs/developers/embedding/authoring/v1) — Create and edit documents and templates using V1 components
- [V2 Authoring](/docs/developers/embedding/authoring/v2) — Create and edit envelopes using V2 components
- [CSS Variables](/docs/developers/embedding/css-variables) — Customize the appearance of embedded components
- [SDKs](/docs/developers/embedding/sdks) — Framework-specific SDK documentation
@@ -0,0 +1,4 @@
{
"title": "Authoring",
"pages": ["v1", "v2"]
}

Some files were not shown because too many files have changed in this diff Show More