mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 20:32:07 +10:00
Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b59f1a273 | |||
| 15549a6758 | |||
| eb45d1e5a9 | |||
| 0aa84cecc8 | |||
| 702e747375 | |||
| 3887aa67c8 | |||
| b84b87cea6 | |||
| ac0a0086d6 | |||
| 8c11266747 | |||
| 3c0345f755 | |||
| 3e47f1913c | |||
| 9ccf50ed95 | |||
| ecc98fbd41 | |||
| 58f0f5da43 | |||
| d5c6cf4ad5 | |||
| 90462bf414 | |||
| 791a54bb8b | |||
| 5f4e0ccf6b | |||
| 583e35c768 | |||
| 1e129580b8 | |||
| 184cbd6770 | |||
| b53295a9d5 | |||
| 8448e333cf | |||
| 03b5fe6117 | |||
| f60698a353 | |||
| 7c48ae6ff4 | |||
| 4ee789ea37 | |||
| ebf5b75a19 | |||
| 0ecde7ac1e | |||
| c41e387220 | |||
| 7f796ed74e | |||
| 0a21598fec | |||
| 240bef1a66 | |||
| 9583e79056 | |||
| 993a494784 | |||
| 743d31651f | |||
| ce96238464 | |||
| 8b8e7e9f2e | |||
| 50006ca053 | |||
| c3135a3ce7 | |||
| d2f60b13fd | |||
| c50a01d004 | |||
| 4bda501d51 | |||
| a7713f7228 | |||
| 536142be03 | |||
| 44c4826e92 | |||
| 61138cdd81 | |||
| 22ceff43e3 | |||
| a84da2f2c7 | |||
| 7e8da85bd8 | |||
| d304d8720c | |||
| 9da2db2e67 | |||
| 993df7dc21 | |||
| 807d094cf2 | |||
| 3cef238f46 | |||
| 886c40a46b | |||
| b1b82b775c | |||
| 0fe697c26c | |||
| 7c0031679a | |||
| 6bb0496224 | |||
| eedf483957 | |||
| 5421b0d1cc | |||
| fa2c53bd72 | |||
| 6ac67e646c | |||
| 6a20fefd7b | |||
| 43fe558459 | |||
| 0a6b0452dc | |||
| fec5d55250 | |||
| f1b235819e | |||
| d0f9f68689 | |||
| f93a98e9a5 | |||
| c0ea4c60e4 | |||
| 2cb4cc29ea | |||
| d9b5f01e21 | |||
| bc3acba72c | |||
| 247a0158bd | |||
| 9e0b567686 | |||
| 8f6be474a9 | |||
| 8f5bdef384 | |||
| 999942014e | |||
| 194b2134cc | |||
| b8df02750b | |||
| 191170923a | |||
| 4078c6b46d | |||
| abbca79b48 | |||
| d6dd2b3292 | |||
| cfaad6efc9 | |||
| 9a45b3564f | |||
| 8b171c9a30 | |||
| a8efb6f495 | |||
| bc184d445f | |||
| 8dfd548c08 | |||
| 73a7335c89 | |||
| be3e45427f | |||
| 57eb40d6aa | |||
| 684fab1909 | |||
| d794ceb8da | |||
| 87315adb0f | |||
| 0a7794be61 | |||
| f15d6f0150 | |||
| 0b86ece1d5 | |||
| a197bf113f | |||
| ec8728b33e | |||
| 22122f51da | |||
| 8671f269e8 | |||
| edbf65969b | |||
| 207135d6f3 | |||
| 4877d1964a | |||
| f66751668a | |||
| bc3aa9c858 | |||
| b79b4bd111 | |||
| 36c10d1a92 | |||
| 8c0e029b1b | |||
| f10d3284ba | |||
| 6a6ef8d2ad | |||
| 690491c3b1 | |||
| 6243a514af | |||
| a697832b43 | |||
| aebb5e2067 | |||
| e19b1d00d0 | |||
| c428170b5c | |||
| 84fc866cfb | |||
| 5d92aaf20a | |||
| ae497092d7 | |||
| 2f4c3893a3 | |||
| 61338af216 | |||
| 2c7a1be051 | |||
| 8bad62cc92 | |||
| 19c2f7b4a1 | |||
| 135b676cd4 |
@@ -0,0 +1,138 @@
|
||||
---
|
||||
date: 2026-05-06
|
||||
title: Platform Signing Page Branding
|
||||
---
|
||||
|
||||
## What
|
||||
|
||||
Platform-plan organisations (and their teams) can customise the **non-embed
|
||||
signing pages** (`/sign/:token`, `/d/:token`, and the sibling
|
||||
complete/expired/rejected/waiting pages) with:
|
||||
|
||||
- Six brand colour tokens (background, foreground, primary, primary-foreground,
|
||||
border, ring) plus a border-radius length.
|
||||
- A free-text custom CSS block (up to 256 KB).
|
||||
|
||||
Settings live on `OrganisationGlobalSettings` and `TeamGlobalSettings`. Teams
|
||||
inherit from the org via the existing `brandingEnabled === null` mechanism.
|
||||
|
||||
## Why
|
||||
|
||||
- Embed customers already have white-label CSS; Platform customers want the
|
||||
same coverage on direct signing URLs that they iframe or link to.
|
||||
- Persisting on org/team (not per envelope) means it's set-and-forget.
|
||||
- Sanitising **on save** lets us inline the verbatim string at SSR — no
|
||||
per-render parsing cost, no `<style>.innerHTML` injection on the client.
|
||||
- Reusing the existing `embedSigningWhiteLabel` claim flag keeps "if you can
|
||||
white-label an embed, you can white-label this" as one decision.
|
||||
|
||||
## How
|
||||
|
||||
### Storage (`packages/prisma/schema.prisma`)
|
||||
|
||||
Two new fields on each settings model. No new tables.
|
||||
|
||||
| Field | Org type | Team type |
|
||||
| ---------------- | ------------------ | ------------------ |
|
||||
| `brandingColors` | `Json?` (nullable) | `Json?` (nullable) |
|
||||
| `brandingCss` | `String @default("")` | `String?` |
|
||||
|
||||
Colours are validated against `ZCssVarsSchema`. The team's `null` means
|
||||
"inherit"; an empty colour object is collapsed to `null` server-side so a
|
||||
team toggling `brandingEnabled = true` without filling in colours doesn't
|
||||
silently override the org's defaults with nothing.
|
||||
|
||||
### Sanitiser (`packages/lib/utils/sanitize-branding-css.ts`)
|
||||
|
||||
PostCSS + `postcss-selector-parser`. Runs on save only.
|
||||
|
||||
- Drops selectors containing `::before`/`::after`/`::backdrop`/`::marker` or
|
||||
the universal `*`.
|
||||
- Drops integrity-breaking properties (`display`, `position`, `transform`,
|
||||
layout-affecting dimensions, text-hiding properties).
|
||||
- Drops declaration values containing `url(`, `expression(`, `@import`,
|
||||
`javascript:`.
|
||||
- Strips `!important`.
|
||||
- Allows `@media` only; drops other at-rules.
|
||||
- **Does not** rewrite selectors. Scoping happens at render time via native
|
||||
CSS nesting under `.documenso-branded { ... }`.
|
||||
- Final-pass tripwire: if a literal `</style` somehow survives serialization,
|
||||
reject the entire output. PostCSS already escapes `<` to `\3c` whenever it
|
||||
would form `</...`; the explicit check is belt-and-braces in case a future
|
||||
serializer regresses.
|
||||
- Returns `{ css, warnings[] }`. Warnings are surfaced in the UI.
|
||||
|
||||
Border-radius is the only token interpolated raw into a `<style>` block; it
|
||||
is regex-validated (`CSS_LENGTH_REGEX`) at both the Zod schema and the
|
||||
runtime `toNativeCssVars` call. Belt-and-braces against schema drift.
|
||||
|
||||
### Render (`apps/remix/app/components/general/recipient-branding.tsx`)
|
||||
|
||||
Each recipient loader calls `loadRecipientBrandingByTeamId` and threads the
|
||||
payload through to `<RecipientBranding>`, which emits a single
|
||||
nonce-attributed `<style>`:
|
||||
|
||||
```
|
||||
.documenso-branded {
|
||||
--background: ...; ...
|
||||
<user css>
|
||||
}
|
||||
```
|
||||
|
||||
Native CSS nesting expands user rules under the wrapper. The body class is
|
||||
applied unconditionally to recipient routes in `root.tsx` via `useMatches()`
|
||||
so portaled Radix content (dialogs, popovers, tooltips, dropdowns) inherits
|
||||
the scope.
|
||||
|
||||
CSP for recipient routes already supports `<style nonce>`; no policy
|
||||
changes needed.
|
||||
|
||||
### Plan gate
|
||||
|
||||
`organisationClaim.flags.embedSigningWhiteLabel || !IS_BILLING_ENABLED()`.
|
||||
Self-hosted instances always allow. The outer paywall for logo/URL/details
|
||||
stays on `allowCustomBranding` (Team plan and up); only the new
|
||||
colour/CSS section is Platform-only.
|
||||
|
||||
### UI (`apps/remix/app/components/forms/branding-preferences-form.tsx`)
|
||||
|
||||
Extends the existing branding form. Six `<ColorPicker showHex>` (rewritten
|
||||
to use the native `<input type="color">` instead of `react-colorful`, which
|
||||
was removed) in a 2-col grid, plus a free-text radius input and an
|
||||
`<Accordion>` revealing a mono `<Textarea>`. Defaults come from
|
||||
`packages/lib/constants/theme.ts` (light-mode hex mirror of `theme.css`).
|
||||
|
||||
Warnings from the sanitiser are surfaced in an `<Alert variant="warning">`
|
||||
after save, and the `brandingCss` textarea is re-synced from the persisted
|
||||
value so the user sees exactly what was stored. Other fields are
|
||||
deliberately NOT reset on settings refetch — that would clobber in-flight
|
||||
edits.
|
||||
|
||||
### TRPC
|
||||
|
||||
`update-organisation-settings` and `update-team-settings` accept the new
|
||||
fields, run them through `sanitizeBrandingCss` + `normalizeBrandingColors`,
|
||||
and return any sanitiser warnings to the client. The team route treats
|
||||
`null` as "inherit"; an empty post-sanitisation string is collapsed to
|
||||
`null` (team) so an empty override doesn't mask the org's CSS.
|
||||
|
||||
## Known accepted limitations
|
||||
|
||||
- The sanitiser does not prevent hostile-but-syntactically-valid CSS
|
||||
(`color: transparent`, low-contrast values, etc.). The customer is
|
||||
branding **their own** signing pages — we focus on integrity (no
|
||||
overlay/hide/exfiltrate), not aesthetic policing.
|
||||
- User rules targeting `body`/`html`/`:root` no-op once nested under the
|
||||
wrapper class. Documented for users.
|
||||
- CSS nesting baseline is Chrome 120+ / Firefox 117+ / Safari 16.5+.
|
||||
Acceptable for the Platform-tier audience.
|
||||
- No automated `theme.css` ↔ `theme.ts` sync check; fat comment in
|
||||
`theme.ts` reminds devs to update both.
|
||||
- Per-section team inherit is coarse — `brandingEnabled = null` inherits
|
||||
everything from the org. Per-field inherit toggles are deferred.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Live preview, embed-route sanitiser unification, email/PDF certificate
|
||||
branding, custom font upload, the full ~30 colour tokens in the picker UI,
|
||||
wiring `hidePoweredBy` through to the actual footer.
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
date: 2026-05-28
|
||||
title: Custom Brand Logo Url
|
||||
---
|
||||
|
||||
# Problem
|
||||
|
||||
`brandingUrl` (the configured "Brand Website") is persisted and editable in branding
|
||||
settings, but historically it was never consumed anywhere. It flowed into the database,
|
||||
the settings form, and the admin read-only view, but never affected any rendered output.
|
||||
|
||||
We want `brandingUrl` to actually do something, with deliberately different behavior per
|
||||
surface.
|
||||
|
||||
# Relationship we're going for
|
||||
|
||||
`brandingUrl` is an **email-only** linking concept. It is intentionally **not** used on
|
||||
in-app signing surfaces.
|
||||
|
||||
| Surface | Custom branding logo configured | `brandingUrl` behavior |
|
||||
| --- | --- | --- |
|
||||
| Transactional emails (logo) | Logo shown | Logo links to `brandingUrl` when it is a safe http(s) URL; otherwise plain image |
|
||||
| Transactional emails (footer) | n/a | `brandingUrl` rendered as a link in the footer when it is a safe http(s) URL |
|
||||
| Signing pages (V1 + V2, normal + direct-template) | Logo shown | Ignored — logo is a plain image with no link |
|
||||
| Signing pages (no custom logo) | Documenso fallback shown | Fallback keeps its internal `/` link |
|
||||
| Embedded signing | Logo shown | Ignored (logo not linked) |
|
||||
| Embedded authoring/editor | Logo shown | Ignored |
|
||||
| Settings / admin branding previews | n/a | Unchanged (display only) |
|
||||
|
||||
Rationale:
|
||||
|
||||
- On signing pages the recipient is mid-task; sending them off to an external marketing
|
||||
site via the logo is undesirable, so the custom logo is a plain image there.
|
||||
- In emails the logo and a footer link to the brand's own site are a normal, expected
|
||||
pattern and reinforce that the email is legitimately from that brand.
|
||||
|
||||
# Decisions
|
||||
|
||||
## Scope
|
||||
|
||||
- Use `brandingUrl` only in transactional email rendering:
|
||||
- The shared email logo component links the custom branding logo to `brandingUrl`.
|
||||
- The shared email footer renders `brandingUrl` as a link.
|
||||
- On signing surfaces, render a configured custom branding logo as a plain image with no
|
||||
link wrapper. Leave the Documenso fallback logo's internal `/` link untouched.
|
||||
- Do not change embedded signing, embedded authoring/editor, or settings/admin previews.
|
||||
- No Prisma schema or database migration. `brandingUrl` already exists and is editable.
|
||||
|
||||
## URL safety
|
||||
|
||||
Rendering must be defensive because old/imported data can bypass the branding form's URL
|
||||
validation. Only treat the stored value as a usable Brand Website when it parses as an
|
||||
absolute `http:` or `https:` URL.
|
||||
|
||||
- Empty, missing, invalid, relative, or non-http(s) values are treated as "no Brand
|
||||
Website" and produce a plain logo / no footer link.
|
||||
- Do not mutate stored settings or run a cleanup migration.
|
||||
- Factored into a single shared helper so both email logo and footer apply identical rules:
|
||||
- `packages/email/utils/branding-url.ts` -> `getSafeBrandingUrl(value): string | null`.
|
||||
|
||||
## Email rendering
|
||||
|
||||
- New shared component `packages/email/template-components/template-branding-logo.tsx`
|
||||
(`TemplateBrandingLogo`) renders either:
|
||||
- the custom branding logo, wrapped in a `Link` to the safe `brandingUrl` with
|
||||
`target="_blank"` when one exists, or a plain `Img` when not; or
|
||||
- the Documenso fallback logo (`/static/logo.png`) when custom branding is disabled or
|
||||
no logo is set.
|
||||
- This component replaced the duplicated `brandingEnabled && brandingLogo ? <Img/> : <fallback/>`
|
||||
ternary that was copy-pasted across all transactional email templates.
|
||||
- `packages/email/template-components/template-footer.tsx` renders `brandingUrl` as a
|
||||
footer link (via `getSafeBrandingUrl`) when branding is enabled and the URL is safe.
|
||||
|
||||
The branding context already exposes `brandingUrl` (`packages/email/providers/branding.tsx`),
|
||||
populated by `teamGlobalSettingsToBranding` / `organisationGlobalSettingsToBranding`
|
||||
(which spread `...settings`), so no additional plumbing into the email branding context was
|
||||
required.
|
||||
|
||||
## Signing rendering
|
||||
|
||||
- `apps/remix/app/components/general/document-signing/document-signing-page-view-v1.tsx`:
|
||||
custom logo renders as a bare `<img>`. `brandingUrl` is not read; the local branding type
|
||||
and loader payload no longer carry it.
|
||||
- `apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx` (V2,
|
||||
shared by normal and direct-template signing): custom logo renders as a bare `<img>`; the
|
||||
Documenso fallback keeps its `<Link to="/">`.
|
||||
- `apps/remix/app/routes/_recipient+/sign.$token+/_index.tsx`: V1 loader branding payload no
|
||||
longer includes `brandingUrl`.
|
||||
- `packages/lib/server-only/envelope/get-envelope-for-recipient-signing.ts` and
|
||||
`get-envelope-for-direct-template-signing.ts`: `brandingUrl` removed from the V2
|
||||
`EnvelopeForSigningResponse.settings` schema/payload since it is not consumed there.
|
||||
|
||||
# History
|
||||
|
||||
An earlier iteration of this plan wired `brandingUrl` into the in-app signing pages so a
|
||||
custom logo linked to the Brand Website (external `<a target="_blank">`, internal `/`
|
||||
fallback otherwise) and added `brandingUrl` to the V1/V2 signing payloads. That direction
|
||||
was reversed: signing-page logos are now plain images and `brandingUrl` is email-only. The
|
||||
signing payload additions were removed.
|
||||
|
||||
# Test coverage
|
||||
|
||||
`packages/app-tests/e2e/signing-branding.spec.ts`:
|
||||
|
||||
- V1 normal `/sign/:token`: custom logo is a plain image, not inside a link, and no
|
||||
`brandingUrl` link is present.
|
||||
- V2 normal `/sign/:token` and V2 direct-template: same plain-image assertions.
|
||||
- V2 with no custom logo: Documenso fallback still links to `/`.
|
||||
- Embedded signing: no custom-logo Brand Website link is rendered.
|
||||
|
||||
# Acceptance criteria
|
||||
|
||||
- A custom branding logo on any signing surface (V1, V2 normal, V2 direct-template, embedded)
|
||||
renders as a plain image with no link, and `brandingUrl` is never rendered as a link there.
|
||||
- Documenso fallback logos continue linking to `/`.
|
||||
- In transactional emails, when a custom logo and a safe `brandingUrl` are configured, the
|
||||
email logo links to `brandingUrl` (new tab) and the footer shows the Brand Website link.
|
||||
- In transactional emails, when `brandingUrl` is empty/invalid/relative/non-http(s), the logo
|
||||
is a plain image and no footer Brand Website link is shown.
|
||||
- URL safety is enforced through the single shared `getSafeBrandingUrl` helper.
|
||||
- Settings/admin branding previews are unchanged.
|
||||
- No schema or migration changes.
|
||||
@@ -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.)
|
||||
+23
-1
@@ -160,8 +160,16 @@ NEXT_PRIVATE_REDIS_PREFIX="documenso"
|
||||
NEXT_PUBLIC_POSTHOG_KEY=""
|
||||
# OPTIONAL: Leave blank to disable billing.
|
||||
NEXT_PUBLIC_FEATURE_BILLING_ENABLED=
|
||||
# OPTIONAL: Leave blank to allow users to signup through /signup page.
|
||||
# OPTIONAL: Set to "true" to disable all signup methods (email, Google, Microsoft, OIDC, including the organisation OIDC portal).
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP=
|
||||
# OPTIONAL: Set to "true" to disable email/password signup only.
|
||||
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=
|
||||
# OPTIONAL: Set to "true" to block new-account creation through Google. Existing linked users can still sign in.
|
||||
NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP=
|
||||
# OPTIONAL: Set to "true" to block new-account creation through Microsoft. Existing linked users can still sign in.
|
||||
NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=
|
||||
# OPTIONAL: Set to "true" to block new-account creation through OIDC (including the organisation portal).
|
||||
NEXT_PUBLIC_DISABLE_OIDC_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.
|
||||
@@ -203,3 +211,17 @@ NEXT_PRIVATE_LOGGER_FILE_PATH=
|
||||
|
||||
# [[PLAIN SUPPORT]]
|
||||
NEXT_PRIVATE_PLAIN_API_KEY=
|
||||
|
||||
# [[DOCUMENT CONVERSION]]
|
||||
# OPTIONAL: Base URL of a Gotenberg-compatible service used to convert uploaded
|
||||
# DOCX files to PDF on the server. When unset, DOCX uploads are disabled and
|
||||
# only PDF is accepted. The dev docker compose exposes Gotenberg on port 3005.
|
||||
# NEXT_PRIVATE_DOCUMENT_CONVERSION_URL="http://localhost:3005"
|
||||
# OPTIONAL: Per-request timeout in milliseconds for the conversion service.
|
||||
# Defaults to 30000 (30s) if unset.
|
||||
# NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS=30000
|
||||
# OPTIONAL: HTTP Basic auth credentials for the conversion service. Set both
|
||||
# when the service is started with `--api-enable-basic-auth` (the dev compose
|
||||
# does this; the matching values there are `documenso` / `password`).
|
||||
# NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME=documenso
|
||||
# NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD=password
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Config files
|
||||
*.config.js
|
||||
*.config.cjs
|
||||
|
||||
# Statically hosted javascript files
|
||||
apps/*/public/*.js
|
||||
apps/*/public/*.cjs
|
||||
scripts/
|
||||
@@ -1,16 +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',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
},
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ['apps/*/'],
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['lingui.config.ts', 'packages/lib/translations/**/*.js'],
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
name: 'Setup node and cache node_modules'
|
||||
name: 'Setup node'
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
@@ -16,25 +16,7 @@ runs:
|
||||
shell: bash
|
||||
run: corepack enable npm
|
||||
|
||||
- name: Cache npm
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: npm-
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v3
|
||||
id: cache-node-modules
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
packages/*/node_modules
|
||||
apps/*/node_modules
|
||||
key: modules-${{ hashFiles('package-lock.json') }}
|
||||
|
||||
- name: Install dependencies
|
||||
if: steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
npm ci --no-audit
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
name: Install playwright binaries
|
||||
description: 'Install playwright, cache and restore if necessary'
|
||||
description: 'Install playwright'
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Cache playwright
|
||||
id: cache-playwright
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cache/ms-playwright
|
||||
${{ github.workspace }}/node_modules/playwright
|
||||
key: playwright-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: playwright-
|
||||
|
||||
- name: Install playwright
|
||||
if: steps.cache-playwright.outputs.cache-hit != 'true'
|
||||
run: npx playwright install --with-deps
|
||||
shell: bash
|
||||
|
||||
@@ -41,14 +41,6 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Build Docker Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
@@ -56,13 +48,3 @@ jobs:
|
||||
context: .
|
||||
file: ./docker/Dockerfile
|
||||
tags: documenso-${{ github.sha }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
||||
|
||||
- # Temp fix
|
||||
# https://github.com/docker/build-push-action/issues/252
|
||||
# https://github.com/moby/buildkit/issues/1896
|
||||
name: Move cache
|
||||
run: |
|
||||
rm -rf /tmp/.buildx-cache
|
||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
||||
|
||||
@@ -20,7 +20,6 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: npm
|
||||
|
||||
- name: Install Octokit
|
||||
run: npm install @octokit/rest@18
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 'PR Labeler'
|
||||
|
||||
on:
|
||||
- pull_request_target
|
||||
- pull_request
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
||||
@@ -20,7 +20,6 @@ jobs:
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: npm
|
||||
|
||||
- name: Install Octokit
|
||||
run: npm install @octokit/rest@18
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 'Validate PR Name'
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
node_modules
|
||||
.next
|
||||
public
|
||||
**/**/node_modules
|
||||
**/**/.next
|
||||
**/**/public
|
||||
packages/lib/translations/**/*.js
|
||||
|
||||
*.lock
|
||||
*.log
|
||||
*.test.ts
|
||||
|
||||
.gitignore
|
||||
.npmignore
|
||||
.prettierignore
|
||||
.DS_Store
|
||||
.eslintignore
|
||||
|
||||
# Docs MDX - Prettier strips indentation from code blocks inside components
|
||||
apps/docs/content/**/*.mdx
|
||||
Vendored
+18
-6
@@ -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,8 +15,20 @@
|
||||
"[prisma]": {
|
||||
"editor.defaultFormatter": "Prisma.prisma"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
},
|
||||
"prisma.pinToPrisma6": true
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"prisma.pinToPrisma6": true,
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
- `npm run test:e2e` - Run E2E tests with Playwright
|
||||
- `npm run test:dev -w @documenso/app-tests` - Run single E2E test in dev mode
|
||||
- `npm run test-ui:dev -w @documenso/app-tests` - Run E2E tests with UI
|
||||
- `npm run format` - Format code with Prettier
|
||||
- `npm run 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.
|
||||
|
||||
@@ -65,8 +65,6 @@ Documenso is an open-source document signing platform built as a **monorepo** us
|
||||
| Package | Description |
|
||||
| ---------------------------- | ------------------------- |
|
||||
| `@documenso/app-tests` | E2E tests (Playwright) |
|
||||
| `@documenso/eslint-config` | Shared ESLint config |
|
||||
| `@documenso/prettier-config` | Shared Prettier config |
|
||||
| `@documenso/tailwind-config` | Shared Tailwind config |
|
||||
| `@documenso/tsconfig` | Shared TypeScript configs |
|
||||
|
||||
|
||||
@@ -9,6 +9,10 @@ If you plan to contribute to Documenso, please take a moment to feel awesome ✨
|
||||
- Consider the results from the discussion on the issue
|
||||
- Accept the [Contributor License Agreement](https://documen.so/cla) to ensure we can accept your contributions.
|
||||
|
||||
## English only PRs and Issues
|
||||
|
||||
Please write all issues, pull requests, and related comments in English so maintainers and the wider contributor community can follow the discussion.
|
||||
|
||||
## Taking issues
|
||||
|
||||
Before taking an issue, ensure that:
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
·
|
||||
<a href="https://documenso.com">Website</a>
|
||||
·
|
||||
<a href="https://docs.documenso.com">Documentation</a>
|
||||
·
|
||||
<a href="https://github.com/documenso/documenso/issues">Issues</a>
|
||||
·
|
||||
<a href="https://documen.so/live">Upcoming Releases</a>
|
||||
@@ -146,45 +148,7 @@ npm run d
|
||||
|
||||
### Manual Setup
|
||||
|
||||
Follow these steps to setup Documenso on your local machine:
|
||||
|
||||
1. [Fork this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your GitHub account.
|
||||
|
||||
After forking the repository, clone it to your local device by using the following command:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/<your-username>/documenso
|
||||
```
|
||||
|
||||
2. Run `npm i` in the root directory
|
||||
|
||||
3. Create your `.env` from the `.env.example`. You can use `cp .env.example .env` to get started with our handpicked defaults.
|
||||
|
||||
4. Set the following environment variables:
|
||||
|
||||
- NEXTAUTH_SECRET
|
||||
- NEXT_PUBLIC_WEBAPP_URL
|
||||
- NEXT_PRIVATE_DATABASE_URL
|
||||
- NEXT_PRIVATE_DIRECT_DATABASE_URL
|
||||
- NEXT_PRIVATE_SMTP_FROM_NAME
|
||||
- NEXT_PRIVATE_SMTP_FROM_ADDRESS
|
||||
|
||||
5. Create the database schema by running `npm run prisma:migrate-dev`
|
||||
|
||||
6. Run `npm run translate:compile` in the root directory to compile lingui
|
||||
|
||||
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.
|
||||
Follow the [manual setup guide](https://docs.documenso.com/docs/developers/local-development/manual) to configure Documenso on your local machine.
|
||||
|
||||
### Run in Gitpod
|
||||
|
||||
@@ -204,138 +168,44 @@ If you're a visual learner and prefer to watch a video walkthrough of setting up
|
||||
|
||||
## Docker
|
||||
|
||||
We provide a Docker container for Documenso, which is published on both DockerHub and GitHub Container Registry.
|
||||
We provide official Docker images on [DockerHub](https://hub.docker.com/r/documenso/documenso) and [GitHub Container Registry](https://ghcr.io/documenso/documenso).
|
||||
|
||||
- DockerHub: [https://hub.docker.com/r/documenso/documenso](https://hub.docker.com/r/documenso/documenso)
|
||||
- GitHub Container Registry: [https://ghcr.io/documenso/documenso](https://ghcr.io/documenso/documenso)
|
||||
|
||||
You can pull the Docker image from either of these registries and run it with your preferred container hosting provider.
|
||||
|
||||
Please note that you will need to provide environment variables for connecting to the database, mailserver, and so forth.
|
||||
|
||||
For detailed instructions on how to configure and run the Docker container, please refer to the [Docker README](./docker/README.md) in the `docker` directory.
|
||||
For setup instructions, see the [Docker Deployment](https://docs.documenso.com/docs/self-hosting/deployment/docker) and [Docker Compose](https://docs.documenso.com/docs/self-hosting/deployment/docker-compose) guides.
|
||||
|
||||
## Self Hosting
|
||||
|
||||
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
||||
We support a variety of deployment methods including Docker, Docker Compose, Railway, Kubernetes, and manual deployment.
|
||||
|
||||
### Fetch, configure, and build
|
||||
For full instructions, requirements, and configuration details, see the [Self Hosting documentation](https://docs.documenso.com/docs/self-hosting).
|
||||
|
||||
First, clone the code from Github:
|
||||
### One-Click Deploys
|
||||
|
||||
```
|
||||
git clone https://github.com/documenso/documenso.git
|
||||
```
|
||||
#### Railway
|
||||
|
||||
Then, inside the `documenso` folder, copy the example env file:
|
||||
[](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic)
|
||||
|
||||
```
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
The following environment variables must be set:
|
||||
|
||||
- `NEXTAUTH_SECRET`
|
||||
- `NEXT_PUBLIC_WEBAPP_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 the `NEXT_PUBLIC_WEBAPP_URL` variable!
|
||||
|
||||
Now you can install the dependencies and build it:
|
||||
|
||||
```
|
||||
npm i
|
||||
npm run build
|
||||
npm run prisma:migrate-deploy
|
||||
```
|
||||
|
||||
Finally, you can start it with:
|
||||
|
||||
```
|
||||
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/remix` folder.
|
||||
|
||||
### Run as a service
|
||||
|
||||
You can use a systemd service file to run the app. Here is a simple example of the service running on port 3500 (using 3000 by default):
|
||||
|
||||
```bash
|
||||
[Unit]
|
||||
Description=documenso
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Environment=PATH=/path/to/your/node/binaries
|
||||
Type=simple
|
||||
User=www-data
|
||||
WorkingDirectory=/var/www/documenso/apps/remix
|
||||
ExecStart=/usr/bin/next start -p 3500
|
||||
TimeoutSec=15
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### Railway
|
||||
|
||||
[](https://railway.app/template/bG6D4p)
|
||||
|
||||
### Render
|
||||
#### Render
|
||||
|
||||
[](https://render.com/deploy?repo=https://github.com/documenso/documenso)
|
||||
|
||||
### Koyeb
|
||||
#### Koyeb
|
||||
|
||||
[](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile)
|
||||
|
||||
## Elestio
|
||||
#### Elestio
|
||||
|
||||
[](https://elest.io/open-source/documenso)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For troubleshooting self-hosted deployments, see the [Troubleshooting guide](https://docs.documenso.com/docs/self-hosting/maintenance/troubleshooting) and [Tips & Common Pitfalls](https://docs.documenso.com/docs/self-hosting/getting-started/tips).
|
||||
|
||||
### I'm not receiving any emails when using the developer quickstart.
|
||||
|
||||
When using the developer quickstart, an [Inbucket](https://inbucket.org/) server will be spun up in a docker container that will store all outgoing emails locally for you to view.
|
||||
|
||||
The Web UI can be found at http://localhost:9000, while the SMTP port will be on localhost:2500.
|
||||
|
||||
### 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 Remix start command
|
||||
|
||||
For local docker run
|
||||
|
||||
```bash
|
||||
docker run -it documenso:latest npm run start -- -H ::
|
||||
```
|
||||
|
||||
For k8s or docker-compose
|
||||
|
||||
```yaml
|
||||
containers:
|
||||
- name: documenso
|
||||
image: documenso:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- npm
|
||||
args:
|
||||
- run
|
||||
- start
|
||||
- --
|
||||
- -H
|
||||
- '::'
|
||||
```
|
||||
|
||||
### I can't see environment variables in my package scripts.
|
||||
|
||||
Wrap your package script with the `with:env` script like such:
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ We support a variety of deployment methods, and are actively working on adding m
|
||||
|
||||
## Railway
|
||||
|
||||
[](https://railway.app/template/DjrRRX)
|
||||
[](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic)
|
||||
|
||||
## Render
|
||||
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@
|
||||
"baseDir": "src",
|
||||
"uiLibrary": "radix-ui",
|
||||
"commands": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Callout } from 'fumadocs-ui/components/callout';
|
||||
| 21 CFR Part 11 | Compliant (Enterprise) |
|
||||
| SOC 2 | Compliant |
|
||||
| ISO 27001 | Planned |
|
||||
| HIPAA | Planned |
|
||||
| HIPAA | Compliant (Enterprise) |
|
||||
|
||||
## 21 CFR Part 11
|
||||
|
||||
|
||||
@@ -97,12 +97,12 @@ Documenso implements digital signatures with the following characteristics:
|
||||
- **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.
|
||||
For specific implementation details and configuration options, refer to the [signing certificates](/docs/concepts/signing-certificates) 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
|
||||
- [E-Sign Compliance](/docs/compliance/esign) - Legal frameworks for electronic signatures
|
||||
- [Signing Certificates](/docs/concepts/signing-certificates) - Certificate configuration
|
||||
- [Signing Workflow](/docs/concepts/signing-workflow) - Document activity and audit trail
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
{
|
||||
"title": "Concepts",
|
||||
"pages": [
|
||||
"document-lifecycle",
|
||||
"recipient-roles",
|
||||
"field-types",
|
||||
"signing-workflow",
|
||||
"signing-certificates"
|
||||
]
|
||||
"pages": ["document-lifecycle", "recipient-roles", "field-types", "signing-workflow", "signing-certificates"]
|
||||
}
|
||||
|
||||
@@ -167,5 +167,5 @@ To enable sequential signing:
|
||||
|
||||
## 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
|
||||
- [Add Recipients](/docs/users/documents/add-recipients) - How to add recipients to a document
|
||||
- [Field Types](/docs/concepts/field-types) - Learn about the different field types you can assign to recipients
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
title: Common Errors
|
||||
description: A comprehensive troubleshooting matrix for Documenso API and Webhook integration errors.
|
||||
---
|
||||
|
||||
This guide provides a comprehensive troubleshooting matrix for the standard error codes returned by the Documenso API. Use this reference to diagnose and resolve integration issues related to envelopes, recipients, and webhooks.
|
||||
|
||||
## Application Error Codes
|
||||
|
||||
| Error Code | Description | Recommended Action |
|
||||
| :--- | :--- | :--- |
|
||||
| `ALREADY_EXISTS` | The resource you are attempting to create already exists. | Verify if the entity (e.g., user, envelope, webhook) has already been instantiated. Use a `PUT` or `PATCH` request to update the existing resource instead of `POST`. |
|
||||
| `EXPIRED_CODE` | The provided access code or token has expired. | Generate a new access code or request a new invitation link before retrying the request. |
|
||||
| `INVALID_BODY` | The request payload is malformed. | Inspect your JSON payload structure. Ensure it strictly adheres to the expected schema and that no required fields are missing. |
|
||||
| `INVALID_REQUEST` | The overall request is malformed or invalid. | Review your API call parameters, including the URL, query parameters, and headers. Correct the request syntax. |
|
||||
| `RECIPIENT_EXPIRED` | The signing link or recipient access has expired. | Generate and resend a new invitation to the affected recipient. |
|
||||
| `LIMIT_EXCEEDED` | Your account usage quota has been exceeded. | Check your current plan limits. Upgrade your subscription or wait until your billing cycle renews. |
|
||||
| `NOT_FOUND` | The requested resource could not be found (404). | Verify the resource ID (envelope, document, webhook) passed in the URL. Ensure the resource has not been deleted. |
|
||||
| `NOT_IMPLEMENTED` | The requested feature is not currently supported by the server. | Consult the API documentation to verify available methods. Do not use this endpoint at this time. |
|
||||
| `NOT_SETUP` | The required configuration for this action is incomplete. | Access your account or integration settings and complete the necessary configuration before retrying. |
|
||||
| `INVALID_CAPTCHA` | Security token (Captcha) validation failed. | Ensure the Captcha token is correctly generated on the client side and transmitted without alteration in your request. |
|
||||
| `UNAUTHORIZED` | Missing or invalid authentication (401). | Verify that your API key is correct, active, and properly formatted in the `Authorization` header (e.g., `Bearer <YOUR_API_KEY>`). |
|
||||
| `FORBIDDEN` | Access to the resource is denied (403). | Ensure your API key or user account has the necessary permissions and roles to execute this specific action. |
|
||||
| `UNKNOWN_ERROR` | An unexpected internal server error occurred (500). | Retry the request later. If the issue persists, contact technical support with your request payload and the timestamp of the incident. |
|
||||
| `RETRY_EXCEPTION` | The operation failed temporarily but can be retried. | Implement an automatic retry logic in your integration, ideally using an exponential backoff strategy. |
|
||||
| `SCHEMA_FAILED` | Strict data schema validation failed. | Verify that the data types sent (string, number, boolean) exactly match the OpenAPI specification. |
|
||||
| `TOO_MANY_REQUESTS` | Rate limit exceeded (429). | Reduce the frequency of your API calls. Implement rate-limiting handling based on the response headers. |
|
||||
| `TWO_FACTOR_AUTH_FAILED` | Two-factor authentication (2FA) failed. | Verify the provided 2FA code. Ensure it was entered correctly and has not expired. |
|
||||
| `WEBHOOK_INVALID_REQUEST` | The webhook-related request is invalid. | Check your receiving endpoint configuration. Ensure the URL is correct and that your server accepts `POST` requests from Documenso. |
|
||||
|
||||
## Envelope State Errors
|
||||
|
||||
The following errors occur when attempting to perform actions on an envelope that are incompatible with its current state.
|
||||
|
||||
| Error Code | Description | Recommended Action |
|
||||
| :--- | :--- | :--- |
|
||||
| `ENVELOPE_DRAFT` | The action cannot be performed because the envelope is still in a draft state. | Finalize the envelope configuration and transition it to the `PENDING` (sent) state before attempting this operation. |
|
||||
| `ENVELOPE_COMPLETED` | The action cannot be performed because the envelope is already completed. | No further modifications (e.g., adding signers, modifying documents) can be made to an envelope once the signing process is finished. |
|
||||
| `ENVELOPE_REJECTED` | The action cannot be performed because the envelope was rejected by a recipient. | The signing flow is permanently halted. Create a new envelope if you wish to resubmit the document. |
|
||||
| `ENVELOPE_LEGACY` | The action cannot be performed because the envelope uses an obsolete format. | This envelope was created with a legacy version of the system. Recreate the envelope using the current API version to interact with it. |
|
||||
|
||||
## See Also
|
||||
|
||||
- [Documents API](/docs/developers/api/documents)
|
||||
- [Webhooks](/docs/developers/webhooks)
|
||||
@@ -8,6 +8,7 @@
|
||||
"teams",
|
||||
"rate-limits",
|
||||
"versioning",
|
||||
"developer-mode"
|
||||
"developer-mode",
|
||||
"common-errors"
|
||||
]
|
||||
}
|
||||
|
||||
+9
-9
@@ -1,24 +1,24 @@
|
||||
---
|
||||
title: Authoring
|
||||
title: Editor
|
||||
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.
|
||||
In addition to embedding signing, Documenso supports embedded editor. 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
|
||||
Embedded editor 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:
|
||||
Embedded editor 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.
|
||||
- **[V1 Editor](/docs/developers/embedding/editor/v1)** — Works with V1 Documents and Templates.
|
||||
- **[V2 Editor](/docs/developers/embedding/editor/v2)** — Works with Envelopes, which are the unified model for documents and templates.
|
||||
|
||||
### Comparison
|
||||
|
||||
@@ -32,7 +32,7 @@ Embedded authoring is available in two versions:
|
||||
|
||||
## Presign Tokens
|
||||
|
||||
Before using any authoring component, obtain a presign token from your backend:
|
||||
Before using any editor component, obtain a presign token from your backend:
|
||||
|
||||
```
|
||||
POST /api/v2/embedding/create-presign-token
|
||||
@@ -50,7 +50,7 @@ See the [API documentation](https://openapi.documenso.com/reference#tag/embeddin
|
||||
|
||||
## 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
|
||||
- [V1 Editor](/docs/developers/embedding/editor/v1) — Create and edit documents and templates using V1 components
|
||||
- [V2 Editor](/docs/developers/embedding/editor/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
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Authoring",
|
||||
"title": "Editor",
|
||||
"pages": ["v1", "v2"]
|
||||
}
|
||||
+9
-9
@@ -1,21 +1,21 @@
|
||||
---
|
||||
title: V1 Authoring
|
||||
title: V1 Editor
|
||||
description: Embed V1 document and template creation directly in your application.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
V1 authoring components allow your users to create and edit documents and templates using the V1 Documents and Templates API without leaving your application.
|
||||
V1 editor components allow your users to create and edit documents and templates using the V1 Documents and Templates API without leaving your application.
|
||||
|
||||
<Callout type="warn">
|
||||
Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
|
||||
Embedded editor is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
|
||||
also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing).
|
||||
Contact sales for access.
|
||||
</Callout>
|
||||
|
||||
## Components
|
||||
|
||||
The SDK provides four V1 authoring components:
|
||||
The SDK provides four V1 editor components:
|
||||
|
||||
| Component | Purpose |
|
||||
| ----------------------- | ----------------------- |
|
||||
@@ -29,7 +29,7 @@ The SDK provides four V1 authoring components:
|
||||
|
||||
## Presign Tokens
|
||||
|
||||
All authoring components require a **presign token** for authentication. See the [Authoring overview](/docs/developers/embedding/authoring) for details on obtaining presign tokens.
|
||||
All editor components require a **presign token** for authentication. See the [Editor overview](/docs/developers/embedding/editor) for details on obtaining presign tokens.
|
||||
|
||||
|
||||
<Callout type="warn">
|
||||
@@ -131,7 +131,7 @@ const TemplateEditor = ({ presignToken, templateId }) => {
|
||||
|
||||
## Props
|
||||
|
||||
### All Authoring Components
|
||||
### All Editor Components
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| ------------------ | --------- | -------- | -------------------------------------------------------- |
|
||||
@@ -143,7 +143,7 @@ const TemplateEditor = ({ presignToken, templateId }) => {
|
||||
| `darkModeDisabled` | `boolean` | No | Disable dark mode (Platform Plan) |
|
||||
| `language` | `string` | No | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts) |
|
||||
| `className` | `string` | No | CSS class for the iframe |
|
||||
| `features` | `object` | No | Feature toggles for the authoring experience |
|
||||
| `features` | `object` | No | Feature toggles for the editor experience |
|
||||
|
||||
### Update Components Only
|
||||
|
||||
@@ -157,7 +157,7 @@ const TemplateEditor = ({ presignToken, templateId }) => {
|
||||
|
||||
## Feature Toggles
|
||||
|
||||
Customize what options are available in the authoring experience:
|
||||
Customize what options are available in the editor experience:
|
||||
|
||||
```jsx
|
||||
<EmbedCreateDocumentV1
|
||||
@@ -294,7 +294,7 @@ Pass extra props to the iframe for testing experimental features:
|
||||
## See Also
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props
|
||||
- [V2 Authoring](/docs/developers/embedding/authoring/v2) - V2 envelope authoring
|
||||
- [V2 Editor](/docs/developers/embedding/editor/v2) - V2 envelope editor
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Documents API](/docs/developers/api/documents) - Create documents via API
|
||||
- [Templates API](/docs/developers/api/templates) - Create templates via API
|
||||
+24
-23
@@ -1,21 +1,21 @@
|
||||
---
|
||||
title: V2 Authoring
|
||||
title: V2 Editor
|
||||
description: Embed envelope creation and editing directly in your application.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
V2 authoring components allow your users to create and edit envelopes without leaving your application. Envelopes are the unified model for documents and templates in the V2 API.
|
||||
V2 editor components allow your users to create and edit envelopes without leaving your application. Envelopes are the unified model for documents and templates in the V2 API.
|
||||
|
||||
<Callout type="warn">
|
||||
Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
|
||||
Embedded editor is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
|
||||
also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing).
|
||||
Contact sales for access.
|
||||
</Callout>
|
||||
|
||||
## Components
|
||||
|
||||
The SDK provides two V2 authoring components:
|
||||
The SDK provides two V2 editor components:
|
||||
|
||||
| Component | Purpose |
|
||||
| ---------------------- | ------------------------ |
|
||||
@@ -26,7 +26,7 @@ The SDK provides two V2 authoring components:
|
||||
|
||||
## Presign Tokens
|
||||
|
||||
All authoring components require a **presign token** for authentication. See the [Authoring overview](/docs/developers/embedding/authoring) for details on obtaining presign tokens.
|
||||
All editor components require a **presign token** for authentication. See the [Editor overview](/docs/developers/embedding/editor) for details on obtaining presign tokens.
|
||||
|
||||
<Callout type="warn">
|
||||
A presigned token is NOT an API token
|
||||
@@ -100,19 +100,20 @@ const EnvelopeEditor = ({ presignToken, envelopeId }) => {
|
||||
|
||||
## Props
|
||||
|
||||
### All V2 Authoring Components
|
||||
### All V2 Editor Components
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| ------------------ | --------- | -------- | -------------------------------------------------------- |
|
||||
| `presignToken` | `string` | Yes | Authentication token from your backend |
|
||||
| `externalId` | `string` | No | Your reference ID to link with the envelope |
|
||||
| `host` | `string` | No | Custom host URL. Defaults to `https://app.documenso.com` |
|
||||
| `css` | `string` | No | Custom CSS string (Platform Plan) |
|
||||
| `cssVars` | `object` | No | [CSS variable](/docs/developers/embedding/css-variables) overrides (Platform Plan) |
|
||||
| `darkModeDisabled` | `boolean` | No | Disable dark mode (Platform Plan) |
|
||||
| `language` | `string` | No | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts) |
|
||||
| `className` | `string` | No | CSS class for the iframe |
|
||||
| `features` | `object` | No | Feature toggles for the authoring experience |
|
||||
| Prop | Type | Required | Description |
|
||||
| ---------------- | --------- | -------- | -------------------------------------------------------- |
|
||||
| `presignToken` | `string` | Yes | Authentication token from your backend |
|
||||
| `externalId` | `string` | No | Your reference ID to link with the envelope |
|
||||
| `host` | `string` | No | Custom host URL. Defaults to `https://app.documenso.com` |
|
||||
| `css` | `string` | No | Custom CSS string (Platform Plan) |
|
||||
| `cssVars` | `object` | No | [CSS variable](/docs/developers/embedding/css-variables) overrides (Platform Plan) |
|
||||
| `darkModeDisabled` | `boolean` | No | Disable dark mode (Platform Plan) |
|
||||
| `language` | `string` | No | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts) |
|
||||
| `className` | `string` | No | CSS class for the iframe |
|
||||
| `user` | `object` | No | Current user info. When provided, enables the "Add Myself" button in the recipients list. Object with optional `email` and `name` fields |
|
||||
| `features` | `object` | No | Feature toggles for the editor experience |
|
||||
|
||||
### Create Component Only
|
||||
|
||||
@@ -131,7 +132,7 @@ const EnvelopeEditor = ({ presignToken, envelopeId }) => {
|
||||
|
||||
## Feature Toggles
|
||||
|
||||
V2 authoring provides rich, structured feature toggles organized into sections. Pass a partial configuration to customize the authoring experience — any omitted fields will use their defaults.
|
||||
V2 editor provides rich, structured feature toggles organized into sections. Pass a partial configuration to customize the editor experience — any omitted fields will use their defaults.
|
||||
|
||||
```jsx
|
||||
<EmbedCreateEnvelope
|
||||
@@ -159,7 +160,7 @@ V2 authoring provides rich, structured feature toggles organized into sections.
|
||||
|
||||
### General
|
||||
|
||||
Controls the overall authoring flow and UI:
|
||||
Controls the overall editor flow and UI:
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| ------------------------------- | --------- | ------- | ------------------------------------------------ |
|
||||
@@ -187,7 +188,7 @@ Controls envelope configuration options. Set to `null` to hide envelope settings
|
||||
|
||||
### Actions
|
||||
|
||||
Controls available actions during authoring:
|
||||
Controls available actions during editing:
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| ------------------ | --------- | ------- | ------------------------ |
|
||||
@@ -220,7 +221,7 @@ Controls recipient configuration options. Set to `null` to prevent any recipient
|
||||
|
||||
### Disabling Steps
|
||||
|
||||
You can also disable entire steps of the authoring flow. This allows you to skip steps that are not relevant to your use case:
|
||||
You can also disable entire steps of the editor flow. This allows you to skip steps that are not relevant to your use case:
|
||||
|
||||
```jsx
|
||||
<EmbedCreateEnvelope
|
||||
@@ -337,7 +338,7 @@ const EnvelopeManager = ({ presignToken }) => {
|
||||
|
||||
## See Also
|
||||
|
||||
- [Authoring Overview](/docs/developers/embedding/authoring) - V1 vs V2 comparison and presign tokens
|
||||
- [V1 Authoring](/docs/developers/embedding/authoring/v1) - V1 document and template authoring
|
||||
- [Editor Overview](/docs/developers/embedding/editor) - V1 vs V2 comparison and presign tokens
|
||||
- [V1 Editor](/docs/developers/embedding/editor/v1) - V1 document and template editor
|
||||
- [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: iframe
|
||||
description: Embed the signing experience directly in your application using an iframe.
|
||||
---
|
||||
|
||||
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" title="iframes are not recommended">
|
||||
Embedding via iframe is not recommended. We strongly recommend using the [official SDKs](/docs/developers/embedding/sdks) instead.
|
||||
</Callout>
|
||||
|
||||
### Basic iframe Embedding
|
||||
|
||||
```html
|
||||
<iframe
|
||||
src="https://app.documenso.com/embed/sign/abc123xyz"
|
||||
width="100%"
|
||||
height="800"
|
||||
frameborder="0"
|
||||
allow="clipboard-write"
|
||||
></iframe>
|
||||
```
|
||||
|
||||
<Callout title="Use the correct embed URL">
|
||||
The URL you embed depends on the embed mode you’re using (for example direct links vs sign-token embeds). Use the
|
||||
embed URL provided by Documenso for your flow.
|
||||
</Callout>
|
||||
|
||||
### iframe Customization
|
||||
|
||||
You can customize the embedded signing experience by passing **encoded options in the iframe URL fragment** (everything
|
||||
after `#`).
|
||||
|
||||
Documenso expects the fragment to be **base64** of:
|
||||
|
||||
- `encodeURIComponent(JSON.stringify(options))`
|
||||
|
||||
#### Supported options
|
||||
|
||||
| Option | Type | Description |
|
||||
| ------ | ---- | ----------- |
|
||||
| `name` | `string` | Prefill signer name. |
|
||||
| `email` | `string` | Prefill signer email. |
|
||||
| `lockName` | `boolean` | Lock the name field (prevents editing). |
|
||||
| `lockEmail` | `boolean` | Lock the email field (prevents editing). |
|
||||
| `language` | `string` | Force the embed language (e.g. `en`). |
|
||||
| `darkModeDisabled` | `boolean` | Disable dark mode behavior. |
|
||||
| `allowDocumentRejection` | `boolean` | Allow or disallow document rejection. |
|
||||
| `css` | `string` | Inject custom CSS into the embed. |
|
||||
| `cssVars` | `object` | Override embed CSS variables (see the CSS Variables page). |
|
||||
|
||||
#### Example
|
||||
|
||||
```ts
|
||||
const buildEmbedSrc = (host: string, token: string) => {
|
||||
const options = {
|
||||
name: 'Ada Lovelace',
|
||||
email: 'ada@example.com',
|
||||
lockName: true,
|
||||
lockEmail: true,
|
||||
language: 'en',
|
||||
darkModeDisabled: false,
|
||||
allowDocumentRejection: true,
|
||||
css: ':root { --radius: 12px; }',
|
||||
cssVars: {},
|
||||
};
|
||||
|
||||
const encodedOptions = btoa(encodeURIComponent(JSON.stringify(options)));
|
||||
|
||||
return `${new URL(`/embed/sign/${token}`, host).toString()}#${encodedOptions}`;
|
||||
};
|
||||
```
|
||||
|
||||
A complete example can be found in the [Embeds repository](https://github.com/documenso/embeds/blob/main/packages/mitosis/src/sign-document.lite.tsx).
|
||||
|
||||
<Callout type="info" title="Why use the URL fragment?">
|
||||
The fragment is **not sent to the server** as part of the HTTP request, but it is available to the embedded app in
|
||||
the browser. This makes it a convenient way to pass client-side configuration without changing the base embed URL.
|
||||
</Callout>
|
||||
@@ -6,14 +6,14 @@ description: Embed document signing experiences directly in your application usi
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
## Embedded Signing vs Embedded Authoring
|
||||
## Embedded Signing vs Embedded Editor
|
||||
|
||||
Documenso offers two types of embedding:
|
||||
|
||||
- **Embedded Signing** lets you embed the signing experience in your application. Your users sign documents without leaving your site. Available on Teams Plan and above.
|
||||
- **Embedded Authoring** lets you embed document and template _creation and editing_ in your application. This is an [Enterprise](/docs/policies/enterprise-edition) feature (also available as a Platform Plan add-on). See the [Authoring](/docs/developers/embedding/authoring) guide.
|
||||
- **Embedded Editor** lets you embed document and template _creation and editing_ in your application. This is an [Enterprise](/docs/policies/enterprise-edition) feature (also available as a Platform Plan add-on). See the [Editor](/docs/developers/embedding/editor) guide.
|
||||
|
||||
This page covers **embedded signing**. If you need your users to create or edit documents inside your app, see [Authoring](/docs/developers/embedding/authoring).
|
||||
This page covers **embedded signing**. If you need your users to create or edit documents inside your app, see [Editor](/docs/developers/embedding/editor).
|
||||
|
||||
---
|
||||
|
||||
@@ -229,9 +229,9 @@ Receives an object with:
|
||||
href="/docs/developers/embedding/css-variables"
|
||||
/>
|
||||
<Card
|
||||
title="Authoring"
|
||||
title="Editor"
|
||||
description="Embed document and template creation."
|
||||
href="/docs/developers/embedding/authoring"
|
||||
href="/docs/developers/embedding/editor"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Embedding",
|
||||
"pages": ["sdks", "direct-links", "css-variables", "authoring"]
|
||||
"pages": ["sdks", "direct-links", "css-variables", "editor", "iframe"]
|
||||
}
|
||||
|
||||
@@ -89,4 +89,4 @@ export class SigningComponent {
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
|
||||
@@ -93,4 +93,4 @@ See [CSS Variables](/docs/developers/embedding/css-variables) for all available
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
|
||||
@@ -133,4 +133,4 @@ const DocumentSigning = ({ token }: { token: string }) => {
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
|
||||
@@ -93,4 +93,4 @@ See [CSS Variables](/docs/developers/embedding/css-variables) for all available
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
|
||||
@@ -101,4 +101,4 @@ See [CSS Variables](/docs/developers/embedding/css-variables) for all available
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
|
||||
@@ -104,4 +104,4 @@ See [CSS Variables](/docs/developers/embedding/css-variables) for all available
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Props reference and concepts
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Authoring](/docs/developers/embedding/authoring) - Embed document creation
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
|
||||
@@ -73,14 +73,14 @@ Include the token in the `Authorization` header of your HTTP requests.
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
curl https://app.documenso.com/api/v2/documents \
|
||||
curl https://app.documenso.com/api/v2/document \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx"
|
||||
```
|
||||
|
||||
### JavaScript / TypeScript
|
||||
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/documents', {
|
||||
const response = await fetch('https://app.documenso.com/api/v2/document', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
|
||||
@@ -83,6 +83,15 @@ npm run prisma:seed -w @documenso/prisma
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Optional: configure job provider
|
||||
|
||||
The default local job provider does not support scheduled jobs required for document reminders.
|
||||
|
||||
See the [Background Jobs](/docs/self-hosting/configuration/background-jobs) page for more information.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Start the application
|
||||
|
||||
@@ -105,6 +114,20 @@ Access the Documenso application by visiting `http://localhost:3000` in your web
|
||||
certificate](/docs/developers/local-development/signing-certificate)**.
|
||||
</Callout>
|
||||
|
||||
## Running Scripts with Environment Variables
|
||||
|
||||
If a package script does not automatically load your `.env` and `.env.local` files, wrap it with the `with:env` script:
|
||||
|
||||
```bash
|
||||
npm run with:env -- npm run myscript
|
||||
```
|
||||
|
||||
The same works for `npx` when running bin scripts:
|
||||
|
||||
```bash
|
||||
npm run with:env -- npx myscript
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Developer Quickstart](/docs/developers/local-development/quickstart) - Quick Docker-based setup
|
||||
|
||||
@@ -53,8 +53,8 @@ The Enterprise Edition is required when you:
|
||||
- Document Action Reauthentication (Passkeys and 2FA)
|
||||
- 21 CFR Part 11 Compliance
|
||||
- Email Domains (custom sender addresses)
|
||||
- Embed Authoring
|
||||
- Embed Authoring White Label
|
||||
- Embed Editor
|
||||
- Embed Editor White Label
|
||||
- Custom signing certificates
|
||||
- Priority feature requests
|
||||
|
||||
|
||||
@@ -19,16 +19,19 @@ Use the limitless plans as much as you like. They are meant to offer a lot. Plea
|
||||
|
||||
### Do
|
||||
|
||||
- Sign as many documents as you need with the individual plan for your single business or organisation
|
||||
- Use the API and automation tools to automate your signing workflows
|
||||
- Experiment with plans and integrations while testing what you want to build
|
||||
- Use team or platform plans to run your workflows, even with significant volume, as long as it aligns with the plan’s intended purpose.
|
||||
- Experiment and automate freely within the plan features.
|
||||
- If volume grows beyond what’s sustainable on your plan, we’ll reach out to discuss an upgrade.
|
||||
- Assume that extreme usage will lead to us contacting you. You can scale up—or scale back. It’s about finding the right fit.
|
||||
|
||||
### Don't
|
||||
|
||||
- Use an individual account API to power a platform or product
|
||||
- Run a large company signing thousands of documents per day on a small team plan
|
||||
- Expect enterprise-level support on a fair support plan
|
||||
- Overthink this policy — if you are a paying customer, we want you to win
|
||||
- Use an individual account's API to power a platform or product.
|
||||
- Run a large company signing thousands of documents per day on a small team plan.
|
||||
- Expect enterprise-level support on a fair support plan (i.e. business edition).
|
||||
- Use a team plan to power an external platform or commercial product or platform beyond moderate testing.
|
||||
- Expect a platform plan to support enterprise-level volumes indefinitely without a conversation.
|
||||
- Don’t expect the platform plan to cover enterprise-scale volume or support. If you reach that point, we’ll reach out to guide you to the right fit.
|
||||
- Don’t overthink this – if you’re building something valuable, we want to see you succeed. If we need to talk, we will.
|
||||
|
||||
## Rate Limits
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"privacy",
|
||||
"terms",
|
||||
"security",
|
||||
"verify-email",
|
||||
"support"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Verifying Emails from Documenso
|
||||
description: How to confirm that an email is genuinely from Documenso, and what to do if you receive a suspicious message.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
## Check the Sender Domain
|
||||
|
||||
All email sent by Documenso originates from one of the following domains. If you receive an email claiming to be from Documenso and the sender address does not end in one of these domains, treat it as suspicious.
|
||||
|
||||
| Domain | Used for |
|
||||
| ------------------------ | -------------------------------------------------------------- |
|
||||
| `app.documenso.com` | Transactional email |
|
||||
| `documensomail.com` | Transactional email |
|
||||
| `documensoemail.com` | Transactional email |
|
||||
| Custom domain | [Enterprise organisations](/docs/users/organisations/email-domains) using a custom email domain |
|
||||
|
||||
Typical sender addresses include:
|
||||
|
||||
- `noreply@app.documenso.com`
|
||||
- `noreply@free.documensomail.com`
|
||||
- `noreply@send.documensoemail.com`
|
||||
|
||||
<Callout type="warn">
|
||||
A misspelling such as `documenso-email.com`, `documensoemaiI.com` (capital i instead of l), or any other variation is not a Documenso domain.
|
||||
</Callout>
|
||||
|
||||
## Types of Email Documenso Sends
|
||||
|
||||
Documenso sends email only for the following purposes:
|
||||
|
||||
- **Account verification** — confirming your email address when you sign up or change it
|
||||
- **Password reset** — a link to reset your password that you requested
|
||||
- **Document invitations** — notifying you that a document has been shared with you to sign, approve, or view
|
||||
- **Signing reminders** — follow-up reminders for pending document actions
|
||||
- **Completed document notifications** — confirmation that all parties have signed a document
|
||||
- **Team invitations** — inviting you to join an organisation or team
|
||||
|
||||
## What Documenso Will Never Do
|
||||
|
||||
- Ask for your password via email
|
||||
- Send you an attachment and ask you to open it to verify your identity
|
||||
- Ask you to confirm payment details or billing information over email
|
||||
- Send unsolicited marketing emails if you have not opted in
|
||||
|
||||
## How to Tell If an Email Is Legitimate
|
||||
|
||||
1. **Check the sender address** — the domain must be `documenso.com` or `documensomail.com`
|
||||
2. **Look at the link destination** — hover over any link before clicking; it should point to `app.documenso.com`
|
||||
3. **Watch for urgency or threats** — legitimate Documenso emails do not threaten account suspension to pressure you into clicking a link immediately
|
||||
4. **Verify the action yourself** — if in doubt, log in to [app.documenso.com](https://app.documenso.com) directly (not via the email link) and check whether the document or notification exists there
|
||||
|
||||
## Report a Suspicious Email
|
||||
|
||||
If you receive an email that appears to impersonate Documenso:
|
||||
|
||||
1. Do not click any links or download any attachments
|
||||
2. Forward the email as an attachment to **support@documenso.com**
|
||||
3. Delete the email from your inbox
|
||||
|
||||
You can also report phishing emails directly to your email provider using their built-in reporting tools.
|
||||
|
||||
## Related
|
||||
|
||||
- [Security Policy](/docs/policies/security) — Documenso's security practices and vulnerability disclosure process
|
||||
- [Create an Account](/docs/users/getting-started/create-account) — What to expect during sign-up
|
||||
- [Security Settings](/docs/users/settings/security) — Enable two-factor authentication and manage sessions
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
title: AI Recipient & Field Detection (Self-hosting)
|
||||
title: AI Recipient & Field Detection
|
||||
description: Configure Google Vertex AI so Documenso can detect recipients and fields automatically.
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
---
|
||||
title: Document Conversion
|
||||
description: Enable DOCX uploads on a self-hosted Documenso instance by running a Gotenberg sidecar that converts Word documents to PDF.
|
||||
---
|
||||
|
||||
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';
|
||||
|
||||
## Overview
|
||||
|
||||
Documenso can accept `.docx` uploads in addition to PDFs. When a user uploads a Word document, the Documenso server sends it to a [Gotenberg](https://gotenberg.dev) service which uses LibreOffice to convert it to PDF. The converted PDF is what gets stored, signed, and downloaded. The original DOCX is discarded.
|
||||
|
||||
This feature is **opt-in for self-hosted instances**. When the conversion service is not configured, DOCX uploads are rejected in the UI and only PDFs are accepted.
|
||||
|
||||
| Property | Value |
|
||||
| ----------------------- | -------------------------------------------------------------------- |
|
||||
| Conversion engine | [Gotenberg](https://gotenberg.dev) + LibreOffice |
|
||||
| Input format | `.docx` (Office Open XML Word documents) |
|
||||
| Output format | PDF |
|
||||
| Network requirement | Documenso must reach the Gotenberg HTTP API |
|
||||
| Default request timeout | 30 seconds per file |
|
||||
| Failure handling | An internal circuit breaker opens for 30 seconds after a failure |
|
||||
|
||||
<Callout type="info">
|
||||
Only `.docx` is accepted. Legacy `.doc`, `.odt`, `.rtf`, and other LibreOffice-supported formats
|
||||
are rejected at the upload step even when Gotenberg is configured.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- A running Gotenberg 8 instance with the LibreOffice module (`gotenberg/gotenberg:8-libreoffice` or newer).
|
||||
- Network reachability from the Documenso container to the Gotenberg HTTP API.
|
||||
- A version of Documenso that includes the document conversion feature.
|
||||
|
||||
## Build the Gotenberg Image
|
||||
|
||||
The upstream `gotenberg/gotenberg:8-libreoffice` image works out of the box, but it ships only **metric-compatible font substitutes** (Carlito for Calibri, Liberation for Arial/Times/Courier). Layout widths are preserved but documents will look noticeably different from Word.
|
||||
|
||||
For better fidelity, especially for non-Latin scripts, build a derived image that adds Microsoft Core Fonts and additional language fonts. The Documenso repository ships a reference Dockerfile at [`docker/development/Dockerfile.gotenberg`](https://github.com/documenso/documenso/blob/main/docker/development/Dockerfile.gotenberg) that you can use as a starting point:
|
||||
|
||||
```dockerfile
|
||||
FROM gotenberg/gotenberg:8-libreoffice
|
||||
|
||||
USER root
|
||||
|
||||
RUN echo "deb http://deb.debian.org/debian trixie contrib non-free" \
|
||||
> /etc/apt/sources.list.d/contrib.list \
|
||||
&& echo "ttf-mscorefonts-installer msttcorefonts/accepted-mscorefonts-eula select true" \
|
||||
| debconf-set-selections \
|
||||
&& apt-get update -qq \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends \
|
||||
ca-certificates \
|
||||
ttf-mscorefonts-installer \
|
||||
fonts-symbola \
|
||||
fonts-noto-extra \
|
||||
fonts-hosny-amiri \
|
||||
fonts-thai-tlwg \
|
||||
fonts-sil-padauk \
|
||||
fonts-sarai \
|
||||
fonts-samyak-taml \
|
||||
culmus \
|
||||
libfribidi0 \
|
||||
libharfbuzz0b \
|
||||
&& fc-cache -f \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
USER gotenberg
|
||||
```
|
||||
|
||||
<Callout type="warn">
|
||||
`ttf-mscorefonts-installer` accepts the Microsoft Core Fonts EULA on your behalf via debconf. By
|
||||
installing this image you are agreeing to those licence terms. Review them before publishing the
|
||||
image.
|
||||
</Callout>
|
||||
|
||||
Build and publish the image to a registry you control:
|
||||
|
||||
```bash
|
||||
docker build -t registry.example.com/documenso/gotenberg:8 \
|
||||
-f Dockerfile.gotenberg .
|
||||
docker push registry.example.com/documenso/gotenberg:8
|
||||
```
|
||||
|
||||
If you do not need extra fonts, skip the build step entirely and reference `gotenberg/gotenberg:8-libreoffice` directly in the next section.
|
||||
|
||||
## Deploy the Service
|
||||
|
||||
The Gotenberg service should run **alongside** your Documenso container, not exposed to the public internet. The conversion service has no built-in authorisation beyond HTTP Basic auth, so it should sit on a private network or behind your existing reverse proxy.
|
||||
|
||||
<Tabs items={['Docker Compose', 'Kubernetes', 'External Instance']}>
|
||||
<Tab value="Docker Compose">
|
||||
|
||||
Add a `gotenberg` service to the `compose.yml` you use for Documenso:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
gotenberg:
|
||||
image: registry.example.com/documenso/gotenberg:8
|
||||
# Or use upstream directly:
|
||||
# image: gotenberg/gotenberg:8-libreoffice
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GOTENBERG_API_BASIC_AUTH_USERNAME: ${GOTENBERG_USERNAME}
|
||||
GOTENBERG_API_BASIC_AUTH_PASSWORD: ${GOTENBERG_PASSWORD}
|
||||
command:
|
||||
- gotenberg
|
||||
- --api-enable-basic-auth
|
||||
- --libreoffice-deny-private-ips
|
||||
- --api-timeout=500s
|
||||
- --libreoffice-auto-start
|
||||
- --libreoffice-start-timeout=300s
|
||||
- --pdfengines-disable-routes
|
||||
- --webhook-disable
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '-fsS', 'http://localhost:3000/health']
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 20s
|
||||
|
||||
documenso:
|
||||
# existing config
|
||||
environment:
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_URL: http://gotenberg:3000
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME: ${GOTENBERG_USERNAME}
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD: ${GOTENBERG_PASSWORD}
|
||||
depends_on:
|
||||
gotenberg:
|
||||
condition: service_healthy
|
||||
```
|
||||
|
||||
Do **not** publish Gotenberg's port (`3000`) to the host. Documenso reaches it over the internal Docker network using the service name (`http://gotenberg:3000`).
|
||||
|
||||
</Tab>
|
||||
<Tab value="Kubernetes">
|
||||
|
||||
Create a Deployment, Service, and Secret. Example manifests:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: gotenberg-auth
|
||||
namespace: documenso
|
||||
stringData:
|
||||
username: documenso
|
||||
password: replace-me-with-a-strong-password
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: gotenberg
|
||||
namespace: documenso
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels: { app: gotenberg }
|
||||
template:
|
||||
metadata:
|
||||
labels: { app: gotenberg }
|
||||
spec:
|
||||
containers:
|
||||
- name: gotenberg
|
||||
image: registry.example.com/documenso/gotenberg:8
|
||||
args:
|
||||
- gotenberg
|
||||
- --api-enable-basic-auth
|
||||
- --libreoffice-deny-private-ips
|
||||
- --api-timeout=500s
|
||||
- --libreoffice-auto-start
|
||||
- --libreoffice-start-timeout=300s
|
||||
- --pdfengines-disable-routes
|
||||
- --webhook-disable
|
||||
env:
|
||||
- name: GOTENBERG_API_BASIC_AUTH_USERNAME
|
||||
valueFrom: { secretKeyRef: { name: gotenberg-auth, key: username } }
|
||||
- name: GOTENBERG_API_BASIC_AUTH_PASSWORD
|
||||
valueFrom: { secretKeyRef: { name: gotenberg-auth, key: password } }
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
readinessProbe:
|
||||
httpGet: { path: /health, port: 3000 }
|
||||
livenessProbe:
|
||||
httpGet: { path: /health, port: 3000 }
|
||||
initialDelaySeconds: 30
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: gotenberg
|
||||
namespace: documenso
|
||||
spec:
|
||||
selector: { app: gotenberg }
|
||||
ports:
|
||||
- port: 3000
|
||||
targetPort: 3000
|
||||
```
|
||||
|
||||
Then reference the in-cluster URL from Documenso's environment:
|
||||
|
||||
```
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_URL=http://gotenberg.documenso.svc.cluster.local:3000
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME=documenso
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD=replace-me-with-a-strong-password
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="External Instance">
|
||||
|
||||
Documenso does not have to colocate with Gotenberg. You can point it at any reachable Gotenberg deployment: a managed instance, a shared internal service, or a Gotenberg-compatible API.
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_URL=https://gotenberg.internal.example.com
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME=documenso
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD=replace-me-with-a-strong-password
|
||||
```
|
||||
|
||||
The remote instance must:
|
||||
|
||||
- Expose the LibreOffice route `/forms/libreoffice/convert`.
|
||||
- Be reachable from the Documenso container with low enough latency that the 30 second per-request timeout is comfortable.
|
||||
- Be on a private network or require authentication. Uploaded documents are sent to it as multipart form data and may contain sensitive content.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Recommended Gotenberg Flags
|
||||
|
||||
The flags in the examples above are not arbitrary. Each one matters for a production deployment.
|
||||
|
||||
| Flag | Why it matters |
|
||||
| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--api-enable-basic-auth` | Requires HTTP Basic credentials on every API route. Without this, anyone with network access to the container can convert arbitrary documents. |
|
||||
| `--libreoffice-deny-private-ips` | Rejects any outbound fetch LibreOffice tries to make to private, loopback, link-local, or cloud-metadata addresses while processing a document. Mitigates SSRF via malicious `.docx` files that embed `TargetMode="External"` references. Requires Gotenberg 8.32.0. |
|
||||
| `--api-timeout=500s` | Server-side request ceiling. Documenso aborts at 30 s by default, so this is a safety net for very large documents. |
|
||||
| `--libreoffice-auto-start` | Starts LibreOffice at container boot so the first request is not slow. |
|
||||
| `--libreoffice-start-timeout=300s`| Allows LibreOffice up to 5 minutes to come up under load. |
|
||||
| `--pdfengines-disable-routes` | Disables the PDF engines routes Documenso does not use. Shrinks the attack surface. |
|
||||
| `--webhook-disable` | Disables webhook callbacks. Documenso uses synchronous requests only. |
|
||||
|
||||
## Configure Documenso
|
||||
|
||||
Set the following environment variables on the Documenso container and restart it.
|
||||
|
||||
### Required
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL`| Base URL of the Gotenberg service (e.g., `http://gotenberg:3000`). Leave unset to disable the feature. |
|
||||
|
||||
### Optional
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------------------- | ------- | -------------------------------------------------------------------------------------------- |
|
||||
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME` | | HTTP Basic auth username. Set when Gotenberg runs with `--api-enable-basic-auth`. |
|
||||
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD` | | HTTP Basic auth password. Set together with the username. |
|
||||
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS`| `30000` | Per-request timeout in milliseconds. Increase for very large documents. |
|
||||
|
||||
<Callout type="info">
|
||||
When `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` is set, the public flag
|
||||
`NEXT_PUBLIC_DOCUMENT_CONVERSION_ENABLED` is derived automatically on server start. You do not
|
||||
need to set it yourself, and setting it manually has no effect.
|
||||
</Callout>
|
||||
|
||||
### Example `.env` Snippet
|
||||
|
||||
```bash
|
||||
# Document conversion (DOCX -> PDF)
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_URL=http://gotenberg:3000
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME=documenso
|
||||
NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD=replace-me-with-a-strong-password
|
||||
# NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS=60000
|
||||
```
|
||||
|
||||
## Verify the Setup
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Restart the Documenso container
|
||||
|
||||
Restart so the new environment variables are picked up.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Confirm Gotenberg is healthy
|
||||
|
||||
From a shell inside the Documenso container or another container on the same network:
|
||||
|
||||
```bash
|
||||
curl -fsS http://gotenberg:3000/health
|
||||
```
|
||||
|
||||
The endpoint is exempt from basic auth and should return `200 OK`.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Upload a test DOCX
|
||||
|
||||
In the Documenso web UI, open **Documents** and try uploading a small `.docx` file. The upload dropzone should accept it, and after a few seconds the editor should open with the converted PDF.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Check the server logs
|
||||
|
||||
Successful conversions log a `document_conversion_attempt` event with `result: "success"`, the duration, and the file size. Failures log the same event with `result: "error"` and an error code (`CONVERSION_SERVICE_UNAVAILABLE`, `CONVERSION_FAILED`, or `UNSUPPORTED_FILE_TYPE`).
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- **Treat the conversion service as untrusted internal infrastructure.** Documents pass through Gotenberg in plain form. Run it on a private network and require HTTP Basic auth.
|
||||
- **Run with `--libreoffice-deny-private-ips`.** Without this flag, a malicious `.docx` can trigger LibreOffice to fetch URLs from your internal network (SSRF).
|
||||
- **Disable unused routes.** `--pdfengines-disable-routes` and `--webhook-disable` reduce attack surface. Documenso only uses the LibreOffice convert route.
|
||||
- **Do not expose Gotenberg to the public internet.** Even with basic auth, this is a document-processing service with a non-trivial CPU and memory footprint; exposing it invites abuse.
|
||||
- **Rotate credentials.** Rotating the basic auth secret is a config change in both Gotenberg and Documenso, followed by a restart of each.
|
||||
|
||||
## Resource Sizing
|
||||
|
||||
Conversion is CPU- and memory-bound on LibreOffice. As a starting point:
|
||||
|
||||
| Workload | Suggested resources |
|
||||
| ----------------------------- | ------------------------------------ |
|
||||
| Light (a few DOCX per minute) | 1 vCPU, 1 GB RAM |
|
||||
| Moderate (sustained uploads) | 2 vCPU, 2 GB RAM |
|
||||
| Heavy / multi-tenant | Horizontally scale Gotenberg replicas behind a load balancer |
|
||||
|
||||
Gotenberg is stateless. Each container handles one or more concurrent requests independently. Scale horizontally rather than vertically once a single replica is saturated.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
<Accordions type="multiple">
|
||||
<Accordion title="DOCX uploads are rejected with 'Only PDF and DOCX files are allowed'">
|
||||
The Documenso server does not see `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL`. Check the value is set
|
||||
on the running container (`docker exec documenso printenv | grep DOCUMENT_CONVERSION`) and
|
||||
restart after changing it.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Uploads fail with 'Document conversion service is currently unavailable'">
|
||||
Documenso could not reach Gotenberg. Verify:
|
||||
|
||||
- The URL in `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` is resolvable from the Documenso container
|
||||
(use the Docker service name or in-cluster DNS, not `localhost`).
|
||||
- Gotenberg's `/health` endpoint returns `200`.
|
||||
- Basic auth credentials match between the two services.
|
||||
|
||||
After repeated failures, an internal circuit breaker opens for 30 seconds. Subsequent uploads
|
||||
will fail fast during that window; this is intentional and self-recovers.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Uploads fail with 'Failed to convert document to PDF'">
|
||||
Gotenberg was reachable but returned a non-2xx response. Check the Gotenberg container logs:
|
||||
|
||||
```bash
|
||||
docker compose logs -f gotenberg
|
||||
```
|
||||
|
||||
Common causes: corrupted `.docx` file, exotic embedded objects LibreOffice cannot render, or a
|
||||
file that genuinely exceeded the conversion timeout. Increase
|
||||
`NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS` for very large documents.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Converted PDFs look different from the Word document">
|
||||
LibreOffice is not byte-identical to Microsoft Word. Layout, font metrics, and complex elements
|
||||
(Charts, SmartArt, ActiveX controls) may differ. To improve fidelity:
|
||||
|
||||
- Use the custom Dockerfile in this guide to install Microsoft Core Fonts and additional
|
||||
language fonts.
|
||||
- Make sure any custom fonts referenced by your documents are installed in the Gotenberg image.
|
||||
- For pixel-perfect output, ask users to export to PDF from Word before uploading.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Form controls in the DOCX appear blank or missing">
|
||||
Documenso disables Gotenberg's `exportFormFields` flag during conversion. Word content controls
|
||||
(`<w:sdt>`) become static graphics in the output PDF, which prevents Documenso's later
|
||||
flattening step from making them invisible. This is intentional. Use Documenso fields
|
||||
(signature, text, date, etc.) for anything that needs to be filled in by signers.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Conversion is slow on the first request">
|
||||
LibreOffice starts lazily by default. Pass `--libreoffice-auto-start` to Gotenberg so it warms
|
||||
up at container boot. Allow up to a minute on first start before considering the service
|
||||
unhealthy.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="The circuit breaker keeps opening">
|
||||
Repeated failures open an in-process circuit breaker for 30 seconds. If you see this in
|
||||
production, the underlying problem is the Gotenberg service. Check its logs, resource usage,
|
||||
and connectivity. The breaker is per-process and resets on restart.
|
||||
</Accordion>
|
||||
</Accordions>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Upload Documents (User Guide)](/docs/users/documents/upload) - End-user view of DOCX uploads
|
||||
- [Environment Variables](/docs/self-hosting/configuration/environment) - Full configuration reference
|
||||
- [Docker Compose Deployment](/docs/self-hosting/deployment/docker-compose) - Compose-based deployment patterns
|
||||
- [Gotenberg Documentation](https://gotenberg.dev/docs/getting-started/introduction) - Upstream Gotenberg docs
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Advanced
|
||||
description: Optional configuration for OAuth providers, AI features, and other advanced settings.
|
||||
description: Optional configuration for OAuth providers, AI features, document conversion, and other advanced settings.
|
||||
---
|
||||
|
||||
<Cards>
|
||||
@@ -14,4 +14,9 @@ description: Optional configuration for OAuth providers, AI features, and other
|
||||
description="Enable AI-powered recipient and field detection."
|
||||
href="/docs/self-hosting/configuration/advanced/ai-features"
|
||||
/>
|
||||
<Card
|
||||
title="Document Conversion"
|
||||
description="Accept DOCX uploads by running a Gotenberg sidecar that converts Word documents to PDF."
|
||||
href="/docs/self-hosting/configuration/advanced/document-conversion"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Advanced",
|
||||
"pages": ["oauth-providers", "ai-features"]
|
||||
"pages": ["oauth-providers", "document-conversion", "ai-features"]
|
||||
}
|
||||
|
||||
@@ -224,28 +224,41 @@ For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/con
|
||||
|
||||
## Feature Flags
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public user registration entirely | `false` |
|
||||
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | |
|
||||
| Variable | Description | Default |
|
||||
| -------------------------------------------- | ----------------------------------------------------------------------------------- | ------- |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch. Disable all signup methods application-wide | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only. SSO signup is unaffected | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google. Existing Google-linked users can still sign in | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft. Existing linked users can still sign in | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC, including the organisation portal | `false` |
|
||||
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of email domains allowed to sign up (e.g., `example.com,acme.org`) | |
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | |
|
||||
| `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` |
|
||||
|
||||
### Signup Restrictions
|
||||
|
||||
You can control who is allowed to create accounts on your instance using two environment variables:
|
||||
You can control who is allowed to create accounts on your instance with the following environment variables:
|
||||
|
||||
- **`NEXT_PUBLIC_DISABLE_SIGNUP`**: Set to `true` to block all new signups. Existing users can still sign in. This applies to both email/password and OAuth signups.
|
||||
- **`NEXT_PUBLIC_DISABLE_SIGNUP`** (master switch): Set to `true` to block all new signups across every method (email/password, Google, Microsoft, OIDC). When set, this also blocks new-account creation through the organisation OIDC authentication portal.
|
||||
- **`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP`**: Set to `true` to disable email/password signup only. SSO signup is still allowed.
|
||||
- **`NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP`**, **`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`**, **`NEXT_PUBLIC_DISABLE_OIDC_SIGNUP`**: Set to `true` to block brand-new account creation through the matching SSO provider. Existing users with the provider already linked can still sign in, and existing users can still link the provider to their account. `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` also blocks new-account creation through the organisation authentication portal.
|
||||
- **`NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`**: Restrict signups to specific email domains. When set, only users whose email address matches one of the listed domains can create an account. Leave empty to allow all domains.
|
||||
|
||||
Both restrictions apply to email/password registration and OAuth (Google, Microsoft, OIDC). If a user attempts to sign up via OAuth with a disallowed domain, they are redirected to the sign-in page with an error.
|
||||
Sign-in for existing users is never affected, only the creation of brand-new accounts.
|
||||
|
||||
When both variables are set, `NEXT_PUBLIC_DISABLE_SIGNUP` takes precedence. Signups are blocked regardless of the domain list.
|
||||
Both the master switch and the domain allowlist apply to email/password registration and OAuth (Google, Microsoft, OIDC). If a user attempts to sign up via OAuth with a disallowed domain, they are redirected to the sign-in page with an error.
|
||||
|
||||
When both the master switch and the domain allowlist are set, the master switch takes precedence. Signups are blocked regardless of the domain list.
|
||||
|
||||
```bash
|
||||
# Allow signups only from specific domains
|
||||
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
|
||||
|
||||
# Allow OIDC signup only; block email/password, Google, Microsoft
|
||||
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP="true"
|
||||
NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP="true"
|
||||
NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true"
|
||||
|
||||
# Or disable signups entirely
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP="true"
|
||||
```
|
||||
@@ -266,6 +279,23 @@ AI features must also be enabled in organisation/team settings after configurati
|
||||
|
||||
---
|
||||
|
||||
## Document Conversion
|
||||
|
||||
Documenso can accept `.docx` uploads by sending them to a [Gotenberg](https://gotenberg.dev) service that converts them to PDF. When `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` is unset, DOCX uploads are rejected and only PDFs are accepted.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` | Base URL of the Gotenberg service (e.g., `http://gotenberg:3000`). Unset disables the feature. | |
|
||||
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_USERNAME` | HTTP Basic auth username. Required when Gotenberg runs with `--api-enable-basic-auth`. | |
|
||||
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_PASSWORD` | HTTP Basic auth password. Set together with the username. | |
|
||||
| `NEXT_PRIVATE_DOCUMENT_CONVERSION_TIMEOUT_MS` | Per-request timeout in milliseconds. Increase for very large documents. | `30000` |
|
||||
|
||||
The public flag `NEXT_PUBLIC_DOCUMENT_CONVERSION_ENABLED` is derived automatically from `NEXT_PRIVATE_DOCUMENT_CONVERSION_URL` on server start. Do not set it manually.
|
||||
|
||||
For setup, image-build instructions, and security recommendations, see [Document Conversion](/docs/self-hosting/configuration/advanced/document-conversion).
|
||||
|
||||
---
|
||||
|
||||
## Background Jobs
|
||||
|
||||
Documenso supports multiple background job providers for processing emails, documents, webhooks, and scheduled tasks.
|
||||
@@ -329,7 +359,7 @@ Telemetry collects only: app version, installation ID, and node ID. No personal
|
||||
|
||||
## Enterprise Features
|
||||
|
||||
These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed authoring, and 21 CFR Part 11 compliance.
|
||||
These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance.
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------------ | ------------------------------------------------ |
|
||||
@@ -371,6 +401,10 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password"
|
||||
|
||||
# Signup restrictions (optional)
|
||||
# NEXT_PUBLIC_DISABLE_SIGNUP="true"
|
||||
# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP="true"
|
||||
# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP="true"
|
||||
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true"
|
||||
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP="true"
|
||||
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
|
||||
```
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Storage Configuration
|
||||
description: Configure file storage for uploaded documents and signed PDFs using database storage (default) or S3-compatible object storage.
|
||||
description: Configure file storage for uploaded documents and signed PDFs using database storage (default), S3-compatible object storage, or Azure Blob Storage.
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
@@ -10,10 +10,11 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Storage Options
|
||||
|
||||
| Backend | Best For | Scalability | Configuration |
|
||||
| ---------- | -------------------------------- | ----------- | ------------- |
|
||||
| `database` | Small deployments, simplicity | Limited | None required |
|
||||
| `s3` | Production, large files, backups | High | Required |
|
||||
| Backend | Best For | Scalability | Configuration |
|
||||
| ------------ | --------------------------------------- | ----------- | ------------- |
|
||||
| `database` | Small deployments, simplicity | Limited | None required |
|
||||
| `s3` | Production, large files, backups | High | Required |
|
||||
| `azure-blob` | Production on Azure, native Blob access | High | Required |
|
||||
|
||||
Select the storage backend with the `NEXT_PUBLIC_UPLOAD_TRANSPORT` environment variable:
|
||||
|
||||
@@ -23,6 +24,9 @@ NEXT_PUBLIC_UPLOAD_TRANSPORT=database
|
||||
|
||||
# S3-compatible storage
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
|
||||
|
||||
# Azure Blob Storage (native)
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
|
||||
```
|
||||
|
||||
---
|
||||
@@ -283,6 +287,111 @@ NEXT_PRIVATE_UPLOAD_REGION=us-east-1
|
||||
|
||||
---
|
||||
|
||||
## Azure Blob Storage
|
||||
|
||||
Azure Blob Storage is supported as a native transport (not S3-compatible). Documenso uses the official `@azure/storage-blob` SDK and signs SAS URLs with the Storage Account key for browser uploads and downloads.
|
||||
|
||||
### Required Variables
|
||||
|
||||
| Variable | Description |
|
||||
| --------------------------------------- | ------------------------------------------------- |
|
||||
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Set to `azure-blob` |
|
||||
| `NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME` | Azure Storage Account name |
|
||||
| `NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY` | Azure Storage Account access key |
|
||||
| `NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER` | Container name where uploads are stored |
|
||||
|
||||
### Optional Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||
| `NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT` | Custom Blob endpoint URL. Useful for local development against Azurite (for example `http://127.0.0.1:10000`). | `https://<account>.blob.core.windows.net` |
|
||||
|
||||
### Azure Setup
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Create a Storage Account and Container
|
||||
|
||||
Create a Storage Account in the Azure Portal or via the Azure CLI, then create a container inside it:
|
||||
|
||||
```bash
|
||||
az storage account create \
|
||||
--name yourstorageaccount \
|
||||
--resource-group your-rg \
|
||||
--location eastus \
|
||||
--sku Standard_LRS
|
||||
|
||||
az storage container create \
|
||||
--name documenso-documents \
|
||||
--account-name yourstorageaccount
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Configure CORS on the container
|
||||
|
||||
The browser uploads documents directly to Azure Blob using a SAS URL, and downloads them the same way, so the Storage Account needs CORS rules that allow your application origin:
|
||||
|
||||
```bash
|
||||
az storage cors add \
|
||||
--services b \
|
||||
--methods GET PUT \
|
||||
--origins https://your-documenso-domain.com \
|
||||
--allowed-headers "Content-Type" "x-ms-blob-type" "Authorization" \
|
||||
--exposed-headers "*" \
|
||||
--max-age 3600 \
|
||||
--account-name yourstorageaccount
|
||||
```
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Configure Environment Variables
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
|
||||
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME=yourstorageaccount
|
||||
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY=your-account-key
|
||||
NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER=documenso-documents
|
||||
```
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
### Local Development with Azurite
|
||||
|
||||
Azurite is the official Azure Storage emulator. It supports the Blob REST API with account-key authentication.
|
||||
|
||||
```bash
|
||||
docker run -d --name azurite \
|
||||
-p 10000:10000 -p 10001:10001 -p 10002:10002 \
|
||||
mcr.microsoft.com/azure-storage/azurite
|
||||
```
|
||||
|
||||
Create the container against the well-known development account:
|
||||
|
||||
```bash
|
||||
az storage container create \
|
||||
--name documenso-documents \
|
||||
--connection-string "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;"
|
||||
```
|
||||
|
||||
Configure environment variables to point at the emulator:
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
|
||||
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_NAME=devstoreaccount1
|
||||
NEXT_PRIVATE_UPLOAD_AZURE_ACCOUNT_KEY=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==
|
||||
NEXT_PRIVATE_UPLOAD_AZURE_CONTAINER=documenso-documents
|
||||
NEXT_PRIVATE_UPLOAD_AZURE_ENDPOINT=http://127.0.0.1:10000
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The Azurite key shown above is the public well-known development key, published by Microsoft for emulator use. Never reuse it in production.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## CloudFront CDN (Optional)
|
||||
|
||||
Use Amazon CloudFront to serve documents with lower latency and reduced S3 costs. CloudFront integration uses signed URLs for secure access.
|
||||
|
||||
@@ -155,7 +155,13 @@ PORT=3000
|
||||
NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password
|
||||
|
||||
# Signup restrictions (optional)
|
||||
# Master switch — disables every signup method
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP=false
|
||||
# Per-method switches (optional). Each disables brand-new account creation through that method.
|
||||
# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=true
|
||||
# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP=true
|
||||
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=true
|
||||
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=true
|
||||
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org
|
||||
```
|
||||
|
||||
@@ -252,7 +258,10 @@ Navigate to the signup page and create your account. Verify your email address
|
||||
<Callout type="info">
|
||||
All accounts created through signup are regular user accounts. Admin access must be granted
|
||||
directly in the database. Once your accounts are set up, consider disabling public signups by
|
||||
setting `NEXT_PUBLIC_DISABLE_SIGNUP=true`, or restrict signups to specific email domains with
|
||||
setting `NEXT_PUBLIC_DISABLE_SIGNUP=true`. For finer control, use the per-method switches
|
||||
`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP`, `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP`,
|
||||
`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`, `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP`, or restrict
|
||||
signups to specific email domains with
|
||||
`NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`.
|
||||
</Callout>
|
||||
|
||||
|
||||
@@ -26,8 +26,14 @@ docker --version
|
||||
|
||||
## Pulling the Docker Image
|
||||
|
||||
The Documenso image is available on both DockerHub and GitHub Container Registry:
|
||||
|
||||
```bash
|
||||
# DockerHub
|
||||
docker pull documenso/documenso:latest
|
||||
|
||||
# GitHub Container Registry
|
||||
docker pull ghcr.io/documenso/documenso:latest
|
||||
```
|
||||
|
||||
### Available Tags
|
||||
@@ -100,7 +106,11 @@ See [Email Configuration](/docs/self-hosting/configuration/email) for other tran
|
||||
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for the signing certificate | - |
|
||||
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | Base64-encoded `.p12` certificate (alternative to file path) | - |
|
||||
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Document storage: `database` or `s3` | `database` |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public signups | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch — disable all signup methods | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google OAuth | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft OAuth | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal) | `false` |
|
||||
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
|
||||
|
||||
For the complete list, see [Environment Variables](/docs/self-hosting/configuration/environment).
|
||||
@@ -192,6 +202,14 @@ Documenso provides health check endpoints for monitoring:
|
||||
| `/api/health` | Checks database connectivity and certificate status |
|
||||
| `/api/certificate-status` | Returns whether a signing certificate is configured and usable |
|
||||
|
||||
Both endpoints return a JSON response with a `status` field:
|
||||
|
||||
| Status | Meaning |
|
||||
| ----------- | -------------------------------------------------------------------- |
|
||||
| `"ok"` | Everything is working properly |
|
||||
| `"warning"` | Application is running but there are certificate issues |
|
||||
| `"error"` | Critical issues (database unreachable, missing configuration, etc.) |
|
||||
|
||||
### Docker Health Check
|
||||
|
||||
Add a health check to your container:
|
||||
|
||||
@@ -24,7 +24,7 @@ Before deploying, you need:
|
||||
|
||||
The fastest way to deploy Documenso on Railway is using the official template:
|
||||
|
||||
[](https://railway.app/template/bG6D4p)
|
||||
[](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic)
|
||||
|
||||
This template automatically provisions:
|
||||
|
||||
@@ -153,7 +153,11 @@ NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@yourdomain.com
|
||||
| Variable | Description | Default |
|
||||
| --------------------------------- | ---------------------------------- | ------- |
|
||||
| `PORT` | Application port | `3000` |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public signups | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch — disable all signup methods | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google OAuth | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`| Block new accounts via Microsoft OAuth | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal)| `false` |
|
||||
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
|
||||
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - |
|
||||
| `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` |
|
||||
|
||||
@@ -3,6 +3,8 @@ title: Getting Started
|
||||
description: Requirements and quick start guide for self-hosting Documenso.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="Requirements"
|
||||
@@ -15,3 +17,11 @@ description: Requirements and quick start guide for self-hosting Documenso.
|
||||
href="/docs/self-hosting/getting-started/quick-start"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
<Callout type="error">
|
||||
**You must generate a signing certificate.** Documenso does not ship with one. Without a
|
||||
certificate, the application starts normally but document signing will fail on completion with
|
||||
errors.
|
||||
|
||||
Please see all the [requirements](/docs/self-hosting/getting-started/requirements) before proceeding.
|
||||
</Callout>
|
||||
|
||||
@@ -7,14 +7,29 @@ import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
## What You Need
|
||||
|
||||
Documenso requires the following external services:
|
||||
Documenso requires the following items and external services:
|
||||
|
||||
| Service | Purpose | Minimum Version |
|
||||
| ------------- | ---------------------------- | --------------- |
|
||||
| Signing certificate | Digital signature for documents | N/A |
|
||||
| PostgreSQL | Primary database | 14+ |
|
||||
| SMTP server | Sending emails to recipients | Any |
|
||||
| Reverse proxy | SSL termination, routing | Any |
|
||||
|
||||
|
||||
### Signing Certificate
|
||||
|
||||
<Callout type="error">
|
||||
Documenso does not ship with a signing certificate. Without one, the application starts normally
|
||||
but all document signing will fail. You must generate or provide a `.p12` certificate before going
|
||||
to production.
|
||||
</Callout>
|
||||
|
||||
Every completed document is digitally signed using an X.509 certificate. You can generate a self-signed certificate for free or use one from a Certificate Authority (CA).
|
||||
|
||||
- [Generate a local certificate](/docs/self-hosting/configuration/signing-certificate/local) — step-by-step instructions to create a `.p12` certificate
|
||||
- [All certificate options](/docs/self-hosting/configuration/signing-certificate) — self-signed, CA-issued, and Google Cloud HSM
|
||||
|
||||
### PostgreSQL Database
|
||||
|
||||
Documenso uses PostgreSQL for all data storage including documents, users, and audit logs. You cannot use MySQL, SQLite, or other databases.
|
||||
|
||||
@@ -154,6 +154,34 @@ See [Background Jobs Configuration](/docs/self-hosting/configuration/background-
|
||||
|
||||
---
|
||||
|
||||
## IPv6-Only Deployments
|
||||
|
||||
If you are deploying to an environment that uses only IPv6, set the `HOST` environment variable to `::` so the application binds to all IPv6 addresses:
|
||||
|
||||
**Docker:**
|
||||
|
||||
```bash
|
||||
docker run -it -e HOST=:: documenso/documenso:latest npm run start
|
||||
```
|
||||
|
||||
**Kubernetes or Docker Compose:**
|
||||
|
||||
```yaml
|
||||
containers:
|
||||
- name: documenso
|
||||
image: documenso/documenso:latest
|
||||
command:
|
||||
- npm
|
||||
args:
|
||||
- run
|
||||
- start
|
||||
env:
|
||||
- name: HOST
|
||||
value: '::'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Docker File Permissions
|
||||
|
||||
The Documenso container runs as a non-root user (UID 1001). If you mount files into the container (certificates, configuration), ensure they're readable:
|
||||
|
||||
@@ -3,6 +3,8 @@ title: Self-Hosting
|
||||
description: Deploy and manage your own Documenso instance for complete control over your data, compliance, and customization.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
## Getting Started
|
||||
|
||||
<Cards>
|
||||
@@ -18,6 +20,13 @@ description: Deploy and manage your own Documenso instance for complete control
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
<Callout type="error">
|
||||
**You must generate a signing certificate.** Documenso does not ship with one. Without a
|
||||
certificate, the application starts normally but document signing will fail.
|
||||
|
||||
Please see all the [requirements](/docs/self-hosting/getting-started/requirements) before proceeding.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Deployment Options
|
||||
@@ -122,7 +131,7 @@ See the [Quick Start guide](/docs/self-hosting/getting-started/quick-start) for
|
||||
|
||||
## Enterprise Edition
|
||||
|
||||
Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed authoring white label, or 21 CFR Part 11 compliance, you can activate them with a license key.
|
||||
Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed editor white label, or 21 CFR Part 11 compliance, you can activate them with a license key.
|
||||
|
||||
See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison.
|
||||
|
||||
|
||||
@@ -29,4 +29,9 @@ description: Advanced document features including PDF placeholders, AI detection
|
||||
description="Set a signing deadline so document links expire after a configurable period."
|
||||
href="/docs/users/documents/advanced/recipient-expiration"
|
||||
/>
|
||||
<Card
|
||||
title="Signing Reminders"
|
||||
description="Automatically email recipients who have not yet signed on a configurable schedule."
|
||||
href="/docs/users/documents/advanced/signing-reminders"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"ai-detection",
|
||||
"default-recipients",
|
||||
"document-visibility",
|
||||
"recipient-expiration"
|
||||
"recipient-expiration",
|
||||
"signing-reminders"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
---
|
||||
title: Signing Reminders
|
||||
description: Automatically email recipients who have not yet signed on a configurable schedule.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Overview
|
||||
|
||||
Signing reminders automatically email recipients who have not completed their signing. You configure when the first reminder goes out and how often it repeats — Documenso handles the rest until the recipient signs or the document leaves a pending state.
|
||||
|
||||
This is useful when:
|
||||
|
||||
- You want recipients nudged without having to track them down manually
|
||||
- A deadline is approaching and you want to escalate gradually
|
||||
- You send a high volume of documents and cannot chase each one
|
||||
|
||||
Reminders are tracked **per recipient**, not per document. Each unsigned recipient is on their own schedule based on when they were emailed.
|
||||
|
||||
This is different from the **Resend** action covered in [Send Documents](/docs/users/documents/send), which is a one-off manual nudge. Reminders are scheduled and recurring.
|
||||
|
||||
## Default Behaviour
|
||||
|
||||
Every **newly created** organisation starts with reminders **enabled**:
|
||||
|
||||
- First reminder sent **5 days** after the recipient is emailed
|
||||
- Repeats **every 2 days** until the recipient signs
|
||||
|
||||
You can change this default at the organisation or team level, or override it per document.
|
||||
|
||||
<Callout type="info">
|
||||
Organisations created **before this feature shipped** have reminders left blank (no default) so
|
||||
recipients of in-flight or future documents are not unexpectedly sent reminders. To enable
|
||||
reminders for an existing organisation, configure **Default Signing Reminders** under
|
||||
**Organisation Settings > Preferences > Document**.
|
||||
</Callout>
|
||||
|
||||
## Settings Cascade
|
||||
|
||||
Reminder settings follow a three-level cascade: **Organisation → Team → Document**. Each level can override the one above it, or inherit from it.
|
||||
|
||||
<Tabs items={['Organisation', 'Team', 'Document']}>
|
||||
<Tab value="Organisation">
|
||||
|
||||
Sets the default for all teams in the organisation. Options are **Enabled** (set when the first reminder fires and how often it repeats) or **No reminders**.
|
||||
|
||||
To configure, navigate to **Organisation Settings > Preferences > Document** and find **Default Signing Reminders**.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Team">
|
||||
|
||||
Overrides the organisation default for documents created within this team. Options are **Enabled**, **No reminders**, or **Inherit from organisation**.
|
||||
|
||||
New teams default to **Inherit from organisation**.
|
||||
|
||||
To configure, navigate to **Team Settings > Preferences > Document** and find **Default Signing Reminders**.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Document">
|
||||
|
||||
Overrides the team or organisation default for a single document. Options are **Enabled**, **No reminders**, or **Inherit from organisation**.
|
||||
|
||||
If you do not change reminders when editing a document, the team or organisation default applies.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Set Reminders for a Document
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
### Open the document settings
|
||||
|
||||
In the document editor, open the **Settings** dialog and go to the **Reminders** tab.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Choose a mode
|
||||
|
||||
- **Enabled** — Documenso sends reminders on the schedule you configure below
|
||||
- **No reminders** — no automatic reminders for this document
|
||||
- **Inherit from organisation** — use the team or organisation default
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Configure the schedule
|
||||
|
||||
When **Enabled**, set:
|
||||
|
||||
- **Send first reminder after** — a number and unit (days, weeks, or months) measured from when the recipient is first emailed
|
||||
- **Then repeat every** — either **Custom interval** (a number and unit) or **Don't repeat** (only one reminder is ever sent)
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Send the document
|
||||
|
||||
The first reminder is scheduled when the recipient receives the initial signing email. Subsequent reminders are scheduled from the time the previous reminder was sent.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
Editing reminder settings on a document that is already pending recalculates the next reminder for
|
||||
every unsigned recipient immediately.
|
||||
</Callout>
|
||||
|
||||
## Set a Default Reminder Schedule
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
### Navigate to document preferences
|
||||
|
||||
Go to **Organisation Settings > Preferences > Document** (or **Team Settings > Preferences > Document** for team-level overrides).
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Configure the default
|
||||
|
||||
Find **Default Signing Reminders** and choose:
|
||||
|
||||
- **Enabled** — set the first-reminder delay and repeat interval
|
||||
- **No reminders** — disable reminders by default
|
||||
- **Inherit from organisation** (team level only) — use whatever the organisation has configured
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Save
|
||||
|
||||
Click **Save** to apply. New documents created after this change use the updated default. Documents already in flight are unaffected unless you edit their reminder settings directly.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## What Happens When a Reminder Fires
|
||||
|
||||
When a recipient's next reminder time arrives:
|
||||
|
||||
1. Documenso sends the reminder email — same template as the original signing request, but the subject and preview are prefixed with **"Reminder:"**.
|
||||
2. An audit log entry is created for the recipient (an `EMAIL_SENT` entry of type `REMINDER`).
|
||||
3. The `document.reminder.sent` webhook fires. See [webhook events](/docs/developers/webhooks/events).
|
||||
4. The next reminder is scheduled based on **Then repeat every**, or no further reminder is scheduled if you chose **Don't repeat**.
|
||||
|
||||
Reminders stop automatically when the recipient signs, declines, the recipient's signing deadline passes, or the document leaves the pending state (completed, rejected, or deleted).
|
||||
|
||||
## When Reminders Are Skipped
|
||||
|
||||
A configured reminder will not be sent in the following cases:
|
||||
|
||||
- The recipient is a **CC** — CCs are notified once and never reminded
|
||||
- The recipient's **signing deadline has passed** — see [Recipient Expiration](/docs/users/documents/advanced/recipient-expiration). Resending the document refreshes the deadline and resumes reminders.
|
||||
- The document uses **manual link distribution** — Documenso never emails recipients for these documents
|
||||
- The envelope's email settings have **signing request emails disabled**
|
||||
- The recipient has not yet been emailed (for example, an unreached step in a sequential workflow)
|
||||
|
||||
<Callout type="info">
|
||||
Reminders stop automatically **30 days after the recipient was first emailed**, regardless of the
|
||||
repeat interval. This hard cap prevents runaway reminder chains for recipients who never sign and
|
||||
have no expiration set. If you need a different stop condition, set a shorter repeat interval,
|
||||
use **Don't repeat**, or configure [recipient expiration](/docs/users/documents/advanced/recipient-expiration).
|
||||
</Callout>
|
||||
|
||||
<Callout type="info">
|
||||
Reminders are dispatched by a background sweep that runs every 15 minutes, so the actual send time
|
||||
may be up to ~15 minutes after the scheduled time.
|
||||
</Callout>
|
||||
|
||||
## Reminder Options Reference
|
||||
|
||||
| Setting | Options | Notes |
|
||||
| --------------------------- | ---------------------------------------- | -------------------------------------------------------------------- |
|
||||
| **Mode** | Enabled / No reminders / Inherit | Inherit is only available at the team and document levels |
|
||||
| **Send first reminder after** | 1+ days, weeks, or months | Measured from when the recipient receives the initial signing email. Reminders past 30 days from that moment are skipped. |
|
||||
| **Then repeat every** | Custom interval (1+ days/weeks/months) or Don't repeat | Custom interval keeps reminding until the recipient signs or the 30-day cap is reached |
|
||||
|
||||
The organisation default out of the box is **first reminder after 5 days, repeating every 2 days**, which falls well inside the 30-day cap.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Send Documents](/docs/users/documents/send) - Send documents and trigger a one-off resend
|
||||
- [Recipient Expiration](/docs/users/documents/advanced/recipient-expiration) - Set a hard signing deadline
|
||||
- [Document Preferences](/docs/users/organisations/preferences/document) - Configure default document settings
|
||||
- [Webhook Events](/docs/developers/webhooks/events) - Subscribe to `document.reminder.sent`
|
||||
@@ -11,16 +11,41 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
| Limitation | Value |
|
||||
| ----------------------- | ----------------------------------- |
|
||||
| Supported format | PDF only |
|
||||
| Supported formats | PDF, DOCX |
|
||||
| Maximum file size | 50MB (configurable for self-hosted) |
|
||||
| Encrypted PDFs | Not supported |
|
||||
| Password-protected PDFs | Not supported |
|
||||
| Legacy `.doc` files | Not supported (convert to DOCX) |
|
||||
|
||||
<Callout type="warn">
|
||||
Documenso does not support password-protected or encrypted PDF files. Remove encryption before
|
||||
uploading.
|
||||
</Callout>
|
||||
|
||||
## Supported Formats
|
||||
|
||||
Documenso accepts two file formats:
|
||||
|
||||
- **PDF** (`.pdf`): used as-is. **Recommended.**
|
||||
- **Word** (`.docx`): converted to PDF on the server during upload. The converted PDF is what recipients sign.
|
||||
|
||||
Other formats (`.doc`, `.odt`, `.rtf`, images) are not supported. Convert them to PDF or DOCX before uploading.
|
||||
|
||||
<Callout type="warn">
|
||||
**Upload a PDF whenever you can.** DOCX files are converted to PDF using LibreOffice, which is not
|
||||
byte-identical to Microsoft Word. Spacing, line breaks, fonts, and complex elements (tables,
|
||||
charts, headers, footers) can shift in the converted PDF. For the final document to look exactly
|
||||
the way you designed it, export to PDF from Word, Google Docs, or Pages and upload the PDF
|
||||
directly.
|
||||
</Callout>
|
||||
|
||||
<Callout type="info">
|
||||
DOCX support requires the document conversion service. It is enabled on
|
||||
[documenso.com](https://app.documenso.com). Self-hosted instances must
|
||||
[configure it](/docs/self-hosting/configuration/advanced/document-conversion) before DOCX uploads
|
||||
are accepted.
|
||||
</Callout>
|
||||
|
||||
## Upload Methods
|
||||
|
||||

|
||||
@@ -38,15 +63,15 @@ You can upload documents in two ways:
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Drag and drop your PDF
|
||||
### Drag and drop your file
|
||||
|
||||
Drag a PDF file from your computer and drop it anywhere on the page.
|
||||
Drag a PDF or DOCX file from your computer and drop it anywhere on the page.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Wait for the upload to complete
|
||||
|
||||
The document will process and the editor will open when ready.
|
||||
The document will process and the editor will open when ready. DOCX files take a few extra seconds while they are converted to PDF.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -70,7 +95,7 @@ You can upload documents in two ways:
|
||||
<Step>
|
||||
### Select your file
|
||||
|
||||
Choose a PDF file from your computer.
|
||||
Choose a PDF or DOCX file from your computer.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
@@ -81,16 +106,32 @@ You can upload documents in two ways:
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## DOCX Conversion
|
||||
|
||||
We always recommend uploading a PDF rather than a DOCX. If you have the original document open in Word, Google Docs, or Pages, export to PDF from there and upload the PDF. The result is guaranteed to match what you see on screen.
|
||||
|
||||
If you do upload a `.docx` file, Documenso converts it to PDF before adding it to the envelope. The original `.docx` is discarded. Only the converted PDF is stored, signed, and downloaded.
|
||||
|
||||
Things to keep in mind when uploading DOCX:
|
||||
|
||||
- **The converted PDF will not be pixel-identical to your Word document.** Conversion uses LibreOffice, which renders most documents faithfully but differs from Microsoft Word in subtle ways. Spacing, font metrics, line breaks, and complex layout features can shift.
|
||||
- **Always review the converted PDF before adding fields or sending.** Open the document in the editor and scroll through every page to confirm it looks the way you expect.
|
||||
- **Form controls are flattened.** Word content controls (drop-downs, date pickers, checkboxes) become static text or graphics. Use Documenso fields for anything that needs to be filled in.
|
||||
- **Fonts not installed on the server fall back to substitutes.** On documenso.com, common fonts (Calibri, Arial, Times New Roman, etc.) are installed. On self-hosted instances, font fidelity depends on the operator's setup.
|
||||
- **Tracked changes and comments are preserved as they appear in Word.** Accept or reject changes and remove comments before uploading if you do not want them in the final document.
|
||||
|
||||
If the converted PDF does not match what you expect, export the document to PDF from Word, Google Docs, or another tool and upload the PDF directly.
|
||||
|
||||
## Uploading Multiple Documents
|
||||
|
||||
You can upload multiple PDF files at once to create a single envelope containing multiple documents. The number of files you can upload per envelope depends on your plan.
|
||||
You can upload multiple files at once to create a single envelope containing multiple documents. The number of files you can upload per envelope depends on your plan.
|
||||
|
||||
To upload multiple files:
|
||||
|
||||
- Select multiple PDF files when using the file picker, or
|
||||
- Drag and drop multiple PDF files at once
|
||||
- Select multiple PDF or DOCX files when using the file picker, or
|
||||
- Drag and drop multiple files at once
|
||||
|
||||
All files in the same upload become part of the same envelope and share the same recipients and signing workflow.
|
||||
You can mix PDF and DOCX files in the same upload. All files become part of the same envelope and share the same recipients and signing workflow.
|
||||
|
||||
<Callout type="info">
|
||||
If you need separate signing workflows for each document, upload them individually.
|
||||
@@ -114,15 +155,37 @@ The document remains in `Draft` status until you send it. You can close the edit
|
||||
<Accordion title="File is larger than 50MB">
|
||||
Reduce the file size before uploading:
|
||||
|
||||
- Compress images within the PDF
|
||||
- Compress images within the document
|
||||
- Remove unnecessary pages
|
||||
- Use a PDF compression tool
|
||||
- Use a PDF compression tool (for PDFs) or save with images downsampled (for DOCX)
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="Only PDF files are allowed">
|
||||
Convert your document to PDF before uploading. Most applications (Word, Google Docs, etc.) can
|
||||
export to PDF format.
|
||||
<Accordion title="Only PDF and DOCX files are allowed">
|
||||
Documenso accepts PDF and DOCX. For other formats (`.doc`, `.odt`, `.rtf`, etc.), export to PDF
|
||||
from your editor (Word, Google Docs, Pages) and upload the PDF.
|
||||
|
||||
If you are self-hosted and DOCX is rejected, the [document conversion
|
||||
service](/docs/self-hosting/configuration/advanced/document-conversion) is not configured on your
|
||||
instance.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="DOCX upload fails with a conversion error">
|
||||
The document conversion service was reachable but could not convert the file. Common causes:
|
||||
|
||||
- The `.docx` file is corrupted. Open it in Word, save a new copy, and try again.
|
||||
- The file uses very unusual fonts or embedded objects that LibreOffice cannot render.
|
||||
- The file is unusually large or complex and exceeded the conversion timeout.
|
||||
|
||||
If the problem persists, export the document to PDF from Word and upload the PDF directly.
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="DOCX upload fails with 'conversion service unavailable'">
|
||||
The document conversion service is down or temporarily unreachable. Try again in a minute. If you
|
||||
self-host, check the [document conversion
|
||||
service](/docs/self-hosting/configuration/advanced/document-conversion) logs.
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="You cannot upload encrypted PDFs">
|
||||
|
||||
@@ -39,7 +39,11 @@ Navigate to [documen.so/free](https://documen.so/free) to create a free account.
|
||||
|
||||
Provide your name, email address, and create a password. Alternatively, sign up with Google for faster access.
|
||||
|
||||
{/* TODO: Add screenshot of registration form */}
|
||||
<img
|
||||
src="/get-started-images/documenso-registration-form.webp"
|
||||
alt="Documenso registration form with name, email, and password fields"
|
||||
style={{width: '500px', height: '650px', objectFit: 'contain' }}
|
||||
/>
|
||||
|
||||
</Step>
|
||||
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
{
|
||||
"title": "Organisations",
|
||||
"pages": [
|
||||
"overview",
|
||||
"create-team",
|
||||
"members",
|
||||
"groups",
|
||||
"email-domains",
|
||||
"preferences",
|
||||
"single-sign-on",
|
||||
"billing"
|
||||
]
|
||||
"pages": ["overview", "create-team", "members", "groups", "email-domains", "preferences", "single-sign-on", "billing"]
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ To access the preferences, navigate to either the organisation or teams settings
|
||||
| **Include the Audit Logs** | Whether the audit logs are embedded in the document when downloaded. The audit logs are always available separately from the logs page. |
|
||||
| **Default Recipients** | Recipients that are automatically added to new documents. Can be overridden per document. |
|
||||
| **Default Envelope Expiration** | How long recipients have to sign before the signing link expires. See [recipient expiration](/docs/users/documents/advanced/recipient-expiration). |
|
||||
| **Default Signing Reminders** | When and how often to email recipients who have not yet signed. See [signing reminders](/docs/users/documents/advanced/signing-reminders). |
|
||||
| **Delegate Document Ownership** | Allow team API tokens to delegate document ownership to another team member. |
|
||||
| **AI Features** | Enable AI-powered features such as automatic recipient detection. Only shown if AI features are configured on the instance. |
|
||||
|
||||
|
||||
@@ -134,6 +134,13 @@ Leave empty to allow any domain authenticated by your identity provider.
|
||||
team.
|
||||
</Callout>
|
||||
|
||||
### Allow Personal Organisations
|
||||
|
||||
Controls whether users signing in via SSO for the first time also receive their own personal organisation in addition to joining your organisation.
|
||||
|
||||
- **Enabled**: New SSO users get a personal organisation where they can create and manage their own documents independently.
|
||||
- **Disabled**: New SSO users only join your organisation and do not receive a personal organisation.
|
||||
|
||||
## User Provisioning
|
||||
|
||||
When a user signs in through your SSO portal for the first time:
|
||||
|
||||
@@ -7,14 +7,14 @@ import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
<Callout type="error">
|
||||
Account deletion is permanent and irreversible. All documents, signatures, templates, and account
|
||||
data will be permanently removed. Any active subscription will be cancelled.
|
||||
Account deletion is permanent and irreversible. Your account, signatures, and personal data will be
|
||||
permanently removed, and any active subscription will be cancelled. How your organisations and
|
||||
documents are handled is explained below.
|
||||
</Callout>
|
||||
|
||||
## Before Deleting
|
||||
|
||||
- Download any documents you need to keep
|
||||
- Cancel any active subscriptions
|
||||
- Disable two-factor authentication (required before deletion)
|
||||
|
||||
## Delete Your Account
|
||||
@@ -36,6 +36,31 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
If you have two-factor authentication enabled, you must disable it before deleting your account.
|
||||
</Callout>
|
||||
|
||||
## What Happens to Your Organisations
|
||||
|
||||
When you delete your account, the organisations you **own** are permanently deleted along with all of
|
||||
their teams. If an owned organisation has an active subscription, it is scheduled for cancellation at
|
||||
the end of the current billing period.
|
||||
|
||||
Organisations that you are only a **member** of are not deleted. You are simply removed from them, and
|
||||
the organisation continues to operate as normal.
|
||||
|
||||
## What Happens to Your Documents
|
||||
|
||||
The way your documents and templates are handled depends on whether you owned the organisation they
|
||||
belong to:
|
||||
|
||||
- **Organisations you owned** — Completed and in-progress documents are retained in an anonymized form
|
||||
(reassigned to an internal system account) so the other parties keep their records. Draft documents
|
||||
and templates are permanently removed.
|
||||
- **Organisations you were a member of** — Your documents and templates are transferred to the
|
||||
organisation owner, so they remain accessible to the organisation after you leave.
|
||||
|
||||
<Callout type="warn">
|
||||
Documents that are retained in anonymized form are no longer associated with your account and cannot
|
||||
be recovered or accessed by you after deletion. Download anything you need to keep beforehand.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
@@ -296,12 +296,27 @@ const config = {
|
||||
},
|
||||
{
|
||||
source: '/developers/embedding/authoring',
|
||||
destination: '/docs/developers/embedding/authoring',
|
||||
destination: '/docs/developers/embedding/editor',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/developers/embedding/authoring/:path*',
|
||||
destination: '/docs/developers/embedding/editor/:path*',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/developers/embedded-authoring',
|
||||
destination: '/docs/developers/embedding/authoring',
|
||||
destination: '/docs/developers/embedding/editor',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/docs/developers/embedding/authoring',
|
||||
destination: '/docs/developers/embedding/editor',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/docs/developers/embedding/authoring/:path*',
|
||||
destination: '/docs/developers/embedding/editor/:path*',
|
||||
permanent: true,
|
||||
},
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"fumadocs-ui": "16.5.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"next": "16.2.4",
|
||||
"next": "16.2.6",
|
||||
"next-plausible": "^3.12.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.4",
|
||||
@@ -29,7 +29,7 @@
|
||||
"@types/node": "^25.1.0",
|
||||
"@types/react": "^19.2.10",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss": "^8.5.14",
|
||||
"tailwindcss": "^4.1.18",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
@@ -1,5 +1,5 @@
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { BookOpenIcon, CodeIcon, FileTextIcon, GithubIcon, ServerIcon, ShieldCheckIcon, UserIcon } from 'lucide-react';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
import {
|
||||
BookOpenIcon,
|
||||
CodeIcon,
|
||||
FileTextIcon,
|
||||
GithubIcon,
|
||||
ServerIcon,
|
||||
ShieldCheckIcon,
|
||||
UserIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Documenso Docs',
|
||||
description:
|
||||
@@ -22,21 +13,21 @@ export default function HomePage() {
|
||||
<main className="mx-auto max-w-4xl px-4 py-12">
|
||||
{/* Hero */}
|
||||
<div className="mb-16 pt-6 text-center">
|
||||
<h1 className="mb-4 text-4xl font-bold tracking-tight">Documenso Documentation</h1>
|
||||
<p className="text-fd-muted-foreground mx-auto mb-8 max-w-2xl text-lg">
|
||||
The open-source document signing platform. Send documents for signatures, integrate with
|
||||
your apps, or self-host with full control.
|
||||
<h1 className="mb-4 font-bold text-4xl tracking-tight">Documenso Documentation</h1>
|
||||
<p className="mx-auto mb-8 max-w-2xl text-fd-muted-foreground text-lg">
|
||||
The open-source document signing platform. Send documents for signatures, integrate with your apps, or
|
||||
self-host with full control.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<Link
|
||||
href="/docs/users"
|
||||
className="bg-documenso text-fd-primary-foreground hover:bg-documenso-dark/90 inline-flex items-center gap-2 rounded-lg px-5 py-2.5 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-documenso px-5 py-2.5 font-medium text-fd-primary-foreground text-sm transition-colors hover:bg-documenso-dark/90"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
<a
|
||||
href="https://github.com/documenso/documenso"
|
||||
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-5 py-2.5 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center gap-2 rounded-lg border bg-fd-background px-5 py-2.5 font-medium text-sm transition-colors hover:bg-fd-accent"
|
||||
>
|
||||
<GithubIcon className="size-4" />
|
||||
View on GitHub
|
||||
@@ -48,64 +39,60 @@ export default function HomePage() {
|
||||
<div className="mb-16 grid gap-4 md:grid-cols-3">
|
||||
<Link
|
||||
href="/docs/users"
|
||||
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
|
||||
className="group relative flex flex-col rounded-xl border bg-fd-card p-6 transition-all hover:border-fd-primary/50 hover:shadow-md"
|
||||
>
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
|
||||
<UserIcon className="size-6" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-lg font-semibold">User Guide</h2>
|
||||
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
|
||||
<h2 className="mb-2 font-semibold text-lg">User Guide</h2>
|
||||
<p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
|
||||
Send documents, create templates, and manage your team using the web application.
|
||||
</p>
|
||||
<span className="text-fd-primary text-sm font-medium">Get started →</span>
|
||||
<span className="font-medium text-fd-primary text-sm">Get started →</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/docs/developers"
|
||||
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
|
||||
className="group relative flex flex-col rounded-xl border bg-fd-card p-6 transition-all hover:border-fd-primary/50 hover:shadow-md"
|
||||
>
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-400">
|
||||
<CodeIcon className="size-6" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-lg font-semibold">Developer Guide</h2>
|
||||
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
|
||||
Integrate document signing into your applications with the REST API, webhooks, and
|
||||
embedding.
|
||||
<h2 className="mb-2 font-semibold text-lg">Developer Guide</h2>
|
||||
<p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
|
||||
Integrate document signing into your applications with the REST API, webhooks, and embedding.
|
||||
</p>
|
||||
<span className="text-fd-primary text-sm font-medium">View API docs →</span>
|
||||
<span className="font-medium text-fd-primary text-sm">View API docs →</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/docs/self-hosting"
|
||||
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
|
||||
className="group relative flex flex-col rounded-xl border bg-fd-card p-6 transition-all hover:border-fd-primary/50 hover:shadow-md"
|
||||
>
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-purple-500/10 text-purple-600 dark:text-purple-400">
|
||||
<ServerIcon className="size-6" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-lg font-semibold">Self-Hosting Guide</h2>
|
||||
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
|
||||
<h2 className="mb-2 font-semibold text-lg">Self-Hosting Guide</h2>
|
||||
<p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
|
||||
Deploy your own Documenso instance with Docker, Kubernetes, or Railway.
|
||||
</p>
|
||||
<span className="text-fd-primary text-sm font-medium">Deploy now →</span>
|
||||
<span className="font-medium text-fd-primary text-sm">Deploy now →</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Quick Start & Core Concepts */}
|
||||
<div className="mb-16 grid gap-8 md:grid-cols-2">
|
||||
<div className="bg-fd-card/50 rounded-xl border p-6">
|
||||
<div className="rounded-xl border bg-fd-card/50 p-6">
|
||||
<h3 className="mb-4 flex items-center gap-2 font-semibold">
|
||||
<BookOpenIcon className="text-fd-muted-foreground size-5" />
|
||||
<BookOpenIcon className="size-5 text-fd-muted-foreground" />
|
||||
Quick Start
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="mb-2 text-sm font-medium">Send your first document</h4>
|
||||
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
|
||||
<h4 className="mb-2 font-medium text-sm">Send your first document</h4>
|
||||
<ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
|
||||
<li>
|
||||
<Link
|
||||
href="/docs/users/getting-started/create-account"
|
||||
className="text-fd-primary hover:underline"
|
||||
>
|
||||
<Link href="/docs/users/getting-started/create-account" className="text-fd-primary hover:underline">
|
||||
Create an account
|
||||
</Link>
|
||||
</li>
|
||||
@@ -120,8 +107,8 @@ export default function HomePage() {
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-sm font-medium">Integrate with the API</h4>
|
||||
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
|
||||
<h4 className="mb-2 font-medium text-sm">Integrate with the API</h4>
|
||||
<ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
|
||||
<li>
|
||||
<Link
|
||||
href="/docs/developers/getting-started/authentication"
|
||||
@@ -141,8 +128,8 @@ export default function HomePage() {
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-sm font-medium">Deploy self-hosted</h4>
|
||||
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
|
||||
<h4 className="mb-2 font-medium text-sm">Deploy self-hosted</h4>
|
||||
<ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
|
||||
<li>
|
||||
<Link
|
||||
href="/docs/self-hosting/getting-started/requirements"
|
||||
@@ -164,36 +151,36 @@ export default function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-fd-card/50 rounded-xl border p-6">
|
||||
<div className="rounded-xl border bg-fd-card/50 p-6">
|
||||
<h3 className="mb-4 flex items-center gap-2 font-semibold">
|
||||
<BookOpenIcon className="text-fd-muted-foreground size-5" />
|
||||
<BookOpenIcon className="size-5 text-fd-muted-foreground" />
|
||||
Core Concepts
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link
|
||||
href="/docs/concepts/document-lifecycle"
|
||||
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
|
||||
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="mb-1 font-medium">Document Lifecycle</div>
|
||||
<div className="text-fd-muted-foreground text-xs">Draft to completed</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs/concepts/recipient-roles"
|
||||
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
|
||||
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="mb-1 font-medium">Recipient Roles</div>
|
||||
<div className="text-fd-muted-foreground text-xs">Signers and approvers</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs/concepts/field-types"
|
||||
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
|
||||
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="mb-1 font-medium">Field Types</div>
|
||||
<div className="text-fd-muted-foreground text-xs">Signatures and inputs</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs/concepts/signing-certificates"
|
||||
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
|
||||
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="mb-1 font-medium">Signing Certificates</div>
|
||||
<div className="text-fd-muted-foreground text-xs">Digital verification</div>
|
||||
@@ -206,7 +193,7 @@ export default function HomePage() {
|
||||
<div className="mb-16 grid gap-4 md:grid-cols-2">
|
||||
<Link
|
||||
href="/docs/compliance"
|
||||
className="bg-fd-card/50 hover:border-fd-primary/50 flex items-start gap-4 rounded-xl border p-5 transition-all"
|
||||
className="flex items-start gap-4 rounded-xl border bg-fd-card/50 p-5 transition-all hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400">
|
||||
<ShieldCheckIcon className="size-5" />
|
||||
@@ -221,7 +208,7 @@ export default function HomePage() {
|
||||
|
||||
<Link
|
||||
href="/docs/policies"
|
||||
className="bg-fd-card/50 hover:border-fd-primary/50 flex items-start gap-4 rounded-xl border p-5 transition-all"
|
||||
className="flex items-start gap-4 rounded-xl border bg-fd-card/50 p-5 transition-all hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-slate-500/10 text-slate-600 dark:text-slate-400">
|
||||
<FileTextIcon className="size-5" />
|
||||
@@ -236,22 +223,22 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{/* Community CTA */}
|
||||
<div className="from-fd-primary/5 to-fd-primary/10 rounded-xl border bg-gradient-to-r p-8 text-center">
|
||||
<h3 className="mb-2 text-lg font-semibold">Join the Community</h3>
|
||||
<p className="text-fd-muted-foreground mb-6 text-sm">
|
||||
<div className="rounded-xl border bg-gradient-to-r from-fd-primary/5 to-fd-primary/10 p-8 text-center">
|
||||
<h3 className="mb-2 font-semibold text-lg">Join the Community</h3>
|
||||
<p className="mb-6 text-fd-muted-foreground text-sm">
|
||||
Documenso is open source. Contribute, ask questions, or share feedback.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<a
|
||||
href="https://github.com/documenso/documenso"
|
||||
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center gap-2 rounded-lg border bg-fd-background px-4 py-2 font-medium text-sm transition-colors hover:bg-fd-accent"
|
||||
>
|
||||
<GithubIcon className="size-4" />
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://documen.so/discord"
|
||||
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center gap-2 rounded-lg border bg-fd-background px-4 py-2 font-medium text-sm transition-colors hover:bg-fd-accent"
|
||||
>
|
||||
<svg className="size-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||
@@ -260,7 +247,7 @@ export default function HomePage() {
|
||||
</a>
|
||||
<a
|
||||
href="https://app.documenso.com/signup"
|
||||
className="bg-documenso text-fd-primary-foreground hover:bg-documenso/90 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-documenso px-4 py-2 font-medium text-fd-primary-foreground text-sm transition-colors hover:bg-documenso/90"
|
||||
>
|
||||
Try Documenso
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
export const { GET } = createFromSource(source, {
|
||||
// https://docs.orama.com/docs/orama-js/supported-languages
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { LLMCopyButton, ViewOptions } from '@/components/ai/page-actions';
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import { getMDXComponents } from '@/mdx-components';
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
|
||||
const gitConfig = {
|
||||
user: 'documenso',
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { cn } from '@/lib/cn';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { getFilteredPageTree, source } from '@/lib/source';
|
||||
import type * as PageTree from 'fumadocs-core/page-tree';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { CodeIcon, ServerIcon, UserIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { cn } from '@/lib/cn';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { getFilteredPageTree, source } from '@/lib/source';
|
||||
|
||||
const ROOT_SECTIONS = [
|
||||
{
|
||||
@@ -44,7 +42,9 @@ function getFirstPageUrl(children: PageTree.Node[]): string | undefined {
|
||||
}
|
||||
if (child.type === 'folder' && child.children.length > 0) {
|
||||
const url = getFirstPageUrl(child.children);
|
||||
if (url) return url;
|
||||
if (url) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@@ -69,13 +69,8 @@ function SectionSwitcher({ activeSection }: { activeSection: string | null }) {
|
||||
>
|
||||
<Icon className={cn('mt-0.5 size-4 shrink-0', isActive ? 'text-fd-primary' : '')} />
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-medium">{section.label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs',
|
||||
isActive ? 'text-fd-muted-foreground' : 'text-fd-muted-foreground/70',
|
||||
)}
|
||||
>
|
||||
<span className="font-medium text-sm">{section.label}</span>
|
||||
<span className={cn('text-xs', isActive ? 'text-fd-muted-foreground' : 'text-fd-muted-foreground/70')}>
|
||||
{section.subtitle}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/shadcn.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
@import "tailwindcss";
|
||||
@import "fumadocs-ui/css/shadcn.css";
|
||||
@import "fumadocs-ui/css/preset.css";
|
||||
|
||||
@theme {
|
||||
/* Brand utility colors */
|
||||
@@ -43,13 +43,11 @@
|
||||
--sidebar-border: hsl(223.8136 0.0001% 89.8161%);
|
||||
--sidebar-ring: hsl(223.8136 0% 63.0163%);
|
||||
--font-sans:
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
@@ -103,13 +101,11 @@
|
||||
--sidebar-border: hsl(223.8136 0% 15.5096%);
|
||||
--sidebar-ring: hsl(223.8136 0% 32.1993%);
|
||||
--font-sans:
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import PlausibleProvider from 'next-plausible';
|
||||
|
||||
import './global.css';
|
||||
@@ -16,8 +15,7 @@ export const metadata: Metadata = {
|
||||
template: '%s | Documenso Docs',
|
||||
default: 'Documenso Docs',
|
||||
},
|
||||
description:
|
||||
'The official documentation for Documenso, the open-source document signing platform.',
|
||||
description: 'The official documentation for Documenso, the open-source document signing platform.',
|
||||
openGraph: {
|
||||
siteName: 'Documenso Docs',
|
||||
type: 'website',
|
||||
|
||||
@@ -3,20 +3,20 @@ import Link from 'next/link';
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="mx-auto flex max-w-xl flex-col items-center justify-center px-4 py-32 text-center">
|
||||
<h1 className="text-4xl font-bold tracking-tight">Page not found</h1>
|
||||
<p className="text-fd-muted-foreground mt-4 text-lg">
|
||||
<h1 className="font-bold text-4xl tracking-tight">Page not found</h1>
|
||||
<p className="mt-4 text-fd-muted-foreground text-lg">
|
||||
The page you are looking for may have moved. Our documentation was recently restructured.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
||||
<Link
|
||||
href="/docs/users"
|
||||
className="bg-documenso text-fd-primary-foreground hover:bg-documenso/90 inline-flex items-center rounded-lg px-5 py-2.5 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center rounded-lg bg-documenso px-5 py-2.5 font-medium text-fd-primary-foreground text-sm transition-colors hover:bg-documenso/90"
|
||||
>
|
||||
Browse documentation
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="bg-fd-background hover:bg-fd-accent inline-flex items-center rounded-lg border px-5 py-2.5 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center rounded-lg border bg-fd-background px-5 py-2.5 font-medium text-sm transition-colors hover:bg-fd-accent"
|
||||
>
|
||||
Go to homepage
|
||||
</Link>
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
const loadAssets = async () => {
|
||||
const [logoBuffer, interRegularData, interSemiBoldData, interBoldData] = await Promise.all([
|
||||
readFile(fileURLToPath(new URL('../../../../../public/logo.png', import.meta.url))),
|
||||
readFile(
|
||||
fileURLToPath(new URL('../../../../../public/fonts/inter-regular.ttf', import.meta.url)),
|
||||
),
|
||||
readFile(
|
||||
fileURLToPath(new URL('../../../../../public/fonts/inter-semibold.ttf', import.meta.url)),
|
||||
),
|
||||
readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-regular.ttf', import.meta.url))),
|
||||
readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-semibold.ttf', import.meta.url))),
|
||||
readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-bold.ttf', import.meta.url))),
|
||||
]);
|
||||
|
||||
@@ -40,104 +35,100 @@ export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...
|
||||
const { logoSrc, fonts } = await loadAssets();
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'white',
|
||||
padding: '60px 80px',
|
||||
fontFamily: 'Inter',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Green accent bar */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '6px',
|
||||
backgroundColor: '#6DC947',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Top: Logo */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={logoSrc} alt="Documenso" height="28" />
|
||||
<span
|
||||
style={{
|
||||
color: '#D4D4D8',
|
||||
fontSize: '28px',
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
|
|
||||
</span>
|
||||
<span style={{ color: '#71717A', fontSize: '20px', fontWeight: 400 }}>Docs</span>
|
||||
</div>
|
||||
|
||||
{/* Middle: Title + description */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'white',
|
||||
padding: '60px 80px',
|
||||
fontFamily: 'Inter',
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
gap: '16px',
|
||||
}}
|
||||
>
|
||||
{/* Green accent bar */}
|
||||
<div
|
||||
<h1
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '6px',
|
||||
backgroundColor: '#6DC947',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Top: Logo */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
color: '#18181B',
|
||||
fontSize: page.data.title.length > 40 ? '48px' : '56px',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.15,
|
||||
letterSpacing: '-0.025em',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={logoSrc} alt="Documenso" height="28" />
|
||||
<span
|
||||
{page.data.title}
|
||||
</h1>
|
||||
{page.data.description && (
|
||||
<p
|
||||
style={{
|
||||
color: '#D4D4D8',
|
||||
fontSize: '28px',
|
||||
color: '#71717A',
|
||||
fontSize: '22px',
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
|
|
||||
</span>
|
||||
<span style={{ color: '#71717A', fontSize: '20px', fontWeight: 400 }}>Docs</span>
|
||||
</div>
|
||||
|
||||
{/* Middle: Title + description */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
gap: '16px',
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
color: '#18181B',
|
||||
fontSize: page.data.title.length > 40 ? '48px' : '56px',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.15,
|
||||
letterSpacing: '-0.025em',
|
||||
lineHeight: 1.4,
|
||||
margin: 0,
|
||||
maxWidth: '900px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{page.data.title}
|
||||
</h1>
|
||||
{page.data.description && (
|
||||
<p
|
||||
style={{
|
||||
color: '#71717A',
|
||||
fontSize: '22px',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.4,
|
||||
margin: 0,
|
||||
maxWidth: '900px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{page.data.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom: URL */}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ color: '#A1A1AA', fontSize: '16px', fontWeight: 400 }}>
|
||||
docs.documenso.com{page.url}
|
||||
</span>
|
||||
</div>
|
||||
{page.data.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
|
||||
{/* Bottom: URL */}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ color: '#A1A1AA', fontSize: '16px', fontWeight: 400 }}>docs.documenso.com{page.url}</span>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { cn } from '@/lib/cn';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/ui/popover';
|
||||
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';
|
||||
import { Check, ChevronDown, Copy, ExternalLinkIcon, MessageCircleIcon } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
const cache = new Map<string, string>();
|
||||
|
||||
@@ -21,7 +20,9 @@ export function LLMCopyButton({
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [checked, onClick] = useCopyButton(async () => {
|
||||
const cached = cache.get(markdownUrl);
|
||||
if (cached) return navigator.clipboard.writeText(cached);
|
||||
if (cached) {
|
||||
return navigator.clipboard.writeText(cached);
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
@@ -48,7 +49,7 @@ export function LLMCopyButton({
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
size: 'sm',
|
||||
className: '[&_svg]:text-fd-muted-foreground gap-2 [&_svg]:size-3.5',
|
||||
className: 'gap-2 [&_svg]:size-3.5 [&_svg]:text-fd-muted-foreground',
|
||||
}),
|
||||
)}
|
||||
onClick={onClick}
|
||||
@@ -74,8 +75,7 @@ export function ViewOptions({
|
||||
githubUrl: string;
|
||||
}) {
|
||||
const items = useMemo(() => {
|
||||
const fullMarkdownUrl =
|
||||
typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading';
|
||||
const fullMarkdownUrl = typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading';
|
||||
const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`;
|
||||
|
||||
return [
|
||||
@@ -96,12 +96,7 @@ export function ViewOptions({
|
||||
q,
|
||||
})}`,
|
||||
icon: (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>OpenAI</title>
|
||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
||||
</svg>
|
||||
@@ -113,12 +108,7 @@ export function ViewOptions({
|
||||
q,
|
||||
})}`,
|
||||
icon: (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Anthropic</title>
|
||||
<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
|
||||
</svg>
|
||||
@@ -146,7 +136,7 @@ export function ViewOptions({
|
||||
)}
|
||||
>
|
||||
Open
|
||||
<ChevronDown className="text-fd-muted-foreground size-3.5" />
|
||||
<ChevronDown className="size-3.5 text-fd-muted-foreground" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="flex flex-col">
|
||||
{items.map((item) => (
|
||||
@@ -155,11 +145,11 @@ export function ViewOptions({
|
||||
href={item.href}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className="hover:text-fd-accent-foreground hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg p-2 text-sm [&_svg]:size-4"
|
||||
className="inline-flex items-center gap-2 rounded-lg p-2 text-sm hover:bg-fd-accent hover:text-fd-accent-foreground [&_svg]:size-4"
|
||||
>
|
||||
{item.icon}
|
||||
{item.title}
|
||||
<ExternalLinkIcon className="text-fd-muted-foreground ms-auto size-3.5" />
|
||||
<ExternalLinkIcon className="ms-auto size-3.5 text-fd-muted-foreground" />
|
||||
</a>
|
||||
))}
|
||||
</PopoverContent>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useId, useRef, useState } from 'react';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect, useId, useRef, useState } from 'react';
|
||||
|
||||
export const Mermaid = ({ chart }: { chart: string }) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { docs } from 'fumadocs-mdx:collections/server';
|
||||
import type * as PageTree from 'fumadocs-core/page-tree';
|
||||
import { type InferPageType, loader } from 'fumadocs-core/source';
|
||||
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
|
||||
import { docs } from 'fumadocs-mdx:collections/server';
|
||||
|
||||
// See https://fumadocs.dev/docs/headless/source-api for more info
|
||||
export const source = loader({
|
||||
@@ -30,9 +30,7 @@ export function getFilteredPageTree(rootName: string): PageTree.Root {
|
||||
// Find the main section folder
|
||||
const rootFolder = fullTree.children.find(
|
||||
(child): child is PageTree.Folder =>
|
||||
child.type === 'folder' &&
|
||||
typeof child.name === 'string' &&
|
||||
child.name.toLowerCase() === rootName.toLowerCase(),
|
||||
child.type === 'folder' && typeof child.name === 'string' && child.name.toLowerCase() === rootName.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!rootFolder) {
|
||||
@@ -42,9 +40,7 @@ export function getFilteredPageTree(rootName: string): PageTree.Root {
|
||||
// Find shared section folders
|
||||
const sharedFolders = fullTree.children.filter(
|
||||
(child): child is PageTree.Folder =>
|
||||
child.type === 'folder' &&
|
||||
typeof child.name === 'string' &&
|
||||
SHARED_SECTIONS.includes(child.name.toLowerCase()),
|
||||
child.type === 'folder' && typeof child.name === 'string' && SHARED_SECTIONS.includes(child.name.toLowerCase()),
|
||||
);
|
||||
|
||||
// Create separator for main section
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Mermaid } from '@/components/mdx/mermaid';
|
||||
import * as TabsComponents from 'fumadocs-ui/components/tabs';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import { Mermaid } from '@/components/mdx/mermaid';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function getMDXComponents(components?: MDXComponents): any {
|
||||
|
||||
+6
-22
@@ -2,11 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -20,12 +16,8 @@
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"fumadocs-mdx:collections/*": [
|
||||
".source/*"
|
||||
]
|
||||
"@/*": ["./src/*"],
|
||||
"fumadocs-mdx:collections/*": [".source/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
@@ -33,14 +25,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -10,10 +10,7 @@ export type TransformedData = {
|
||||
|
||||
const FORMAT = 'MMM yyyy';
|
||||
|
||||
export const addZeroMonth = (
|
||||
transformedData: TransformedData,
|
||||
isCumulative = false,
|
||||
): TransformedData => {
|
||||
export const addZeroMonth = (transformedData: TransformedData, isCumulative = false): TransformedData => {
|
||||
const result: TransformedData = {
|
||||
labels: [...transformedData.labels],
|
||||
datasets: transformedData.datasets.map((dataset) => ({
|
||||
|
||||
@@ -60,7 +60,9 @@ async function originHeadersFromReq(req: Request, origin: StaticOrigin | OriginF
|
||||
const reqOrigin = req.headers.get('Origin') || undefined;
|
||||
const value = typeof origin === 'function' ? await origin(reqOrigin, req) : origin;
|
||||
|
||||
if (!value) return;
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
return getOriginHeaders(reqOrigin, value);
|
||||
}
|
||||
|
||||
@@ -85,12 +87,17 @@ export default async function cors(req: Request, res: Response, options?: CorsOp
|
||||
const { headers } = res;
|
||||
const originHeaders = await originHeadersFromReq(req, opts.origin ?? false);
|
||||
const mergeHeaders = (v: string, k: string) => {
|
||||
if (k === 'Vary') headers.append(k, v);
|
||||
else headers.set(k, v);
|
||||
if (k === 'Vary') {
|
||||
headers.append(k, v);
|
||||
} else {
|
||||
headers.set(k, v);
|
||||
}
|
||||
};
|
||||
|
||||
// If there's no origin we won't touch the response
|
||||
if (!originHeaders) return res;
|
||||
if (!originHeaders) {
|
||||
return res;
|
||||
}
|
||||
|
||||
originHeaders.forEach(mergeHeaders);
|
||||
|
||||
@@ -98,9 +105,7 @@ export default async function cors(req: Request, res: Response, options?: CorsOp
|
||||
headers.set('Access-Control-Allow-Credentials', 'true');
|
||||
}
|
||||
|
||||
const exposed = Array.isArray(opts.exposedHeaders)
|
||||
? opts.exposedHeaders.join(',')
|
||||
: opts.exposedHeaders;
|
||||
const exposed = Array.isArray(opts.exposedHeaders) ? opts.exposedHeaders.join(',') : opts.exposedHeaders;
|
||||
|
||||
if (exposed) {
|
||||
headers.set('Access-Control-Expose-Headers', exposed);
|
||||
@@ -120,7 +125,9 @@ export default async function cors(req: Request, res: Response, options?: CorsOp
|
||||
headers.set('Access-Control-Max-Age', String(opts.maxAge));
|
||||
}
|
||||
|
||||
if (opts.preflightContinue) return res;
|
||||
if (opts.preflightContinue) {
|
||||
return res;
|
||||
}
|
||||
|
||||
headers.set('Content-Length', '0');
|
||||
return new Response(null, { status: opts.optionsSuccessStatus, headers });
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
import { DocumentStatus, EnvelopeType } from '@prisma/client';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
|
||||
import { addZeroMonth } from '../add-zero-month';
|
||||
|
||||
export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative' = 'count') => {
|
||||
@@ -30,9 +29,7 @@ export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative'
|
||||
datasets: [
|
||||
{
|
||||
label: type === 'count' ? 'Completed Documents per Month' : 'Total Completed Documents',
|
||||
data: result
|
||||
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
|
||||
.reverse(),
|
||||
data: result.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count))).reverse(),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -40,6 +37,4 @@ export const getCompletedDocumentsMonthly = async (type: 'count' | 'cumulative'
|
||||
return addZeroMonth(transformedData, type === 'cumulative');
|
||||
};
|
||||
|
||||
export type GetCompletedDocumentsMonthlyResult = Awaited<
|
||||
ReturnType<typeof getCompletedDocumentsMonthly>
|
||||
>;
|
||||
export type GetCompletedDocumentsMonthlyResult = Awaited<ReturnType<typeof getCompletedDocumentsMonthly>>;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { addZeroMonth } from '../add-zero-month';
|
||||
|
||||
@@ -29,9 +28,7 @@ export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' =
|
||||
datasets: [
|
||||
{
|
||||
label: type === 'count' ? 'Signers That Signed Up' : 'Total Signers That Signed Up',
|
||||
data: result
|
||||
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
|
||||
.reverse(),
|
||||
data: result.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count))).reverse(),
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -39,6 +36,4 @@ export const getSignerConversionMonthly = async (type: 'count' | 'cumulative' =
|
||||
return addZeroMonth(transformedData, type === 'cumulative');
|
||||
};
|
||||
|
||||
export type GetSignerConversionMonthlyResult = Awaited<
|
||||
ReturnType<typeof getSignerConversionMonthly>
|
||||
>;
|
||||
export type GetSignerConversionMonthlyResult = Awaited<ReturnType<typeof getSignerConversionMonthly>>;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { kyselyPrisma, sql } from '@documenso/prisma';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { addZeroMonth } from '../add-zero-month';
|
||||
|
||||
@@ -26,9 +25,7 @@ export const getUserMonthlyGrowth = async (type: 'count' | 'cumulative' = 'count
|
||||
datasets: [
|
||||
{
|
||||
label: type === 'count' ? 'New Users' : 'Total Users',
|
||||
data: result
|
||||
.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count)))
|
||||
.reverse(),
|
||||
data: result.map((row) => (type === 'count' ? Number(row.count) : Number(row.cume_count))).reverse(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { type TransformedData, addZeroMonth } from './add-zero-month';
|
||||
import { addZeroMonth, type TransformedData } from './add-zero-month';
|
||||
|
||||
type MetricKeys = {
|
||||
stars: number;
|
||||
@@ -24,13 +24,7 @@ const FRIENDLY_METRIC_NAMES: { [key in MetricKey]: string } = {
|
||||
earlyAdopters: 'Customers',
|
||||
};
|
||||
|
||||
export function transformData({
|
||||
data,
|
||||
metric,
|
||||
}: {
|
||||
data: DataEntry;
|
||||
metric: MetricKey;
|
||||
}): TransformedData {
|
||||
export function transformData({ data, metric }: { data: DataEntry; metric: MetricKey }): TransformedData {
|
||||
try {
|
||||
if (!data || Object.keys(data).length === 0) {
|
||||
return {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"dependencies": {
|
||||
"@documenso/prisma": "*",
|
||||
"luxon": "^3.7.2",
|
||||
"next": "16.2.4"
|
||||
"next": "16.2.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
|
||||
@@ -22,12 +22,6 @@
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -73,5 +73,12 @@ if [ -z "$NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET" ]; then
|
||||
echo "╚═════════════════════════════════════════════════════════════════════╝"
|
||||
fi
|
||||
|
||||
NEXT_PUBLIC_WEBAPP_URL=$(load_env_var "NEXT_PUBLIC_WEBAPP_URL")
|
||||
|
||||
if [ -z "$NEXT_PUBLIC_WEBAPP_URL" ]; then
|
||||
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
||||
echo "[INFO]: NEXT_PUBLIC_WEBAPP_URL not set, defaulting to $NEXT_PUBLIC_WEBAPP_URL"
|
||||
fi
|
||||
|
||||
echo "[INFO]: Starting Stripe webhook listener..."
|
||||
stripe listen --api-key "$NEXT_PRIVATE_STRIPE_API_KEY" --forward-to http://localhost:3000/api/stripe/webhook
|
||||
stripe listen --api-key "$NEXT_PRIVATE_STRIPE_API_KEY" --forward-to "$NEXT_PUBLIC_WEBAPP_URL/api/stripe/webhook"
|
||||
|
||||
+18
-18
@@ -1,9 +1,9 @@
|
||||
@import '@documenso/ui/styles/theme.css';
|
||||
@import "@documenso/ui/styles/theme.css";
|
||||
|
||||
/* Inter Variable Fonts */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('/fonts/inter-variablefont_opsz,wght.ttf') format('truetype-variations');
|
||||
font-family: "Inter";
|
||||
src: url("/fonts/inter-variablefont_opsz,wght.ttf") format("truetype-variations");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -11,8 +11,8 @@
|
||||
|
||||
/* Inter Italic Variable Fonts */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
src: url('/fonts/inter-italic-variablefont_opsz,wght.ttf') format('truetype-variations');
|
||||
font-family: "Inter";
|
||||
src: url("/fonts/inter-italic-variablefont_opsz,wght.ttf") format("truetype-variations");
|
||||
font-weight: 100 900;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
@@ -20,16 +20,16 @@
|
||||
|
||||
/* Caveat Variable Font */
|
||||
@font-face {
|
||||
font-family: 'Caveat';
|
||||
src: url('/fonts/caveat-variablefont_wght.ttf') format('truetype-variations');
|
||||
font-family: "Caveat";
|
||||
src: url("/fonts/caveat-variablefont_wght.ttf") format("truetype-variations");
|
||||
font-weight: 400 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
src: url('/fonts/noto-sans.ttf') format('truetype-variations');
|
||||
font-family: "Noto Sans";
|
||||
src: url("/fonts/noto-sans.ttf") format("truetype-variations");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -37,8 +37,8 @@
|
||||
|
||||
/* Korean noto sans */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans Korean';
|
||||
src: url('/fonts/noto-sans-korean.ttf') format('truetype-variations');
|
||||
font-family: "Noto Sans Korean";
|
||||
src: url("/fonts/noto-sans-korean.ttf") format("truetype-variations");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -46,8 +46,8 @@
|
||||
|
||||
/* Japanese noto sans */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans Japanese';
|
||||
src: url('/fonts/noto-sans-japanese.ttf') format('truetype-variations');
|
||||
font-family: "Noto Sans Japanese";
|
||||
src: url("/fonts/noto-sans-japanese.ttf") format("truetype-variations");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -55,8 +55,8 @@
|
||||
|
||||
/* Chinese noto sans */
|
||||
@font-face {
|
||||
font-family: 'Noto Sans Chinese';
|
||||
src: url('/fonts/noto-sans-chinese.ttf') format('truetype-variations');
|
||||
font-family: "Noto Sans Chinese";
|
||||
src: url("/fonts/noto-sans-chinese.ttf") format("truetype-variations");
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
@@ -64,8 +64,8 @@
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--font-sans: 'Inter';
|
||||
--font-signature: 'Caveat';
|
||||
--font-noto: 'Noto Sans', 'Noto Sans Korean', 'Noto Sans Japanese', 'Noto Sans Chinese';
|
||||
--font-sans: "Inter";
|
||||
--font-signature: "Caveat";
|
||||
--font-noto: "Noto Sans", "Noto Sans Korean", "Noto Sans Japanese", "Noto Sans Chinese";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { authClient } from '@documenso/auth/client';
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
@@ -21,6 +15,10 @@ import {
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
|
||||
export type AccountDeleteDialogProps = {
|
||||
className?: string;
|
||||
@@ -36,8 +34,7 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
|
||||
|
||||
const [enteredEmail, setEnteredEmail] = useState<string>('');
|
||||
|
||||
const { mutateAsync: deleteAccount, isPending: isDeletingAccount } =
|
||||
trpc.profile.deleteAccount.useMutation();
|
||||
const { mutateAsync: deleteAccount, isPending: isDeletingAccount } = trpc.profile.deleteAccount.useMutation();
|
||||
|
||||
const onDeleteAccount = async () => {
|
||||
try {
|
||||
@@ -63,18 +60,15 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Alert
|
||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
|
||||
variant="neutral"
|
||||
>
|
||||
<Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
|
||||
<div>
|
||||
<AlertTitle>
|
||||
<Trans>Delete Account</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription className="mr-2">
|
||||
<Trans>
|
||||
Delete your account and all its contents, including completed documents. This action
|
||||
is irreversible and will cancel your subscription, so proceed with caution.
|
||||
Delete your account and all its contents, including completed documents. This action is irreversible and
|
||||
will cancel your subscription, so proceed with caution.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
@@ -109,10 +103,8 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Documenso will delete{' '}
|
||||
<span className="font-semibold">all of your documents</span>, along with all of
|
||||
your completed documents, signatures, and all other resources belonging to your
|
||||
Account.
|
||||
Documenso will delete <span className="font-semibold">all of your documents</span>, along with all
|
||||
of your completed documents, signatures, and all other resources belonging to your Account.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@@ -121,9 +113,7 @@ export const AccountDeleteDialog = ({ className }: AccountDeleteDialogProps) =>
|
||||
<div>
|
||||
<Label>
|
||||
<Trans>
|
||||
Please type{' '}
|
||||
<span className="text-muted-foreground font-semibold">{user.email}</span> to
|
||||
confirm.
|
||||
Please type <span className="font-semibold text-muted-foreground">{user.email}</span> to confirm.
|
||||
</Trans>
|
||||
</Label>
|
||||
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -19,6 +12,11 @@ import {
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
export type AdminDocumentDeleteDialogProps = {
|
||||
envelopeId: string;
|
||||
@@ -32,8 +30,7 @@ export const AdminDocumentDeleteDialog = ({ envelopeId }: AdminDocumentDeleteDia
|
||||
|
||||
const [reason, setReason] = useState('');
|
||||
|
||||
const { mutateAsync: deleteDocument, isPending: isDeletingDocument } =
|
||||
trpc.admin.document.delete.useMutation();
|
||||
const { mutateAsync: deleteDocument, isPending: isDeletingDocument } = trpc.admin.document.delete.useMutation();
|
||||
|
||||
const handleDeleteDocument = async () => {
|
||||
try {
|
||||
@@ -64,18 +61,13 @@ export const AdminDocumentDeleteDialog = ({ envelopeId }: AdminDocumentDeleteDia
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<Alert
|
||||
className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row"
|
||||
variant="neutral"
|
||||
>
|
||||
<Alert className="flex flex-col items-center justify-between gap-4 p-6 md:flex-row" variant="neutral">
|
||||
<div>
|
||||
<AlertTitle>
|
||||
<Trans>Delete Document</Trans>
|
||||
</AlertTitle>
|
||||
<AlertDescription className="mr-2">
|
||||
<Trans>
|
||||
Delete the document. This action is irreversible so proceed with caution.
|
||||
</Trans>
|
||||
<Trans>Delete the document. This action is irreversible so proceed with caution.</Trans>
|
||||
</AlertDescription>
|
||||
</div>
|
||||
|
||||
@@ -105,12 +97,7 @@ export const AdminDocumentDeleteDialog = ({ envelopeId }: AdminDocumentDeleteDia
|
||||
<Trans>To confirm, please enter the reason</Trans>
|
||||
</DialogDescription>
|
||||
|
||||
<Input
|
||||
className="mt-2"
|
||||
type="text"
|
||||
value={reason}
|
||||
onChange={(e) => setReason(e.target.value)}
|
||||
/>
|
||||
<Input className="mt-2" type="text" value={reason} onChange={(e) => setReason(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { ZCreateAdminOrganisationRequestSchema } from '@documenso/trpc/server/admin-router/create-admin-organisation.types';
|
||||
@@ -22,16 +12,16 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import type { z } from 'zod';
|
||||
|
||||
export type OrganisationCreateDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
@@ -44,11 +34,7 @@ const ZCreateAdminOrganisationFormSchema = ZCreateAdminOrganisationRequestSchema
|
||||
|
||||
type TCreateOrganisationFormSchema = z.infer<typeof ZCreateAdminOrganisationFormSchema>;
|
||||
|
||||
export const AdminOrganisationCreateDialog = ({
|
||||
trigger,
|
||||
ownerUserId,
|
||||
...props
|
||||
}: OrganisationCreateDialogProps) => {
|
||||
export const AdminOrganisationCreateDialog = ({ trigger, ownerUserId, ...props }: OrganisationCreateDialogProps) => {
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
@@ -101,11 +87,7 @@ export const AdminOrganisationCreateDialog = ({
|
||||
}, [open, form]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
{...props}
|
||||
open={open}
|
||||
onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}
|
||||
>
|
||||
<Dialog {...props} open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
|
||||
<DialogTrigger onClick={(e) => e.stopPropagation()} asChild={true}>
|
||||
{trigger ?? (
|
||||
<Button className="flex-shrink-0" variant="secondary">
|
||||
@@ -127,10 +109,7 @@ export const AdminOrganisationCreateDialog = ({
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset
|
||||
className="flex h-full flex-col space-y-4"
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -149,10 +128,7 @@ export const AdminOrganisationCreateDialog = ({
|
||||
|
||||
<Alert variant="neutral">
|
||||
<AlertDescription className="mt-0">
|
||||
<Trans>
|
||||
You will need to configure any claims or subscription after creating this
|
||||
organisation
|
||||
</Trans>
|
||||
<Trans>You will need to configure any claims or subscription after creating this organisation</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
export type AdminOrganisationDeleteDialogProps = {
|
||||
organisationId: string;
|
||||
organisationName: string;
|
||||
trigger?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const AdminOrganisationDeleteDialog = ({
|
||||
organisationId,
|
||||
organisationName,
|
||||
trigger,
|
||||
}: AdminOrganisationDeleteDialogProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { t } = useLingui();
|
||||
const { toast } = useToast();
|
||||
|
||||
const deleteMessage = t`delete ${organisationName}`;
|
||||
|
||||
const ZAdminDeleteOrganisationFormSchema = z.object({
|
||||
organisationName: z.literal(deleteMessage, {
|
||||
errorMap: () => ({ message: t`You must enter '${deleteMessage}' to proceed` }),
|
||||
}),
|
||||
sendEmailToOwner: z.boolean(),
|
||||
});
|
||||
|
||||
type TAdminDeleteOrganisationFormSchema = z.infer<typeof ZAdminDeleteOrganisationFormSchema>;
|
||||
|
||||
const form = useForm<TAdminDeleteOrganisationFormSchema>({
|
||||
resolver: zodResolver(ZAdminDeleteOrganisationFormSchema),
|
||||
defaultValues: {
|
||||
organisationName: '',
|
||||
sendEmailToOwner: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteOrganisation } = trpc.admin.organisation.delete.useMutation();
|
||||
|
||||
const onFormSubmit = async (values: TAdminDeleteOrganisationFormSchema) => {
|
||||
try {
|
||||
await deleteOrganisation({
|
||||
organisationId,
|
||||
organisationName,
|
||||
sendEmailToOwner: values.sendEmailToOwner,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: t`Deletion scheduled`,
|
||||
description: t`The organisation will be deleted in the background. Documents will be orphaned, not deleted.`,
|
||||
duration: 7500,
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
} catch (err) {
|
||||
const error = AppError.parseError(err);
|
||||
console.error(error);
|
||||
|
||||
toast({
|
||||
title: t`An error occurred`,
|
||||
description: t`We encountered an error while attempting to delete this organisation. Please try again later.`,
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
form.reset();
|
||||
}
|
||||
}, [open, form]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !form.formState.isSubmitting && setOpen(value)}>
|
||||
<DialogTrigger asChild>
|
||||
{trigger ?? (
|
||||
<Button variant="destructive">
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent position="center">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Delete organisation</Trans>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
You are about to delete <span className="font-semibold">{organisationName}</span>. This action is not
|
||||
reversible. All teams will be removed and all documents will be orphaned to the deleted-account service
|
||||
account.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>
|
||||
<Trans>
|
||||
The deletion will run in the background, and can take up to a few minutes to complete. Do not re-run this
|
||||
deletion.
|
||||
</Trans>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset className="flex h-full flex-col space-y-4" disabled={form.formState.isSubmitting}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="organisationName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<Trans>
|
||||
Confirm by typing <span className="text-destructive">{deleteMessage}</span>
|
||||
</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input className="bg-background" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sendEmailToOwner"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
id="admin-delete-organisation-send-email"
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<label
|
||||
htmlFor="admin-delete-organisation-send-email"
|
||||
className="font-normal text-muted-foreground text-sm leading-snug"
|
||||
>
|
||||
<Trans>Email the organisation owner to notify them of the deletion.</Trans>
|
||||
</label>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="secondary" onClick={() => setOpen(false)}>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<Button type="submit" variant="destructive" loading={form.formState.isSubmitting}>
|
||||
<Trans>Delete</Trans>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,3 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { AppError } from '@documenso/lib/errors/app-error';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
|
||||
@@ -20,6 +13,11 @@ import {
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
export type AdminOrganisationMemberDeleteDialogProps = {
|
||||
organisationId: string;
|
||||
@@ -42,33 +40,32 @@ export const AdminOrganisationMemberDeleteDialog = ({
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { mutateAsync: deleteOrganisationMember, isPending } =
|
||||
trpc.admin.organisationMember.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Member has been removed from the organisation.`),
|
||||
duration: 5000,
|
||||
});
|
||||
const { mutateAsync: deleteOrganisationMember, isPending } = trpc.admin.organisationMember.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
toast({
|
||||
title: _(msg`Success`),
|
||||
description: _(msg`Member has been removed from the organisation.`),
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
setOpen(false);
|
||||
setOpen(false);
|
||||
|
||||
// Refresh the page to show updated data
|
||||
await navigate(0);
|
||||
},
|
||||
onError: (err) => {
|
||||
const error = AppError.parseError(err);
|
||||
// Refresh the page to show updated data
|
||||
await navigate(0);
|
||||
},
|
||||
onError: (err) => {
|
||||
const error = AppError.parseError(err);
|
||||
|
||||
console.error(error);
|
||||
console.error(error);
|
||||
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
description: _(msg`We couldn't remove this member. Please try again later.`),
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
});
|
||||
},
|
||||
});
|
||||
toast({
|
||||
title: _(msg`An error occurred`),
|
||||
description: _(msg`We couldn't remove this member. Please try again later.`),
|
||||
variant: 'destructive',
|
||||
duration: 10000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(value) => !isPending && setOpen(value)}>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user