mirror of
https://github.com/documenso/documenso.git
synced 2026-06-26 14:22:14 +10:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b171f7d1b | |||
| d05d051053 | |||
| 30c5cf6d70 | |||
| dfac91916c | |||
| a5a795cb7b | |||
| b6da33282e | |||
| 0d1496bc26 | |||
| b5e1874ced | |||
| 0b8d107291 | |||
| f66f01a09a | |||
| 960217c78d |
@@ -1,239 +0,0 @@
|
||||
---
|
||||
date: 2026-03-26
|
||||
title: Bullmq Background Jobs
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The codebase has a well-designed background job provider abstraction (`BaseJobProvider`) with two existing implementations:
|
||||
|
||||
- **InngestJobProvider** — cloud/SaaS provider, externally hosted
|
||||
- **LocalJobProvider** — database-backed (Postgres via Prisma), uses HTTP self-calls to dispatch
|
||||
|
||||
The goal is to add a third provider backed by a proper job queue library for self-hosted deployments that need more reliability than the Local provider offers.
|
||||
|
||||
### Current Architecture
|
||||
|
||||
All code lives in `packages/lib/jobs/`:
|
||||
|
||||
- `client/base.ts` — Abstract `BaseJobProvider` with 4 methods: `defineJob()`, `triggerJob()`, `getApiHandler()`, `startCron()`
|
||||
- `client/client.ts` — `JobClient` facade, selects provider via `NEXT_PRIVATE_JOBS_PROVIDER` env var
|
||||
- `client/inngest.ts` — Inngest implementation
|
||||
- `client/local.ts` — Local/Postgres implementation
|
||||
- `client/_internal/job.ts` — Core types: `JobDefinition`, `JobRunIO`, `SimpleTriggerJobOptions`
|
||||
- `definitions/` — 19 job definitions (15 event-triggered, 4 cron)
|
||||
|
||||
The `JobRunIO` interface provided to handlers includes:
|
||||
|
||||
- `runTask(cacheKey, callback)` — idempotent task execution (cached via `BackgroundJobTask` table)
|
||||
- `triggerJob(cacheKey, options)` — chain jobs from within handlers
|
||||
- `wait(cacheKey, ms)` — delay/sleep (not implemented in Local provider)
|
||||
- `logger` — structured logging
|
||||
|
||||
### Local Provider Limitations
|
||||
|
||||
The current Local provider has several issues that motivate this work:
|
||||
|
||||
1. `io.wait()` throws "Not implemented"
|
||||
2. HTTP self-call with 150ms fire-and-forget `Promise.race` is fragile
|
||||
3. No concurrency control — jobs run in the web server process
|
||||
4. No real retry backoff (immediate re-dispatch)
|
||||
5. No monitoring/visibility into job status
|
||||
6. Jobs compete for resources with HTTP request handling
|
||||
|
||||
---
|
||||
|
||||
## Provider Evaluation
|
||||
|
||||
Three alternatives were evaluated against the existing provider interface and project requirements.
|
||||
|
||||
### BullMQ (Redis-backed) — Recommended
|
||||
|
||||
| Attribute | Detail |
|
||||
| ------------------- | -------------------------- |
|
||||
| Backend | Redis 7.x |
|
||||
| npm downloads/month | ~15M |
|
||||
| TypeScript | Native |
|
||||
| Delayed jobs | Yes (ms precision) |
|
||||
| Cron/repeatable | Yes (`upsertJobScheduler`) |
|
||||
| Retries + backoff | Yes (exponential, custom) |
|
||||
| Concurrency control | Yes (per-worker) |
|
||||
| Rate limiting | Yes (per-queue, per-group) |
|
||||
| Dashboard | Bull Board (mature) |
|
||||
| New infrastructure | Yes — Redis required |
|
||||
|
||||
**Why BullMQ**: Most mature and widely-adopted Node.js queue. Native delayed jobs solve the `io.wait()` gap. Redis is purpose-built for queue workloads and keeps Postgres clean for application data. Bull Board gives immediate operational visibility. The provider abstraction already exists so wrapping BullMQ is straightforward.
|
||||
|
||||
**Trade-off**: Requires Redis, which is additional infrastructure. However, Redis is a single Docker Compose service or a free Upstash tier, and the operational benefit is significant.
|
||||
|
||||
### pg-boss (PostgreSQL-backed) — Strong Alternative
|
||||
|
||||
| Attribute | Detail |
|
||||
| ------------------- | ----------------------------- |
|
||||
| Backend | PostgreSQL (existing) |
|
||||
| npm downloads/month | ~1.4M |
|
||||
| TypeScript | Native |
|
||||
| Delayed jobs | Yes (`startAfter`) |
|
||||
| Cron/repeatable | Yes (`schedule()`) |
|
||||
| New infrastructure | No — reuses existing Postgres |
|
||||
|
||||
**Why it could work**: Zero new infrastructure since the project already uses Postgres. API maps well to existing patterns.
|
||||
|
||||
**Why it's second choice**: Polling-based (no LISTEN/NOTIFY), adds write amplification to the primary database, smaller ecosystem, no dashboard. At scale, queue operations on the primary database become a concern.
|
||||
|
||||
### Graphile Worker (PostgreSQL-backed) — Less Suitable
|
||||
|
||||
Uses LISTEN/NOTIFY for instant pickup but has a file-based task convention and separate schema that don't mesh well with the existing Prisma-centric architecture. Would require more adapter work.
|
||||
|
||||
### Improving the Local Provider — Not Recommended
|
||||
|
||||
Fixing the Local provider's issues (wait support, replacing HTTP self-calls, adding concurrency control, backoff) essentially means rebuilding a queue library from scratch with less robustness and no community maintenance.
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Proceed with BullMQ.** It's the most capable option, maps cleanly to the existing provider interface, and is the standard choice for production Node.js applications. Redis is lightweight infrastructure with managed options available at every cloud provider.
|
||||
|
||||
**If Redis is a hard blocker**, pg-boss is the clear fallback — but the plan below assumes BullMQ.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: BullMQ Provider Core
|
||||
|
||||
**File: `packages/lib/jobs/client/bullmq.ts`**
|
||||
|
||||
Create `BullMQJobProvider extends BaseJobProvider` with singleton pattern matching the existing providers.
|
||||
|
||||
Key implementation details:
|
||||
|
||||
1. **Constructor / `getInstance()`**
|
||||
- Initialize a Redis `IORedis` connection using new env var: `NEXT_PRIVATE_REDIS_URL`
|
||||
- Create a single `Queue` instance for dispatching jobs, using `NEXT_PRIVATE_REDIS_PREFIX` as the BullMQ `prefix` option (defaults to `documenso` if unset). This namespaces all Redis keys so multiple environments (worktrees, branches, developers) sharing the same Redis instance don't collide.
|
||||
- Create a single `Worker` instance for processing jobs (in-process, same prefix)
|
||||
- Store job definitions in a `_jobDefinitions` record (same pattern as Local provider)
|
||||
|
||||
2. **`defineJob()`**
|
||||
- Store definition in `_jobDefinitions` keyed by ID
|
||||
- If the definition has a `trigger.cron`, register it via `queue.upsertJobScheduler()` with the cron expression
|
||||
|
||||
3. **`triggerJob(options)`**
|
||||
- Find eligible definitions by `trigger.name` (same lookup as Local provider)
|
||||
- For each, call `queue.add(jobDefinitionId, payload)` with appropriate options
|
||||
- Support `options.id` for deduplication via BullMQ's `jobId` option
|
||||
|
||||
4. **`getApiHandler()`**
|
||||
- Return a minimal health-check / queue-status handler. Unlike the Local provider, BullMQ workers don't need an HTTP endpoint to receive jobs — they pull from Redis directly. The API handler can return queue metrics for monitoring.
|
||||
|
||||
5. **`startCron()`**
|
||||
- No-op — cron is handled by BullMQ's `upsertJobScheduler` registered during `defineJob()`
|
||||
|
||||
6. **Worker setup**
|
||||
- Single worker processes all job types by dispatching to the correct handler from `_jobDefinitions`
|
||||
- Configure concurrency with a default of 10 (overridable via `NEXT_PRIVATE_BULLMQ_CONCURRENCY` env var for those who need to tune it)
|
||||
- Configure retry with exponential backoff: `backoff: { type: 'exponential', delay: 1000 }`
|
||||
- Default 3 retries (matching current Local provider behavior)
|
||||
|
||||
7. **`createJobRunIO(jobId)`** — Implement `JobRunIO`:
|
||||
- `runTask()`: Reuse the existing `BackgroundJobTask` Prisma table for idempotent task tracking (same pattern as Local provider)
|
||||
- `triggerJob()`: Delegate to `this.triggerJob()`
|
||||
- `wait()`: Throw "Not implemented" (same as Local provider). No handler uses `io.wait()` so this has zero impact
|
||||
- `logger`: Same console-based logger pattern as Local provider
|
||||
|
||||
### Phase 2: Provider Registration
|
||||
|
||||
**File: `packages/lib/jobs/client/client.ts`**
|
||||
|
||||
Add `'bullmq'` case to the provider match:
|
||||
|
||||
```typescript
|
||||
this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
|
||||
.with('inngest', () => InngestJobProvider.getInstance())
|
||||
.with('bullmq', () => BullMQJobProvider.getInstance())
|
||||
.otherwise(() => LocalJobProvider.getInstance());
|
||||
```
|
||||
|
||||
**File: `packages/tsconfig/process-env.d.ts`**
|
||||
|
||||
Add `'bullmq'` to the `NEXT_PRIVATE_JOBS_PROVIDER` type union and add Redis env var types:
|
||||
|
||||
```typescript
|
||||
NEXT_PRIVATE_JOBS_PROVIDER?: 'inngest' | 'local' | 'bullmq';
|
||||
NEXT_PRIVATE_REDIS_URL?: string;
|
||||
NEXT_PRIVATE_REDIS_PREFIX?: string;
|
||||
NEXT_PRIVATE_BULLMQ_CONCURRENCY?: string;
|
||||
```
|
||||
|
||||
**File: `.env.example`**
|
||||
|
||||
Add Redis configuration examples:
|
||||
|
||||
```env
|
||||
NEXT_PRIVATE_JOBS_PROVIDER="local" # Options: local, inngest, bullmq
|
||||
NEXT_PRIVATE_REDIS_URL="redis://localhost:63790"
|
||||
NEXT_PRIVATE_REDIS_PREFIX="documenso" # Namespace for Redis keys (useful when sharing a Redis instance)
|
||||
```
|
||||
|
||||
**File: `turbo.json`**
|
||||
|
||||
Add `NEXT_PRIVATE_REDIS_URL`, `NEXT_PRIVATE_REDIS_PREFIX`, and `NEXT_PRIVATE_BULLMQ_CONCURRENCY` to the env vars list for cache invalidation.
|
||||
|
||||
### Phase 3: Infrastructure & Dependencies
|
||||
|
||||
**File: `packages/lib/package.json`**
|
||||
|
||||
Add dependencies:
|
||||
|
||||
- `bullmq` — the queue library
|
||||
- `ioredis` — Redis client (peer dependency of BullMQ, but explicit is better)
|
||||
|
||||
**File: `docker-compose.yml` (or equivalent)**
|
||||
|
||||
Add Redis service for local development:
|
||||
|
||||
```yaml
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- '6379:6379'
|
||||
```
|
||||
|
||||
### Phase 4: Optional Enhancements
|
||||
|
||||
These are not required for the initial implementation but worth considering for follow-up:
|
||||
|
||||
1. **Bull Board integration** — Add a `/api/jobs/dashboard` route that serves Bull Board UI for monitoring. Gate behind an admin auth check.
|
||||
|
||||
2. **Separate worker process** — Add an `apps/worker` entry point that runs BullMQ workers without the web server, for deployments that want to isolate job processing from request handling.
|
||||
|
||||
3. **Graceful shutdown** — Register `SIGTERM`/`SIGINT` handlers to call `worker.close()` and `queue.close()` for clean shutdown.
|
||||
|
||||
4. **BackgroundJob table integration** — Optionally continue writing to the `BackgroundJob` Prisma table for audit/history, using BullMQ events (`completed`, `failed`) to update status. This preserves the existing database-level visibility.
|
||||
|
||||
---
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
| File | Action | Description |
|
||||
| ------------------------------------ | ---------- | ---------------------------------------- |
|
||||
| `packages/lib/jobs/client/bullmq.ts` | **Create** | BullMQ provider implementation |
|
||||
| `packages/lib/jobs/client/client.ts` | Modify | Add `'bullmq'` provider case |
|
||||
| `packages/tsconfig/process-env.d.ts` | Modify | Add type for `'bullmq'` + Redis env vars |
|
||||
| `.env.example` | Modify | Add Redis config example |
|
||||
| `turbo.json` | Modify | Add Redis env var to cache keys |
|
||||
| `packages/lib/package.json` | Modify | Add `bullmq` + `ioredis` dependencies |
|
||||
| `docker-compose.yml` | Modify | Add Redis service |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Should the BullMQ provider also write to the `BackgroundJob` Prisma table?** This would maintain audit history and allow existing admin tooling to query job status. Trade-off is dual-write complexity.
|
||||
|
||||
2. **Redis connection resilience**: Should the provider gracefully degrade if Redis is unavailable (e.g., fall back to Local provider), or fail hard? Failing hard is simpler and more predictable.
|
||||
|
||||
## Resolved Questions
|
||||
|
||||
- **`io.wait()`**: Not a concern. Only Inngest implements it (via `step.sleep`), the Local provider throws "Not implemented", and no job handler calls `io.wait()`. The BullMQ provider can throw "Not implemented" identically to the Local provider.
|
||||
@@ -1,138 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,122 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
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.)
|
||||
@@ -1,113 +0,0 @@
|
||||
---
|
||||
date: 2026-03-07
|
||||
title: Search Query Optimization
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
The `searchDocumentsWithKeyword` and `searchTemplatesWithKeyword` functions generate a single massive Prisma `findMany` with 7 OR branches. This produces a SQL query that:
|
||||
|
||||
- Joins `Team` twice (aliased j3 and j8) for the two team-access branches
|
||||
- Embeds 4-level deep EXISTS subqueries (`TeamGroup -> OrganisationGroup -> OrganisationGroupMember -> OrganisationMember`) for each team branch
|
||||
- Uses `ILIKE` across multiple columns with no way for Postgres to use indexes effectively across the OR
|
||||
- Includes `recipients: true` on the result even though only a small subset of fields are needed
|
||||
- Fetches all matching rows then filters visibility **in application code**
|
||||
|
||||
With 1,000 documents seeded under `medium-account@documenso.com`, this query is noticeably slow.
|
||||
|
||||
---
|
||||
|
||||
## Option A: Pre-resolve team IDs, keep Prisma
|
||||
|
||||
**Change:** Before the envelope query, resolve the user's accessible team IDs in a single query:
|
||||
|
||||
```ts
|
||||
const teamIds = await prisma.teamGroup
|
||||
.findMany({
|
||||
where: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: { organisationMember: { userId } },
|
||||
},
|
||||
},
|
||||
},
|
||||
select: { teamId: true },
|
||||
})
|
||||
.then((rows) => [...new Set(rows.map((r) => r.teamId))]);
|
||||
```
|
||||
|
||||
Then replace `team: buildTeamWhereQuery(...)` with `teamId: { in: teamIds }` in the envelope query.
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Eliminates the duplicated 4-level deep join chain from the envelope query
|
||||
- The team ID resolution is a simple indexed lookup (runs once, not twice)
|
||||
- Minimal code change -- still Prisma, same structure
|
||||
- Can also pre-resolve team roles to move visibility filtering into the WHERE clause
|
||||
|
||||
**Drawbacks:**
|
||||
|
||||
- Still a single large OR query with ILIKE branches
|
||||
- Prisma still generates suboptimal SQL for the remaining OR conditions
|
||||
|
||||
---
|
||||
|
||||
## Option B: Kysely rewrite with pre-resolved teams
|
||||
|
||||
**Change:** Rewrite using Kysely (already set up in codebase as `kyselyPrisma.$kysely`). Follow the pattern in `find-envelopes.ts` -- use Kysely for filtering/ID fetching, then Prisma for hydration.
|
||||
|
||||
Structure as a UNION of targeted queries instead of a single OR:
|
||||
|
||||
```
|
||||
Query 1: owned docs matching title/externalId (simple indexed lookup)
|
||||
Query 2: docs where user is recipient matching title (EXISTS on Recipient)
|
||||
Query 3: team docs matching title/externalId (using pre-resolved teamIds)
|
||||
UNION ALL -> deduplicate -> ORDER BY createdAt DESC -> LIMIT 20
|
||||
```
|
||||
|
||||
Then hydrate the 20 IDs with Prisma for the include data.
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Each sub-query is simple and independently optimizable by Postgres
|
||||
- UNION eliminates the massive OR which forces bad query plans
|
||||
- Kysely gives control over exact SQL structure
|
||||
- Only hydrate the final 20 results (not all matches)
|
||||
- Follows existing `find-envelopes.ts` pattern -- not a new paradigm
|
||||
|
||||
**Drawbacks:**
|
||||
|
||||
- More code than Option A
|
||||
- Two query layers (Kysely for IDs, Prisma for hydration)
|
||||
|
||||
---
|
||||
|
||||
## Option C: Hybrid -- pre-resolve teams + simplify Prisma OR
|
||||
|
||||
**Change:** Pre-resolve team IDs (like Option A), but also restructure the Prisma query to reduce OR branches:
|
||||
|
||||
- Merge "owned + title" and "owned + externalId" and "owned + recipient email" into a single owned-docs branch with nested OR
|
||||
- Merge "team + title" and "team + externalId" into a single team-docs branch
|
||||
- Keep "recipient inbox" branches separate
|
||||
|
||||
This reduces from 7 OR branches to ~3-4, with simpler conditions in each.
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Simpler than Kysely rewrite
|
||||
- Fewer OR branches = better query plan
|
||||
- Pre-resolved team IDs eliminate the deep joins
|
||||
- Still pure Prisma
|
||||
|
||||
**Drawbacks:**
|
||||
|
||||
- Postgres still has to handle OR across different access patterns in one query
|
||||
- Less control over SQL than Kysely
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Option B (Kysely)** is the strongest choice. The codebase already uses this exact pattern for `find-envelopes.ts` which solves the same class of problem. The UNION approach gives Postgres the best chance at using indexes per sub-query. Pre-resolving team IDs is a prerequisite for all options and is trivially cheap.
|
||||
|
||||
The template search query has the same structure and should get the same treatment.
|
||||
@@ -1,337 +0,0 @@
|
||||
---
|
||||
name: create-documentation
|
||||
description: Generate markdown documentation for a module or feature
|
||||
---
|
||||
|
||||
You are creating proper markdown documentation for a feature or guide in the Documenso documentation site.
|
||||
|
||||
**Read [WRITING_STYLE.md](../../../WRITING_STYLE.md) first** for tone, formatting conventions, and anti-patterns to avoid.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Identify the scope** - Based on the conversation context, determine what feature or topic needs documentation. Ask the user if unclear.
|
||||
2. **Identify the audience** - Is this for Users, Developers, or Self-Hosters?
|
||||
3. **Read the source code** - Understand the feature, API, or configuration being documented.
|
||||
4. **Read existing docs** - Check `apps/docs/content/docs/` for documentation to update.
|
||||
5. **Write comprehensive documentation** - Create or update MDX docs following the patterns below.
|
||||
6. **Update navigation** - Add to the relevant `meta.json` if creating a new page.
|
||||
|
||||
## Documentation Framework
|
||||
|
||||
This project uses [Fumadocs](https://fumadocs.dev). All documentation lives in `apps/docs/content/docs/` as MDX files. The docs app is a Next.js app at `apps/docs/`.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
```
|
||||
apps/docs/content/docs/
|
||||
├── index.mdx # Landing page with audience navigation
|
||||
├── meta.json # Root navigation: guides + resources
|
||||
├── users/ # Application usage guides
|
||||
│ ├── meta.json # { "root": true, "pages": [...] }
|
||||
│ ├── getting-started/ # Account creation, first document
|
||||
│ ├── documents/ # Upload, recipients, fields, send
|
||||
│ │ └── advanced/ # AI detection, visibility, placeholders
|
||||
│ ├── templates/ # Create and use templates
|
||||
│ ├── organisations/ # Overview, members, groups, SSO, billing
|
||||
│ │ ├── single-sign-on/
|
||||
│ │ └── preferences/
|
||||
│ └── settings/ # Profile, security, API tokens
|
||||
├── developers/ # API and integration docs
|
||||
│ ├── meta.json # { "root": true, "pages": [...] }
|
||||
│ ├── getting-started/ # Authentication, first API call
|
||||
│ ├── api/ # Documents, recipients, fields, templates, teams
|
||||
│ ├── webhooks/ # Setup, events, verification
|
||||
│ ├── embedding/ # Authoring, direct links, CSS vars, SDKs
|
||||
│ │ └── sdks/ # React, Vue, Svelte, Solid, Preact, Angular
|
||||
│ ├── examples/ # Common workflows
|
||||
│ ├── local-development/ # Quickstart, manual, translations
|
||||
│ └── contributing/ # Contributing translations
|
||||
├── self-hosting/ # Self-hosting documentation
|
||||
│ ├── meta.json # { "root": true, "pages": [...] }
|
||||
│ ├── getting-started/ # Quick start, requirements, tips
|
||||
│ ├── deployment/ # Docker, docker-compose, Kubernetes, Railway
|
||||
│ ├── configuration/ # Environment, database, email, storage
|
||||
│ │ ├── signing-certificate/ # Local, Google Cloud HSM, timestamp
|
||||
│ │ └── advanced/ # OAuth providers, AI features
|
||||
│ └── maintenance/ # Upgrades, backups, troubleshooting
|
||||
├── concepts/ # Shared across audiences
|
||||
│ └── ... # Document lifecycle, field types, signing
|
||||
├── compliance/ # eSign, GDPR, standards, certifications
|
||||
└── policies/ # Terms, privacy, security, licenses
|
||||
```
|
||||
|
||||
### Where to Put Documentation
|
||||
|
||||
| Type | Location | When to use |
|
||||
| ------------------- | ------------------------------------------------ | -------------------------------------------------- |
|
||||
| **User Guide** | `apps/docs/content/docs/users/<section>/` | UI workflows for using the Documenso web app |
|
||||
| **Developer Guide** | `apps/docs/content/docs/developers/<section>/` | API reference, SDK guides, webhooks, embedding |
|
||||
| **Self-Hosting** | `apps/docs/content/docs/self-hosting/<section>/` | Deployment, configuration, environment variables |
|
||||
| **Concept** | `apps/docs/content/docs/concepts/` | Cross-audience concepts (document lifecycle, etc.) |
|
||||
| **Compliance** | `apps/docs/content/docs/compliance/` | Legal and regulatory documentation |
|
||||
|
||||
### Navigation (meta.json)
|
||||
|
||||
Each directory has a `meta.json` controlling navigation order:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Section Title",
|
||||
"pages": ["getting-started", "documents", "templates"]
|
||||
}
|
||||
```
|
||||
|
||||
Top-level audience sections use `"root": true`:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Users",
|
||||
"description": "Send and sign documents",
|
||||
"root": true,
|
||||
"pages": ["getting-started", "documents", "templates", "organisations", "settings"]
|
||||
}
|
||||
```
|
||||
|
||||
Root `meta.json` uses `---Label---` for section dividers:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Documentation",
|
||||
"pages": [
|
||||
"---Guides---",
|
||||
"users",
|
||||
"developers",
|
||||
"self-hosting",
|
||||
"---Resources---",
|
||||
"concepts",
|
||||
"compliance",
|
||||
"policies"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## MDX File Format
|
||||
|
||||
### Frontmatter
|
||||
|
||||
Every page needs frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Upload Documents
|
||||
description: Upload documents to Documenso to prepare them for signing. Covers supported formats, file size limits, and upload methods.
|
||||
---
|
||||
```
|
||||
|
||||
### Fumadocs Components
|
||||
|
||||
Import components at the top of the file (after frontmatter):
|
||||
|
||||
```mdx
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
;
|
||||
```
|
||||
|
||||
Callouts (use sparingly for warnings, beta features, security):
|
||||
|
||||
```mdx
|
||||
<Callout type="info">Informational note about behavior.</Callout>
|
||||
|
||||
<Callout type="warn">Warning about potential issues or breaking changes.</Callout>
|
||||
|
||||
<Callout type="error">Critical warning about data loss or security.</Callout>
|
||||
```
|
||||
|
||||
Steps (for sequential UI instructions):
|
||||
|
||||
```mdx
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Step title
|
||||
|
||||
Step description.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Next step
|
||||
|
||||
Next description.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
```
|
||||
|
||||
Tabs (for multiple approaches or platforms):
|
||||
|
||||
````mdx
|
||||
<Tabs items={['cURL', 'JavaScript', 'Python']}>
|
||||
<Tab value="cURL">```bash curl -X POST ... ```</Tab>
|
||||
<Tab value="JavaScript">```typescript const response = await fetch(...) ```</Tab>
|
||||
</Tabs>
|
||||
````
|
||||
|
||||
## Page Structure by Audience
|
||||
|
||||
### User Documentation
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Feature Name
|
||||
description: Brief description for SEO and previews.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
## Limitations
|
||||
|
||||
| Limitation | Value |
|
||||
| ----------------- | -------- |
|
||||
| Supported format | PDF only |
|
||||
| Maximum file size | 50MB |
|
||||
|
||||
## How to Do the Thing
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Navigate to the page
|
||||
|
||||
Open **Settings > Feature**.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Configure the setting
|
||||
|
||||
Fill in the required fields and click **Save**.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Related Guide](/docs/users/related)
|
||||
```
|
||||
|
||||
### Developer Documentation
|
||||
|
||||
````mdx
|
||||
---
|
||||
title: Documents API
|
||||
description: Create, manage, and send documents for signing via the API.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
<Callout type="warn">
|
||||
This guide may not reflect the latest endpoints. For an always up-to-date reference, see the
|
||||
[OpenAPI Reference](https://openapi.documenso.com).
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
Brief description of the resource and what you can do with it.
|
||||
|
||||
## Resource Object
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------- | ------ | ----------------- |
|
||||
| `id` | string | Unique identifier |
|
||||
| `status` | string | Current status |
|
||||
|
||||
## Create a Resource
|
||||
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/document', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer YOUR_API_TOKEN',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: 'Service Agreement',
|
||||
}),
|
||||
});
|
||||
```
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Related Guide](/docs/developers/related)
|
||||
|
||||
````
|
||||
|
||||
### Self-Hosting Documentation
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Environment Variables
|
||||
description: Complete reference for all environment variables used to configure Documenso.
|
||||
---
|
||||
|
||||
## Required Variables
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| `NEXTAUTH_SECRET` | Secret key for session encryption (min 32 chars) |
|
||||
| `DATABASE_URL` | PostgreSQL connection URL |
|
||||
|
||||
---
|
||||
|
||||
## Optional Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------------- | ------- | ---------------------- |
|
||||
| `PORT` | `3000` | Port the server runs on |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Database Configuration](/docs/self-hosting/configuration/database)
|
||||
````
|
||||
|
||||
## Documentation Audiences
|
||||
|
||||
Tailor content to the audience:
|
||||
|
||||
- **User docs**: Focus on UI workflows, bold UI elements (**Settings**, **Save**), use `>` for navigation paths (**Settings > Team > Members**), number sequential steps, no code required
|
||||
- **Developer docs**: API/SDK examples, authentication, webhooks, code samples in TypeScript, link to OpenAPI reference
|
||||
- **Self-hosting docs**: Deployment guides, environment variables, Docker/non-Docker approaches, system requirements, troubleshooting
|
||||
|
||||
## Guidelines
|
||||
|
||||
See [WRITING_STYLE.md](../../../WRITING_STYLE.md) for complete guidelines. Key points:
|
||||
|
||||
- **Tone**: Direct, second-person, no emojis, no excessive personality
|
||||
- **Examples**: Progressive complexity, all must be valid TypeScript
|
||||
- **Tables**: Use Sharp-style nested parameter tables for API docs
|
||||
- **Callouts**: Use sparingly for warnings, beta features, security
|
||||
- **Cross-references**: Link related docs, add "See Also" sections
|
||||
- **Navigation**: Update `meta.json` when adding new pages
|
||||
- **Limitations**: Explicitly list what is NOT supported
|
||||
- **Images**: Use `.webp` format, store in `apps/docs/public/`
|
||||
|
||||
## Process
|
||||
|
||||
1. **Identify the audience** - Users, Developers, or Self-Hosters?
|
||||
2. **Explore the code** - Read source files to understand the feature
|
||||
3. **Check existing docs** - Look in `apps/docs/content/docs/` for related pages
|
||||
4. **Draft the structure** - Outline sections before writing
|
||||
5. **Write content** - Fill in each section following audience-specific patterns
|
||||
6. **Update navigation** - Add to relevant `meta.json` if creating a new page
|
||||
7. **Add cross-references** - Link from related docs, add "See Also" section
|
||||
|
||||
## Begin
|
||||
|
||||
Analyze the conversation context to determine the documentation scope, read the relevant source code, and create comprehensive MDX documentation in `apps/docs/content/docs/`.
|
||||
+3
-52
@@ -35,8 +35,6 @@ NEXT_PRIVATE_OIDC_PROMPT="login"
|
||||
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
|
||||
# URL used by the web app to request itself (e.g. local background jobs)
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000"
|
||||
# OPTIONAL: Comma-separated hostnames or IPs whose webhooks are allowed to resolve to private/loopback addresses. (e.g., internal.example.com,192.168.1.5).
|
||||
NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS=
|
||||
|
||||
# [[SERVER]]
|
||||
# OPTIONAL: The port the server will listen on. Defaults to 3000.
|
||||
@@ -48,7 +46,7 @@ NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documen
|
||||
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
|
||||
|
||||
# [[SIGNING]]
|
||||
# The transport to use for document signing. Available options: local (default) | gcloud-hsm | csc
|
||||
# The transport to use for document signing. Available options: local (default) | gcloud-hsm
|
||||
NEXT_PRIVATE_SIGNING_TRANSPORT="local"
|
||||
# OPTIONAL: The passphrase to use for the local file-based signing transport.
|
||||
NEXT_PRIVATE_SIGNING_PASSPHRASE=
|
||||
@@ -70,14 +68,6 @@ NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_FILE_PATH=
|
||||
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS=
|
||||
# OPTIONAL: The Google Secret Manager path to retrieve the certificate for the gcloud-hsm signing transport.
|
||||
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH=
|
||||
# OPTIONAL: The base URL of the Cloud Signature Consortium (CSC) provider for the csc signing transport.
|
||||
NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL=
|
||||
# OPTIONAL: The OAuth client ID registered with the CSC provider for the csc signing transport.
|
||||
NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID=
|
||||
# OPTIONAL: The OAuth client secret registered with the CSC provider for the csc signing transport.
|
||||
NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET=
|
||||
# OPTIONAL: Default signature level for envelopes created on a CSC instance when the caller doesn't specify one. Available options: AES (default) | QES. Explicit AES/QES requests always pass through unchanged.
|
||||
NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL=
|
||||
# OPTIONAL: Comma-separated list of timestamp authority URLs for PDF signing (enables LTV and archival timestamps).
|
||||
NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=
|
||||
# OPTIONAL: Contact info to embed in PDF signatures. Defaults to the webapp URL.
|
||||
@@ -103,7 +93,7 @@ NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID="documenso"
|
||||
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY="password"
|
||||
|
||||
# [[SMTP]]
|
||||
# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | resend | mailchannels
|
||||
# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | mailchannels
|
||||
NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth"
|
||||
# OPTIONAL: Defines the host to use for sending emails.
|
||||
NEXT_PRIVATE_SMTP_HOST="127.0.0.1"
|
||||
@@ -153,33 +143,16 @@ NEXT_PRIVATE_STRIPE_API_KEY=
|
||||
NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
# [[BACKGROUND JOBS]]
|
||||
# Available options: local (default) | inngest | bullmq
|
||||
NEXT_PRIVATE_JOBS_PROVIDER="local"
|
||||
NEXT_PRIVATE_INNGEST_EVENT_KEY=
|
||||
# OPTIONAL: Redis URL for the BullMQ jobs provider.
|
||||
NEXT_PRIVATE_REDIS_URL="redis://localhost:63790"
|
||||
# OPTIONAL: Key prefix for Redis to namespace queues (useful when sharing a Redis instance).
|
||||
NEXT_PRIVATE_REDIS_PREFIX="documenso"
|
||||
# OPTIONAL: Number of concurrent jobs to process. Defaults to 10.
|
||||
# NEXT_PRIVATE_BULLMQ_CONCURRENCY=10
|
||||
|
||||
# [[FEATURES]]
|
||||
# OPTIONAL: Leave blank to disable PostHog and feature flags.
|
||||
NEXT_PUBLIC_POSTHOG_KEY=""
|
||||
# OPTIONAL: Leave blank to disable billing.
|
||||
NEXT_PUBLIC_FEATURE_BILLING_ENABLED=
|
||||
# OPTIONAL: Set to "true" to disable all signup methods (email, Google, Microsoft, OIDC, including the organisation OIDC portal).
|
||||
# OPTIONAL: Leave blank to allow users to signup through /signup page.
|
||||
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.
|
||||
NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false
|
||||
|
||||
@@ -198,14 +171,6 @@ GOOGLE_VERTEX_LOCATION="global"
|
||||
# https://console.cloud.google.com/vertex-ai/studio/settings/api-keys
|
||||
GOOGLE_VERTEX_API_KEY=""
|
||||
|
||||
# [[CLOUDFLARE TURNSTILE]]
|
||||
# OPTIONAL: Cloudflare Turnstile site key (public). When configured, Turnstile challenges
|
||||
# will be shown on sign-up (visible) and sign-in (invisible) pages.
|
||||
# See: https://developers.cloudflare.com/turnstile/
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY=
|
||||
# OPTIONAL: Cloudflare Turnstile secret key (server-side verification).
|
||||
NEXT_PRIVATE_TURNSTILE_SECRET_KEY=
|
||||
|
||||
# [[E2E Tests]]
|
||||
E2E_TEST_AUTHENTICATE_USERNAME="Test User"
|
||||
E2E_TEST_AUTHENTICATE_USER_EMAIL="testuser@mail.com"
|
||||
@@ -219,17 +184,3 @@ 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
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# Config files
|
||||
*.config.js
|
||||
*.config.cjs
|
||||
|
||||
# Statically hosted javascript files
|
||||
apps/*/public/*.js
|
||||
apps/*/public/*.cjs
|
||||
scripts/
|
||||
@@ -0,0 +1,16 @@
|
||||
/** @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'],
|
||||
};
|
||||
@@ -34,7 +34,7 @@ body:
|
||||
label: Browser [e.g., Chrome, Firefox]
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version [e.g., 2.13.0]
|
||||
label: Version [e.g., 2.0.1]
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please check the boxes that apply to this issue report.
|
||||
@@ -44,3 +44,4 @@ body:
|
||||
- label: I have included relevant environment information.
|
||||
- label: I have included any relevant screenshots.
|
||||
- label: I understand that this is a voluntary contribution and that there is no guarantee of resolution.
|
||||
- label: I want to work on creating a PR for this issue if approved
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Security vulnerability
|
||||
url: https://github.com/documenso/documenso/security/advisories/new
|
||||
about: Please report security vulnerabilities privately via GitHub Security Advisories. Do not open a public issue.
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/documenso/documenso/discussions
|
||||
about: Ask questions, share ideas, and discuss Documenso with the community.
|
||||
- name: Discord
|
||||
url: https://documen.so/discord
|
||||
about: Chat with the community and the team.
|
||||
@@ -33,3 +33,4 @@ body:
|
||||
- label: I have explained the use case or scenario for this feature.
|
||||
- label: I have included any relevant technical details or design suggestions.
|
||||
- label: I understand that this is a suggestion and that there is no guarantee of implementation.
|
||||
- label: I want to work on creating a PR for this issue if approved
|
||||
|
||||
@@ -15,6 +15,17 @@ body:
|
||||
description: 'Are there any additional context or information that might be relevant to the improvement suggestion.'
|
||||
validations:
|
||||
required: false
|
||||
- type: dropdown
|
||||
id: assignee
|
||||
attributes:
|
||||
label: 'Do you want to work on this improvement?'
|
||||
multiple: false
|
||||
options:
|
||||
- 'No'
|
||||
- 'Yes'
|
||||
default: 0
|
||||
validations:
|
||||
required: true
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: 'Please check the boxes that apply to this improvement suggestion.'
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
<!--
|
||||
We are no longer accepting external pull requests.
|
||||
|
||||
Aside from a small group of trusted contributors we reach out to directly,
|
||||
new external PRs will usually be closed with a request to open an issue instead.
|
||||
This is a security decision. See https://documenso.com/blog/why-we-re-pausing-external-pull-requests
|
||||
|
||||
If you're a trusted contributor or maintainer, continue below.
|
||||
Otherwise, please open a detailed issue: https://github.com/documenso/documenso/issues/new/choose
|
||||
-->
|
||||
|
||||
## Description
|
||||
|
||||
<!--- Describe the changes introduced by this pull request. -->
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
name: Test Addition
|
||||
about: Submit a new test, either unit or end-to-end (E2E), for review and inclusion
|
||||
---
|
||||
|
||||
## Description
|
||||
|
||||
<!--- Provide a clear and concise description of the new test you are adding. -->
|
||||
<!--- Explain the purpose of the test and what it aims to validate. -->
|
||||
|
||||
## Related Issue
|
||||
|
||||
<!--- If this test addition is related to a specific issue, reference it here using #issue_number. -->
|
||||
<!--- For example, "Fixes #123" or "Addresses #456". -->
|
||||
|
||||
## Test Details
|
||||
|
||||
<!--- Describe the details of the test you're adding. -->
|
||||
<!--- Include information about inputs, expected outputs, and any specific scenarios. -->
|
||||
|
||||
- Test Name: Name of the test
|
||||
- Type: [Unit / E2E]
|
||||
- Description: Brief description of what the test checks
|
||||
- Inputs: What inputs the test uses (if applicable)
|
||||
- Expected Output: What output or behavior the test expects
|
||||
|
||||
## Checklist
|
||||
|
||||
<!--- Please check the boxes that apply to this pull request. -->
|
||||
<!--- You can add or remove items as needed. -->
|
||||
|
||||
- [ ] I have written the new test and ensured it works as intended.
|
||||
- [ ] I have added necessary documentation to explain the purpose of the test.
|
||||
- [ ] I have followed the project's testing guidelines and coding style.
|
||||
- [ ] I have addressed any review feedback from previous submissions, if applicable.
|
||||
|
||||
## Additional Notes
|
||||
|
||||
<!--- Provide any additional context or notes for the reviewers. -->
|
||||
<!--- This might include explanations about the testing approach or any potential concerns. -->
|
||||
@@ -1,4 +1,4 @@
|
||||
name: 'Setup node'
|
||||
name: 'Setup node and install dependencies with pnpm'
|
||||
inputs:
|
||||
node_version:
|
||||
required: false
|
||||
@@ -7,19 +7,19 @@ inputs:
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up Node ${{ inputs.node_version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
|
||||
- name: Enable corepack
|
||||
shell: bash
|
||||
run: corepack enable npm
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
npm ci --no-audit
|
||||
npm run prisma:generate
|
||||
pnpm install --frozen-lockfile
|
||||
pnpm run prisma:generate
|
||||
env:
|
||||
HUSKY: '0'
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
name: Install playwright binaries
|
||||
description: 'Install playwright'
|
||||
description: 'Install playwright, cache and restore if necessary'
|
||||
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('**/pnpm-lock.yaml') }}
|
||||
restore-keys: playwright-
|
||||
|
||||
- name: Install playwright
|
||||
run: npx playwright install --with-deps
|
||||
if: steps.cache-playwright.outputs.cache-hit != 'true'
|
||||
run: pnpm exec playwright install --with-deps
|
||||
shell: bash
|
||||
|
||||
@@ -12,11 +12,10 @@ updates:
|
||||
open-pull-requests-limit: 0
|
||||
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/apps/web'
|
||||
directory: '/'
|
||||
schedule:
|
||||
interval: 'weekly'
|
||||
target-branch: 'main'
|
||||
labels:
|
||||
- 'npm dependencies'
|
||||
- 'frontend'
|
||||
- 'dependencies'
|
||||
open-pull-requests-limit: 0
|
||||
|
||||
+1
-4
@@ -1,8 +1,5 @@
|
||||
'apps: web':
|
||||
- apps/remix/**
|
||||
|
||||
'type: documentation':
|
||||
- apps/docs/**
|
||||
- apps/web/**
|
||||
|
||||
'version bump 👀':
|
||||
- '**/package.json'
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Build app
|
||||
run: npm run build
|
||||
run: pnpm run build
|
||||
|
||||
build_docker:
|
||||
name: Build Docker Image
|
||||
@@ -41,6 +41,14 @@ 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:
|
||||
@@ -48,3 +56,13 @@ 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
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
- name: Build app
|
||||
run: npm run build
|
||||
run: pnpm run build
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
||||
@@ -23,21 +23,21 @@ jobs:
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
- name: Start Services
|
||||
run: npm run dx:up
|
||||
run: pnpm run dx:up
|
||||
|
||||
- uses: ./.github/actions/playwright-install
|
||||
|
||||
- name: Create the database
|
||||
run: npm run prisma:migrate-dev
|
||||
run: pnpm run prisma:migrate-dev
|
||||
|
||||
- name: Seed the database
|
||||
run: npm run prisma:seed
|
||||
run: pnpm run prisma:seed
|
||||
|
||||
- name: Install playwright browsers
|
||||
run: npx playwright install --with-deps
|
||||
run: pnpm exec playwright install --with-deps
|
||||
|
||||
- name: Run Playwright tests
|
||||
run: npm run ci
|
||||
run: pnpm run ci
|
||||
env:
|
||||
# Needed since we use next start which will set the NODE_ENV to production
|
||||
NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: './example/cert.p12'
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
name: 'Welcome New Contributors'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: ['opened']
|
||||
issues:
|
||||
types: ['opened']
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
@@ -17,7 +20,10 @@ jobs:
|
||||
- uses: actions/first-interaction@v1
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
pr-message: |
|
||||
Thank you for creating your first Pull Request and for being a part of the open signing revolution! 💚🚀
|
||||
<br /> Feel free to hop into our community in [Discord](https://documen.so/discord)
|
||||
issue-message: |
|
||||
Thank you for opening your first issue and for being a part of the open signing revolution!
|
||||
<br /> One of our team members will review it and get back to you as soon as possible 💚
|
||||
<br /> One of our team members will review it and get back to you as soon as it possible 💚
|
||||
<br /> Meanwhile, please feel free to hop into our community in [Discord](https://documen.so/discord)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
name: 'Issue Assignee Check'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: ['assigned']
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
countIssues:
|
||||
if: ${{ github.event.issue.assignee }} && github.event.action == 'assigned' && github.event.sender.type == 'User'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- name: Install Octokit
|
||||
run: pnpm install @octokit/rest@18
|
||||
|
||||
- name: Check Assigned User's Issue Count
|
||||
id: parse-comment
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { Octokit } = require("@octokit/rest");
|
||||
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
||||
|
||||
const username = context.payload.issue.assignee.login;
|
||||
console.log(`Username Extracted: ${username}`);
|
||||
|
||||
const { data: issues } = await octokit.issues.listForRepo({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
assignee: username,
|
||||
state: 'open'
|
||||
});
|
||||
|
||||
const issueCount = issues.length;
|
||||
console.log(`Issue Count For ${username}: ${issueCount}`);
|
||||
|
||||
if (issueCount > 3) {
|
||||
let issueCountMessage = `### 🚨 Documenso Police 🚨`;
|
||||
issueCountMessage += `\n@${username} has ${issueCount} open issues assigned already. Consider whether this issue should be assigned to them or left open for another contributor.`;
|
||||
|
||||
await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: issueCountMessage,
|
||||
headers: {
|
||||
'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`,
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 'PR Labeler'
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
- pull_request_target
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
name: 'PR Review Reminder'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: ['opened', 'ready_for_review']
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
checkPRs:
|
||||
if: ${{ github.event.pull_request.user.login }} && github.event.action == ('opened' || 'ready_for_review')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Install Octokit
|
||||
run: pnpm install @octokit/rest@18
|
||||
|
||||
- name: Check user's PRs awaiting review
|
||||
id: parse-prs
|
||||
uses: actions/github-script@v5
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { Octokit } = require("@octokit/rest");
|
||||
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
|
||||
|
||||
const username = context.payload.pull_request.user.login;
|
||||
console.log(`Username Extracted: ${username}`);
|
||||
|
||||
const { data: pullRequests } = await octokit.pulls.list({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: 'open',
|
||||
sort: 'created',
|
||||
direction: 'asc',
|
||||
});
|
||||
|
||||
const userPullRequests = pullRequests.filter(pr => pr.user.login === username && (pr.state === 'open' || pr.state === 'ready_for_review'));
|
||||
const prCount = userPullRequests.length;
|
||||
console.log(`PR Count for ${username}: ${prCount}`);
|
||||
|
||||
if (prCount > 3) {
|
||||
let prReminderMessage = `🚨 @${username} has ${prCount} pull requests awaiting review. Please consider reviewing them when possible. 🚨`;
|
||||
|
||||
await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.pull_request.number,
|
||||
body: prReminderMessage,
|
||||
headers: {
|
||||
'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`,
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 'Validate PR Name'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
@@ -16,6 +16,24 @@ jobs:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check PR creator's previous activity
|
||||
id: check_activity
|
||||
run: |
|
||||
CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login')
|
||||
ACTIVITY=$(curl -s "https://api.github.com/search/commits?q=author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count')
|
||||
if [ "$ACTIVITY" -eq 0 ]; then
|
||||
echo "::set-output name=is_new::true"
|
||||
else
|
||||
echo "::set-output name=is_new::false"
|
||||
fi
|
||||
|
||||
- name: Count PRs created by user
|
||||
id: count_prs
|
||||
run: |
|
||||
CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login')
|
||||
PR_COUNT=$(curl -s "https://api.github.com/search/issues?q=type:pr+is:open+author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count')
|
||||
echo "::set-output name=pr_count::$PR_COUNT"
|
||||
|
||||
- uses: amannn/action-semantic-pull-request@v5
|
||||
id: lint_pr_title
|
||||
env:
|
||||
@@ -26,6 +44,8 @@ jobs:
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Hey There! and thank you for opening this pull request! 📝👋🏼
|
||||
|
||||
We require pull request titles to follow the [Conventional Commits Spec](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
|
||||
|
||||
Details:
|
||||
@@ -33,3 +53,10 @@ jobs:
|
||||
```
|
||||
${{ steps.lint_pr_title.outputs.error_message }}
|
||||
```
|
||||
|
||||
- if: ${{ steps.lint_pr_title.outputs.error_message == null && steps.check_activity.outputs.is_new == 'false' && steps.count_prs.outputs.pr_count < 2}}
|
||||
uses: marocchino/sticky-pull-request-comment@v2
|
||||
with:
|
||||
header: pr-title-lint-error
|
||||
message: |
|
||||
Thank you for following the naming conventions for pull request titles! 💚🚀
|
||||
|
||||
@@ -21,4 +21,4 @@ jobs:
|
||||
stale-pr-message: 'This PR has not seen activitiy for a while. It will be closed in 30 days unless further activity is detected.'
|
||||
close-pr-message: 'This PR has been closed because of inactivity.'
|
||||
exempt-pr-labels: 'WIP,on-hold,needs review'
|
||||
exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,status: assigned,status: triage'
|
||||
exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,assigned,needs triage'
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
|
||||
- name: Compile translations
|
||||
id: compile_translations
|
||||
run: npm run translate:compile -- -- --strict
|
||||
run: pnpm run translate:compile -- --strict
|
||||
continue-on-error: true
|
||||
|
||||
- name: Pull translations from Crowdin
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
- uses: ./.github/actions/node-install
|
||||
|
||||
- name: Extract translations
|
||||
run: npm run translate:extract
|
||||
run: pnpm run translate:extract
|
||||
|
||||
- name: Commit changes and push to reserved branch
|
||||
env:
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
|
||||
- name: Compile translations
|
||||
id: compile_translations
|
||||
run: npm run translate:compile -- -- --strict
|
||||
run: pnpm run translate:compile -- --strict
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload missing translations
|
||||
|
||||
+1
-7
@@ -20,9 +20,7 @@ build
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
@@ -63,7 +61,6 @@ CLAUDE.md
|
||||
|
||||
# scripts
|
||||
scripts/output*
|
||||
scripts/bench-*
|
||||
|
||||
# license
|
||||
.documenso-license.json
|
||||
@@ -71,6 +68,3 @@ scripts/bench-*
|
||||
|
||||
# tmp
|
||||
tmp/
|
||||
|
||||
# opencode
|
||||
.opencode/package-lock.json
|
||||
|
||||
+2
-1
@@ -31,7 +31,8 @@ vscode:
|
||||
extensions:
|
||||
- aaron-bond.better-comments
|
||||
- bradlc.vscode-tailwindcss
|
||||
- biomejs.biome
|
||||
- dbaeumer.vscode-eslint
|
||||
- esbenp.prettier-vscode
|
||||
- mikestead.dotenv
|
||||
- unifiedjs.vscode-mdx
|
||||
- GitHub.vscode-pull-request-github
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
legacy-peer-deps = true
|
||||
prefer-dedupe = true
|
||||
min-release-age = 7
|
||||
# Hoist packages that expect to be resolvable from any workspace.
|
||||
# Start strict, add patterns here only as needed.
|
||||
shamefully-hoist=true
|
||||
|
||||
@@ -24,8 +24,8 @@ Run these commands to understand where the previous session left off:
|
||||
```bash
|
||||
git status # See uncommitted changes
|
||||
git log --oneline -10 # See recent commits
|
||||
npm run typecheck -w @documenso/remix # Check for type errors
|
||||
npm run lint:fix # Check for linting issues
|
||||
pnpm run typecheck -w @documenso/remix # Check for type errors
|
||||
pnpm run lint:fix # Check for linting issues
|
||||
```
|
||||
|
||||
Review the code that's already been written to understand:
|
||||
@@ -67,9 +67,9 @@ Review the code that's already been written to understand:
|
||||
Work continuously through these steps:
|
||||
|
||||
1. **Implement** - Write the code for the current task
|
||||
2. **Typecheck** - Run `npm run typecheck -w @documenso/remix` to verify types
|
||||
3. **Lint** - Run `npm run lint:fix` to fix linting issues
|
||||
4. **Test** - If non-trivial, run E2E tests: `npm run test:dev -w @documenso/app-tests`
|
||||
2. **Typecheck** - Run `pnpm run typecheck -w @documenso/remix` to verify types
|
||||
3. **Lint** - Run `pnpm run lint:fix` to fix linting issues
|
||||
4. **Test** - If non-trivial, run E2E tests: `pnpm run test:dev -w @documenso/app-tests`
|
||||
5. **Fix** - If tests fail, fix and re-run
|
||||
6. **Repeat** - Move to next task
|
||||
|
||||
@@ -93,18 +93,18 @@ Work continuously through these steps:
|
||||
|
||||
```bash
|
||||
# Type checking
|
||||
npm run typecheck -w @documenso/remix
|
||||
pnpm run typecheck -w @documenso/remix
|
||||
|
||||
# Linting
|
||||
npm run lint:fix
|
||||
pnpm run lint:fix
|
||||
|
||||
# E2E Tests (only for non-trivial work)
|
||||
npm run test:dev -w @documenso/app-tests # Run E2E tests in dev mode
|
||||
npm run test-ui:dev -w @documenso/app-tests # Run E2E tests with UI
|
||||
npm run test:e2e # Run full E2E test suite
|
||||
pnpm run test:dev -w @documenso/app-tests # Run E2E tests in dev mode
|
||||
pnpm run test-ui:dev -w @documenso/app-tests # Run E2E tests with UI
|
||||
pnpm run test:e2e # Run full E2E test suite
|
||||
|
||||
# Development
|
||||
npm run dev # Start dev server
|
||||
pnpm run dev # Start dev server
|
||||
```
|
||||
|
||||
## Begin
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
description: Generate markdown documentation for a module or feature
|
||||
argument-hint: <topic-or-feature>
|
||||
---
|
||||
|
||||
You are generating documentation for the Documenso project.
|
||||
|
||||
## Your Task
|
||||
|
||||
Load and follow the skill at `.agents/skills/create-documentation/SKILL.md`. It contains the complete instructions for writing documentation including:
|
||||
|
||||
- Documentation structure and file locations
|
||||
- MDX format and Fumadocs components
|
||||
- Audience-specific patterns (Users, Developers, Self-Hosters)
|
||||
- Navigation (`meta.json`) updates
|
||||
- Writing style guidelines
|
||||
|
||||
## Context
|
||||
|
||||
The topic or feature to document is: `$ARGUMENTS`
|
||||
|
||||
## Begin
|
||||
|
||||
1. **Read the skill** at `.agents/skills/create-documentation/SKILL.md`
|
||||
2. **Read WRITING_STYLE.md** for tone and formatting conventions
|
||||
3. **Follow the skill instructions** to create comprehensive documentation
|
||||
4. **Use TodoWrite** to track your progress throughout
|
||||
@@ -7,17 +7,70 @@ You are creating a new justification file in the `.agents/justifications/` direc
|
||||
|
||||
## Your Task
|
||||
|
||||
Load and follow the skill at `.agents/skills/create-justification/SKILL.md`. It contains the complete instructions for creating justification files including:
|
||||
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
|
||||
2. **Gather content** - Collect or generate the justification content
|
||||
3. **Create the file** - Use the create-justification script to generate the file
|
||||
|
||||
- Unique three-word ID generation
|
||||
- Frontmatter format with date and title
|
||||
- Script usage (`scripts/create-justification.ts`)
|
||||
## Usage
|
||||
|
||||
## Context
|
||||
The script will automatically:
|
||||
|
||||
The justification slug and optional content: `$ARGUMENTS`
|
||||
- Generate a unique three-word ID (e.g., `swift-emerald-river`)
|
||||
- Create frontmatter with current date and formatted title
|
||||
- Save the file as `{id}-{slug}.md` in `.agents/justifications/`
|
||||
|
||||
## Creating the File
|
||||
|
||||
### Option 1: Direct Content
|
||||
|
||||
If you have the content ready, run:
|
||||
|
||||
```bash
|
||||
pnpm exec tsx scripts/create-justification.ts "$ARGUMENTS" "Your justification content here"
|
||||
```
|
||||
|
||||
### Option 2: Multi-line Content (Heredoc)
|
||||
|
||||
For multi-line content, use heredoc:
|
||||
|
||||
```bash
|
||||
pnpm exec tsx scripts/create-justification.ts "$ARGUMENTS" << HEREDOC
|
||||
Your multi-line
|
||||
justification content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
### Option 3: Pipe Content
|
||||
|
||||
You can also pipe content:
|
||||
|
||||
```bash
|
||||
echo "Your content" | pnpm exec tsx scripts/create-justification.ts "$ARGUMENTS"
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
The created file will have:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Justification Title
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
The title is automatically formatted from the slug (e.g., `architecture-decision` → `Architecture Decision`).
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Use descriptive slugs in kebab-case (e.g., `tech-stack-choice`, `api-design-rationale`)
|
||||
- Include clear reasoning and context for the decision
|
||||
- The unique ID ensures no filename conflicts
|
||||
- Files are automatically dated for organization
|
||||
|
||||
## Begin
|
||||
|
||||
1. **Read the skill** at `.agents/skills/create-justification/SKILL.md`
|
||||
2. **Create the justification file** using the slug from `$ARGUMENTS` and appropriate content documenting the reasoning or justification
|
||||
Create a justification file using the slug from `$ARGUMENTS` and appropriate content documenting the reasoning or justification.
|
||||
|
||||
@@ -7,17 +7,70 @@ You are creating a new plan file in the `.agents/plans/` directory.
|
||||
|
||||
## Your Task
|
||||
|
||||
Load and follow the skill at `.agents/skills/create-plan/SKILL.md`. It contains the complete instructions for creating plan files including:
|
||||
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
|
||||
2. **Gather content** - Collect or generate the plan content
|
||||
3. **Create the file** - Use the create-plan script to generate the file
|
||||
|
||||
- Unique three-word ID generation
|
||||
- Frontmatter format with date and title
|
||||
- Script usage (`scripts/create-plan.ts`)
|
||||
## Usage
|
||||
|
||||
## Context
|
||||
The script will automatically:
|
||||
|
||||
The plan slug and optional content: `$ARGUMENTS`
|
||||
- Generate a unique three-word ID (e.g., `happy-blue-moon`)
|
||||
- Create frontmatter with current date and formatted title
|
||||
- Save the file as `{id}-{slug}.md` in `.agents/plans/`
|
||||
|
||||
## Creating the File
|
||||
|
||||
### Option 1: Direct Content
|
||||
|
||||
If you have the content ready, run:
|
||||
|
||||
```bash
|
||||
pnpm exec tsx scripts/create-plan.ts "$ARGUMENTS" "Your plan content here"
|
||||
```
|
||||
|
||||
### Option 2: Multi-line Content (Heredoc)
|
||||
|
||||
For multi-line content, use heredoc:
|
||||
|
||||
```bash
|
||||
pnpm exec tsx scripts/create-plan.ts "$ARGUMENTS" << HEREDOC
|
||||
Your multi-line
|
||||
plan content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
### Option 3: Pipe Content
|
||||
|
||||
You can also pipe content:
|
||||
|
||||
```bash
|
||||
echo "Your content" | pnpm exec tsx scripts/create-plan.ts "$ARGUMENTS"
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
The created file will have:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Plan Title
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
The title is automatically formatted from the slug (e.g., `my-feature` → `My Feature`).
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Use descriptive slugs in kebab-case (e.g., `user-authentication`, `api-integration`)
|
||||
- Include clear, actionable plan content
|
||||
- The unique ID ensures no filename conflicts
|
||||
- Files are automatically dated for organization
|
||||
|
||||
## Begin
|
||||
|
||||
1. **Read the skill** at `.agents/skills/create-plan/SKILL.md`
|
||||
2. **Create the plan file** using the slug from `$ARGUMENTS` and appropriate content
|
||||
Create a plan file using the slug from `$ARGUMENTS` and appropriate content for the planning task.
|
||||
|
||||
@@ -7,17 +7,70 @@ You are creating a new scratch file in the `.agents/scratches/` directory.
|
||||
|
||||
## Your Task
|
||||
|
||||
Load and follow the skill at `.agents/skills/create-scratch/SKILL.md`. It contains the complete instructions for creating scratch files including:
|
||||
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
|
||||
2. **Gather content** - Collect or generate the scratch content
|
||||
3. **Create the file** - Use the create-scratch script to generate the file
|
||||
|
||||
- Unique three-word ID generation
|
||||
- Frontmatter format with date and title
|
||||
- Script usage (`scripts/create-scratch.ts`)
|
||||
## Usage
|
||||
|
||||
## Context
|
||||
The script will automatically:
|
||||
|
||||
The scratch slug and optional content: `$ARGUMENTS`
|
||||
- Generate a unique three-word ID (e.g., `calm-teal-cloud`)
|
||||
- Create frontmatter with current date and formatted title
|
||||
- Save the file as `{id}-{slug}.md` in `.agents/scratches/`
|
||||
|
||||
## Creating the File
|
||||
|
||||
### Option 1: Direct Content
|
||||
|
||||
If you have the content ready, run:
|
||||
|
||||
```bash
|
||||
pnpm exec tsx scripts/create-scratch.ts "$ARGUMENTS" "Your scratch content here"
|
||||
```
|
||||
|
||||
### Option 2: Multi-line Content (Heredoc)
|
||||
|
||||
For multi-line content, use heredoc:
|
||||
|
||||
```bash
|
||||
pnpm exec tsx scripts/create-scratch.ts "$ARGUMENTS" << HEREDOC
|
||||
Your multi-line
|
||||
scratch content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
### Option 3: Pipe Content
|
||||
|
||||
You can also pipe content:
|
||||
|
||||
```bash
|
||||
echo "Your content" | pnpm exec tsx scripts/create-scratch.ts "$ARGUMENTS"
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
The created file will have:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Scratch Title
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
The title is automatically formatted from the slug (e.g., `quick-notes` → `Quick Notes`).
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Use descriptive slugs in kebab-case (e.g., `exploration-ideas`, `temporary-notes`)
|
||||
- Scratch files are for temporary notes, explorations, or ideas
|
||||
- The unique ID ensures no filename conflicts
|
||||
- Files are automatically dated for organization
|
||||
|
||||
## Begin
|
||||
|
||||
1. **Read the skill** at `.agents/skills/create-scratch/SKILL.md`
|
||||
2. **Create the scratch file** using the slug from `$ARGUMENTS` and appropriate content for notes or exploration
|
||||
Create a scratch file using the slug from `$ARGUMENTS` and appropriate content for notes or exploration.
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
---
|
||||
description: Generate MDX documentation for a module or feature
|
||||
argument-hint: <module-path-or-feature>
|
||||
---
|
||||
|
||||
You are creating proper MDX documentation for a module or feature in Documenso using Nextra.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Identify the scope** - What does `$ARGUMENTS` refer to? (file, directory, or feature name)
|
||||
2. **Read the source code** - Understand the public API, types, and behavior
|
||||
3. **Read existing docs** - Check if there's documentation to update or reference
|
||||
4. **Write comprehensive documentation** - Create or update MDX docs in the appropriate location
|
||||
5. **Update navigation** - Add entry to `_meta.js` if creating a new page
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
Create documentation in the appropriate location:
|
||||
|
||||
- **Developer docs**: `apps/docs/`
|
||||
- **User docs**: `apps/docs/`
|
||||
|
||||
### File Format
|
||||
|
||||
All documentation files must be `.mdx` files with frontmatter:
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Page Title
|
||||
description: Brief description for SEO and meta tags
|
||||
---
|
||||
|
||||
# Page Title
|
||||
|
||||
Content starts here...
|
||||
```
|
||||
|
||||
### Navigation
|
||||
|
||||
Each directory should have a `_meta.js` file that defines the navigation structure:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
index: 'Introduction',
|
||||
'feature-name': 'Feature Name',
|
||||
'another-feature': 'Another Feature',
|
||||
};
|
||||
```
|
||||
|
||||
If creating a new page, add it to the appropriate `_meta.js` file.
|
||||
|
||||
### Documentation Format
|
||||
|
||||
````mdx
|
||||
---
|
||||
title: <Module|Feature Name>
|
||||
description: Brief description of what this does and when to use it
|
||||
---
|
||||
|
||||
# <Module|Feature Name>
|
||||
|
||||
Brief description of what this module/feature does and when to use it.
|
||||
|
||||
## Installation
|
||||
|
||||
If there are specific packages or imports needed:
|
||||
|
||||
```bash
|
||||
pnpm install @documenso/package-name
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```jsx
|
||||
// Minimal working example
|
||||
import { Component } from '@documenso/package';
|
||||
|
||||
const Example = () => {
|
||||
return <Component />;
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Component/Function Name
|
||||
|
||||
Description of what it does.
|
||||
|
||||
#### Props/Parameters
|
||||
|
||||
| Prop/Param | Type | Description |
|
||||
| ---------- | -------------------- | ------------------------- |
|
||||
| prop | `string` | Description of the prop |
|
||||
| optional | `boolean` (optional) | Optional prop description |
|
||||
|
||||
#### Example
|
||||
|
||||
```jsx
|
||||
import { Component } from '@documenso/package';
|
||||
|
||||
<Component prop="value" optional={true} />;
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
#### `TypeName`
|
||||
|
||||
```typescript
|
||||
type TypeName = {
|
||||
property: string;
|
||||
optional?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Common Use Case
|
||||
|
||||
```jsx
|
||||
// Full working example
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
```jsx
|
||||
// More complex example
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [Link to related documentation](/developers/path)
|
||||
- [Another related page](/users/path)
|
||||
````
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Content Quality
|
||||
|
||||
- **Be accurate** - Verify behavior by reading the code
|
||||
- **Be complete** - Document all public API surface
|
||||
- **Be practical** - Include real, working examples
|
||||
- **Be concise** - Don't over-explain obvious things
|
||||
- **Be user-focused** - Write for the target audience (developers or users)
|
||||
|
||||
### Code Examples
|
||||
|
||||
- Use appropriate language tags: `jsx`, `tsx`, `typescript`, `bash`, `json`
|
||||
- Show imports when not obvious
|
||||
- Include expected output in comments where helpful
|
||||
- Progress from simple to complex
|
||||
- Use real examples from the codebase when possible
|
||||
|
||||
### Formatting
|
||||
|
||||
- Always include frontmatter with `title` and `description`
|
||||
- Use proper markdown headers (h1 for title, h2 for sections)
|
||||
- Use tables for props/parameters documentation (matching existing style)
|
||||
- Use code fences with appropriate language tags
|
||||
- Use Nextra components when appropriate:
|
||||
- `<Callout type="info">` for notes
|
||||
- `<Steps>` for step-by-step instructions
|
||||
- Use relative links for internal documentation (e.g., `/developers/embedding/react`)
|
||||
|
||||
### Nextra Components
|
||||
|
||||
You can import and use Nextra components:
|
||||
|
||||
```jsx
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
<Callout type="info">
|
||||
This is an informational note.
|
||||
</Callout>
|
||||
|
||||
<Steps>
|
||||
<Steps.Step>First step</Steps.Step>
|
||||
<Steps.Step>Second step</Steps.Step>
|
||||
</Steps>
|
||||
```
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Include types inline so docs don't get stale
|
||||
- Reference source file locations for complex behavior
|
||||
- Keep examples up-to-date with the codebase
|
||||
- Update `_meta.js` when adding new pages
|
||||
|
||||
## Process
|
||||
|
||||
1. **Explore the code** - Read source files to understand the API
|
||||
2. **Identify the audience** - Is this for developers or users?
|
||||
3. **Check existing docs** - Look for similar pages to match style
|
||||
4. **Draft the structure** - Outline sections before writing
|
||||
5. **Write content** - Fill in each section with frontmatter
|
||||
6. **Add examples** - Create working code samples
|
||||
7. **Update navigation** - Add to `_meta.js` if needed
|
||||
8. **Review** - Read through for clarity and accuracy
|
||||
|
||||
## Begin
|
||||
|
||||
Analyze `$ARGUMENTS`, read the relevant source code, check existing documentation patterns, and create comprehensive MDX documentation following the Documenso documentation style.
|
||||
@@ -55,9 +55,9 @@ You are implementing a specification from the `.agents/plans/` directory. Work a
|
||||
Work continuously through these steps:
|
||||
|
||||
1. **Implement** - Write the code for the current task
|
||||
2. **Typecheck** - Run `npm run typecheck -w @documenso/remix` to verify types
|
||||
3. **Lint** - Run `npm run lint:fix` to fix linting issues
|
||||
4. **Test** - If non-trivial, run E2E tests: `npm run test:dev -w @documenso/app-tests`
|
||||
2. **Typecheck** - Run `pnpm run typecheck -w @documenso/remix` to verify types
|
||||
3. **Lint** - Run `pnpm run lint:fix` to fix linting issues
|
||||
4. **Test** - If non-trivial, run E2E tests: `pnpm run test:dev -w @documenso/app-tests`
|
||||
5. **Fix** - If tests fail, fix and re-run
|
||||
6. **Repeat** - Move to next task
|
||||
|
||||
@@ -81,18 +81,18 @@ Work continuously through these steps:
|
||||
|
||||
```bash
|
||||
# Type checking
|
||||
npm run typecheck -w @documenso/remix
|
||||
pnpm run typecheck -w @documenso/remix
|
||||
|
||||
# Linting
|
||||
npm run lint:fix
|
||||
pnpm run lint:fix
|
||||
|
||||
# E2E Tests (only for non-trivial work)
|
||||
npm run test:dev -w @documenso/app-tests # Run E2E tests in dev mode
|
||||
npm run test-ui:dev -w @documenso/app-tests # Run E2E tests with UI
|
||||
npm run test:e2e # Run full E2E test suite
|
||||
pnpm run test:dev -w @documenso/app-tests # Run E2E tests in dev mode
|
||||
pnpm run test-ui:dev -w @documenso/app-tests # Run E2E tests with UI
|
||||
pnpm run test:e2e # Run full E2E test suite
|
||||
|
||||
# Development
|
||||
npm run dev # Start dev server
|
||||
pnpm run dev # Start dev server
|
||||
```
|
||||
|
||||
## Begin
|
||||
|
||||
+2
-2
@@ -21,13 +21,13 @@ I help you create new justification files in the `.agents/justifications/` direc
|
||||
Run the script with a slug and content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-justification.ts "decision-name" "Justification content here"
|
||||
pnpm exec tsx scripts/create-justification.ts "decision-name" "Justification content here"
|
||||
```
|
||||
|
||||
Or use heredoc for multi-line content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-justification.ts "decision-name" << HEREDOC
|
||||
pnpm exec tsx scripts/create-justification.ts "decision-name" << HEREDOC
|
||||
Multi-line
|
||||
justification content
|
||||
goes here
|
||||
@@ -21,13 +21,13 @@ I help you create new plan files in the `.agents/plans/` directory. Each plan fi
|
||||
Run the script with a slug and content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-plan.ts "feature-name" "Plan content here"
|
||||
pnpm exec tsx scripts/create-plan.ts "feature-name" "Plan content here"
|
||||
```
|
||||
|
||||
Or use heredoc for multi-line content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-plan.ts "feature-name" << HEREDOC
|
||||
pnpm exec tsx scripts/create-plan.ts "feature-name" << HEREDOC
|
||||
Multi-line
|
||||
plan content
|
||||
goes here
|
||||
@@ -21,13 +21,13 @@ I help you create new scratch files in the `.agents/scratches/` directory. Each
|
||||
Run the script with a slug and content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-scratch.ts "note-name" "Scratch content here"
|
||||
pnpm exec tsx scripts/create-scratch.ts "note-name" "Scratch content here"
|
||||
```
|
||||
|
||||
Or use heredoc for multi-line content:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-scratch.ts "note-name" << HEREDOC
|
||||
pnpm exec tsx scripts/create-scratch.ts "note-name" << HEREDOC
|
||||
Multi-line
|
||||
scratch content
|
||||
goes here
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../.agents/skills/agent-browser
|
||||
@@ -0,0 +1,20 @@
|
||||
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
+5
-17
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
"source.fixAll": "explicit"
|
||||
},
|
||||
"eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"],
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative",
|
||||
"javascript.preferences.useAliasesForRenames": false,
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
@@ -15,20 +15,8 @@
|
||||
"[prisma]": {
|
||||
"editor.defaultFormatter": "Prisma.prisma"
|
||||
},
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"prisma.pinToPrisma6": true,
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
}
|
||||
"prisma.pinToPrisma6": true
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
# Report security vulnerabilities privately via GitHub Security Advisories (preferred).
|
||||
Contact: https://github.com/documenso/documenso/security/advisories/new
|
||||
|
||||
# Alternatively, report critical issues privately by email.
|
||||
Contact: mailto:security@documenso.com
|
||||
|
||||
# Security policy
|
||||
Policy: https://github.com/documenso/documenso/security/policy
|
||||
|
||||
# General (non-security) issues
|
||||
# General Issues
|
||||
Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
|
||||
|
||||
# Report critical issues privately to let us take appropriate action before publishing.
|
||||
Contact: mailto:security@documenso.com
|
||||
Preferred-Languages: en
|
||||
Canonical: https://documenso.com/.well-known/security.txt
|
||||
Canonical: https://documenso.com/.well-known/security.txt
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
## Build/Test/Lint Commands
|
||||
|
||||
- `npm run build` - Build all packages
|
||||
- `npm run lint` - Lint all packages
|
||||
- `npm run lint:fix` - Auto-fix linting issues
|
||||
- `npm run test:e2e` - Run E2E tests with Playwright
|
||||
- `npm run test:dev -w @documenso/app-tests` - Run single E2E test in dev mode
|
||||
- `npm run test-ui:dev -w @documenso/app-tests` - Run E2E tests with UI
|
||||
- `npm run format` - Format code with Biome
|
||||
- `npm run dev` - Start development server for Remix app
|
||||
- `pnpm run build` - Build all packages
|
||||
- `pnpm run lint` - Lint all packages
|
||||
- `pnpm run lint:fix` - Auto-fix linting issues
|
||||
- `pnpm run test:e2e` - Run E2E tests with Playwright
|
||||
- `pnpm run test:dev -w @documenso/app-tests` - Run single E2E test in dev mode
|
||||
- `pnpm run test-ui:dev -w @documenso/app-tests` - Run E2E tests with UI
|
||||
- `pnpm run format` - Format code with Prettier
|
||||
- `pnpm 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.
|
||||
**Important:** Do not run `pnpm run build` to verify changes unless explicitly asked. Builds take a long time (~2 minutes). Use `pnpm exec tsc --noEmit` for type checking specific packages if needed.
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
|
||||
+8
-7
@@ -4,7 +4,7 @@ This document provides a high-level overview of the Documenso codebase to help h
|
||||
|
||||
## Overview
|
||||
|
||||
Documenso is an open-source document signing platform built as a **monorepo** using npm workspaces and Turborepo. The application enables users to create, send, and sign documents electronically.
|
||||
Documenso is an open-source document signing platform built as a **monorepo** using pnpm workspaces and Turborepo. The application enables users to create, send, and sign documents electronically.
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
@@ -65,6 +65,8 @@ 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 |
|
||||
|
||||
@@ -233,7 +235,6 @@ Processes async jobs.
|
||||
| Provider | Description | Env Value |
|
||||
| -------- | --------------------- | ----------------- |
|
||||
| Local | Database-backed queue | `local` (default) |
|
||||
| BullMQ | Redis-backed queue | `bullmq` |
|
||||
| Inngest | Managed cloud service | `inngest` |
|
||||
|
||||
**Config**: `NEXT_PRIVATE_JOBS_PROVIDER`
|
||||
@@ -323,19 +324,19 @@ documenso/
|
||||
|
||||
```bash
|
||||
# Full setup (install, docker, migrate, seed, dev)
|
||||
npm run d
|
||||
pnpm run d
|
||||
|
||||
# Start development server
|
||||
npm run dev
|
||||
pnpm run dev
|
||||
|
||||
# Database GUI
|
||||
npm run prisma:studio
|
||||
pnpm run prisma:studio
|
||||
|
||||
# Type checking (faster than build)
|
||||
npx tsc --noEmit
|
||||
pnpm exec tsc --noEmit
|
||||
|
||||
# E2E tests
|
||||
npm run test:e2e
|
||||
pnpm run test:e2e
|
||||
```
|
||||
|
||||
### Docker Services (Development)
|
||||
|
||||
+7
-25
@@ -1,31 +1,13 @@
|
||||
# Contributing to Documenso
|
||||
|
||||
> **We are no longer accepting external pull requests.**
|
||||
>
|
||||
> Aside from a small group of trusted contributors we reach out to directly, we no longer merge external PRs. New pull requests will usually be closed with a request to open an issue instead. This is a security decision, not a judgement on your work. Read [Why We're Pausing External Pull Requests](https://documenso.com/blog/why-we-re-pausing-external-pull-requests) for the full reasoning.
|
||||
>
|
||||
> Documenso stays open source. You can still read, audit, run, and fork the code. The best way to contribute is through detailed issues.
|
||||
If you plan to contribute to Documenso, please take a moment to feel awesome ✨ People like you are what open source is about ♥. Any contributions, no matter how big or small, are highly appreciated.
|
||||
|
||||
## How to contribute now
|
||||
## Before getting started
|
||||
|
||||
The most useful contribution is a detailed issue. Treat it like a spec. The more detail, the better:
|
||||
|
||||
- The problem you're trying to solve, and who it affects
|
||||
- How you expect the feature or change to behave
|
||||
- Edge cases, constraints, and anything you've already considered
|
||||
- Examples, mockups, or references where they help
|
||||
|
||||
Before opening an issue, search [existing issues](https://github.com/documenso/documenso/issues) and [discussions](https://github.com/documenso/documenso/discussions) for related items. If a proposal is detailed and fits where Documenso is heading, we'll pick it up and build against it.
|
||||
|
||||
For security vulnerabilities, do not open a public issue. Follow our [Security Policy](./SECURITY.md) instead.
|
||||
|
||||
---
|
||||
|
||||
The sections below are for trusted contributors working with us directly, and for anyone running Documenso locally or maintaining a fork.
|
||||
|
||||
## 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.
|
||||
- Before jumping into a PR be sure to search [existing PRs](https://github.com/documenso/documenso/pulls) or [issues](https://github.com/documenso/documenso/issues) for an open or closed item that relates to your submission.
|
||||
- Select an issue from [here](https://github.com/documenso/documenso/issues) or create a new one
|
||||
- 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.
|
||||
|
||||
## Taking issues
|
||||
|
||||
@@ -68,7 +50,7 @@ The development branch is <code>main</code>. All pull requests should be made ag
|
||||
You can build the project with:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## AI-Assisted Development with OpenCode
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
·
|
||||
<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>
|
||||
@@ -51,18 +49,16 @@ Join us in creating the next generation of open trust infrastructure.
|
||||
|
||||
## Community and Next Steps 🎯
|
||||
|
||||
- Try Documenso by self-hosting it or signing up at [documenso.com](https://documenso.com).
|
||||
- Check out the first source code release in this repository and test it.
|
||||
- Tell us what you think in the [Discussions](https://github.com/documenso/documenso/discussions).
|
||||
- Join the [Discord server](https://documen.so/discord) for any questions and getting to know other community members.
|
||||
- Join the [Discord server](https://documen.so/discord) for any questions and getting to know to other community members.
|
||||
- ⭐ the repository to help us raise awareness.
|
||||
- Open detailed [issues](https://github.com/documenso/documenso/issues) to report bugs or propose features.
|
||||
- Spread the word on Twitter that Documenso is working towards a more open signing tool.
|
||||
- Fix or create [issues](https://github.com/documenso/documenso/issues), that are needed for the first production release.
|
||||
|
||||
## Contributing
|
||||
|
||||
> **Note**: We no longer accept external pull requests, aside from a small group of trusted contributors we reach out to directly. The best way to contribute is through detailed issues. Read [Why We're Pausing External Pull Requests](https://documenso.com/blog/why-we-re-pausing-external-pull-requests) for the reasoning.
|
||||
|
||||
- Documenso stays open source. You can read, audit, run, and fork the code.
|
||||
- To report issues or propose changes, see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
|
||||
- To contribute, please see our [contribution guide](https://github.com/documenso/documenso/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Contact us
|
||||
|
||||
@@ -83,21 +79,17 @@ Contact us if you are interested in our Enterprise plan for large organizations
|
||||
<a href=""><img src="" alt=""></a>
|
||||
</p>
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/) - Language
|
||||
- [React Router v7](https://reactrouter.com/) - Framework
|
||||
- [Hono](https://hono.dev/) - Server
|
||||
- [Typescript](https://www.typescriptlang.org/) - Language
|
||||
- [ReactRouter](https://reactrouter.com/) - Framework
|
||||
- [Prisma](https://www.prisma.io/) - ORM
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - CSS
|
||||
- [shadcn/ui](https://ui.shadcn.com/) + [Radix UI](https://www.radix-ui.com/) - Component Library
|
||||
- [Tailwind](https://tailwindcss.com/) - CSS
|
||||
- [shadcn/ui](https://ui.shadcn.com/) - Component Library
|
||||
- [react-email](https://react.email/) - Email Templates
|
||||
- [Lingui](https://lingui.dev/) - Internationalization
|
||||
- [tRPC](https://trpc.io/) - API
|
||||
- [@libpdf/core](https://www.npmjs.com/package/@libpdf/core) - PDF Signatures
|
||||
- [pdf.js](https://mozilla.github.io/pdf.js/) - Viewing PDFs
|
||||
- [@cantoo/pdf-lib](https://github.com/cantoo-scribe/pdf-lib) - PDF manipulation
|
||||
- [@documenso/pdf-sign](https://www.npmjs.com/package/@documenso/pdf-sign) - PDF Signatures (launching soon)
|
||||
- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs
|
||||
- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation
|
||||
- [Stripe](https://stripe.com/) - Payments
|
||||
- [Biome](https://biomejs.dev/) - Linting & Formatting
|
||||
- [Playwright](https://playwright.dev/) - E2E Testing
|
||||
|
||||
<!-- - Support for [opensignpdf (requires Java on server)](https://github.com/open-pdf-sign) is currently planned. -->
|
||||
|
||||
@@ -127,16 +119,15 @@ git clone https://github.com/<your-username>/documenso
|
||||
|
||||
2. Set up your `.env` file using the recommendations in the `.env.example` file. Alternatively, just run `cp .env.example .env` to get started with our handpicked defaults.
|
||||
|
||||
3. Run `npm run dx` in the root directory
|
||||
|
||||
3. Run `pnpm run dx` in the root directory
|
||||
- This will spin up a postgres database and inbucket mailserver in a docker container.
|
||||
|
||||
4. Run `npm run dev` in the root directory
|
||||
4. Run `pnpm run dev` in the root directory
|
||||
|
||||
5. Want it even faster? Just use
|
||||
|
||||
```sh
|
||||
npm run d
|
||||
pnpm run d
|
||||
```
|
||||
|
||||
#### Access Points for Your Application
|
||||
@@ -144,7 +135,6 @@ npm run d
|
||||
1. **App** - http://localhost:3000
|
||||
2. **Incoming Mail Access** - http://localhost:9000
|
||||
3. **Database Connection Details**
|
||||
|
||||
- **Port**: 54320
|
||||
- **Connection**: Use your favorite database client to connect using the provided port.
|
||||
|
||||
@@ -154,7 +144,41 @@ npm run d
|
||||
|
||||
### Manual Setup
|
||||
|
||||
Follow the [manual setup guide](https://docs.documenso.com/docs/developers/local-development/manual) to configure Documenso on your local machine.
|
||||
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 `pnpm install` 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 `pnpm run prisma:migrate-dev`
|
||||
|
||||
6. Run `pnpm run translate:compile` in the root directory to compile lingui
|
||||
|
||||
7. Run `pnpm run dev` in the root directory to start
|
||||
|
||||
8. Register a new user at http://localhost:3000/signup
|
||||
|
||||
---
|
||||
|
||||
- Optional: Seed the database using `pnpm 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)**.
|
||||
|
||||
### Run in Gitpod
|
||||
|
||||
@@ -174,60 +198,150 @@ If you're a visual learner and prefer to watch a video walkthrough of setting up
|
||||
|
||||
## Docker
|
||||
|
||||
We provide official Docker images on [DockerHub](https://hub.docker.com/r/documenso/documenso) and [GitHub Container Registry](https://ghcr.io/documenso/documenso).
|
||||
We provide a Docker container for Documenso, which is published on both DockerHub and GitHub Container Registry.
|
||||
|
||||
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.
|
||||
- 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.
|
||||
|
||||
## Self Hosting
|
||||
|
||||
We support a variety of deployment methods including Docker, Docker Compose, Railway, Kubernetes, and manual deployment.
|
||||
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
||||
|
||||
For full instructions, requirements, and configuration details, see the [Self Hosting documentation](https://docs.documenso.com/docs/self-hosting).
|
||||
### Fetch, configure, and build
|
||||
|
||||
### One-Click Deploys
|
||||
First, clone the code from Github:
|
||||
|
||||
#### Railway
|
||||
```
|
||||
git clone https://github.com/documenso/documenso.git
|
||||
```
|
||||
|
||||
[](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic)
|
||||
Then, inside the `documenso` folder, copy the example env file:
|
||||
|
||||
#### Render
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
pnpm install
|
||||
pnpm run build
|
||||
pnpm run prisma:migrate-deploy
|
||||
```
|
||||
|
||||
Finally, you can start it with:
|
||||
|
||||
```
|
||||
cd apps/remix
|
||||
pnpm 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
|
||||
|
||||
[](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)
|
||||
|
||||
## Security
|
||||
|
||||
If you believe you have found a security vulnerability in Documenso, please report it through our [Security Policy](https://github.com/documenso/documenso/security/policy). We prioritize private reports via [GitHub Security Advisories](https://github.com/documenso/documenso/security/advisories/new). See [SECURITY.md](./SECURITY.md) for scope and details.
|
||||
|
||||
## 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 pnpm run start -- -H ::
|
||||
```
|
||||
|
||||
For k8s or docker-compose
|
||||
|
||||
```yaml
|
||||
containers:
|
||||
- name: documenso
|
||||
image: documenso:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- pnpm
|
||||
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:
|
||||
|
||||
```
|
||||
npm run with:env -- npm run myscript
|
||||
pnpm run with:env pnpm run myscript
|
||||
```
|
||||
|
||||
The same can be done when using `npx` for one of the bin scripts:
|
||||
The same can be done when using `pnpm exec` for one of the bin scripts:
|
||||
|
||||
```
|
||||
npm run with:env -- npx myscript
|
||||
pnpm run with:env pnpm exec myscript
|
||||
```
|
||||
|
||||
This will load environment variables from your `.env` and `.env.local` files.
|
||||
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
We take the security of Documenso seriously. As a platform trusted with legally binding documents, the safety of the project and the people who rely on it is a priority for us. We're grateful to the security researchers who help keep it that way. If you've found an issue, we'd genuinely like to hear about it.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Report security vulnerabilities privately. Do not open a public issue, discussion, or pull request for security reports.
|
||||
|
||||
We accept reports through two channels, in order of preference:
|
||||
|
||||
1. **GitHub Security Advisories (preferred)**. Use the [private vulnerability reporting form](https://github.com/documenso/documenso/security/advisories/new). This is our primary channel and lets us triage and work with you on a fix.
|
||||
2. **Email**. If you cannot use GitHub Security Advisories, email [security@documenso.com](mailto:security@documenso.com).
|
||||
|
||||
Include the affected version, a clear description, steps to reproduce, and the potential impact.
|
||||
|
||||
## Triage and Response
|
||||
|
||||
We triage reports as we have availability. We read every report we receive, and we appreciate the time and effort it takes to put one together.
|
||||
|
||||
We also run [Codex](https://openai.com/codex/) security analysis across the codebase. If Codex has already reported the issue you're sending us, we may close your report as a duplicate. Please don't take this as a reflection on your work; it just means our automated tooling happened to surface the same thing first.
|
||||
|
||||
## Scope
|
||||
|
||||
This policy covers vulnerabilities in the Documenso application code in this repository.
|
||||
|
||||
The items below are out of scope and will not be accepted. They are deployment, infrastructure, and configuration concerns that belong with the operator's firewall, network, and environment setup, not the application:
|
||||
|
||||
- Server-Side Request Forgery (SSRF) and related network-egress concerns
|
||||
- DNS rebinding and other DNS-level issues
|
||||
- Rate limiting, denial of service, and volumetric attacks
|
||||
- TLS and certificate configuration, HTTP security headers, and other reverse-proxy or web-server configuration
|
||||
- Findings that depend on insecure self-hosted infrastructure or misconfiguration
|
||||
|
||||
If you're unsure whether something is in scope, report it privately anyway and we'll happily take a look.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Security fixes are applied to the latest release. Run the most recent version of Documenso.
|
||||
+64
-6
@@ -1,9 +1,67 @@
|
||||
# Signing Certificate
|
||||
# Creating your own signing certificate
|
||||
|
||||
Documenso needs a signing certificate to digitally sign documents. For full, up-to-date instructions on generating, converting, and configuring a certificate, see the official documentation:
|
||||
For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
|
||||
|
||||
- [Signing Certificate](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate): Overview and all certificate options
|
||||
- [Local Certificate](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate/local): Generate a self-signed `.p12` certificate with OpenSSL
|
||||
- [Google Cloud HSM](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm): Sign using Google Cloud KMS
|
||||
1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:
|
||||
|
||||
For deploying Documenso with Docker, 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.
|
||||
`openssl genrsa -out private.key 2048`
|
||||
|
||||
2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
|
||||
|
||||
`openssl req -new -x509 -key private.key -out certificate.crt -days 365`
|
||||
|
||||
This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The `-days` parameter sets the number of days for which the certificate is valid.
|
||||
|
||||
3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following commands to do this:
|
||||
|
||||
```bash
|
||||
# Set certificate password securely (won't appear in command history)
|
||||
read -s -p "Enter certificate password: " CERT_PASS
|
||||
echo
|
||||
|
||||
# Create the p12 certificate using the environment variable
|
||||
openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt \
|
||||
-password env:CERT_PASS \
|
||||
-keypbe PBE-SHA1-3DES \
|
||||
-certpbe PBE-SHA1-3DES \
|
||||
-macalg sha1
|
||||
```
|
||||
|
||||
4. **IMPORTANT**: A certificate password is required to prevent signing failures. Make sure to use a strong password (minimum 4 characters) when prompted. Certificates without passwords will cause "Failed to get private key bags" errors during document signing.
|
||||
|
||||
5. Place the certificate `/apps/remix/resources/certificate.p12` (If the path does not exist, it needs to be created)
|
||||
|
||||
## Docker
|
||||
|
||||
> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
|
||||
|
||||
Want to create a production ready docker image? Follow these steps:
|
||||
|
||||
- cd into `docker` directory
|
||||
- Make `build.sh` executable by running `chmod +x build.sh`
|
||||
- Run `./build.sh` to start building the docker image.
|
||||
- Publish the image to your docker registry of choice (or) If you prefer running the image from local, run the below command
|
||||
|
||||
```
|
||||
docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest
|
||||
```
|
||||
|
||||
Command Breakdown:
|
||||
|
||||
- `-d` - Let's you run the container in background
|
||||
- `-p` - Passes down which ports to use. First half is the host port, Second half is the app port. You can change the first half anything you want and reverse proxy to that port.
|
||||
- `-v` - Volume let's you persist the data
|
||||
- `--name` - Name of the container
|
||||
- `documenso:latest` - Image you have built
|
||||
|
||||
## Deployment
|
||||
|
||||
We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
|
||||
|
||||
## Railway
|
||||
|
||||
[](https://railway.app/template/DjrRRX)
|
||||
|
||||
## Render
|
||||
|
||||
[](https://render.com/deploy?repo=https://github.com/documenso/documenso)
|
||||
|
||||
+38
-9
@@ -1,16 +1,45 @@
|
||||
# @documenso/docs
|
||||
# docs
|
||||
|
||||
The Documenso documentation site, built with [Next.js](https://nextjs.org/) and [Fumadocs](https://fumadocs.dev/). Published at [docs.documenso.com](https://docs.documenso.com).
|
||||
This is a Next.js application generated with
|
||||
[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
|
||||
|
||||
Content lives under `content/docs/` as MDX. See [WRITING_STYLE.md](../../WRITING_STYLE.md) for the documentation writing conventions.
|
||||
Run development server:
|
||||
|
||||
```bash
|
||||
# From the monorepo root
|
||||
npm run dev --filter=@documenso/docs
|
||||
npm run dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
## Structure
|
||||
Open http://localhost:3000 with your browser to see the result.
|
||||
|
||||
- `content/docs/`: Documentation pages (MDX).
|
||||
- `lib/source.ts`: Content source adapter.
|
||||
- `lib/layout.shared.tsx`: Shared layout options.
|
||||
## Explore
|
||||
|
||||
In the project, you can see:
|
||||
|
||||
- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
|
||||
- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.
|
||||
|
||||
| Route | Description |
|
||||
| ------------------------- | ------------------------------------------------------ |
|
||||
| `app/(home)` | The route group for your landing page and other pages. |
|
||||
| `app/docs` | The documentation layout and pages. |
|
||||
| `app/api/search/route.ts` | The Route Handler for search. |
|
||||
|
||||
### Fumadocs MDX
|
||||
|
||||
A `source.config.ts` config file has been included, you can customise different options like frontmatter schema.
|
||||
|
||||
Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js and Fumadocs, take a look at the following
|
||||
resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
|
||||
features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs
|
||||
|
||||
+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 | Compliant (Enterprise) |
|
||||
| HIPAA | Planned |
|
||||
|
||||
## 21 CFR Part 11
|
||||
|
||||
@@ -43,7 +43,7 @@ ISO 27001 is an international standard for managing information security, specif
|
||||
|
||||
## HIPAA
|
||||
|
||||
<Callout type="info">Status: [Compliant](https://documen.so/trust)</Callout>
|
||||
<Callout type="warn">Status: [Planned](https://github.com/documenso/backlog/issues/25)</Callout>
|
||||
|
||||
The HIPAA (Health Insurance Portability and Accountability Act) is a U.S. law designed to protect patient health information's privacy and security and improve the healthcare system's efficiency and effectiveness.
|
||||
|
||||
|
||||
@@ -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](/docs/concepts/signing-certificates) documentation.
|
||||
For specific implementation details and configuration options, refer to the [signing certificates](/signing-certificates/overview) documentation.
|
||||
|
||||
Self-hosted deployments can configure their own signing certificates and timestamp authorities to meet specific compliance requirements.
|
||||
|
||||
## Related
|
||||
|
||||
- [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
|
||||
- [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
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"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](/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
|
||||
- [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
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
---
|
||||
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)
|
||||
@@ -1,22 +1,17 @@
|
||||
---
|
||||
title: Developer Mode
|
||||
description: Advanced development tools for debugging field IDs, recipient IDs, coordinates and integrating with the Documenso API.
|
||||
description: Advanced development tools for debugging field coordinates and integrating with the Documenso API.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Developer mode provides additional tools and features to help you integrate and debug Documenso.
|
||||
|
||||
## Field Information
|
||||
## Field Coordinates
|
||||
|
||||
When enabled, developer mode displays the following information for each field:
|
||||
Field coordinates represent the position of a field in a document. They are returned in the `pageX`, `pageY`, `width` and `height` properties of the field.
|
||||
|
||||
- **Field ID** - The unique identifier of the field
|
||||
- **Recipient ID** - The ID of the recipient assigned to the field
|
||||
- **Pos X / Pos Y** - The position of the field on the page
|
||||
- **Width / Height** - The dimensions of the field
|
||||
|
||||
To enable developer mode, add the `devmode=true` query parameter to the editor URL.
|
||||
To enable field coordinates, add the `devmode=true` query parameter to the editor URL.
|
||||
|
||||
```bash
|
||||
# Legacy editor
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"teams",
|
||||
"rate-limits",
|
||||
"versioning",
|
||||
"developer-mode",
|
||||
"common-errors"
|
||||
"developer-mode"
|
||||
]
|
||||
}
|
||||
|
||||
+19
-13
@@ -1,21 +1,21 @@
|
||||
---
|
||||
title: V1 Editor
|
||||
description: Embed V1 document and template creation directly in your application.
|
||||
title: Authoring
|
||||
description: Embed document and template creation directly in your application.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
V1 editor components allow your users to create and edit documents and templates using the V1 Documents and Templates API without leaving your application.
|
||||
In addition to embedding signing, Documenso supports embedded authoring. It allows your users to create and edit documents and templates without leaving your application.
|
||||
|
||||
<Callout type="warn">
|
||||
Embedded editor is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
|
||||
Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
|
||||
also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing).
|
||||
Contact sales for access.
|
||||
</Callout>
|
||||
|
||||
## Components
|
||||
|
||||
The SDK provides four V1 editor components:
|
||||
The SDK provides four authoring components:
|
||||
|
||||
| Component | Purpose |
|
||||
| ----------------------- | ----------------------- |
|
||||
@@ -24,16 +24,24 @@ The SDK provides four V1 editor components:
|
||||
| `EmbedUpdateDocumentV1` | Edit existing documents |
|
||||
| `EmbedUpdateTemplateV1` | Edit existing templates |
|
||||
|
||||
All authoring components require a **presign token** for authentication instead of a regular token.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
Before using any authoring component, obtain a presign token from your backend:
|
||||
|
||||
```
|
||||
POST /api/v2/embedding/create-presign-token
|
||||
```
|
||||
|
||||
This endpoint requires your Documenso API key. The token has a default expiration of 1 hour.
|
||||
|
||||
See the [API documentation](https://openapi.documenso.com/reference#tag/embedding) for full details.
|
||||
|
||||
<Callout type="warn">
|
||||
A presigned token is NOT an API token
|
||||
Presign tokens should be created server-side. Never expose your API key in client-side code.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
@@ -131,7 +139,7 @@ const TemplateEditor = ({ presignToken, templateId }) => {
|
||||
|
||||
## Props
|
||||
|
||||
### All Editor Components
|
||||
### All Authoring Components
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| ------------------ | --------- | -------- | -------------------------------------------------------- |
|
||||
@@ -139,11 +147,10 @@ const TemplateEditor = ({ presignToken, templateId }) => {
|
||||
| `externalId` | `string` | No | Your reference ID to link with the document or template |
|
||||
| `host` | `string` | No | Custom host URL. Defaults to `https://app.documenso.com` |
|
||||
| `css` | `string` | No | Custom CSS string (Platform Plan) |
|
||||
| `cssVars` | `object` | No | [CSS variable](/docs/developers/embedding/css-variables) overrides (Platform Plan) |
|
||||
| `cssVars` | `object` | No | CSS variable 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 editor experience |
|
||||
| `features` | `object` | No | Feature toggles for the authoring experience |
|
||||
|
||||
### Update Components Only
|
||||
|
||||
@@ -157,7 +164,7 @@ const TemplateEditor = ({ presignToken, templateId }) => {
|
||||
|
||||
## Feature Toggles
|
||||
|
||||
Customize what options are available in the editor experience:
|
||||
Customize what options are available in the authoring experience:
|
||||
|
||||
```jsx
|
||||
<EmbedCreateDocumentV1
|
||||
@@ -294,7 +301,6 @@ Pass extra props to the iframe for testing experimental features:
|
||||
## See Also
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props
|
||||
- [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
|
||||
@@ -59,7 +59,6 @@ Use the `cssVars` prop on any embed component to override default colors, spacin
|
||||
| `destructiveForeground` | Destructive/danger text color |
|
||||
| `ring` | Focus ring color |
|
||||
| `warning` | Warning/alert color |
|
||||
| `envelopeEditorBackground` | Envelope editor background color. _V2 Envelope Editor only._ |
|
||||
|
||||
### Spacing
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
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 editor. It allows your users to create and edit documents, templates, and envelopes without leaving your application.
|
||||
|
||||
<Callout type="warn">
|
||||
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 editor is available in two versions:
|
||||
|
||||
- **[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
|
||||
|
||||
| Aspect | V1 | V2 |
|
||||
| --- | --- | --- |
|
||||
| Entity model | Documents and Templates (separate) | Envelopes (unified, can be documents or templates) |
|
||||
| API compatibility | V1 Documents/Templates API | V2 Envelopes API |
|
||||
| Customization | 6 simple boolean flags | Rich structured settings with sections (general, settings, actions, envelope items, recipients) |
|
||||
|
||||
---
|
||||
|
||||
## Presign Tokens
|
||||
|
||||
Before using any editor component, obtain a presign token from your backend:
|
||||
|
||||
```
|
||||
POST /api/v2/embedding/create-presign-token
|
||||
```
|
||||
|
||||
This endpoint requires your Documenso API key. The token has a default expiration of 1 hour.
|
||||
|
||||
See the [API documentation](https://openapi.documenso.com/reference#tag/embedding) for full details.
|
||||
|
||||
<Callout type="warn">
|
||||
Presign tokens should be created server-side. Never expose your API key in client-side code.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [V1 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,4 +0,0 @@
|
||||
{
|
||||
"title": "Editor",
|
||||
"pages": ["v1", "v2"]
|
||||
}
|
||||
@@ -1,344 +0,0 @@
|
||||
---
|
||||
title: V2 Editor
|
||||
description: Embed envelope creation and editing directly in your application.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
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 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 editor components:
|
||||
|
||||
| Component | Purpose |
|
||||
| ---------------------- | ------------------------ |
|
||||
| `EmbedCreateEnvelope` | Create new envelopes |
|
||||
| `EmbedUpdateEnvelope` | Edit existing envelopes |
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Creating Envelopes
|
||||
|
||||
Use `EmbedCreateEnvelope` to embed envelope creation. The `type` prop determines whether the envelope is created as a document or template.
|
||||
|
||||
```jsx
|
||||
import { EmbedCreateEnvelope } from '@documenso/embed-react';
|
||||
|
||||
const EnvelopeCreator = ({ presignToken }) => {
|
||||
return (
|
||||
<div style={{ height: '800px', width: '100%' }}>
|
||||
<EmbedCreateEnvelope
|
||||
presignToken={presignToken}
|
||||
type="DOCUMENT"
|
||||
externalId="order-12345"
|
||||
onEnvelopeCreated={(data) => {
|
||||
console.log('Envelope created:', data.envelopeId);
|
||||
console.log('External ID:', data.externalId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
To create a template instead of a document, set `type` to `"TEMPLATE"`:
|
||||
|
||||
```jsx
|
||||
<EmbedCreateEnvelope
|
||||
presignToken={presignToken}
|
||||
type="TEMPLATE"
|
||||
externalId="template-12345"
|
||||
onEnvelopeCreated={(data) => {
|
||||
console.log('Template envelope created:', data.envelopeId);
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Editing Envelopes
|
||||
|
||||
Use `EmbedUpdateEnvelope` to embed envelope editing:
|
||||
|
||||
```jsx
|
||||
import { EmbedUpdateEnvelope } from '@documenso/embed-react';
|
||||
|
||||
const EnvelopeEditor = ({ presignToken, envelopeId }) => {
|
||||
return (
|
||||
<div style={{ height: '800px', width: '100%' }}>
|
||||
<EmbedUpdateEnvelope
|
||||
presignToken={presignToken}
|
||||
envelopeId={envelopeId}
|
||||
externalId="order-12345"
|
||||
onEnvelopeUpdated={(data) => {
|
||||
console.log('Envelope updated:', data.envelopeId);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Props
|
||||
|
||||
### 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 |
|
||||
| `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
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| ---------- | ------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | `"DOCUMENT"` \| `"TEMPLATE"` | Yes | Whether to create a document or template envelope |
|
||||
| `folderId` | `string` | No | The ID of the folder to create the envelope in. If not provided, the envelope is created in the root folder. The folder must match the envelope type and team. |
|
||||
|
||||
### Update Component Only
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
| ------------ | -------- | -------- | ---------------------------- |
|
||||
| `envelopeId` | `string` | Yes | The envelope ID to edit |
|
||||
|
||||
---
|
||||
|
||||
## Feature Toggles
|
||||
|
||||
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
|
||||
presignToken={presignToken}
|
||||
type="DOCUMENT"
|
||||
features={{
|
||||
general: {
|
||||
allowConfigureEnvelopeTitle: false,
|
||||
allowUploadAndRecipientStep: false,
|
||||
allowAddFieldsStep: false,
|
||||
allowPreviewStep: false,
|
||||
},
|
||||
settings: {
|
||||
allowConfigureSignatureTypes: false,
|
||||
allowConfigureLanguage: false,
|
||||
allowConfigureDateFormat: false,
|
||||
},
|
||||
recipients: {
|
||||
allowApproverRole: false,
|
||||
allowViewerRole: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### General
|
||||
|
||||
Controls the overall editor flow and UI:
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| ------------------------------- | --------- | ------- | ------------------------------------------------ |
|
||||
| `allowConfigureEnvelopeTitle` | `boolean` | `true` | Allow editing the envelope title |
|
||||
| `allowUploadAndRecipientStep` | `boolean` | `true` | Show the upload and recipient configuration step |
|
||||
| `allowAddFieldsStep` | `boolean` | `true` | Show the add fields step |
|
||||
| `allowPreviewStep` | `boolean` | `true` | Show the preview step |
|
||||
| `minimizeLeftSidebar` | `boolean` | `true` | Minimize the left sidebar by default |
|
||||
|
||||
### Settings
|
||||
|
||||
Controls envelope configuration options. Set to `null` to hide envelope settings entirely.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| ----------------------------------- | --------- | ------- | ----------------------------------------- |
|
||||
| `allowConfigureSignatureTypes` | `boolean` | `true` | Allow configuring signature types |
|
||||
| `allowConfigureLanguage` | `boolean` | `true` | Allow configuring the language |
|
||||
| `allowConfigureDateFormat` | `boolean` | `true` | Allow configuring the date format |
|
||||
| `allowConfigureTimezone` | `boolean` | `true` | Allow configuring the timezone |
|
||||
| `allowConfigureRedirectUrl` | `boolean` | `true` | Allow configuring a redirect URL |
|
||||
| `allowConfigureDistribution` | `boolean` | `true` | Allow configuring distribution settings |
|
||||
| `allowConfigureExpirationPeriod` | `boolean` | `true` | Allow configuring the expiration period |
|
||||
| `allowConfigureEmailSender` | `boolean` | `true` | Allow configuring the email sender |
|
||||
| `allowConfigureEmailReplyTo` | `boolean` | `true` | Allow configuring the email reply-to |
|
||||
|
||||
### Actions
|
||||
|
||||
Controls available actions during editing:
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| ------------------ | --------- | ------- | ------------------------ |
|
||||
| `allowAttachments` | `boolean` | `true` | Allow adding attachments |
|
||||
|
||||
### Envelope Items
|
||||
|
||||
Controls how envelope items (individual files within the envelope) can be managed. Set to `null` to prevent any item modifications.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| --------------------- | --------- | ------- | ------------------------------------ |
|
||||
| `allowConfigureTitle` | `boolean` | `true` | Allow editing item titles |
|
||||
| `allowConfigureOrder` | `boolean` | `true` | Allow reordering items |
|
||||
| `allowUpload` | `boolean` | `true` | Allow uploading new items |
|
||||
| `allowDelete` | `boolean` | `true` | Allow deleting items |
|
||||
| `allowReplace` | `boolean` | `true` | Allow replacing an item's PDF |
|
||||
|
||||
### Recipients
|
||||
|
||||
Controls recipient configuration options. Set to `null` to prevent any recipient modifications.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| --------------------------------- | --------- | ------- | ---------------------------------------- |
|
||||
| `allowConfigureSigningOrder` | `boolean` | `true` | Allow configuring the signing order |
|
||||
| `allowConfigureDictateNextSigner` | `boolean` | `true` | Allow configuring dictate next signer |
|
||||
| `allowApproverRole` | `boolean` | `true` | Allow the approver recipient role |
|
||||
| `allowViewerRole` | `boolean` | `true` | Allow the viewer recipient role |
|
||||
| `allowCCerRole` | `boolean` | `true` | Allow the CC recipient role |
|
||||
| `allowAssistantRole` | `boolean` | `true` | Allow the assistant recipient role |
|
||||
|
||||
### Disabling Steps
|
||||
|
||||
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
|
||||
presignToken={presignToken}
|
||||
type="DOCUMENT"
|
||||
features={{
|
||||
general: {
|
||||
allowUploadAndRecipientStep: false, // Skip the upload and recipient step
|
||||
allowAddFieldsStep: false, // Skip the add fields step
|
||||
allowPreviewStep: false, // Skip the preview step
|
||||
},
|
||||
settings: null, // Hide all envelope settings
|
||||
envelopeItems: null, // Prevent item modifications
|
||||
recipients: null, // Prevent recipient modifications
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Callbacks
|
||||
|
||||
### `onEnvelopeCreated`
|
||||
|
||||
Fired when an envelope is successfully created:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------ | ---------------- | --------------------------------------- |
|
||||
| `envelopeId` | `string` | The ID of the created envelope |
|
||||
| `externalId` | `string \| null` | Your external reference ID, if provided |
|
||||
|
||||
### `onEnvelopeUpdated`
|
||||
|
||||
Fired when an envelope is successfully updated:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ------------ | ---------------- | --------------------------------------- |
|
||||
| `envelopeId` | `string` | The ID of the updated envelope |
|
||||
| `externalId` | `string \| null` | Your external reference ID, if provided |
|
||||
|
||||
---
|
||||
|
||||
## Complete Integration Example
|
||||
|
||||
This example shows a full workflow where users create an envelope and then edit it:
|
||||
|
||||
```tsx
|
||||
import { useState } from 'react';
|
||||
|
||||
import { EmbedCreateEnvelope, EmbedUpdateEnvelope } from '@documenso/embed-react';
|
||||
|
||||
const EnvelopeManager = ({ presignToken }) => {
|
||||
const [envelopeId, setEnvelopeId] = useState(null);
|
||||
const [mode, setMode] = useState('create');
|
||||
|
||||
if (mode === 'success') {
|
||||
return (
|
||||
<div>
|
||||
<h2>Envelope updated successfully</h2>
|
||||
<button
|
||||
onClick={() => {
|
||||
setEnvelopeId(null);
|
||||
setMode('create');
|
||||
}}
|
||||
>
|
||||
Create Another Envelope
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mode === 'edit' && envelopeId) {
|
||||
return (
|
||||
<div style={{ height: '800px', width: '100%' }}>
|
||||
<button onClick={() => setMode('create')}>Back to Create</button>
|
||||
<EmbedUpdateEnvelope
|
||||
presignToken={presignToken}
|
||||
envelopeId={envelopeId}
|
||||
onEnvelopeUpdated={(data) => {
|
||||
console.log('Envelope updated:', data.envelopeId);
|
||||
setMode('success');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ height: '800px', width: '100%' }}>
|
||||
<EmbedCreateEnvelope
|
||||
presignToken={presignToken}
|
||||
type="DOCUMENT"
|
||||
features={{
|
||||
general: {
|
||||
allowConfigureEnvelopeTitle: false,
|
||||
},
|
||||
settings: {
|
||||
allowConfigureSignatureTypes: false,
|
||||
allowConfigureLanguage: false,
|
||||
},
|
||||
}}
|
||||
onEnvelopeCreated={(data) => {
|
||||
console.log('Envelope created:', data.envelopeId);
|
||||
setEnvelopeId(data.envelopeId);
|
||||
setMode('edit');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [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
|
||||
@@ -1,81 +0,0 @@
|
||||
---
|
||||
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 Editor
|
||||
## Embedded Signing vs Embedded Authoring
|
||||
|
||||
Documenso offers two types of embedding:
|
||||
|
||||
- **Embedded Signing** lets you embed the signing experience in your application. Your users sign documents without leaving your site. Available on Teams Plan and above.
|
||||
- **Embedded 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.
|
||||
- **Embedded Authoring** lets you embed document and template _creation and editing_ in your application. This is an [Enterprise](/docs/policies/enterprise-edition) feature (also available as a Platform Plan add-on). See the [Authoring](/docs/developers/embedding/authoring) guide.
|
||||
|
||||
This page covers **embedded signing**. If you need your users to create or edit documents inside your app, see [Editor](/docs/developers/embedding/editor).
|
||||
This page covers **embedded signing**. If you need your users to create or edit documents inside your app, see [Authoring](/docs/developers/embedding/authoring).
|
||||
|
||||
---
|
||||
|
||||
@@ -161,7 +161,6 @@ If you prefer not to use any SDK, you can embed signing using [Direct Links](/do
|
||||
| `css` | `string` | Custom CSS string (Platform Plan). |
|
||||
| `cssVars` | `object` | CSS variable overrides for theming (Platform Plan). |
|
||||
| `darkModeDisabled` | `boolean` | Disable dark mode in the embed (Platform Plan). |
|
||||
| `language` | `string` | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts). |
|
||||
| `onDocumentReady` | `function` | Called when the document is loaded and ready. |
|
||||
| `onDocumentCompleted` | `function` | Called when signing is completed. |
|
||||
| `onDocumentError` | `function` | Called when an error occurs. |
|
||||
@@ -176,7 +175,6 @@ If you prefer not to use any SDK, you can embed signing using [Direct Links](/do
|
||||
| `host` | `string` | Documenso instance URL. Defaults to `https://app.documenso.com`. |
|
||||
| `name` | `string` | Pre-fill the signer's name. |
|
||||
| `lockName` | `boolean` | Prevent the signer from changing their name. |
|
||||
| `language` | `string` | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts). |
|
||||
| `onDocumentReady` | `function` | Called when the document is loaded and ready. |
|
||||
| `onDocumentCompleted` | `function` | Called when signing is completed. |
|
||||
| `onDocumentError` | `function` | Called when an error occurs. |
|
||||
@@ -229,9 +227,9 @@ Receives an object with:
|
||||
href="/docs/developers/embedding/css-variables"
|
||||
/>
|
||||
<Card
|
||||
title="Editor"
|
||||
title="Authoring"
|
||||
description="Embed document and template creation."
|
||||
href="/docs/developers/embedding/editor"
|
||||
href="/docs/developers/embedding/authoring"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Embedding",
|
||||
"pages": ["sdks", "direct-links", "css-variables", "editor", "iframe"]
|
||||
"pages": ["sdks", "direct-links", "css-variables", "authoring"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
- [Authoring](/docs/developers/embedding/authoring) - 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
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
- [Authoring](/docs/developers/embedding/authoring) - 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
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
- [Authoring](/docs/developers/embedding/authoring) - 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
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
- [Authoring](/docs/developers/embedding/authoring) - 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
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
- [Authoring](/docs/developers/embedding/authoring) - 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
|
||||
- [Editor](/docs/developers/embedding/editor) - Embed document creation
|
||||
- [Authoring](/docs/developers/embedding/authoring) - 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/document \
|
||||
curl https://app.documenso.com/api/v2/documents \
|
||||
-H "Authorization: api_xxxxxxxxxxxxxxxx"
|
||||
```
|
||||
|
||||
### JavaScript / TypeScript
|
||||
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/document', {
|
||||
const response = await fetch('https://app.documenso.com/api/v2/documents', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: 'api_xxxxxxxxxxxxxxxx',
|
||||
|
||||
@@ -15,17 +15,16 @@ Pick the one that fits your needs the best.
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- [TypeScript](https://www.typescriptlang.org/) - Language
|
||||
- [React Router v7](https://reactrouter.com/) - Framework
|
||||
- [Hono](https://hono.dev/) - Server
|
||||
- [Typescript](https://www.typescriptlang.org/) - Language
|
||||
- [React Router](https://reactrouter.com/) - Framework
|
||||
- [Prisma](https://www.prisma.io/) - ORM
|
||||
- [Tailwind CSS](https://tailwindcss.com/) - CSS
|
||||
- [shadcn/ui](https://ui.shadcn.com/) + [Radix UI](https://www.radix-ui.com/) - Component Library
|
||||
- [Tailwind](https://tailwindcss.com/) - CSS
|
||||
- [shadcn/ui](https://ui.shadcn.com/) - Component Library
|
||||
- [react-email](https://react.email/) - Email Templates
|
||||
- [Lingui](https://lingui.dev/) - Internationalization
|
||||
- [tRPC](https://trpc.io/) - API
|
||||
- [@libpdf/core](https://www.npmjs.com/package/@libpdf/core) - PDF Signing and Manipulation
|
||||
- [pdf.js](https://mozilla.github.io/pdf.js/) - Viewing PDFs
|
||||
- [@documenso/pdf-sign](https://www.npmjs.com/package/@documenso/pdf-sign) - PDF Signatures
|
||||
- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs
|
||||
- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation
|
||||
- [Stripe](https://stripe.com/) - Payments
|
||||
|
||||
<div className="mt-16 flex items-center justify-center gap-4">
|
||||
|
||||
@@ -83,15 +83,6 @@ 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
|
||||
|
||||
@@ -114,20 +105,6 @@ 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
|
||||
|
||||
@@ -13,7 +13,7 @@ All webhook events share a common structure:
|
||||
{
|
||||
"event": "DOCUMENT_COMPLETED",
|
||||
"payload": {
|
||||
// Document or template data with recipients
|
||||
// Document data with recipients
|
||||
},
|
||||
"createdAt": "2024-04-22T11:52:18.277Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
@@ -33,13 +33,14 @@ All webhook events share a common structure:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------------- | --------- | ------------------------------------------------------ |
|
||||
| `id` | number | Document or template ID |
|
||||
| `id` | number | Document ID |
|
||||
| `externalId` | string? | External identifier for integration |
|
||||
| `userId` | number | Owner's user ID |
|
||||
| `authOptions` | object? | Document-level authentication options |
|
||||
| `formValues` | object? | PDF form values associated with the document |
|
||||
| `title` | string | Document or template title |
|
||||
| `title` | string | Document title |
|
||||
| `status` | string | Current status: `DRAFT`, `PENDING`, `COMPLETED` |
|
||||
| `documentDataId` | string | Reference to the document's PDF data |
|
||||
| `visibility` | string | Document visibility setting |
|
||||
| `createdAt` | datetime | Document creation timestamp |
|
||||
| `updatedAt` | datetime | Last modification timestamp |
|
||||
@@ -49,50 +50,45 @@ All webhook events share a common structure:
|
||||
| `templateId` | number? | Template ID if created from a template |
|
||||
| `source` | string | Source: `DOCUMENT` or `TEMPLATE` |
|
||||
| `documentMeta` | object | Document metadata (subject, message, signing options) |
|
||||
| `recipients` | array | List of recipient objects |
|
||||
| `Recipient` | array | List of recipient objects (legacy, same as recipients) |
|
||||
| `Recipient` | array | List of recipient objects |
|
||||
|
||||
### Document Metadata Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
| -------------------------- | ------- | --------------------------------------- |
|
||||
| `id` | string | Metadata record identifier |
|
||||
| `subject` | string? | Email subject line |
|
||||
| `message` | string? | Email message body |
|
||||
| `timezone` | string | Timezone for date display |
|
||||
| `password` | string? | Document access password (if set) |
|
||||
| `dateFormat` | string | Date format string |
|
||||
| `redirectUrl` | string? | URL to redirect after signing |
|
||||
| `signingOrder` | string | `PARALLEL` or `SEQUENTIAL` |
|
||||
| `allowDictateNextSigner` | boolean | Whether signers can choose the next signer |
|
||||
| `typedSignatureEnabled` | boolean | Whether typed signatures are allowed |
|
||||
| `uploadSignatureEnabled` | boolean | Whether uploaded signatures are allowed |
|
||||
| `drawSignatureEnabled` | boolean | Whether drawn signatures are allowed |
|
||||
| `language` | string | Document language code |
|
||||
| `distributionMethod` | string | How document is distributed |
|
||||
| `emailSettings` | object? | Custom email settings for this document |
|
||||
| Field | Type | Description |
|
||||
| ----------------------- | ------- | --------------------------------------- |
|
||||
| `id` | string | Metadata record identifier |
|
||||
| `subject` | string? | Email subject line |
|
||||
| `message` | string? | Email message body |
|
||||
| `timezone` | string | Timezone for date display |
|
||||
| `password` | string? | Document access password (if set) |
|
||||
| `dateFormat` | string | Date format string |
|
||||
| `redirectUrl` | string? | URL to redirect after signing |
|
||||
| `signingOrder` | string | `PARALLEL` or `SEQUENTIAL` |
|
||||
| `typedSignatureEnabled` | boolean | Whether typed signatures are allowed |
|
||||
| `language` | string | Document language code |
|
||||
| `distributionMethod` | string | How document is distributed |
|
||||
| `emailSettings` | object? | Custom email settings for this document |
|
||||
|
||||
### Recipient Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------------------- | --------- | ------------------------------------------ |
|
||||
| `id` | number | Recipient ID |
|
||||
| `documentId` | number? | Parent document ID |
|
||||
| `templateId` | number? | Template ID if created from a template |
|
||||
| `email` | string | Recipient email address |
|
||||
| `name` | string | Recipient name |
|
||||
| `token` | string | Unique signing token |
|
||||
| `documentDeletedAt` | datetime? | When the recipient hid the document |
|
||||
| `expiresAt` | datetime? | When the recipient's signing link expires |
|
||||
| `expirationNotifiedAt` | datetime? | When the expiration notification was sent |
|
||||
| `signedAt` | datetime? | When recipient signed |
|
||||
| `authOptions` | object? | Per-recipient authentication options |
|
||||
| `role` | string | Role: `SIGNER`, `VIEWER`, `APPROVER`, `ASSISTANT`, `CC` |
|
||||
| `signingOrder` | number? | Position in signing sequence |
|
||||
| `readStatus` | string | `NOT_OPENED` or `OPENED` |
|
||||
| `signingStatus` | string | `NOT_SIGNED`, `SIGNED`, or `REJECTED` |
|
||||
| `sendStatus` | string | `NOT_SENT` or `SENT` |
|
||||
| `rejectionReason` | string? | Reason if recipient rejected |
|
||||
| Field | Type | Description |
|
||||
| ------------------- | --------- | ------------------------------------------ |
|
||||
| `id` | number | Recipient ID |
|
||||
| `documentId` | number | Parent document ID |
|
||||
| `templateId` | number? | Template ID if created from a template |
|
||||
| `email` | string | Recipient email address |
|
||||
| `name` | string | Recipient name |
|
||||
| `token` | string | Unique signing token |
|
||||
| `documentDeletedAt` | datetime? | When the document was deleted (if deleted) |
|
||||
| `expired` | boolean? | Whether the recipient's link has expired |
|
||||
| `signedAt` | datetime? | When recipient signed |
|
||||
| `authOptions` | object? | Per-recipient authentication options |
|
||||
| `role` | string | Role: `SIGNER`, `VIEWER`, `APPROVER`, `CC` |
|
||||
| `signingOrder` | number? | Position in signing sequence |
|
||||
| `readStatus` | string | `NOT_OPENED` or `OPENED` |
|
||||
| `signingStatus` | string | `NOT_SIGNED`, `SIGNED`, or `REJECTED` |
|
||||
| `sendStatus` | string | `NOT_SENT` or `SENT` |
|
||||
| `rejectionReason` | string? | Reason if recipient rejected |
|
||||
|
||||
---
|
||||
|
||||
@@ -102,7 +98,7 @@ These events track the document through its lifecycle.
|
||||
|
||||
### `document.created`
|
||||
|
||||
Triggered when a new document is created.
|
||||
Triggered when a new document is uploaded.
|
||||
|
||||
**Event name:** `DOCUMENT_CREATED`
|
||||
|
||||
@@ -118,6 +114,7 @@ Triggered when a new document is created.
|
||||
"visibility": "EVERYONE",
|
||||
"title": "contract.pdf",
|
||||
"status": "DRAFT",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:44:43.341Z",
|
||||
"completedAt": null,
|
||||
@@ -134,35 +131,11 @@ Triggered when a new document is created.
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"allowDictateNextSigner": false,
|
||||
"typedSignatureEnabled": true,
|
||||
"uploadSignatureEnabled": true,
|
||||
"drawSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "NOT_SENT"
|
||||
}
|
||||
],
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
@@ -172,8 +145,7 @@ Triggered when a new document is created.
|
||||
"name": "John Doe",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
@@ -210,6 +182,7 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT"
|
||||
"visibility": "EVERYONE",
|
||||
"title": "contract.pdf",
|
||||
"status": "PENDING",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:48:07.569Z",
|
||||
"completedAt": null,
|
||||
@@ -226,35 +199,11 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT"
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"allowDictateNextSigner": false,
|
||||
"typedSignatureEnabled": true,
|
||||
"uploadSignatureEnabled": true,
|
||||
"drawSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
],
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
@@ -264,8 +213,7 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT"
|
||||
"name": "John Doe",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
@@ -282,106 +230,6 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT"
|
||||
}
|
||||
```
|
||||
|
||||
### `document.opened`
|
||||
|
||||
Triggered when a recipient opens the document for the first time.
|
||||
|
||||
**Event name:** `DOCUMENT_OPENED`
|
||||
|
||||
The recipient's `readStatus` changes to `OPENED`.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_OPENED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"status": "PENDING",
|
||||
"title": "contract.pdf",
|
||||
"source": "DOCUMENT",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:50:26.174Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
### `document.signed`
|
||||
|
||||
Triggered when a recipient signs the document. This fires for each individual signature, not just when the document is fully completed.
|
||||
|
||||
**Event name:** `DOCUMENT_SIGNED`
|
||||
|
||||
The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_SIGNED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"status": "COMPLETED",
|
||||
"title": "contract.pdf",
|
||||
"source": "DOCUMENT",
|
||||
"completedAt": "2024-04-22T11:52:05.707Z",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 51,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
"signedAt": "2024-04-22T11:52:05.688Z",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:52:18.577Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
### `document.recipient.completed`
|
||||
|
||||
Triggered when an individual recipient completes their required action (signing, approving, or viewing). This is useful for tracking per-recipient progress in documents with multiple recipients.
|
||||
|
||||
**Event name:** `DOCUMENT_RECIPIENT_COMPLETED`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_RECIPIENT_COMPLETED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"status": "PENDING",
|
||||
"title": "contract.pdf",
|
||||
"source": "DOCUMENT",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
"signedAt": "2024-04-22T11:52:05.688Z",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:52:06.000Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
### `document.completed`
|
||||
|
||||
Triggered when all recipients have completed their required actions.
|
||||
@@ -402,6 +250,7 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
"visibility": "EVERYONE",
|
||||
"title": "contract.pdf",
|
||||
"status": "COMPLETED",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:52:05.708Z",
|
||||
"completedAt": "2024-04-22T11:52:05.707Z",
|
||||
@@ -418,60 +267,11 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"allowDictateNextSigner": false,
|
||||
"typedSignatureEnabled": true,
|
||||
"uploadSignatureEnabled": true,
|
||||
"drawSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"recipients": [
|
||||
{
|
||||
"id": 50,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "reviewer@example.com",
|
||||
"name": "Jane Smith",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"signedAt": "2024-04-22T11:51:10.055Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "VIEWER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"token": "HkrptwS42ZBXdRKj1TyUo",
|
||||
"documentDeletedAt": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"signedAt": "2024-04-22T11:52:05.688Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"signingOrder": 2,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
],
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 50,
|
||||
@@ -481,8 +281,7 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
"name": "Jane Smith",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": "2024-04-22T11:51:10.055Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
@@ -503,8 +302,7 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
"name": "John Doe",
|
||||
"token": "HkrptwS42ZBXdRKj1TyUo",
|
||||
"documentDeletedAt": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": "2024-04-22T11:52:05.688Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
@@ -537,17 +335,53 @@ The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` cont
|
||||
"event": "DOCUMENT_REJECTED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"status": "PENDING",
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "contract.pdf",
|
||||
"status": "PENDING",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:48:07.569Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"recipients": [
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"role": "SIGNER",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": "2024-04-22T11:48:07.569Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": "I do not agree with the terms",
|
||||
"role": "SIGNER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "REJECTED",
|
||||
"sendStatus": "SENT"
|
||||
@@ -561,9 +395,7 @@ The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` cont
|
||||
|
||||
### `document.cancelled`
|
||||
|
||||
Triggered when the document owner or a team member deletes a document. Draft and pending documents are hard-deleted, while completed documents are soft-deleted.
|
||||
|
||||
This event is **not** triggered when a recipient hides a document from their inbox.
|
||||
Triggered when the document owner cancels a pending document.
|
||||
|
||||
**Event name:** `DOCUMENT_CANCELLED`
|
||||
|
||||
@@ -579,6 +411,7 @@ This event is **not** triggered when a recipient hides a document from their inb
|
||||
"visibility": "EVERYONE",
|
||||
"title": "contract.pdf",
|
||||
"status": "PENDING",
|
||||
"documentDataId": "cm6exvn93006hi02ru90a265a",
|
||||
"createdAt": "2025-01-27T11:02:14.393Z",
|
||||
"updatedAt": "2025-01-27T11:03:16.387Z",
|
||||
"completedAt": null,
|
||||
@@ -595,35 +428,11 @@ This event is **not** triggered when a recipient hides a document from their inb
|
||||
"dateFormat": "yyyy-MM-dd hh:mm a",
|
||||
"redirectUrl": "",
|
||||
"signingOrder": "PARALLEL",
|
||||
"allowDictateNextSigner": false,
|
||||
"typedSignatureEnabled": true,
|
||||
"uploadSignatureEnabled": true,
|
||||
"drawSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"recipients": [
|
||||
{
|
||||
"id": 7,
|
||||
"documentId": 7,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"token": "XkKx1HCs6Znm2UBJA2j6o",
|
||||
"documentDeletedAt": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"signedAt": null,
|
||||
"authOptions": { "accessAuth": null, "actionAuth": null },
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
],
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 7,
|
||||
@@ -633,8 +442,7 @@ This event is **not** triggered when a recipient hides a document from their inb
|
||||
"name": "John Doe",
|
||||
"token": "XkKx1HCs6Znm2UBJA2j6o",
|
||||
"documentDeletedAt": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": { "accessAuth": null, "actionAuth": null },
|
||||
"signingOrder": 1,
|
||||
@@ -651,127 +459,147 @@ This event is **not** triggered when a recipient hides a document from their inb
|
||||
}
|
||||
```
|
||||
|
||||
### `document.reminder.sent`
|
||||
---
|
||||
|
||||
Triggered when a reminder email is sent to a recipient who has not yet completed their action.
|
||||
## Recipient Events
|
||||
|
||||
**Event name:** `DOCUMENT_REMINDER_SENT`
|
||||
Recipient events track individual signer actions. These events use the same payload structure as document events, but focus on a specific recipient's action.
|
||||
|
||||
### `document.opened`
|
||||
|
||||
Triggered when a recipient opens the document for the first time.
|
||||
|
||||
**Event name:** `DOCUMENT_OPENED`
|
||||
|
||||
The recipient's `readStatus` changes to `OPENED`.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_REMINDER_SENT",
|
||||
"event": "DOCUMENT_OPENED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"status": "PENDING",
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "contract.pdf",
|
||||
"status": "PENDING",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:48:07.569Z",
|
||||
"completedAt": null,
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"recipients": [
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 52,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-23T09:00:00.000Z",
|
||||
"createdAt": "2024-04-22T11:50:26.174Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
### `document.signed`
|
||||
|
||||
## Template Events
|
||||
Triggered when a recipient signs the document.
|
||||
|
||||
Template events track changes to reusable document templates. Template payloads use the same structure as document payloads, with `source` set to `TEMPLATE` and `templateId` populated.
|
||||
**Event name:** `DOCUMENT_SIGNED`
|
||||
|
||||
### `template.created`
|
||||
|
||||
Triggered when a new template is created.
|
||||
|
||||
**Event name:** `TEMPLATE_CREATED`
|
||||
The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "TEMPLATE_CREATED",
|
||||
"event": "DOCUMENT_SIGNED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"title": "My Template",
|
||||
"status": "DRAFT",
|
||||
"templateId": 10,
|
||||
"source": "TEMPLATE",
|
||||
"recipients": []
|
||||
"externalId": null,
|
||||
"userId": 1,
|
||||
"authOptions": null,
|
||||
"formValues": null,
|
||||
"visibility": "EVERYONE",
|
||||
"title": "contract.pdf",
|
||||
"status": "COMPLETED",
|
||||
"documentDataId": "hs8qz1ktr9204jn7mg6c5dxy0",
|
||||
"createdAt": "2024-04-22T11:44:43.341Z",
|
||||
"updatedAt": "2024-04-22T11:52:05.708Z",
|
||||
"completedAt": "2024-04-22T11:52:05.707Z",
|
||||
"deletedAt": null,
|
||||
"teamId": null,
|
||||
"templateId": null,
|
||||
"source": "DOCUMENT",
|
||||
"documentMeta": {
|
||||
"id": "doc_meta_123",
|
||||
"subject": "Please sign this document",
|
||||
"message": "Hello, please review and sign this document.",
|
||||
"timezone": "UTC",
|
||||
"password": null,
|
||||
"dateFormat": "MM/DD/YYYY",
|
||||
"redirectUrl": null,
|
||||
"signingOrder": "PARALLEL",
|
||||
"typedSignatureEnabled": true,
|
||||
"language": "en",
|
||||
"distributionMethod": "EMAIL",
|
||||
"emailSettings": null
|
||||
},
|
||||
"Recipient": [
|
||||
{
|
||||
"id": 51,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"token": "HkrptwS42ZBXdRKj1TyUo",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"signedAt": "2024-04-22T11:52:05.688Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
"actionAuth": null
|
||||
},
|
||||
"signingOrder": 1,
|
||||
"rejectionReason": null,
|
||||
"role": "SIGNER",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:44:44.779Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
### `template.updated`
|
||||
|
||||
Triggered when a template's settings, recipients, or fields are modified.
|
||||
|
||||
**Event name:** `TEMPLATE_UPDATED`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "TEMPLATE_UPDATED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"title": "My Updated Template",
|
||||
"status": "DRAFT",
|
||||
"templateId": 10,
|
||||
"source": "TEMPLATE",
|
||||
"recipients": []
|
||||
},
|
||||
"createdAt": "2024-04-22T12:00:00.000Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
### `template.deleted`
|
||||
|
||||
Triggered when a template is deleted.
|
||||
|
||||
**Event name:** `TEMPLATE_DELETED`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "TEMPLATE_DELETED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"title": "Deleted Template",
|
||||
"status": "DRAFT",
|
||||
"templateId": 10,
|
||||
"source": "TEMPLATE",
|
||||
"recipients": []
|
||||
},
|
||||
"createdAt": "2024-04-22T13:00:00.000Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
### `template.used`
|
||||
|
||||
Triggered when a document is created from a template. This event fires alongside `document.created`, giving you a way to specifically track template usage.
|
||||
|
||||
**Event name:** `TEMPLATE_USED`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "TEMPLATE_USED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"title": "Document from Template",
|
||||
"status": "DRAFT",
|
||||
"templateId": 10,
|
||||
"source": "TEMPLATE",
|
||||
"recipients": []
|
||||
},
|
||||
"createdAt": "2024-04-22T14:00:00.000Z",
|
||||
"createdAt": "2024-04-22T11:52:18.577Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
@@ -780,28 +608,15 @@ Triggered when a document is created from a template. This event fires alongside
|
||||
|
||||
## Event Summary
|
||||
|
||||
### Document Events
|
||||
|
||||
| Event | Trigger | Key Changes |
|
||||
| ---------------------------- | ------------------------------------------- | ------------------------------------------------------------ |
|
||||
| `DOCUMENT_CREATED` | Document uploaded or created from template | `status: "DRAFT"` |
|
||||
| `DOCUMENT_SENT` | Document sent to recipients | `status: "PENDING"`, recipients `sendStatus: "SENT"` |
|
||||
| `DOCUMENT_OPENED` | Recipient opens document for the first time | Recipient `readStatus: "OPENED"` |
|
||||
| `DOCUMENT_SIGNED` | Recipient signs document | Recipient `signingStatus: "SIGNED"`, `signedAt` set |
|
||||
| `DOCUMENT_RECIPIENT_COMPLETED` | Recipient completes their action | Recipient `signingStatus: "SIGNED"`, `signedAt` set |
|
||||
| `DOCUMENT_COMPLETED` | All recipients complete actions | `status: "COMPLETED"`, `completedAt` set |
|
||||
| `DOCUMENT_REJECTED` | Recipient rejects document | Recipient `signingStatus: "REJECTED"`, `rejectionReason` set |
|
||||
| `DOCUMENT_CANCELLED` | Owner or team member deletes document | Document cancelled or deleted |
|
||||
| `DOCUMENT_REMINDER_SENT` | Reminder email sent to recipient | No status changes |
|
||||
|
||||
### Template Events
|
||||
|
||||
| Event | Trigger | Key Changes |
|
||||
| ------------------ | ------------------------------------ | ------------------------- |
|
||||
| `TEMPLATE_CREATED` | New template created | `source: "TEMPLATE"` |
|
||||
| `TEMPLATE_UPDATED` | Template settings or fields modified | `source: "TEMPLATE"` |
|
||||
| `TEMPLATE_DELETED` | Template deleted | `source: "TEMPLATE"` |
|
||||
| `TEMPLATE_USED` | Document created from template | `source: "TEMPLATE"` |
|
||||
| Event | Trigger | Key Changes |
|
||||
| -------------------- | ------------------------------- | ------------------------------------------------------------ |
|
||||
| `DOCUMENT_CREATED` | Document uploaded | `status: "DRAFT"` |
|
||||
| `DOCUMENT_SENT` | Document sent to recipients | `status: "PENDING"`, recipients `sendStatus: "SENT"` |
|
||||
| `DOCUMENT_OPENED` | Recipient opens document | Recipient `readStatus: "OPENED"` |
|
||||
| `DOCUMENT_SIGNED` | Recipient signs document | Recipient `signingStatus: "SIGNED"`, `signedAt` set |
|
||||
| `DOCUMENT_COMPLETED` | All recipients complete actions | `status: "COMPLETED"`, `completedAt` set |
|
||||
| `DOCUMENT_REJECTED` | Recipient rejects document | Recipient `signingStatus: "REJECTED"`, `rejectionReason` set |
|
||||
| `DOCUMENT_CANCELLED` | Owner cancels document | Document cancelled while pending |
|
||||
|
||||
---
|
||||
|
||||
@@ -837,22 +652,19 @@ app.post('/webhook', (req, res) => {
|
||||
|
||||
switch (event) {
|
||||
case 'DOCUMENT_COMPLETED':
|
||||
// Handle completed document
|
||||
console.log(`Document ${payload.id} completed`);
|
||||
break;
|
||||
case 'DOCUMENT_RECIPIENT_COMPLETED':
|
||||
const signer = payload.recipients.find((r) => r.signingStatus === 'SIGNED');
|
||||
console.log(`${signer?.name} completed their action on document ${payload.id}`);
|
||||
break;
|
||||
case 'DOCUMENT_SIGNED':
|
||||
console.log(`Signature added to document ${payload.id}`);
|
||||
// Handle signature
|
||||
const signer = payload.Recipient.find((r) => r.signingStatus === 'SIGNED');
|
||||
console.log(`${signer?.name} signed document ${payload.id}`);
|
||||
break;
|
||||
case 'DOCUMENT_REJECTED':
|
||||
const rejecter = payload.recipients.find((r) => r.signingStatus === 'REJECTED');
|
||||
// Handle rejection
|
||||
const rejecter = payload.Recipient.find((r) => r.signingStatus === 'REJECTED');
|
||||
console.log(`${rejecter?.name} rejected: ${rejecter?.rejectionReason}`);
|
||||
break;
|
||||
case 'TEMPLATE_USED':
|
||||
console.log(`Template ${payload.templateId} used to create document ${payload.id}`);
|
||||
break;
|
||||
}
|
||||
|
||||
res.status(200).send('OK');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Webhooks
|
||||
description: Receive real-time notifications for document and template events.
|
||||
description: Receive real-time notifications when documents are signed, completed, or updated.
|
||||
---
|
||||
|
||||
## How Webhooks Work
|
||||
@@ -9,8 +9,6 @@ description: Receive real-time notifications for document and template events.
|
||||
2. When an event occurs, Documenso sends an HTTP POST to your URL
|
||||
3. Your application processes the event and responds with 200 OK
|
||||
|
||||
Documenso supports webhook events for the full document lifecycle (created, sent, opened, signed, completed, rejected, cancelled) as well as template events (created, updated, deleted, used).
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
@@ -43,15 +41,7 @@ Documenso supports webhook events for the full document lifecycle (created, sent
|
||||
"payload": {
|
||||
"id": 123,
|
||||
"title": "Contract",
|
||||
"status": "COMPLETED",
|
||||
"completedAt": "2024-01-15T10:30:00.000Z",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 1,
|
||||
"email": "signer@example.com",
|
||||
"signingStatus": "SIGNED"
|
||||
}
|
||||
]
|
||||
"status": "COMPLETED"
|
||||
},
|
||||
"createdAt": "2024-01-15T10:30:00.000Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
|
||||
@@ -220,17 +220,11 @@ When creating a webhook, you can subscribe to one or more events:
|
||||
| ----- | ------- |
|
||||
| `DOCUMENT_CREATED` | A new document is created |
|
||||
| `DOCUMENT_SENT` | A document is sent to recipients |
|
||||
| `DOCUMENT_OPENED` | A recipient opens the document for the first time |
|
||||
| `DOCUMENT_OPENED` | A recipient opens the document |
|
||||
| `DOCUMENT_SIGNED` | A recipient signs the document |
|
||||
| `DOCUMENT_RECIPIENT_COMPLETED` | A recipient completes their required action |
|
||||
| `DOCUMENT_COMPLETED` | All recipients have completed their actions |
|
||||
| `DOCUMENT_COMPLETED` | All recipients have signed the document |
|
||||
| `DOCUMENT_REJECTED` | A recipient rejects the document |
|
||||
| `DOCUMENT_CANCELLED` | The document owner deletes the document |
|
||||
| `DOCUMENT_REMINDER_SENT` | A reminder email is sent to a recipient |
|
||||
| `TEMPLATE_CREATED` | A new template is created |
|
||||
| `TEMPLATE_UPDATED` | A template is modified |
|
||||
| `TEMPLATE_DELETED` | A template is deleted |
|
||||
| `TEMPLATE_USED` | A document is created from a template |
|
||||
| `DOCUMENT_CANCELLED` | The document owner cancels the document |
|
||||
|
||||
You can subscribe to all events or select specific ones based on your needs. For example, if you only need to know when documents are fully signed, subscribe only to `DOCUMENT_COMPLETED`.
|
||||
|
||||
|
||||
@@ -250,15 +250,9 @@ const validEvents = [
|
||||
'DOCUMENT_SENT',
|
||||
'DOCUMENT_OPENED',
|
||||
'DOCUMENT_SIGNED',
|
||||
'DOCUMENT_RECIPIENT_COMPLETED',
|
||||
'DOCUMENT_COMPLETED',
|
||||
'DOCUMENT_REJECTED',
|
||||
'DOCUMENT_CANCELLED',
|
||||
'DOCUMENT_REMINDER_SENT',
|
||||
'TEMPLATE_CREATED',
|
||||
'TEMPLATE_UPDATED',
|
||||
'TEMPLATE_DELETED',
|
||||
'TEMPLATE_USED',
|
||||
];
|
||||
|
||||
if (!validEvents.includes(event)) {
|
||||
|
||||
@@ -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 Editor
|
||||
- Embed Editor White Label
|
||||
- Embed Authoring
|
||||
- Embed Authoring White Label
|
||||
- Custom signing certificates
|
||||
- Priority feature requests
|
||||
|
||||
|
||||
@@ -19,19 +19,16 @@ Use the limitless plans as much as you like. They are meant to offer a lot. Plea
|
||||
|
||||
### Do
|
||||
|
||||
- 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.
|
||||
- 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
|
||||
|
||||
### Don't
|
||||
- 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.
|
||||
|
||||
- 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
|
||||
|
||||
## Rate Limits
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
"privacy",
|
||||
"terms",
|
||||
"security",
|
||||
"verify-email",
|
||||
"support"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
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
|
||||
title: AI Recipient & Field Detection (Self-hosting)
|
||||
description: Configure Google Vertex AI so Documenso can detect recipients and fields automatically.
|
||||
---
|
||||
|
||||
|
||||
@@ -1,408 +0,0 @@
|
||||
---
|
||||
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, document conversion, and other advanced settings.
|
||||
description: Optional configuration for OAuth providers, AI features, and other advanced settings.
|
||||
---
|
||||
|
||||
<Cards>
|
||||
@@ -14,9 +14,4 @@ description: Optional configuration for OAuth providers, AI features, document c
|
||||
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", "document-conversion", "ai-features"]
|
||||
"pages": ["oauth-providers", "ai-features"]
|
||||
}
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
---
|
||||
title: Background Jobs
|
||||
description: Configure how Documenso processes background tasks like email delivery, document processing, and webhook dispatch.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Overview
|
||||
|
||||
Documenso processes background jobs for email delivery, document sealing, webhook dispatch, and scheduled maintenance tasks. Three providers are available:
|
||||
|
||||
| Provider | Backend | Best For | Infrastructure |
|
||||
| -------- | ---------- | ----------------------------------------------- | -------------- |
|
||||
| Inngest | Managed | Production with zero ops overhead | None |
|
||||
| BullMQ | Redis | Self-hosted production with full control | Redis |
|
||||
| Local | PostgreSQL | Development and small self-hosted deployments | None |
|
||||
|
||||
Select a provider with the `NEXT_PRIVATE_JOBS_PROVIDER` environment variable:
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_JOBS_PROVIDER=inngest # or bullmq, local
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The default provider is `local`. It requires no additional infrastructure and works well for development and small deployments, but is not recommended for production workloads.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Inngest (Recommended)
|
||||
|
||||
[Inngest](https://www.inngest.com/) is a managed background job service. It handles scheduling, retries, concurrency, and observability without any infrastructure to manage. This is the recommended provider for production deployments.
|
||||
|
||||
### Setup
|
||||
|
||||
{/* prettier-ignore */}
|
||||
1. Create an account at [inngest.com](https://www.inngest.com/)
|
||||
2. Create an app and obtain your event key and signing key
|
||||
3. Configure the environment variables:
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_JOBS_PROVIDER=inngest
|
||||
NEXT_PRIVATE_INNGEST_EVENT_KEY=your-event-key
|
||||
INNGEST_SIGNING_KEY=your-signing-key
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Required |
|
||||
| -------------------------------- | -------------------------------------------- | -------- |
|
||||
| `NEXT_PRIVATE_INNGEST_EVENT_KEY` | Inngest event key | Yes |
|
||||
| `INNGEST_EVENT_KEY` | Alternative Inngest event key | No |
|
||||
| `INNGEST_SIGNING_KEY` | Inngest signing key for webhook verification | Yes |
|
||||
| `NEXT_PRIVATE_INNGEST_APP_ID` | Custom Inngest app ID | No |
|
||||
|
||||
### Advantages
|
||||
|
||||
- No infrastructure to manage
|
||||
- Built-in monitoring dashboard
|
||||
- Automatic retries with backoff
|
||||
- Cron scheduling handled externally
|
||||
- Scales automatically
|
||||
|
||||
---
|
||||
|
||||
## BullMQ
|
||||
|
||||
[BullMQ](https://docs.bullmq.io/) is a Redis-backed job queue that runs inside the Documenso process. It provides higher throughput than the local provider, configurable concurrency, and a built-in dashboard for monitoring jobs.
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Redis 6.2+** - any Redis-compatible service works (Redis, KeyDB, Dragonfly, AWS ElastiCache, Upstash, etc.)
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_JOBS_PROVIDER=bullmq
|
||||
NEXT_PRIVATE_REDIS_URL=redis://localhost:6379
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------------- | -------------------------------------------------------------------------- | ----------- |
|
||||
| `NEXT_PRIVATE_REDIS_URL` | Redis connection URL | _(required)_ |
|
||||
| `NEXT_PRIVATE_REDIS_PREFIX` | Key prefix for Redis queues (useful when sharing an instance) | `documenso` |
|
||||
| `NEXT_PRIVATE_BULLMQ_CONCURRENCY` | Number of concurrent jobs to process | `10` |
|
||||
|
||||
### Dashboard
|
||||
|
||||
BullMQ includes a job monitoring dashboard at `/api/jobs/board`. In production, only admin users can access the dashboard. In development, it is open to all users.
|
||||
|
||||
The dashboard provides visibility into queued, active, completed, and failed jobs.
|
||||
|
||||
### Docker Compose with Redis
|
||||
|
||||
If you're using Docker Compose, add a Redis service:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
redis:
|
||||
image: redis:8-alpine
|
||||
ports:
|
||||
- '6379:6379'
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
```
|
||||
|
||||
Then set `NEXT_PRIVATE_REDIS_URL=redis://redis:6379` in your Documenso environment.
|
||||
|
||||
### Advantages
|
||||
|
||||
- Self-hosted with no external service dependencies beyond Redis
|
||||
- Configurable concurrency
|
||||
- Built-in job monitoring dashboard
|
||||
- Reliable retries with exponential backoff
|
||||
- Queue namespacing for shared Redis instances
|
||||
|
||||
---
|
||||
|
||||
## Local
|
||||
|
||||
The local provider uses your PostgreSQL database as a job queue. Jobs are stored in the `BackgroundJob` table and processed via internal HTTP requests that Documenso sends to itself.
|
||||
|
||||
### Setup
|
||||
|
||||
No configuration required. The local provider is the default when `NEXT_PRIVATE_JOBS_PROVIDER` is unset or set to `local`.
|
||||
|
||||
```bash
|
||||
# Optional - this is the default
|
||||
NEXT_PRIVATE_JOBS_PROVIDER=local
|
||||
```
|
||||
|
||||
### Internal URL
|
||||
|
||||
Background jobs in the local provider work by Documenso sending HTTP requests to itself. If your reverse proxy or network setup causes issues with the app reaching its own public URL, set the internal URL:
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_INTERNAL_WEBAPP_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
This tells the job system to use the internal address instead of `NEXT_PUBLIC_WEBAPP_URL` for self-requests.
|
||||
|
||||
<Callout type="warn">
|
||||
The local provider is suitable for development and small deployments. For production workloads, use Inngest or BullMQ.
|
||||
</Callout>
|
||||
|
||||
### Limitations
|
||||
|
||||
- No concurrency control - jobs are processed one at a time per request cycle
|
||||
- No built-in monitoring
|
||||
- Depends on the application being able to reach itself over HTTP
|
||||
- Not suitable for high-throughput workloads
|
||||
|
||||
---
|
||||
|
||||
## Choosing a Provider
|
||||
|
||||
<Tabs items={['Managed hosting', 'Self-hosted production', 'Development']}>
|
||||
<Tab value="Managed hosting">
|
||||
|
||||
Use **Inngest**. Zero infrastructure, automatic scaling, and built-in observability. The simplest path to reliable background jobs in production.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Self-hosted production">
|
||||
|
||||
Use **BullMQ**. Add a Redis instance to your infrastructure and get reliable job processing with a monitoring dashboard. Good fit if you already run Redis or want to keep everything self-hosted.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Development">
|
||||
|
||||
Use **Local** (the default). No additional setup required. Works out of the box with just PostgreSQL.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference
|
||||
- [Requirements](/docs/self-hosting/getting-started/requirements) - Infrastructure requirements
|
||||
- [Docker Compose](/docs/self-hosting/deployment/docker-compose) - Deploy with Docker Compose
|
||||
@@ -278,9 +278,7 @@ Test your email configuration by creating an account or resetting a password. Th
|
||||
|
||||
### Using a Test SMTP Server
|
||||
|
||||
For development or testing, use a local SMTP server like [Inbucket](https://www.inbucket.org/), [Mailpit](https://github.com/axllent/mailpit), or [Mailhog](https://github.com/mailhog/MailHog). The default development setup (`docker/development/compose.yml`) already runs Inbucket, with its web UI on port 9000 and SMTP on port 2500.
|
||||
|
||||
To run one standalone instead:
|
||||
For development or testing, use a local SMTP server like [Mailhog](https://github.com/mailhog/MailHog) or [Mailpit](https://github.com/axllent/mailpit):
|
||||
|
||||
```bash
|
||||
# Using Docker
|
||||
|
||||
@@ -86,21 +86,6 @@ Callback URL: `https://<your-domain>/api/auth/callback/microsoft`
|
||||
| `NEXT_PRIVATE_OIDC_SKIP_VERIFY` | `false` | Skip email verification for OIDC accounts |
|
||||
| `NEXT_PRIVATE_OIDC_PROMPT` | `login` | OIDC prompt parameter. Set to empty string to omit |
|
||||
|
||||
### Webhooks
|
||||
|
||||
| Variable | Default | Description |
|
||||
| --------------------------------------- | ------- | ------------------------------------------------------------------------ |
|
||||
| `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` | - | Comma-separated hostnames or IPs allowed to resolve to private addresses |
|
||||
|
||||
Before delivering a webhook, Documenso checks whether the target resolves to a
|
||||
private or loopback address and blocks it if so. This check is best-effort and
|
||||
fails open. Use `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` to allow specific
|
||||
internal hosts, for example when delivering to a service on your own network:
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS="hooks.internal.example,10.0.0.5"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Email Configuration
|
||||
@@ -201,9 +186,9 @@ Documenso requires a certificate to digitally sign documents.
|
||||
|
||||
### Transport Selection
|
||||
|
||||
| Variable | Description | Default |
|
||||
| -------------------------------- | ------------------------------------------------- | ------- |
|
||||
| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Signing backend: `local`, `gcloud-hsm`, or `csc` | `local` |
|
||||
| Variable | Description | Default |
|
||||
| -------------------------------- | ---------------------------------------- | ------- |
|
||||
| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Signing backend: `local` or `gcloud-hsm` | `local` |
|
||||
|
||||
### Local Signing
|
||||
|
||||
@@ -225,36 +210,11 @@ Documenso requires a certificate to digitally sign documents.
|
||||
| `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS` | Base64-encoded certificate chain |
|
||||
| `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH` | Google Secret Manager path for certificate retrieval |
|
||||
|
||||
### Cloud Signature Consortium (CSC)
|
||||
|
||||
Routes signing through a third-party Trust Service Provider for Advanced and Qualified Electronic Signatures (AES/QES). Instance-wide; set `NEXT_PRIVATE_SIGNING_TRANSPORT=csc` to enable. See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup walkthrough.
|
||||
|
||||
CSC mode requires an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Without a valid license, the instance will refuse to start in `csc` mode.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | Base URL of the CSC provider's API | |
|
||||
| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID` | OAuth client ID registered with the CSC provider | |
|
||||
| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET` | OAuth client secret registered with the CSC provider | |
|
||||
| `NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` | Default legal tier for new envelopes when the caller doesn't specify one. `AES` or `QES`. Explicit requests pass through. | `AES` |
|
||||
|
||||
The OAuth callback URL registered with the CSC provider is fixed at `${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback` — register this exact URL with the TSP.
|
||||
|
||||
#### Derived Public Variables
|
||||
|
||||
The following client-visible variable is **derived automatically** from the private transport at server startup. Do not set it manually — any value set in the environment is overwritten on boot.
|
||||
|
||||
| Variable | Derived from | Value |
|
||||
| ------------------------------------- | -------------------------------------------------- | ------------------------------------------------- |
|
||||
| `NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC` | `NEXT_PRIVATE_SIGNING_TRANSPORT === 'csc'` | `'true'` when CSC mode is active, else `'false'` |
|
||||
|
||||
The authoring UI uses this flag to gate features that AES/QES envelopes cannot support (parallel signing, assistant role, dictate next signer). Deriving it from the private transport prevents the client-side flag from drifting from the real server-side configuration.
|
||||
|
||||
### Signature Options
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------------------- | ----------------------------------------------------------- | ---------- |
|
||||
| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | Comma-separated timestamp authority URLs for LTV signatures. Optional for `local` / `gcloud-hsm` (signatures omit the timestamp when unset). **Required** when `NEXT_PRIVATE_SIGNING_TRANSPORT=csc` — the instance refuses to start without it. See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes#timestamp-authority-resolution). | |
|
||||
| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | Comma-separated timestamp authority URLs for LTV signatures | |
|
||||
| `NEXT_PUBLIC_SIGNING_CONTACT_INFO` | Contact info embedded in PDF signatures | Webapp URL |
|
||||
| `NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER` | Use `adbe.pkcs7.detached` instead of `ETSI.CAdES.detached` | `false` |
|
||||
|
||||
@@ -264,44 +224,11 @@ For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/con
|
||||
|
||||
## Feature Flags
|
||||
|
||||
| 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 with the following environment variables:
|
||||
|
||||
- **`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.
|
||||
|
||||
Sign-in for existing users is never affected, only the creation of brand-new accounts.
|
||||
|
||||
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"
|
||||
```
|
||||
| Variable | Description | Default |
|
||||
| ------------------------------------- | ----------------------------------------------- | ------- |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public user registration | `false` |
|
||||
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | |
|
||||
| `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` |
|
||||
|
||||
---
|
||||
|
||||
@@ -319,59 +246,22 @@ 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.
|
||||
Documenso uses a PostgreSQL-based job queue by default. Jobs (email delivery, document processing, webhook dispatch) are stored in the `BackgroundJob` table and processed via internal HTTP requests. No external queue service like Redis is required.
|
||||
|
||||
### Provider Selection
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------ | ------- |
|
||||
| `NEXT_PRIVATE_JOBS_PROVIDER` | Jobs provider: `local` (PostgreSQL-based queue) or `inngest` (managed service) | `local` |
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------- | ------- |
|
||||
| `NEXT_PRIVATE_JOBS_PROVIDER` | Jobs provider: `local` (PostgreSQL), `bullmq` (Redis), or `inngest` (managed service) | `local` |
|
||||
### Inngest Configuration
|
||||
|
||||
### Local (local)
|
||||
|
||||
No additional configuration required. Jobs are stored in PostgreSQL and processed via internal HTTP requests.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------------- | ------------------------------------------------------------ | -------------------------------- |
|
||||
| `NEXT_PRIVATE_INTERNAL_WEBAPP_URL` | Internal URL for the app to send job requests to itself | Same as `NEXT_PUBLIC_WEBAPP_URL` |
|
||||
|
||||
### BullMQ (bullmq)
|
||||
|
||||
| Variable | Required | Description | Default |
|
||||
| ---------------------------------- | -------- | ------------------------------------------------------------- | ----------- |
|
||||
| `NEXT_PRIVATE_REDIS_URL` | Yes | Redis connection URL (e.g., `redis://localhost:6379`) | |
|
||||
| `NEXT_PRIVATE_REDIS_PREFIX` | No | Key prefix for Redis queues (useful when sharing an instance) | `documenso` |
|
||||
| `NEXT_PRIVATE_BULLMQ_CONCURRENCY` | No | Number of concurrent jobs to process | `10` |
|
||||
|
||||
### Inngest (inngest)
|
||||
|
||||
| Variable | Required | Description |
|
||||
| -------------------------------- | -------- | -------------------------------------------- |
|
||||
| `NEXT_PRIVATE_INNGEST_EVENT_KEY` | Yes | Inngest event key |
|
||||
| `INNGEST_EVENT_KEY` | No | Alternative Inngest event key |
|
||||
| `INNGEST_SIGNING_KEY` | Yes | Inngest signing key for webhook verification |
|
||||
| `NEXT_PRIVATE_INNGEST_APP_ID` | No | Custom Inngest app ID |
|
||||
|
||||
For setup guides and provider recommendations, see [Background Jobs](/docs/self-hosting/configuration/background-jobs).
|
||||
| Variable | Description |
|
||||
| -------------------------------- | -------------------------------------------- |
|
||||
| `NEXT_PRIVATE_INNGEST_EVENT_KEY` | Inngest event key |
|
||||
| `INNGEST_EVENT_KEY` | Alternative Inngest event key |
|
||||
| `INNGEST_SIGNING_KEY` | Inngest signing key for webhook verification |
|
||||
| `NEXT_PRIVATE_INNGEST_APP_ID` | Custom Inngest app ID |
|
||||
|
||||
---
|
||||
|
||||
@@ -381,8 +271,6 @@ For setup guides and provider recommendations, see [Background Jobs](/docs/self-
|
||||
| ----------------------------- | -------------------------------------------- | ------- |
|
||||
| `DOCUMENSO_DISABLE_TELEMETRY` | Set to `true` to disable anonymous telemetry | `false` |
|
||||
|
||||
Telemetry also auto-disables when `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` is configured.
|
||||
|
||||
Telemetry collects only: app version, installation ID, and node ID. No personal data is collected.
|
||||
|
||||
---
|
||||
@@ -399,7 +287,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 editor, 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 authoring, and 21 CFR Part 11 compliance.
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------------------------ | ------------------------------------------------ |
|
||||
@@ -438,14 +326,6 @@ NEXT_PRIVATE_SMTP_FROM_ADDRESS="noreply@example.com"
|
||||
|
||||
# Signing (certificate must be configured)
|
||||
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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
"database",
|
||||
"email",
|
||||
"storage",
|
||||
"background-jobs",
|
||||
"signing-certificate",
|
||||
"telemetry",
|
||||
"advanced"
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
---
|
||||
title: CSC (AES / QES)
|
||||
description: Configure Cloud Signature Consortium signing for Advanced and Qualified Electronic Signatures via a third-party Trust Service Provider.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
The `csc` signing transport routes signatures through a third-party Trust Service Provider (TSP) using the [Cloud Signature Consortium API v1.0.4.0](https://cloudsignatureconsortium.org/). Each recipient authenticates directly with the TSP (Strong Customer Authentication) and the TSP returns a per-recipient signature bound to the document hash. Documenso assembles the resulting PAdES signature inside the PDF.
|
||||
|
||||
This transport enables **Advanced Electronic Signatures (AES)** and **Qualified Electronic Signatures (QES)** under eIDAS. See [Signature Levels](/docs/compliance/signature-levels) for the legal framework.
|
||||
|
||||
<Callout type="warn">
|
||||
CSC mode is **instance-wide**: one CSC provider per Documenso install. All envelopes created
|
||||
while the instance runs in `csc` mode use AES or QES. Switching `NEXT_PRIVATE_SIGNING_TRANSPORT`
|
||||
is a one-way operational migration — see [Switching Transports](#switching-transports).
|
||||
</Callout>
|
||||
|
||||
<Callout type="warn">
|
||||
CSC mode requires an active [Enterprise Edition](/docs/policies/enterprise-edition) license. The
|
||||
instance refuses to start in `csc` mode without it.
|
||||
</Callout>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
### A TSP account
|
||||
|
||||
Establish a relationship with a CSC-compatible Trust Service Provider. The TSP issues qualified or advanced certificates to your signers, holds the private keys in its HSM, and exposes a CSC v1.0.4.0-compliant API.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### OAuth client credentials
|
||||
|
||||
Register Documenso as an OAuth client with the TSP. You will receive a client ID and client secret, and must supply Documenso's callback URL when registering:
|
||||
|
||||
```
|
||||
${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback
|
||||
```
|
||||
|
||||
The callback URL is fixed — Documenso derives it from `NEXT_PUBLIC_WEBAPP_URL` and the route mount path. There is no env var to override it; ensuring the registered URL matches your instance's webapp URL exactly is the operator's responsibility.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Enterprise Edition license
|
||||
|
||||
CSC mode is gated by the `instanceCscSigning` license flag. Without a valid Enterprise license, the transport refuses to start (`CSC_UNLICENSED`).
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### S3 storage (strongly recommended)
|
||||
|
||||
CSC produces multiple `DocumentData` rows per envelope item (one per recipient signature, plus the materialised and source rows). Database-backed storage base64-inflates each row by ~33% and is impractical at meaningful PDF sizes. Configure [S3 storage](/docs/self-hosting/configuration/storage) before enabling CSC.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------- |
|
||||
| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Set to `csc` | |
|
||||
| `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | Base URL of the CSC provider's API | |
|
||||
| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID` | OAuth client ID registered with the CSC provider | |
|
||||
| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET` | OAuth client secret registered with the CSC provider | |
|
||||
| `NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` | Default legal tier for new envelopes when the caller does not specify one. `AES` or `QES`. Explicit requests always pass through. | `AES` |
|
||||
| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | **Required.** Comma-separated RFC 3161 TSA URLs. Always used for B-LTA archival timestamps at seal time, and also serves as the B-T sign-time fallback when the TSP does not expose `signatures/timestamp`. The instance refuses to start in CSC mode without it. See [Timestamp Authority Resolution](#timestamp-authority-resolution). | |
|
||||
|
||||
<Callout type="info">
|
||||
`NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC` is set automatically from
|
||||
`NEXT_PRIVATE_SIGNING_TRANSPORT` at server startup. Do not set it manually — see
|
||||
[Environment Variables](/docs/self-hosting/configuration/environment#derived-public-variables).
|
||||
</Callout>
|
||||
|
||||
## Configuration Example
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_SIGNING_TRANSPORT=csc
|
||||
NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL=https://api.example-tsp.com/csc/v1
|
||||
NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID=documenso-prod
|
||||
NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET=...
|
||||
NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL=QES
|
||||
NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=http://timestamp.example.com
|
||||
```
|
||||
|
||||
Register `${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback` (e.g. `https://sign.example.com/api/csc/oauth/callback`) as the OAuth callback URL with the TSP.
|
||||
|
||||
## Default Signature Level
|
||||
|
||||
`NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` selects the legal tier applied to envelopes that do not specify one explicitly. It is a default, not a capability gate: callers may still create AES or QES envelopes explicitly regardless of this setting.
|
||||
|
||||
| Configured value | Caller passes nothing | Caller passes `AES` | Caller passes `QES` |
|
||||
| ---------------- | --------------------- | ------------------- | ------------------- |
|
||||
| `AES` (default) | Envelope is `AES` | Envelope is `AES` | Envelope is `QES` |
|
||||
| `QES` | Envelope is `QES` | Envelope is `AES` | Envelope is `QES` |
|
||||
|
||||
Any value other than `AES` or `QES` causes the instance to refuse to start. This prevents silent qualified-to-advanced downgrades from a typo.
|
||||
|
||||
## Timestamp Authority Resolution
|
||||
|
||||
AES/QES envelopes use TSA-attested timestamps in two distinct phases. Resolution differs per phase.
|
||||
|
||||
### Sign time — PAdES B-T per recipient
|
||||
|
||||
Each recipient's CMS embeds a signature timestamp (CMS unsigned attribute) so proven time is bound to the recipient's signature itself. Resolution order:
|
||||
|
||||
1. If the TSP advertises `signatures/timestamp` in its `info` response (CSC §11.10), the TSP endpoint is used. The call is authorised with **this recipient's** service-scope bearer token — the same one authorising the `signatures/signHash` call alongside it.
|
||||
2. Otherwise, the first URL from `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is used (RFC 3161 over HTTP).
|
||||
|
||||
Selection is made at boot from the discovered transport, not at runtime; there is no try-then-fall-through. If the chosen source fails, the recipient's sign attempt fails.
|
||||
|
||||
### Seal time — PAdES B-LTA archival
|
||||
|
||||
The seal-document job emits a single archival `/DocTimeStamp` over the fully-signed envelope (plus DSS for the existing signatures and the timestamp's own chain). This phase is **env-only**: the first URL from `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is always used.
|
||||
|
||||
The archival anchor is the operator's long-term trust anchor and SHOULD point at a dedicated qualified archival TSA (e.g. DigiCert) independent of the per-recipient TSP. We deliberately do not fall back to the TSP at seal time: archive longevity should not be coupled to a TSP that may rotate or revoke, and the seal-document job has no recipient context to carry a service-scope bearer.
|
||||
|
||||
### Boot-time guard
|
||||
|
||||
The instance refuses to start in CSC mode unless `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is set (`CSC_PROVIDER_NO_TSA` at transport construction). The env var is required unconditionally — even when the TSP advertises its own `signatures/timestamp`, seal-time B-LTA archival uses the env TSA. Catching this at boot prevents the failure mode where an envelope signs successfully at B-T and then hangs in `WAITING_FOR_SIGNATURE_COMPLETION` when the seal job throws.
|
||||
|
||||
## Switching Transports
|
||||
|
||||
`NEXT_PRIVATE_SIGNING_TRANSPORT` is a one-way operational migration. Existing envelopes route per the `signatureLevel` column they were created with — the runtime branching looks at the envelope, not the env var. After a switch:
|
||||
|
||||
- Envelopes already at `SES` continue to use the new transport for sealing, but the new transport's signer must produce SES-compatible signatures (only `local` and `gcloud-hsm` qualify).
|
||||
- Envelopes already at `AES` / `QES` will fail at sign or seal time if the new transport is not `csc`.
|
||||
|
||||
Plan migrations during a quiet window with no in-flight envelopes.
|
||||
|
||||
## Behavioural Notes
|
||||
|
||||
CSC mode changes a number of envelope-authoring behaviours that operators should communicate to users.
|
||||
|
||||
### Mutation lock at distribution
|
||||
|
||||
For AES/QES envelopes, all authoring routes refuse mutations once the envelope leaves DRAFT. This locks the PDF before any recipient begins Strong Customer Authentication, closing the PDF-swap window that would otherwise allow an owner to replace the PDF between view and sign and break the legal "what you see is what you sign" guarantee.
|
||||
|
||||
In practice: edit envelope, recipients, fields, and items freely while DRAFT; once sent, no changes are accepted (including from the API).
|
||||
|
||||
### Sequential signing only
|
||||
|
||||
Parallel signing produces conflicting incremental updates over the same base PDF, breaking the per-recipient `/ByteRange` invariant. The signing order is forced to `SEQUENTIAL` on AES/QES envelopes — at the schema layer, at send time, and in the UI (the parallel-signing toggle is hidden).
|
||||
|
||||
### Assistant role and Dictate Next Signer disabled
|
||||
|
||||
Both features modify the recipient set after the envelope is sent, which is incompatible with the AES/QES mutation lock. They are hidden in the UI and rejected at the server schema layer.
|
||||
|
||||
### Sidecar PDFs at download
|
||||
|
||||
The signed PDF must remain byte-identical to what each recipient's TSP signature authorised — Documenso cannot decorate it after signing. Audit logs and the Certificate of Completion are generated on demand and delivered as separate PDFs:
|
||||
|
||||
- `GET /sign/{token}/download` returns the signed PDF only (or a ZIP for multi-item envelopes).
|
||||
- `GET /sign/{token}/download?version=bundle` returns a ZIP containing the signed PDFs, audit log PDF, and Certificate of Completion.
|
||||
- The completion email attaches all three.
|
||||
|
||||
## Recipient Flow
|
||||
|
||||
For context when supporting end users, here is what a recipient experiences on an AES/QES envelope:
|
||||
|
||||
1. Opens the email link, lands on the signing page.
|
||||
2. Documenso redirects to the TSP for Strong Customer Authentication (first visit only; cached for the session lifetime).
|
||||
3. Fills fields as normal.
|
||||
4. Clicks Sign → redirected to the TSP for a second authentication round (issues a per-document Signature Activation Data token).
|
||||
5. Returns to Documenso; the signing call completes within ~15 seconds.
|
||||
6. Sees the standard completion screen.
|
||||
|
||||
If the TSP returns no eligible credentials for the recipient (e.g. they have not enrolled), they see a blocking page directing them to enrol with the TSP and retry.
|
||||
|
||||
## Error Codes
|
||||
|
||||
CSC-specific error codes surfaced through the standard error channels:
|
||||
|
||||
| Code | Meaning | Recovery |
|
||||
| -------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------- |
|
||||
| `CSC_UNLICENSED` | License flag absent at transport-create | Operator: enable Enterprise Edition, restart |
|
||||
| `CSC_PROVIDER_INFO_FAILED` | `info` discovery failed at startup | Operator: check TSP availability and `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` |
|
||||
| `CSC_PROVIDER_NO_TSA` | `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is unset | Operator: configure `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` |
|
||||
| `CSC_CREDENTIAL_LIST_EMPTY`| TSP returned no credentials for the user | Recipient: enrol with the TSP |
|
||||
| `CSC_CERT_INVALID` | Certificate refused at credential validation | Recipient: contact the TSP |
|
||||
| `CSC_ALGORITHM_REFUSED` | Signature algorithm fails policy | Operator/recipient: TSP does not meet policy (see below) |
|
||||
| `CSC_SAD_EXPIRED_PRE_SIGN` | Signature Activation Data expired before signing | Recipient: retry from Sign |
|
||||
| `CSC_TSP_TIMEOUT` | 15-second synchronous timeout reached | Recipient: retry (idempotent — the TSP enforces single-use SAD binding) |
|
||||
| `CSC_EMBED_FAILED` | Sign-time digest diverged from prep capture | Recipient: retry from Sign |
|
||||
| `CSC_BASE_DOCUMENT_MUTATED`| Document data changed between prep and sign | Operator: investigate (structural guard violation) |
|
||||
| `CSC_INSTANCE_MODE_MISMATCH`| Envelope created with wrong level for transport | Caller: use a level matching the instance transport |
|
||||
| `CSC_REQUEST_FAILED` | TSP HTTP transport failure — network error, non-2xx, or malformed response | Operator: check TSP availability; carries the TSP HTTP status and error in the message |
|
||||
|
||||
## Algorithm Policy
|
||||
|
||||
Documenso refuses TSP credentials that do not meet the following minimums, at the OAuth callback boundary and again at sign time:
|
||||
|
||||
| Class | Allowed | Refused |
|
||||
| ----- | ---------------------------------- | ------------------------------------------------------ |
|
||||
| RSA | `key.len >= 2048` | Missing `key.len`, `key.len < 2048` |
|
||||
| ECDSA | P-256, P-384, P-521 | Missing `key.curve`, P-192, P-224, other curves |
|
||||
| Hash | SHA-256, SHA-384, SHA-512 | SHA-1, MD5 |
|
||||
| Other | — | DSA |
|
||||
|
||||
This is the union of CSC v1.0.4.0 §11.5 requirements and current cryptographic guidance.
|
||||
|
||||
## Related
|
||||
|
||||
- [Signature Levels](/docs/compliance/signature-levels) — AES / QES legal framework
|
||||
- [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) — overview of all signing transports
|
||||
- [Environment Variables](/docs/self-hosting/configuration/environment) — full env reference
|
||||
- [Enterprise Edition](/docs/policies/enterprise-edition) — license requirements
|
||||
@@ -24,11 +24,6 @@ Self-hosted Documenso instances require a signing certificate. You can generate
|
||||
description="Hardware-based key protection with Google Cloud KMS."
|
||||
href="/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm"
|
||||
/>
|
||||
<Card
|
||||
title="CSC (AES / QES)"
|
||||
description="Route signing through a third-party Trust Service Provider for Advanced and Qualified Electronic Signatures."
|
||||
href="/docs/self-hosting/configuration/signing-certificate/csc-qes"
|
||||
/>
|
||||
<Card
|
||||
title="Timestamp Server"
|
||||
description="Add trusted timestamps and customise signature appearance."
|
||||
@@ -43,7 +38,7 @@ Self-hosted Documenso instances require a signing certificate. You can generate
|
||||
|
||||
## Certificate Options
|
||||
|
||||
<Tabs items={['Self-Signed', 'CA-Issued', 'Google Cloud HSM', 'CSC (AES / QES)']}>
|
||||
<Tabs items={['Self-Signed', 'CA-Issued', 'Google Cloud HSM']}>
|
||||
<Tab value="Self-Signed">
|
||||
|
||||
A self-signed certificate is sufficient for most use cases where your industry has no special signing regulations.
|
||||
@@ -84,18 +79,6 @@ For organisations requiring hardware-based key protection, Documenso supports Go
|
||||
|
||||
See [Google Cloud HSM](/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm) for setup instructions.
|
||||
|
||||
</Tab>
|
||||
<Tab value="CSC (AES / QES)">
|
||||
|
||||
For Advanced and Qualified Electronic Signatures under eIDAS, Documenso integrates with third-party Trust Service Providers via the Cloud Signature Consortium API. Each recipient authenticates directly with the TSP, which holds the private key and issues the signature.
|
||||
|
||||
- Per-recipient identity verification by an accredited TSP
|
||||
- Legally equivalent to a handwritten signature within the EU (QES)
|
||||
- Requires an [Enterprise Edition](/docs/policies/enterprise-edition) license
|
||||
- Instance-wide setting; one CSC provider per Documenso install
|
||||
|
||||
See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes) for setup instructions.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Signing Certificate",
|
||||
"pages": ["...index", "local", "google-cloud-hsm", "csc-qes", "timestamp-server", "troubleshooting"]
|
||||
"pages": ["...index", "local", "google-cloud-hsm", "timestamp-server", "troubleshooting"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
title: Storage Configuration
|
||||
description: Configure file storage for uploaded documents and signed PDFs using database storage (default), S3-compatible object storage, or Azure Blob Storage.
|
||||
description: Configure file storage for uploaded documents and signed PDFs using database storage (default) or S3-compatible object storage.
|
||||
---
|
||||
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
@@ -10,11 +10,10 @@ 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 |
|
||||
| `azure-blob` | Production on Azure, native Blob access | High | Required |
|
||||
| Backend | Best For | Scalability | Configuration |
|
||||
| ---------- | -------------------------------- | ----------- | ------------- |
|
||||
| `database` | Small deployments, simplicity | Limited | None required |
|
||||
| `s3` | Production, large files, backups | High | Required |
|
||||
|
||||
Select the storage backend with the `NEXT_PUBLIC_UPLOAD_TRANSPORT` environment variable:
|
||||
|
||||
@@ -24,9 +23,6 @@ NEXT_PUBLIC_UPLOAD_TRANSPORT=database
|
||||
|
||||
# S3-compatible storage
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT=s3
|
||||
|
||||
# Azure Blob Storage (native)
|
||||
NEXT_PUBLIC_UPLOAD_TRANSPORT=azure-blob
|
||||
```
|
||||
|
||||
---
|
||||
@@ -287,111 +283,6 @@ 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.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user