mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 20:32:07 +10:00
Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f3f5903760 | |||
| 57eb40d6aa | |||
| 684fab1909 | |||
| d794ceb8da | |||
| 87315adb0f | |||
| 0a7794be61 | |||
| f15d6f0150 | |||
| 0b86ece1d5 | |||
| a197bf113f | |||
| ec8728b33e | |||
| 22122f51da | |||
| 8671f269e8 | |||
| edbf65969b | |||
| 207135d6f3 | |||
| 4877d1964a | |||
| f66751668a | |||
| bc3aa9c858 | |||
| b79b4bd111 | |||
| 36c10d1a92 | |||
| 8c0e029b1b | |||
| f10d3284ba | |||
| 6a6ef8d2ad | |||
| 690491c3b1 | |||
| 6243a514af | |||
| a697832b43 | |||
| aebb5e2067 | |||
| e19b1d00d0 | |||
| c428170b5c | |||
| 84fc866cfb | |||
| 5d92aaf20a | |||
| ae497092d7 | |||
| 2f4c3893a3 | |||
| 61338af216 | |||
| 2c7a1be051 | |||
| 8bad62cc92 | |||
| 19c2f7b4a1 | |||
| 135b676cd4 | |||
| 8f3e1893c7 | |||
| e063af628f | |||
| dc575f5c80 | |||
| e5da5bca38 | |||
| d38d703fd3 | |||
| 3249f855fb | |||
| 34b31c0d80 | |||
| f153a7c437 | |||
| f9cb8d84ed | |||
| 198dafc8ec | |||
| 2f1aaa2b5d | |||
| f54a8ed72f | |||
| 5082226e08 | |||
| bc82b2e70e | |||
| 4935f387bf | |||
| 6d7bd212bf | |||
| 283334921b | |||
| 1af83ea854 | |||
| 7cb64c3d04 | |||
| 4c69cb9c66 | |||
| 14b0b4805d | |||
| 9bfaa08d38 | |||
| 229cd2f7e9 | |||
| 6f650e1c2f | |||
| 0b9a23c550 | |||
| 3cca8cdae8 | |||
| b13ec8909c | |||
| e3b7a9e7cb | |||
| 74d79dc6b2 | |||
| 1c82595c12 | |||
| ad559f72dd | |||
| 025a27d385 | |||
| a71c44570b | |||
| f5b3babcbb | |||
| 2346de83a6 | |||
| 814f6e62de | |||
| 0434bdfacf | |||
| 53b6078fa9 | |||
| 5be71cca21 | |||
| ace472c294 | |||
| b2d395e00b | |||
| dd1b6d7dfe | |||
| bef3ea483d | |||
| e87aa29823 | |||
| 4f8132be61 | |||
| 9cf8ed1d00 | |||
| 108d422a2e | |||
| 48fb066b9a | |||
| 0b605d61c6 | |||
| 5dcdac7ecd | |||
| f48aa84c9e | |||
| 455fef70bd | |||
| 647dc5fc2d | |||
| de134afba1 | |||
| 36bbd97514 | |||
| 943a0b50e3 | |||
| 6ef501c9f2 | |||
| ac09a48eaa | |||
| 70fb834a6a | |||
| 66e357c9b3 | |||
| 3106fd7483 | |||
| 32c54e1245 | |||
| 83fbc70a1c | |||
| 1ee6ec87a2 | |||
| 6b1b1d0417 | |||
| 9f680c7a61 | |||
| 76d96d2f65 | |||
| 2f2b5dd232 | |||
| 8d97f1dcfa | |||
| e67e19358a | |||
| 364537e8fe | |||
| 4751c9cecc | |||
| a5fd814fbc | |||
| 1d2c781a6d | |||
| 03ca3971a0 | |||
| 5ea4060fd7 | |||
| af346b179c | |||
| ab69ee627b | |||
| 4daec44550 | |||
| 11eb4dd2cd | |||
| cc71c7d9ba | |||
| f82bf97480 | |||
| 0e20d364ef | |||
| ef57c8448a | |||
| eaaf8f9e63 | |||
| 58f0c98038 | |||
| da7b5d12f8 | |||
| 7cfe876762 | |||
| 15399cbe8e | |||
| c4754553c9 | |||
| 6c8726b58c | |||
| abd031b58b | |||
| 1ff8680c32 | |||
| 7ea664214a | |||
| 7e2cbe46c0 | |||
| c63b4ca3cc | |||
| 6faa01d384 | |||
| 0ce909a298 | |||
| 7f271379b9 | |||
| 406e77e4be | |||
| bff360b084 | |||
| db1087d76d | |||
| ef0a5b54ba | |||
| 1f985e2cd3 | |||
| 525dd92a56 | |||
| d21b99825d | |||
| dfbf68e4cd | |||
| 8b0231825f | |||
| 03e2e4f171 | |||
| 7f5f2b22ed | |||
| 7d3a56a006 | |||
| f1323679aa | |||
| cc67454513 | |||
| 665a0d0ea0 | |||
| 02ff8df09b | |||
| d4bee4aed1 | |||
| f4cfa71379 | |||
| b1763e422c | |||
| 64e0695811 | |||
| 8a4205d808 | |||
| 74db3d7a1c | |||
| 3976531045 | |||
| 7a499270be |
@@ -0,0 +1,239 @@
|
||||
---
|
||||
date: 2026-03-26
|
||||
title: Bullmq Background Jobs
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
The codebase has a well-designed background job provider abstraction (`BaseJobProvider`) with two existing implementations:
|
||||
|
||||
- **InngestJobProvider** — cloud/SaaS provider, externally hosted
|
||||
- **LocalJobProvider** — database-backed (Postgres via Prisma), uses HTTP self-calls to dispatch
|
||||
|
||||
The goal is to add a third provider backed by a proper job queue library for self-hosted deployments that need more reliability than the Local provider offers.
|
||||
|
||||
### Current Architecture
|
||||
|
||||
All code lives in `packages/lib/jobs/`:
|
||||
|
||||
- `client/base.ts` — Abstract `BaseJobProvider` with 4 methods: `defineJob()`, `triggerJob()`, `getApiHandler()`, `startCron()`
|
||||
- `client/client.ts` — `JobClient` facade, selects provider via `NEXT_PRIVATE_JOBS_PROVIDER` env var
|
||||
- `client/inngest.ts` — Inngest implementation
|
||||
- `client/local.ts` — Local/Postgres implementation
|
||||
- `client/_internal/job.ts` — Core types: `JobDefinition`, `JobRunIO`, `SimpleTriggerJobOptions`
|
||||
- `definitions/` — 19 job definitions (15 event-triggered, 4 cron)
|
||||
|
||||
The `JobRunIO` interface provided to handlers includes:
|
||||
|
||||
- `runTask(cacheKey, callback)` — idempotent task execution (cached via `BackgroundJobTask` table)
|
||||
- `triggerJob(cacheKey, options)` — chain jobs from within handlers
|
||||
- `wait(cacheKey, ms)` — delay/sleep (not implemented in Local provider)
|
||||
- `logger` — structured logging
|
||||
|
||||
### Local Provider Limitations
|
||||
|
||||
The current Local provider has several issues that motivate this work:
|
||||
|
||||
1. `io.wait()` throws "Not implemented"
|
||||
2. HTTP self-call with 150ms fire-and-forget `Promise.race` is fragile
|
||||
3. No concurrency control — jobs run in the web server process
|
||||
4. No real retry backoff (immediate re-dispatch)
|
||||
5. No monitoring/visibility into job status
|
||||
6. Jobs compete for resources with HTTP request handling
|
||||
|
||||
---
|
||||
|
||||
## Provider Evaluation
|
||||
|
||||
Three alternatives were evaluated against the existing provider interface and project requirements.
|
||||
|
||||
### BullMQ (Redis-backed) — Recommended
|
||||
|
||||
| Attribute | Detail |
|
||||
| ------------------- | -------------------------- |
|
||||
| Backend | Redis 7.x |
|
||||
| npm downloads/month | ~15M |
|
||||
| TypeScript | Native |
|
||||
| Delayed jobs | Yes (ms precision) |
|
||||
| Cron/repeatable | Yes (`upsertJobScheduler`) |
|
||||
| Retries + backoff | Yes (exponential, custom) |
|
||||
| Concurrency control | Yes (per-worker) |
|
||||
| Rate limiting | Yes (per-queue, per-group) |
|
||||
| Dashboard | Bull Board (mature) |
|
||||
| New infrastructure | Yes — Redis required |
|
||||
|
||||
**Why BullMQ**: Most mature and widely-adopted Node.js queue. Native delayed jobs solve the `io.wait()` gap. Redis is purpose-built for queue workloads and keeps Postgres clean for application data. Bull Board gives immediate operational visibility. The provider abstraction already exists so wrapping BullMQ is straightforward.
|
||||
|
||||
**Trade-off**: Requires Redis, which is additional infrastructure. However, Redis is a single Docker Compose service or a free Upstash tier, and the operational benefit is significant.
|
||||
|
||||
### pg-boss (PostgreSQL-backed) — Strong Alternative
|
||||
|
||||
| Attribute | Detail |
|
||||
| ------------------- | ----------------------------- |
|
||||
| Backend | PostgreSQL (existing) |
|
||||
| npm downloads/month | ~1.4M |
|
||||
| TypeScript | Native |
|
||||
| Delayed jobs | Yes (`startAfter`) |
|
||||
| Cron/repeatable | Yes (`schedule()`) |
|
||||
| New infrastructure | No — reuses existing Postgres |
|
||||
|
||||
**Why it could work**: Zero new infrastructure since the project already uses Postgres. API maps well to existing patterns.
|
||||
|
||||
**Why it's second choice**: Polling-based (no LISTEN/NOTIFY), adds write amplification to the primary database, smaller ecosystem, no dashboard. At scale, queue operations on the primary database become a concern.
|
||||
|
||||
### Graphile Worker (PostgreSQL-backed) — Less Suitable
|
||||
|
||||
Uses LISTEN/NOTIFY for instant pickup but has a file-based task convention and separate schema that don't mesh well with the existing Prisma-centric architecture. Would require more adapter work.
|
||||
|
||||
### Improving the Local Provider — Not Recommended
|
||||
|
||||
Fixing the Local provider's issues (wait support, replacing HTTP self-calls, adding concurrency control, backoff) essentially means rebuilding a queue library from scratch with less robustness and no community maintenance.
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Proceed with BullMQ.** It's the most capable option, maps cleanly to the existing provider interface, and is the standard choice for production Node.js applications. Redis is lightweight infrastructure with managed options available at every cloud provider.
|
||||
|
||||
**If Redis is a hard blocker**, pg-boss is the clear fallback — but the plan below assumes BullMQ.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: BullMQ Provider Core
|
||||
|
||||
**File: `packages/lib/jobs/client/bullmq.ts`**
|
||||
|
||||
Create `BullMQJobProvider extends BaseJobProvider` with singleton pattern matching the existing providers.
|
||||
|
||||
Key implementation details:
|
||||
|
||||
1. **Constructor / `getInstance()`**
|
||||
- Initialize a Redis `IORedis` connection using new env var: `NEXT_PRIVATE_REDIS_URL`
|
||||
- Create a single `Queue` instance for dispatching jobs, using `NEXT_PRIVATE_REDIS_PREFIX` as the BullMQ `prefix` option (defaults to `documenso` if unset). This namespaces all Redis keys so multiple environments (worktrees, branches, developers) sharing the same Redis instance don't collide.
|
||||
- Create a single `Worker` instance for processing jobs (in-process, same prefix)
|
||||
- Store job definitions in a `_jobDefinitions` record (same pattern as Local provider)
|
||||
|
||||
2. **`defineJob()`**
|
||||
- Store definition in `_jobDefinitions` keyed by ID
|
||||
- If the definition has a `trigger.cron`, register it via `queue.upsertJobScheduler()` with the cron expression
|
||||
|
||||
3. **`triggerJob(options)`**
|
||||
- Find eligible definitions by `trigger.name` (same lookup as Local provider)
|
||||
- For each, call `queue.add(jobDefinitionId, payload)` with appropriate options
|
||||
- Support `options.id` for deduplication via BullMQ's `jobId` option
|
||||
|
||||
4. **`getApiHandler()`**
|
||||
- Return a minimal health-check / queue-status handler. Unlike the Local provider, BullMQ workers don't need an HTTP endpoint to receive jobs — they pull from Redis directly. The API handler can return queue metrics for monitoring.
|
||||
|
||||
5. **`startCron()`**
|
||||
- No-op — cron is handled by BullMQ's `upsertJobScheduler` registered during `defineJob()`
|
||||
|
||||
6. **Worker setup**
|
||||
- Single worker processes all job types by dispatching to the correct handler from `_jobDefinitions`
|
||||
- Configure concurrency with a default of 10 (overridable via `NEXT_PRIVATE_BULLMQ_CONCURRENCY` env var for those who need to tune it)
|
||||
- Configure retry with exponential backoff: `backoff: { type: 'exponential', delay: 1000 }`
|
||||
- Default 3 retries (matching current Local provider behavior)
|
||||
|
||||
7. **`createJobRunIO(jobId)`** — Implement `JobRunIO`:
|
||||
- `runTask()`: Reuse the existing `BackgroundJobTask` Prisma table for idempotent task tracking (same pattern as Local provider)
|
||||
- `triggerJob()`: Delegate to `this.triggerJob()`
|
||||
- `wait()`: Throw "Not implemented" (same as Local provider). No handler uses `io.wait()` so this has zero impact
|
||||
- `logger`: Same console-based logger pattern as Local provider
|
||||
|
||||
### Phase 2: Provider Registration
|
||||
|
||||
**File: `packages/lib/jobs/client/client.ts`**
|
||||
|
||||
Add `'bullmq'` case to the provider match:
|
||||
|
||||
```typescript
|
||||
this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
|
||||
.with('inngest', () => InngestJobProvider.getInstance())
|
||||
.with('bullmq', () => BullMQJobProvider.getInstance())
|
||||
.otherwise(() => LocalJobProvider.getInstance());
|
||||
```
|
||||
|
||||
**File: `packages/tsconfig/process-env.d.ts`**
|
||||
|
||||
Add `'bullmq'` to the `NEXT_PRIVATE_JOBS_PROVIDER` type union and add Redis env var types:
|
||||
|
||||
```typescript
|
||||
NEXT_PRIVATE_JOBS_PROVIDER?: 'inngest' | 'local' | 'bullmq';
|
||||
NEXT_PRIVATE_REDIS_URL?: string;
|
||||
NEXT_PRIVATE_REDIS_PREFIX?: string;
|
||||
NEXT_PRIVATE_BULLMQ_CONCURRENCY?: string;
|
||||
```
|
||||
|
||||
**File: `.env.example`**
|
||||
|
||||
Add Redis configuration examples:
|
||||
|
||||
```env
|
||||
NEXT_PRIVATE_JOBS_PROVIDER="local" # Options: local, inngest, bullmq
|
||||
NEXT_PRIVATE_REDIS_URL="redis://localhost:63790"
|
||||
NEXT_PRIVATE_REDIS_PREFIX="documenso" # Namespace for Redis keys (useful when sharing a Redis instance)
|
||||
```
|
||||
|
||||
**File: `turbo.json`**
|
||||
|
||||
Add `NEXT_PRIVATE_REDIS_URL`, `NEXT_PRIVATE_REDIS_PREFIX`, and `NEXT_PRIVATE_BULLMQ_CONCURRENCY` to the env vars list for cache invalidation.
|
||||
|
||||
### Phase 3: Infrastructure & Dependencies
|
||||
|
||||
**File: `packages/lib/package.json`**
|
||||
|
||||
Add dependencies:
|
||||
|
||||
- `bullmq` — the queue library
|
||||
- `ioredis` — Redis client (peer dependency of BullMQ, but explicit is better)
|
||||
|
||||
**File: `docker-compose.yml` (or equivalent)**
|
||||
|
||||
Add Redis service for local development:
|
||||
|
||||
```yaml
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- '6379:6379'
|
||||
```
|
||||
|
||||
### Phase 4: Optional Enhancements
|
||||
|
||||
These are not required for the initial implementation but worth considering for follow-up:
|
||||
|
||||
1. **Bull Board integration** — Add a `/api/jobs/dashboard` route that serves Bull Board UI for monitoring. Gate behind an admin auth check.
|
||||
|
||||
2. **Separate worker process** — Add an `apps/worker` entry point that runs BullMQ workers without the web server, for deployments that want to isolate job processing from request handling.
|
||||
|
||||
3. **Graceful shutdown** — Register `SIGTERM`/`SIGINT` handlers to call `worker.close()` and `queue.close()` for clean shutdown.
|
||||
|
||||
4. **BackgroundJob table integration** — Optionally continue writing to the `BackgroundJob` Prisma table for audit/history, using BullMQ events (`completed`, `failed`) to update status. This preserves the existing database-level visibility.
|
||||
|
||||
---
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
| File | Action | Description |
|
||||
| ------------------------------------ | ---------- | ---------------------------------------- |
|
||||
| `packages/lib/jobs/client/bullmq.ts` | **Create** | BullMQ provider implementation |
|
||||
| `packages/lib/jobs/client/client.ts` | Modify | Add `'bullmq'` provider case |
|
||||
| `packages/tsconfig/process-env.d.ts` | Modify | Add type for `'bullmq'` + Redis env vars |
|
||||
| `.env.example` | Modify | Add Redis config example |
|
||||
| `turbo.json` | Modify | Add Redis env var to cache keys |
|
||||
| `packages/lib/package.json` | Modify | Add `bullmq` + `ioredis` dependencies |
|
||||
| `docker-compose.yml` | Modify | Add Redis service |
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Should the BullMQ provider also write to the `BackgroundJob` Prisma table?** This would maintain audit history and allow existing admin tooling to query job status. Trade-off is dual-write complexity.
|
||||
|
||||
2. **Redis connection resilience**: Should the provider gracefully degrade if Redis is unavailable (e.g., fall back to Local provider), or fail hard? Failing hard is simpler and more predictable.
|
||||
|
||||
## Resolved Questions
|
||||
|
||||
- **`io.wait()`**: Not a concern. Only Inngest implements it (via `step.sleep`), the Local provider throws "Not implemented", and no job handler calls `io.wait()`. The BullMQ provider can throw "Not implemented" identically to the Local provider.
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
date: 2026-03-04
|
||||
title: Swap Subscription Between Orgs
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Add the ability for admins to move a subscription (and its associated Stripe customerId) from one organisation to another, when viewing a user in the admin panel. The target org must be owned by the same user and must be on the free plan (no existing active subscription).
|
||||
|
||||
## Context & Data Model
|
||||
|
||||
- `Organisation` has a 1:1 optional `Subscription` and a `customerId` (Stripe customer ID, `@unique`)
|
||||
- `Organisation` has a 1:1 `OrganisationClaim` that tracks entitlements (team count, member count, feature flags)
|
||||
- `Subscription` also stores a redundant `customerId` and has `organisationId` (`@unique`)
|
||||
- When a subscription is removed from an org, its `OrganisationClaim` should be reset to the FREE claim
|
||||
- Relationship chain: `User --owns--> Organisation --has--> Subscription + OrganisationClaim`
|
||||
|
||||
## Constraints
|
||||
|
||||
- **paid → free only**: The target org must NOT have an active subscription (status ACTIVE or PAST_DUE). It must be on the free plan.
|
||||
- **same owner**: Both source and target orgs must be owned by the same user (the user being viewed).
|
||||
- The `customerId` must move with the subscription to the target org (cleared from source, set on target).
|
||||
- The Stripe subscription object itself is NOT modified — only the DB-level mapping changes. The Stripe customer stays the same; we just reassociate it to a different org.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### 1. Backend: TRPC Admin Route
|
||||
|
||||
**Files to create:**
|
||||
|
||||
- `packages/trpc/server/admin-router/swap-organisation-subscription.types.ts`
|
||||
- `packages/trpc/server/admin-router/swap-organisation-subscription.ts`
|
||||
|
||||
**Request schema (`ZSwapOrganisationSubscriptionRequestSchema`):**
|
||||
|
||||
```ts
|
||||
z.object({
|
||||
sourceOrganisationId: z.string(),
|
||||
targetOrganisationId: z.string(),
|
||||
});
|
||||
```
|
||||
|
||||
**Response schema:** `z.void()`
|
||||
|
||||
**Route logic (in a single `prisma.$transaction`):**
|
||||
|
||||
1. Fetch source org with `subscription` + `organisationClaim`
|
||||
2. Fetch target org with `subscription` + `organisationClaim`
|
||||
3. Validate:
|
||||
- Source org has an active subscription (status `ACTIVE` or `PAST_DUE`)
|
||||
- Target org does NOT have an active subscription (no subscription record, or status `INACTIVE`)
|
||||
- Both orgs have the same `ownerUserId`
|
||||
4. In a transaction:
|
||||
a. Clear `customerId` on source org (set to `null`)
|
||||
b. Set `customerId` on target org to the source's `customerId`
|
||||
c. Move the `Subscription` record: update `organisationId` to target org ID
|
||||
d. Copy the source org's `OrganisationClaim` entitlements to the target org's `OrganisationClaim` (`originalSubscriptionClaimId`, `teamCount`, `memberCount`, `envelopeItemCount`, `flags`)
|
||||
e. Reset the source org's `OrganisationClaim` to the FREE claim (using `createOrganisationClaimUpsertData(internalClaims[INTERNAL_CLAIM_ID.FREE])` pattern from `on-subscription-deleted.ts`)
|
||||
|
||||
**Note on ordering:** Because `Organisation.customerId` is `@unique`, we must clear the source first, then set the target — or do both in a transaction that handles the constraint. Prisma transactions handle this correctly as they apply all writes atomically.
|
||||
|
||||
**Register the route:**
|
||||
|
||||
- Import in `packages/trpc/server/admin-router/router.ts`
|
||||
- Add under `organisation` as `swapSubscription`
|
||||
- Call path: `trpc.admin.organisation.swapSubscription`
|
||||
|
||||
### 2. Frontend: Dialog Component
|
||||
|
||||
**File to create:**
|
||||
|
||||
- `apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx`
|
||||
|
||||
**Props:**
|
||||
|
||||
```ts
|
||||
type AdminSwapSubscriptionDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
sourceOrganisationId: string;
|
||||
sourceOrganisationName: string;
|
||||
userId: number;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
```
|
||||
|
||||
**Dialog behavior:**
|
||||
|
||||
1. Opens when the trigger is clicked (from the organisations table actions dropdown)
|
||||
2. Fetches the user's owned orgs via `trpc.admin.organisation.find.useQuery({ ownerUserId: userId })`
|
||||
3. Filters to only show orgs that are on the free plan (no active subscription) and excludes the source org
|
||||
4. Displays a select dropdown to pick the target org
|
||||
5. Shows a warning alert: "This will move the subscription from {source} to {target}. The source organisation will be reset to the free plan."
|
||||
6. On submit, calls `trpc.admin.organisation.swapSubscription.useMutation()`
|
||||
7. On success, shows a toast, invalidates relevant queries, and closes the dialog
|
||||
|
||||
**UI layout (following existing dialog patterns like `admin-organisation-create-dialog.tsx`):**
|
||||
|
||||
- `DialogHeader` with title "Move Subscription" and description
|
||||
- A select dropdown listing eligible target orgs (name + url)
|
||||
- An `Alert` explaining what will happen
|
||||
- `DialogFooter` with Cancel + "Move Subscription" buttons (submit button uses `loading` prop)
|
||||
|
||||
### 3. Frontend: Wire into the Organisations Table
|
||||
|
||||
**File to modify:**
|
||||
|
||||
- `apps/remix/app/components/tables/admin-organisations-table.tsx`
|
||||
|
||||
**Changes:**
|
||||
|
||||
- Import the `AdminSwapSubscriptionDialog`
|
||||
- Add a new prop `ownerUserId?: number` to `AdminOrganisationsTableOptions` (needed so the dialog can query other owned orgs)
|
||||
- Add a new dropdown menu item in the actions column: "Move Subscription" with `ArrowRightLeftIcon` from lucide
|
||||
- Only render this item when the org row has an active subscription (`subscription?.status === 'ACTIVE' || subscription?.status === 'PAST_DUE'`)
|
||||
- The menu item renders inside `AdminSwapSubscriptionDialog` with `trigger` prop as the menu item
|
||||
|
||||
### 4. Frontend: Pass userId from User Detail Page
|
||||
|
||||
**File to modify:**
|
||||
|
||||
- `apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx`
|
||||
|
||||
**Changes:**
|
||||
|
||||
- Pass `ownerUserId={user.id}` to `<AdminOrganisationsTable>` so it can forward this to the swap dialog
|
||||
|
||||
## File Change Summary
|
||||
|
||||
| File | Action | Description |
|
||||
| --------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------- |
|
||||
| `packages/trpc/server/admin-router/swap-organisation-subscription.types.ts` | **Create** | Request/response Zod schemas + TS types |
|
||||
| `packages/trpc/server/admin-router/swap-organisation-subscription.ts` | **Create** | Admin mutation with prisma transaction |
|
||||
| `packages/trpc/server/admin-router/router.ts` | **Modify** | Register route at `organisation.swapSubscription` |
|
||||
| `apps/remix/app/components/dialogs/admin-swap-subscription-dialog.tsx` | **Create** | Dialog for selecting target org |
|
||||
| `apps/remix/app/components/tables/admin-organisations-table.tsx` | **Modify** | Add "Move Subscription" action + accept `ownerUserId` prop |
|
||||
| `apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx` | **Modify** | Pass `ownerUserId={user.id}` to table |
|
||||
|
||||
## Edge Cases & Considerations
|
||||
|
||||
1. **Stripe customer stays the same**: The Stripe subscription is tied to a Stripe customer. We move the `customerId` to the target org, so webhook lookups (`findFirst where customerId`) will correctly resolve to the target org going forward.
|
||||
|
||||
2. **`@unique` constraint on `Organisation.customerId`**: Must clear source before setting target within the transaction. Prisma interactive transactions handle this correctly.
|
||||
|
||||
3. **`@unique` constraint on `Subscription.organisationId`**: Since the target org should not have a subscription record, updating the existing subscription's `organisationId` to the target should work. If the target has an INACTIVE subscription record, we need to delete it first.
|
||||
|
||||
4. **Target org has INACTIVE subscription**: The target org might have a stale INACTIVE subscription from a previous cancellation. In this case, delete the target's old subscription record before moving the source's subscription over.
|
||||
|
||||
5. **Seat-based plans**: If the subscription is seat-based, the Stripe quantity may not match the target org's member count. Consider calling `syncMemberCountWithStripeSeatPlan` after the swap as a post-transaction step.
|
||||
|
||||
6. **OrganisationClaim transfer**: Copy `originalSubscriptionClaimId`, `teamCount`, `memberCount`, `envelopeItemCount`, and `flags` from source claim to target claim. Reset source claim to FREE.
|
||||
|
||||
7. **No Stripe API calls needed**: This is purely a DB-level reassociation. The Stripe subscription, customer, and payment method all remain unchanged.
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
date: 2026-05-06
|
||||
title: Platform Signing Page Branding
|
||||
---
|
||||
|
||||
## What
|
||||
|
||||
Platform-plan organisations (and their teams) can customise the **non-embed
|
||||
signing pages** (`/sign/:token`, `/d/:token`, and the sibling
|
||||
complete/expired/rejected/waiting pages) with:
|
||||
|
||||
- Six brand colour tokens (background, foreground, primary, primary-foreground,
|
||||
border, ring) plus a border-radius length.
|
||||
- A free-text custom CSS block (up to 256 KB).
|
||||
|
||||
Settings live on `OrganisationGlobalSettings` and `TeamGlobalSettings`. Teams
|
||||
inherit from the org via the existing `brandingEnabled === null` mechanism.
|
||||
|
||||
## Why
|
||||
|
||||
- Embed customers already have white-label CSS; Platform customers want the
|
||||
same coverage on direct signing URLs that they iframe or link to.
|
||||
- Persisting on org/team (not per envelope) means it's set-and-forget.
|
||||
- Sanitising **on save** lets us inline the verbatim string at SSR — no
|
||||
per-render parsing cost, no `<style>.innerHTML` injection on the client.
|
||||
- Reusing the existing `embedSigningWhiteLabel` claim flag keeps "if you can
|
||||
white-label an embed, you can white-label this" as one decision.
|
||||
|
||||
## How
|
||||
|
||||
### Storage (`packages/prisma/schema.prisma`)
|
||||
|
||||
Two new fields on each settings model. No new tables.
|
||||
|
||||
| Field | Org type | Team type |
|
||||
| ---------------- | ------------------ | ------------------ |
|
||||
| `brandingColors` | `Json?` (nullable) | `Json?` (nullable) |
|
||||
| `brandingCss` | `String @default("")` | `String?` |
|
||||
|
||||
Colours are validated against `ZCssVarsSchema`. The team's `null` means
|
||||
"inherit"; an empty colour object is collapsed to `null` server-side so a
|
||||
team toggling `brandingEnabled = true` without filling in colours doesn't
|
||||
silently override the org's defaults with nothing.
|
||||
|
||||
### Sanitiser (`packages/lib/utils/sanitize-branding-css.ts`)
|
||||
|
||||
PostCSS + `postcss-selector-parser`. Runs on save only.
|
||||
|
||||
- Drops selectors containing `::before`/`::after`/`::backdrop`/`::marker` or
|
||||
the universal `*`.
|
||||
- Drops integrity-breaking properties (`display`, `position`, `transform`,
|
||||
layout-affecting dimensions, text-hiding properties).
|
||||
- Drops declaration values containing `url(`, `expression(`, `@import`,
|
||||
`javascript:`.
|
||||
- Strips `!important`.
|
||||
- Allows `@media` only; drops other at-rules.
|
||||
- **Does not** rewrite selectors. Scoping happens at render time via native
|
||||
CSS nesting under `.documenso-branded { ... }`.
|
||||
- Final-pass tripwire: if a literal `</style` somehow survives serialization,
|
||||
reject the entire output. PostCSS already escapes `<` to `\3c` whenever it
|
||||
would form `</...`; the explicit check is belt-and-braces in case a future
|
||||
serializer regresses.
|
||||
- Returns `{ css, warnings[] }`. Warnings are surfaced in the UI.
|
||||
|
||||
Border-radius is the only token interpolated raw into a `<style>` block; it
|
||||
is regex-validated (`CSS_LENGTH_REGEX`) at both the Zod schema and the
|
||||
runtime `toNativeCssVars` call. Belt-and-braces against schema drift.
|
||||
|
||||
### Render (`apps/remix/app/components/general/recipient-branding.tsx`)
|
||||
|
||||
Each recipient loader calls `loadRecipientBrandingByTeamId` and threads the
|
||||
payload through to `<RecipientBranding>`, which emits a single
|
||||
nonce-attributed `<style>`:
|
||||
|
||||
```
|
||||
.documenso-branded {
|
||||
--background: ...; ...
|
||||
<user css>
|
||||
}
|
||||
```
|
||||
|
||||
Native CSS nesting expands user rules under the wrapper. The body class is
|
||||
applied unconditionally to recipient routes in `root.tsx` via `useMatches()`
|
||||
so portaled Radix content (dialogs, popovers, tooltips, dropdowns) inherits
|
||||
the scope.
|
||||
|
||||
CSP for recipient routes already supports `<style nonce>`; no policy
|
||||
changes needed.
|
||||
|
||||
### Plan gate
|
||||
|
||||
`organisationClaim.flags.embedSigningWhiteLabel || !IS_BILLING_ENABLED()`.
|
||||
Self-hosted instances always allow. The outer paywall for logo/URL/details
|
||||
stays on `allowCustomBranding` (Team plan and up); only the new
|
||||
colour/CSS section is Platform-only.
|
||||
|
||||
### UI (`apps/remix/app/components/forms/branding-preferences-form.tsx`)
|
||||
|
||||
Extends the existing branding form. Six `<ColorPicker showHex>` (rewritten
|
||||
to use the native `<input type="color">` instead of `react-colorful`, which
|
||||
was removed) in a 2-col grid, plus a free-text radius input and an
|
||||
`<Accordion>` revealing a mono `<Textarea>`. Defaults come from
|
||||
`packages/lib/constants/theme.ts` (light-mode hex mirror of `theme.css`).
|
||||
|
||||
Warnings from the sanitiser are surfaced in an `<Alert variant="warning">`
|
||||
after save, and the `brandingCss` textarea is re-synced from the persisted
|
||||
value so the user sees exactly what was stored. Other fields are
|
||||
deliberately NOT reset on settings refetch — that would clobber in-flight
|
||||
edits.
|
||||
|
||||
### TRPC
|
||||
|
||||
`update-organisation-settings` and `update-team-settings` accept the new
|
||||
fields, run them through `sanitizeBrandingCss` + `normalizeBrandingColors`,
|
||||
and return any sanitiser warnings to the client. The team route treats
|
||||
`null` as "inherit"; an empty post-sanitisation string is collapsed to
|
||||
`null` (team) so an empty override doesn't mask the org's CSS.
|
||||
|
||||
## Known accepted limitations
|
||||
|
||||
- The sanitiser does not prevent hostile-but-syntactically-valid CSS
|
||||
(`color: transparent`, low-contrast values, etc.). The customer is
|
||||
branding **their own** signing pages — we focus on integrity (no
|
||||
overlay/hide/exfiltrate), not aesthetic policing.
|
||||
- User rules targeting `body`/`html`/`:root` no-op once nested under the
|
||||
wrapper class. Documented for users.
|
||||
- CSS nesting baseline is Chrome 120+ / Firefox 117+ / Safari 16.5+.
|
||||
Acceptable for the Platform-tier audience.
|
||||
- No automated `theme.css` ↔ `theme.ts` sync check; fat comment in
|
||||
`theme.ts` reminds devs to update both.
|
||||
- Per-section team inherit is coarse — `brandingEnabled = null` inherits
|
||||
everything from the org. Per-field inherit toggles are deferred.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Live preview, embed-route sanitiser unification, email/PDF certificate
|
||||
branding, custom font upload, the full ~30 colour tokens in the picker UI,
|
||||
wiring `hidePoweredBy` through to the actual footer.
|
||||
@@ -0,0 +1,94 @@
|
||||
---
|
||||
date: 2026-04-22
|
||||
title: Partial Signed Pdf Download
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Let team members fetch a PDF with all currently-inserted fields burned in while the envelope is still in `PENDING` status. Today the only available bytes for a pending envelope are the original (no fields) - the sealed PDF only materialises after the last recipient signs and the `seal-document` job runs.
|
||||
|
||||
Exposed in two places:
|
||||
|
||||
- v2 API: `GET /api/v2/envelope/item/{envelopeItemId}/download?version=pending` (API-token auth)
|
||||
- UI: a `Partial` button in the existing `EnvelopeDownloadDialog`, alongside `Original`. Replaces the `Signed` slot when the envelope is `PENDING`. Backed by the existing session-authed file route `GET /api/files/envelope/{envelopeId}/envelopeItem/{id}/download/pending`.
|
||||
|
||||
## Scope
|
||||
|
||||
- v2 API only (no v1).
|
||||
- `internalVersion === 2` envelopes only. Legacy v1 returns 400 `ENVELOPE_LEGACY`.
|
||||
- Team-side / owner only. No recipient-token download path - recipients have the in-app overlay viewer for verification, and a downloadable half-signed PDF is a leak vector for partially-executed contracts. Enforced both at the server (the recipient-token file route does not accept `pending`) and at the UI (the dialog hides the Partial button when a recipient token is set).
|
||||
- No PKI signature, no certificate page, no audit log appendix - the response is explicitly not a final executed document.
|
||||
- No watermark or banner text. The filename suffix (`_pending.pdf`), the `Cache-Control: no-store, private` header, and the absence of a PKI signature are sufficient to signal draft status.
|
||||
|
||||
## Behaviour
|
||||
|
||||
API response matrix (both `/api/v2/envelope/item/{id}/download?version=pending` and the UI-facing `/api/files/envelope/{envelopeId}/envelopeItem/{id}/download/pending`):
|
||||
|
||||
| Envelope status | Response |
|
||||
|---|---|
|
||||
| `PENDING` (v2) | 200, PDF with currently-inserted fields burned in |
|
||||
| `PENDING` (v1) | 400 `ENVELOPE_LEGACY` |
|
||||
| `DRAFT` | 400 `ENVELOPE_DRAFT` |
|
||||
| `COMPLETED` | 400 `ENVELOPE_COMPLETED` |
|
||||
| `REJECTED` | 400 `ENVELOPE_REJECTED` |
|
||||
|
||||
All v1-vs-v2 / status-mismatch errors are 4xx so callers can cleanly separate them from real server failures (5xx). Specifically v1 PENDING returns 400 not 501: 5xx is reserved for actual server problems, while "this envelope can't satisfy this request shape" is a client-addressable condition.
|
||||
|
||||
Filename: `{title}_pending.pdf`.
|
||||
|
||||
ETag is content-addressed over `sha256(envelope.status + sorted((field.id, field.customText, field.signature?.id, field.signature?.created) for inserted===true fields))`. Returns 304 on `If-None-Match` match.
|
||||
|
||||
No persistent caching. Generated on-demand per request when ETag misses.
|
||||
|
||||
Error response shape (envelope item v2 download route and the team-side file route): preserves the existing `{ error: <message> }` field for backwards compatibility and adds `code: <APP_ERROR_CODE>` as a new field for callers that want to branch on it. The document download route (`/document/{documentId}/download`) is untouched.
|
||||
|
||||
## UI
|
||||
|
||||
`apps/remix/app/components/dialogs/envelope-download-dialog.tsx`:
|
||||
|
||||
- The dialog shows `Original` plus one of:
|
||||
- `Signed` when status is `COMPLETED` (existing behaviour)
|
||||
- `Partial` when status is `PENDING`, there is no recipient token, and the envelope is not legacy (`!isLegacy`)
|
||||
- nothing otherwise
|
||||
- New optional prop `isLegacy?: boolean`. Only consulted to gate the `Partial` button, so callers whose status can never be `PENDING` (DRAFT/COMPLETED/REJECTED hardcoded, or `isComplete: true` matchers) and callers that always set a recipient token can omit it. Three call sites pass it (`isLegacy={envelope.internalVersion === 1}`): `documents-table-action-dropdown.tsx`, `envelope-editor.tsx`, `document-page-view-dropdown.tsx`. The other eight callers were left alone.
|
||||
|
||||
Trade-off: a future team-side dialog usage where status could be PENDING but the dev forgets `isLegacy` will silently not render the Partial button. The status gate prevents an actively broken click; missing button is discoverable in testing. Required-prop alternative was rejected because eight of eleven call sites would carry a meaningless value.
|
||||
|
||||
## Files
|
||||
|
||||
Server:
|
||||
|
||||
- `apps/remix/server/api/download/download.types.ts` - added `'pending'` to the `version` enum; split the validator into `param` (envelopeItemId) + `query` (version). The original wiring as a path-param validator was a pre-existing bug: requests like `?version=original` were silently returning the signed PDF since `version` actually arrives as a query string. Fixed as a side effect.
|
||||
- `packages/trpc/server/envelope-router/download-envelope-item.types.ts` - mirrored the enum change in the OpenAPI schema.
|
||||
- `apps/remix/server/api/download/download.ts` - the envelope item v2 route now fetches envelope recipients alongside the envelope, branches on `version` when calling the helper, and emits AppError responses as `{ error, code }` consistently across all status codes.
|
||||
- `apps/remix/server/api/files/files.types.ts` - added `'pending'` to the team-side download schema only. The recipient-token download schema is untouched, so `/api/files/token/.../download/pending` is rejected by the schema validator.
|
||||
- `apps/remix/server/api/files/files.ts` - the team-side download handler fetches envelope recipients and dispatches the `pending` branch through the same `handleEnvelopeItemFileRequest` helper. Wrapped in a try/catch that returns `{ error, code }` for AppErrors.
|
||||
- `apps/remix/server/api/files/files.helpers.ts` - `handleEnvelopeItemFileRequest` is now a single entry point taking a discriminated-union options type. The static-file flow (`signed`/`original`) and the on-demand pending flow are private helpers in the same module.
|
||||
- `packages/lib/server-only/pdf/generate-partial-signed-pdf.ts` (new) - small orchestrator that loads the original PDF, groups inserted fields by page, calls the existing `insertFieldInPDFV2` overlay helper for each page, flattens, and saves.
|
||||
- `packages/lib/errors/app-error.ts` - added `ENVELOPE_DRAFT`, `ENVELOPE_COMPLETED`, `ENVELOPE_REJECTED`, `ENVELOPE_LEGACY` codes, all mapped to 400. The legacy-envelope case deliberately returns 4xx rather than 501 to keep "this resource can't satisfy this operation" distinct from real 5xx server failures in caller logs/metrics.
|
||||
|
||||
Client:
|
||||
|
||||
- `packages/lib/utils/envelope-download.ts` - `EnvelopeItemPdfUrlOptions` download variant now allows `'pending'` as a version. The recipient-token URL builder will produce a URL the server rejects, but the dialog gates on no-token at the call site.
|
||||
- `packages/lib/client-only/download-pdf.ts` - `DocumentVersion` extended; filename suffix logic moved into a small switch (`_signed.pdf`, `_pending.pdf`, `.pdf`).
|
||||
- `apps/remix/app/components/dialogs/envelope-download-dialog.tsx` - secondary download derivation with the new `Partial` branch, optional `isLegacy` prop.
|
||||
- `apps/remix/app/components/tables/documents-table-action-dropdown.tsx`, `apps/remix/app/components/general/envelope-editor/envelope-editor.tsx`, `apps/remix/app/components/general/document/document-page-view-dropdown.tsx` - pass `isLegacy={envelope.internalVersion === 1}` (or `row.internalVersion === 1`) to the dialog.
|
||||
|
||||
## Verification
|
||||
|
||||
1. E2E (`packages/app-tests/e2e/api/v2/partial-signed-pdf-download.spec.ts`):
|
||||
- Pending envelope, recipient 1 signs, API token download with `?version=pending` returns 200 + PDF; subsequent call with `If-None-Match: <etag>` returns 304; after recipient 2 completes the envelope flips to `COMPLETED` and the same call returns 400 `ENVELOPE_COMPLETED`; `?version=signed` then succeeds.
|
||||
- Draft envelope returns 400 `ENVELOPE_DRAFT`.
|
||||
- `internalVersion === 1` pending envelope returns 400 `ENVELOPE_LEGACY`.
|
||||
|
||||
2. `npx tsc --noEmit -p apps/remix/tsconfig.json` and `npm run lint`.
|
||||
|
||||
3. Manual: open the Documents table or envelope editor on a PENDING envelope (v2), open the download dialog, confirm `Partial` appears alongside `Original` and produces a `_pending.pdf` with current fields burned in. Same dialog on a COMPLETED envelope shows `Signed`. Same dialog on a v1 PENDING envelope shows neither (status gate would show Partial, but the `isLegacy` flag filters it out).
|
||||
|
||||
## Out of Scope / Follow-ups
|
||||
|
||||
- Recipient-token download path (API and UI) - decided against. Revisit if there is concrete demand and a story for limiting the leak vector.
|
||||
- v1 API parity / v1 partial rendering - not building. Implementing partial for v1 would require porting `legacy_insertFieldInPDF` / `insertFieldInPDFV1` into a partial-only flow, which is code with no long-term home as v1 is being phased out.
|
||||
- Document download route (`/document/{documentId}/download`) - untouched. Same error shape and validator wiring as before. Consider normalising to the same `{ error, code }` shape in a follow-up if any caller wants to branch on `code` from that route.
|
||||
- Persistent caching layer / job-queue generation - revisit if p95 latency on large PDFs becomes an issue.
|
||||
- Specific toast for `ENVELOPE_LEGACY` in the dialog - currently the catch-all "Something went wrong" handles it. Worth a polish if v1 PENDING envelopes are common in your data and we see complaints. (Note: with the `isLegacy` gate at the UI, the error is unreachable from the dialog itself; the API can still surface it for direct callers.)
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
date: 2026-03-07
|
||||
title: Search Query Optimization
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
The `searchDocumentsWithKeyword` and `searchTemplatesWithKeyword` functions generate a single massive Prisma `findMany` with 7 OR branches. This produces a SQL query that:
|
||||
|
||||
- Joins `Team` twice (aliased j3 and j8) for the two team-access branches
|
||||
- Embeds 4-level deep EXISTS subqueries (`TeamGroup -> OrganisationGroup -> OrganisationGroupMember -> OrganisationMember`) for each team branch
|
||||
- Uses `ILIKE` across multiple columns with no way for Postgres to use indexes effectively across the OR
|
||||
- Includes `recipients: true` on the result even though only a small subset of fields are needed
|
||||
- Fetches all matching rows then filters visibility **in application code**
|
||||
|
||||
With 1,000 documents seeded under `medium-account@documenso.com`, this query is noticeably slow.
|
||||
|
||||
---
|
||||
|
||||
## Option A: Pre-resolve team IDs, keep Prisma
|
||||
|
||||
**Change:** Before the envelope query, resolve the user's accessible team IDs in a single query:
|
||||
|
||||
```ts
|
||||
const teamIds = await prisma.teamGroup
|
||||
.findMany({
|
||||
where: {
|
||||
organisationGroup: {
|
||||
organisationGroupMembers: {
|
||||
some: { organisationMember: { userId } },
|
||||
},
|
||||
},
|
||||
},
|
||||
select: { teamId: true },
|
||||
})
|
||||
.then((rows) => [...new Set(rows.map((r) => r.teamId))]);
|
||||
```
|
||||
|
||||
Then replace `team: buildTeamWhereQuery(...)` with `teamId: { in: teamIds }` in the envelope query.
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Eliminates the duplicated 4-level deep join chain from the envelope query
|
||||
- The team ID resolution is a simple indexed lookup (runs once, not twice)
|
||||
- Minimal code change -- still Prisma, same structure
|
||||
- Can also pre-resolve team roles to move visibility filtering into the WHERE clause
|
||||
|
||||
**Drawbacks:**
|
||||
|
||||
- Still a single large OR query with ILIKE branches
|
||||
- Prisma still generates suboptimal SQL for the remaining OR conditions
|
||||
|
||||
---
|
||||
|
||||
## Option B: Kysely rewrite with pre-resolved teams
|
||||
|
||||
**Change:** Rewrite using Kysely (already set up in codebase as `kyselyPrisma.$kysely`). Follow the pattern in `find-envelopes.ts` -- use Kysely for filtering/ID fetching, then Prisma for hydration.
|
||||
|
||||
Structure as a UNION of targeted queries instead of a single OR:
|
||||
|
||||
```
|
||||
Query 1: owned docs matching title/externalId (simple indexed lookup)
|
||||
Query 2: docs where user is recipient matching title (EXISTS on Recipient)
|
||||
Query 3: team docs matching title/externalId (using pre-resolved teamIds)
|
||||
UNION ALL -> deduplicate -> ORDER BY createdAt DESC -> LIMIT 20
|
||||
```
|
||||
|
||||
Then hydrate the 20 IDs with Prisma for the include data.
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Each sub-query is simple and independently optimizable by Postgres
|
||||
- UNION eliminates the massive OR which forces bad query plans
|
||||
- Kysely gives control over exact SQL structure
|
||||
- Only hydrate the final 20 results (not all matches)
|
||||
- Follows existing `find-envelopes.ts` pattern -- not a new paradigm
|
||||
|
||||
**Drawbacks:**
|
||||
|
||||
- More code than Option A
|
||||
- Two query layers (Kysely for IDs, Prisma for hydration)
|
||||
|
||||
---
|
||||
|
||||
## Option C: Hybrid -- pre-resolve teams + simplify Prisma OR
|
||||
|
||||
**Change:** Pre-resolve team IDs (like Option A), but also restructure the Prisma query to reduce OR branches:
|
||||
|
||||
- Merge "owned + title" and "owned + externalId" and "owned + recipient email" into a single owned-docs branch with nested OR
|
||||
- Merge "team + title" and "team + externalId" into a single team-docs branch
|
||||
- Keep "recipient inbox" branches separate
|
||||
|
||||
This reduces from 7 OR branches to ~3-4, with simpler conditions in each.
|
||||
|
||||
**Benefits:**
|
||||
|
||||
- Simpler than Kysely rewrite
|
||||
- Fewer OR branches = better query plan
|
||||
- Pre-resolved team IDs eliminate the deep joins
|
||||
- Still pure Prisma
|
||||
|
||||
**Drawbacks:**
|
||||
|
||||
- Postgres still has to handle OR across different access patterns in one query
|
||||
- Less control over SQL than Kysely
|
||||
|
||||
---
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Option B (Kysely)** is the strongest choice. The codebase already uses this exact pattern for `find-envelopes.ts` which solves the same class of problem. The UNION approach gives Postgres the best chance at using indexes per sub-query. Pre-resolving team IDs is a prerequisite for all options and is trivially cheap.
|
||||
|
||||
The template search query has the same structure and should get the same treatment.
|
||||
@@ -0,0 +1,337 @@
|
||||
---
|
||||
name: create-documentation
|
||||
description: Generate markdown documentation for a module or feature
|
||||
---
|
||||
|
||||
You are creating proper markdown documentation for a feature or guide in the Documenso documentation site.
|
||||
|
||||
**Read [WRITING_STYLE.md](../../../WRITING_STYLE.md) first** for tone, formatting conventions, and anti-patterns to avoid.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Identify the scope** - Based on the conversation context, determine what feature or topic needs documentation. Ask the user if unclear.
|
||||
2. **Identify the audience** - Is this for Users, Developers, or Self-Hosters?
|
||||
3. **Read the source code** - Understand the feature, API, or configuration being documented.
|
||||
4. **Read existing docs** - Check `apps/docs/content/docs/` for documentation to update.
|
||||
5. **Write comprehensive documentation** - Create or update MDX docs following the patterns below.
|
||||
6. **Update navigation** - Add to the relevant `meta.json` if creating a new page.
|
||||
|
||||
## Documentation Framework
|
||||
|
||||
This project uses [Fumadocs](https://fumadocs.dev). All documentation lives in `apps/docs/content/docs/` as MDX files. The docs app is a Next.js app at `apps/docs/`.
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
```
|
||||
apps/docs/content/docs/
|
||||
├── index.mdx # Landing page with audience navigation
|
||||
├── meta.json # Root navigation: guides + resources
|
||||
├── users/ # Application usage guides
|
||||
│ ├── meta.json # { "root": true, "pages": [...] }
|
||||
│ ├── getting-started/ # Account creation, first document
|
||||
│ ├── documents/ # Upload, recipients, fields, send
|
||||
│ │ └── advanced/ # AI detection, visibility, placeholders
|
||||
│ ├── templates/ # Create and use templates
|
||||
│ ├── organisations/ # Overview, members, groups, SSO, billing
|
||||
│ │ ├── single-sign-on/
|
||||
│ │ └── preferences/
|
||||
│ └── settings/ # Profile, security, API tokens
|
||||
├── developers/ # API and integration docs
|
||||
│ ├── meta.json # { "root": true, "pages": [...] }
|
||||
│ ├── getting-started/ # Authentication, first API call
|
||||
│ ├── api/ # Documents, recipients, fields, templates, teams
|
||||
│ ├── webhooks/ # Setup, events, verification
|
||||
│ ├── embedding/ # Authoring, direct links, CSS vars, SDKs
|
||||
│ │ └── sdks/ # React, Vue, Svelte, Solid, Preact, Angular
|
||||
│ ├── examples/ # Common workflows
|
||||
│ ├── local-development/ # Quickstart, manual, translations
|
||||
│ └── contributing/ # Contributing translations
|
||||
├── self-hosting/ # Self-hosting documentation
|
||||
│ ├── meta.json # { "root": true, "pages": [...] }
|
||||
│ ├── getting-started/ # Quick start, requirements, tips
|
||||
│ ├── deployment/ # Docker, docker-compose, Kubernetes, Railway
|
||||
│ ├── configuration/ # Environment, database, email, storage
|
||||
│ │ ├── signing-certificate/ # Local, Google Cloud HSM, timestamp
|
||||
│ │ └── advanced/ # OAuth providers, AI features
|
||||
│ └── maintenance/ # Upgrades, backups, troubleshooting
|
||||
├── concepts/ # Shared across audiences
|
||||
│ └── ... # Document lifecycle, field types, signing
|
||||
├── compliance/ # eSign, GDPR, standards, certifications
|
||||
└── policies/ # Terms, privacy, security, licenses
|
||||
```
|
||||
|
||||
### Where to Put Documentation
|
||||
|
||||
| Type | Location | When to use |
|
||||
| ------------------- | ------------------------------------------------ | -------------------------------------------------- |
|
||||
| **User Guide** | `apps/docs/content/docs/users/<section>/` | UI workflows for using the Documenso web app |
|
||||
| **Developer Guide** | `apps/docs/content/docs/developers/<section>/` | API reference, SDK guides, webhooks, embedding |
|
||||
| **Self-Hosting** | `apps/docs/content/docs/self-hosting/<section>/` | Deployment, configuration, environment variables |
|
||||
| **Concept** | `apps/docs/content/docs/concepts/` | Cross-audience concepts (document lifecycle, etc.) |
|
||||
| **Compliance** | `apps/docs/content/docs/compliance/` | Legal and regulatory documentation |
|
||||
|
||||
### Navigation (meta.json)
|
||||
|
||||
Each directory has a `meta.json` controlling navigation order:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Section Title",
|
||||
"pages": ["getting-started", "documents", "templates"]
|
||||
}
|
||||
```
|
||||
|
||||
Top-level audience sections use `"root": true`:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Users",
|
||||
"description": "Send and sign documents",
|
||||
"root": true,
|
||||
"pages": ["getting-started", "documents", "templates", "organisations", "settings"]
|
||||
}
|
||||
```
|
||||
|
||||
Root `meta.json` uses `---Label---` for section dividers:
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Documentation",
|
||||
"pages": [
|
||||
"---Guides---",
|
||||
"users",
|
||||
"developers",
|
||||
"self-hosting",
|
||||
"---Resources---",
|
||||
"concepts",
|
||||
"compliance",
|
||||
"policies"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## MDX File Format
|
||||
|
||||
### Frontmatter
|
||||
|
||||
Every page needs frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
title: Upload Documents
|
||||
description: Upload documents to Documenso to prepare them for signing. Covers supported formats, file size limits, and upload methods.
|
||||
---
|
||||
```
|
||||
|
||||
### Fumadocs Components
|
||||
|
||||
Import components at the top of the file (after frontmatter):
|
||||
|
||||
```mdx
|
||||
import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
;
|
||||
```
|
||||
|
||||
Callouts (use sparingly for warnings, beta features, security):
|
||||
|
||||
```mdx
|
||||
<Callout type="info">Informational note about behavior.</Callout>
|
||||
|
||||
<Callout type="warn">Warning about potential issues or breaking changes.</Callout>
|
||||
|
||||
<Callout type="error">Critical warning about data loss or security.</Callout>
|
||||
```
|
||||
|
||||
Steps (for sequential UI instructions):
|
||||
|
||||
```mdx
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Step title
|
||||
|
||||
Step description.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Next step
|
||||
|
||||
Next description.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
```
|
||||
|
||||
Tabs (for multiple approaches or platforms):
|
||||
|
||||
````mdx
|
||||
<Tabs items={['cURL', 'JavaScript', 'Python']}>
|
||||
<Tab value="cURL">```bash curl -X POST ... ```</Tab>
|
||||
<Tab value="JavaScript">```typescript const response = await fetch(...) ```</Tab>
|
||||
</Tabs>
|
||||
````
|
||||
|
||||
## Page Structure by Audience
|
||||
|
||||
### User Documentation
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Feature Name
|
||||
description: Brief description for SEO and previews.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
## Limitations
|
||||
|
||||
| Limitation | Value |
|
||||
| ----------------- | -------- |
|
||||
| Supported format | PDF only |
|
||||
| Maximum file size | 50MB |
|
||||
|
||||
## How to Do the Thing
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Navigate to the page
|
||||
|
||||
Open **Settings > Feature**.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
### Configure the setting
|
||||
|
||||
Fill in the required fields and click **Save**.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Related Guide](/docs/users/related)
|
||||
```
|
||||
|
||||
### Developer Documentation
|
||||
|
||||
````mdx
|
||||
---
|
||||
title: Documents API
|
||||
description: Create, manage, and send documents for signing via the API.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
<Callout type="warn">
|
||||
This guide may not reflect the latest endpoints. For an always up-to-date reference, see the
|
||||
[OpenAPI Reference](https://openapi.documenso.com).
|
||||
</Callout>
|
||||
|
||||
## Overview
|
||||
|
||||
Brief description of the resource and what you can do with it.
|
||||
|
||||
## Resource Object
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------- | ------ | ----------------- |
|
||||
| `id` | string | Unique identifier |
|
||||
| `status` | string | Current status |
|
||||
|
||||
## Create a Resource
|
||||
|
||||
```typescript
|
||||
const response = await fetch('https://app.documenso.com/api/v2/document', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: 'Bearer YOUR_API_TOKEN',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: 'Service Agreement',
|
||||
}),
|
||||
});
|
||||
```
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Related Guide](/docs/developers/related)
|
||||
|
||||
````
|
||||
|
||||
### Self-Hosting Documentation
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Environment Variables
|
||||
description: Complete reference for all environment variables used to configure Documenso.
|
||||
---
|
||||
|
||||
## Required Variables
|
||||
|
||||
| Variable | Description |
|
||||
| ------------------ | ------------------------------------------------ |
|
||||
| `NEXTAUTH_SECRET` | Secret key for session encryption (min 32 chars) |
|
||||
| `DATABASE_URL` | PostgreSQL connection URL |
|
||||
|
||||
---
|
||||
|
||||
## Optional Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
| -------------- | ------- | ---------------------- |
|
||||
| `PORT` | `3000` | Port the server runs on |
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Database Configuration](/docs/self-hosting/configuration/database)
|
||||
````
|
||||
|
||||
## Documentation Audiences
|
||||
|
||||
Tailor content to the audience:
|
||||
|
||||
- **User docs**: Focus on UI workflows, bold UI elements (**Settings**, **Save**), use `>` for navigation paths (**Settings > Team > Members**), number sequential steps, no code required
|
||||
- **Developer docs**: API/SDK examples, authentication, webhooks, code samples in TypeScript, link to OpenAPI reference
|
||||
- **Self-hosting docs**: Deployment guides, environment variables, Docker/non-Docker approaches, system requirements, troubleshooting
|
||||
|
||||
## Guidelines
|
||||
|
||||
See [WRITING_STYLE.md](../../../WRITING_STYLE.md) for complete guidelines. Key points:
|
||||
|
||||
- **Tone**: Direct, second-person, no emojis, no excessive personality
|
||||
- **Examples**: Progressive complexity, all must be valid TypeScript
|
||||
- **Tables**: Use Sharp-style nested parameter tables for API docs
|
||||
- **Callouts**: Use sparingly for warnings, beta features, security
|
||||
- **Cross-references**: Link related docs, add "See Also" sections
|
||||
- **Navigation**: Update `meta.json` when adding new pages
|
||||
- **Limitations**: Explicitly list what is NOT supported
|
||||
- **Images**: Use `.webp` format, store in `apps/docs/public/`
|
||||
|
||||
## Process
|
||||
|
||||
1. **Identify the audience** - Users, Developers, or Self-Hosters?
|
||||
2. **Explore the code** - Read source files to understand the feature
|
||||
3. **Check existing docs** - Look in `apps/docs/content/docs/` for related pages
|
||||
4. **Draft the structure** - Outline sections before writing
|
||||
5. **Write content** - Fill in each section following audience-specific patterns
|
||||
6. **Update navigation** - Add to relevant `meta.json` if creating a new page
|
||||
7. **Add cross-references** - Link from related docs, add "See Also" section
|
||||
|
||||
## Begin
|
||||
|
||||
Analyze the conversation context to determine the documentation scope, read the relevant source code, and create comprehensive MDX documentation in `apps/docs/content/docs/`.
|
||||
@@ -0,0 +1,371 @@
|
||||
---
|
||||
name: envelope-editor-v2-e2e
|
||||
description: Writing and maintaining Playwright E2E tests for the Envelope Editor V2. Use when the user needs to create, modify, debug, or extend E2E tests in packages/app-tests/e2e/envelope-editor-v2/. Triggers include requests to "write an e2e test", "add a test for the envelope editor", "test envelope settings/recipients/fields/items/attachments", "fix a failing envelope test", or any task involving Playwright tests for the envelope editor feature.
|
||||
---
|
||||
|
||||
# Envelope Editor V2 E2E Tests
|
||||
|
||||
## Overview
|
||||
|
||||
The Envelope Editor V2 E2E test suite lives in `packages/app-tests/e2e/envelope-editor-v2/`. Each test file covers a distinct feature area of the envelope editor and follows a strict architectural pattern that tests the **same flow** across four surfaces:
|
||||
|
||||
1. **Document** (`documents/<id>`) - Native document editor
|
||||
2. **Template** (`templates/<id>`) - Native template editor
|
||||
3. **Embedded Create** (`/embed/v2/authoring/envelope/create`) - Embedded editor creating a new envelope
|
||||
4. **Embedded Edit** (`/embed/v2/authoring/envelope/edit/<id>`) - Embedded editor updating an existing envelope
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
packages/app-tests/
|
||||
e2e/
|
||||
envelope-editor-v2/
|
||||
envelope-attachments.spec.ts # Attachment CRUD
|
||||
envelope-fields.spec.ts # Field placement on PDF canvas
|
||||
envelope-items.spec.ts # PDF document item CRUD
|
||||
envelope-recipients.spec.ts # Recipient management
|
||||
envelope-settings.spec.ts # Settings dialog
|
||||
fixtures/
|
||||
authentication.ts # apiSignin, apiSignout
|
||||
documents.ts # Document tab helpers
|
||||
envelope-editor.ts # Core fixture: surface openers + locator/action helpers
|
||||
generic.ts # Toast assertions, text visibility
|
||||
signature.ts # Signature pad helpers
|
||||
playwright.config.ts # Test configuration
|
||||
```
|
||||
|
||||
## Core Abstraction: `TEnvelopeEditorSurface`
|
||||
|
||||
Every test revolves around the `TEnvelopeEditorSurface` type from `fixtures/envelope-editor.ts`. This is the central abstraction that normalizes differences between the four surfaces:
|
||||
|
||||
```typescript
|
||||
type TEnvelopeEditorSurface = {
|
||||
root: Page; // The Playwright page
|
||||
isEmbedded: boolean; // true for embed surfaces
|
||||
envelopeId?: string; // Set for document/template/embed-edit, undefined for embed-create
|
||||
envelopeType: 'DOCUMENT' | 'TEMPLATE';
|
||||
userId: number; // Seeded user ID
|
||||
userEmail: string; // Seeded user email
|
||||
userName: string; // Seeded user name
|
||||
teamId: number; // Seeded team ID
|
||||
};
|
||||
```
|
||||
|
||||
### Surface Openers (from `fixtures/envelope-editor.ts`)
|
||||
|
||||
```typescript
|
||||
// Native surfaces - seed user + document/template, sign in, navigate
|
||||
const surface = await openDocumentEnvelopeEditor(page);
|
||||
const surface = await openTemplateEnvelopeEditor(page);
|
||||
|
||||
// Embedded surfaces - seed user, create API token, get presign token, navigate
|
||||
const surface = await openEmbeddedEnvelopeEditor(page, {
|
||||
envelopeType: 'DOCUMENT' | 'TEMPLATE',
|
||||
mode?: 'create' | 'edit', // default: 'create'
|
||||
tokenNamePrefix?: string, // for unique API token names
|
||||
externalId?: string, // optional external ID in hash
|
||||
features?: EmbeddedEditorConfig, // feature flags
|
||||
});
|
||||
```
|
||||
|
||||
## Test Architecture Pattern
|
||||
|
||||
Every test file follows this structure, with four `test.describe` blocks grouping tests by editor surface:
|
||||
|
||||
### 1. Imports
|
||||
|
||||
```typescript
|
||||
import { type Page, expect, test } from '@playwright/test';
|
||||
// Prisma enums if needed for DB assertions
|
||||
import { SomePrismaEnum } from '@prisma/client';
|
||||
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import {
|
||||
type TEnvelopeEditorSurface, // Import needed helpers from the fixture
|
||||
openDocumentEnvelopeEditor,
|
||||
openEmbeddedEnvelopeEditor,
|
||||
openTemplateEnvelopeEditor,
|
||||
persistEmbeddedEnvelope, // ... other helpers
|
||||
} from '../fixtures/envelope-editor';
|
||||
import { expectToastTextToBeVisible } from '../fixtures/generic';
|
||||
```
|
||||
|
||||
### 2. Type definitions and constants
|
||||
|
||||
```typescript
|
||||
type FlowResult = {
|
||||
externalId: string;
|
||||
// ... other data needed for DB assertions
|
||||
};
|
||||
|
||||
const TEST_VALUES = {
|
||||
// Centralized test data constants
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Local helper functions
|
||||
|
||||
```typescript
|
||||
// Common: open settings and set external ID for DB lookup
|
||||
const openSettingsDialog = async (root: Page) => {
|
||||
await getEnvelopeEditorSettingsTrigger(root).click();
|
||||
await expect(root.getByRole('heading', { name: 'Document Settings' })).toBeVisible();
|
||||
};
|
||||
|
||||
const updateExternalId = async (surface: TEnvelopeEditorSurface, externalId: string) => {
|
||||
await openSettingsDialog(surface.root);
|
||||
await surface.root.locator('input[name="externalId"]').fill(externalId);
|
||||
await surface.root.getByRole('button', { name: 'Update' }).click();
|
||||
|
||||
if (!surface.isEmbedded) {
|
||||
await expectToastTextToBeVisible(surface.root, 'Envelope updated');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 4. The flow function
|
||||
|
||||
A single `runXxxFlow` function that works across ALL surfaces. It handles embedded vs non-embedded differences internally:
|
||||
|
||||
```typescript
|
||||
const runMyFeatureFlow = async (surface: TEnvelopeEditorSurface): Promise<FlowResult> => {
|
||||
const externalId = `e2e-feature-${nanoid()}`;
|
||||
|
||||
// For embedded create, may need to add a PDF first
|
||||
if (surface.isEmbedded && !surface.envelopeId) {
|
||||
await addEnvelopeItemPdf(surface.root, 'embedded-feature.pdf');
|
||||
}
|
||||
|
||||
await updateExternalId(surface, externalId);
|
||||
|
||||
// Handle embedded vs native differences
|
||||
if (surface.isEmbedded) {
|
||||
// No "Add Myself" button in embedded mode
|
||||
await setRecipientEmail(surface.root, 0, 'embedded@example.com');
|
||||
} else {
|
||||
await clickAddMyselfButton(surface.root);
|
||||
}
|
||||
|
||||
// ... perform feature-specific actions ...
|
||||
|
||||
// Navigate away and back to verify UI persistence
|
||||
await clickEnvelopeEditorStep(surface.root, 'addFields');
|
||||
await clickEnvelopeEditorStep(surface.root, 'upload');
|
||||
|
||||
// ... assert UI state after navigation ...
|
||||
|
||||
return { externalId /* ... */ };
|
||||
};
|
||||
```
|
||||
|
||||
### 5. Database assertion function
|
||||
|
||||
Uses Prisma directly to verify data was persisted correctly:
|
||||
|
||||
```typescript
|
||||
const assertFeaturePersistedInDatabase = async ({
|
||||
surface,
|
||||
externalId,
|
||||
// ... expected values
|
||||
}: {
|
||||
surface: TEnvelopeEditorSurface;
|
||||
externalId: string;
|
||||
// ...
|
||||
}) => {
|
||||
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||
where: {
|
||||
externalId,
|
||||
userId: surface.userId,
|
||||
teamId: surface.teamId,
|
||||
type: surface.envelopeType,
|
||||
},
|
||||
include: {
|
||||
// Include related data as needed
|
||||
documentMeta: true,
|
||||
recipients: true,
|
||||
fields: true,
|
||||
envelopeAttachments: true,
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
|
||||
// Assert expected values
|
||||
expect(envelope.someField).toBe(expectedValue);
|
||||
};
|
||||
```
|
||||
|
||||
### 6. The four `test.describe` blocks
|
||||
|
||||
Tests are organized into four `test.describe` blocks, one per editor surface. Each describe block contains the tests relevant to that surface. This structure allows adding multiple tests per surface while keeping them grouped:
|
||||
|
||||
```typescript
|
||||
test.describe('document editor', () => {
|
||||
test('description of what is tested', async ({ page }) => {
|
||||
const surface = await openDocumentEnvelopeEditor(page);
|
||||
const result = await runMyFeatureFlow(surface);
|
||||
|
||||
await assertFeaturePersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
// Additional document-editor-specific tests here...
|
||||
});
|
||||
|
||||
test.describe('template editor', () => {
|
||||
test('description of what is tested', async ({ page }) => {
|
||||
const surface = await openTemplateEnvelopeEditor(page);
|
||||
const result = await runMyFeatureFlow(surface);
|
||||
|
||||
await assertFeaturePersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
// Additional template-editor-specific tests here...
|
||||
});
|
||||
|
||||
test.describe('embedded create', () => {
|
||||
test('description of what is tested', async ({ page }) => {
|
||||
const surface = await openEmbeddedEnvelopeEditor(page, {
|
||||
envelopeType: 'DOCUMENT',
|
||||
tokenNamePrefix: 'e2e-embed-feature',
|
||||
});
|
||||
|
||||
const result = await runMyFeatureFlow(surface);
|
||||
|
||||
// IMPORTANT: Must persist before DB assertions for embedded
|
||||
await persistEmbeddedEnvelope(surface);
|
||||
|
||||
await assertFeaturePersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
// Additional embedded-create-specific tests here...
|
||||
});
|
||||
|
||||
test.describe('embedded edit', () => {
|
||||
test('description of what is tested', async ({ page }) => {
|
||||
const surface = await openEmbeddedEnvelopeEditor(page, {
|
||||
envelopeType: 'TEMPLATE',
|
||||
mode: 'edit',
|
||||
tokenNamePrefix: 'e2e-embed-feature',
|
||||
});
|
||||
|
||||
const result = await runMyFeatureFlow(surface);
|
||||
|
||||
// IMPORTANT: Must persist before DB assertions for embedded
|
||||
await persistEmbeddedEnvelope(surface);
|
||||
|
||||
await assertFeaturePersistedInDatabase({
|
||||
surface,
|
||||
...result,
|
||||
});
|
||||
});
|
||||
|
||||
// Additional embedded-edit-specific tests here...
|
||||
});
|
||||
```
|
||||
|
||||
When a test only applies to specific surfaces (e.g., a document-only action like "send document"), only include it in the relevant describe block(s). Not every describe block needs the same tests -- the structure groups tests by surface, not by requiring symmetry.
|
||||
|
||||
## Key Differences Between Surfaces
|
||||
|
||||
| Behavior | Document/Template | Embedded Create | Embedded Edit |
|
||||
| -------------------------- | -------------------------- | ----------------------------------------- | ----------------------------------------- |
|
||||
| User seeding | Seed + sign in | Seed + API token | Seed + API token + seed envelope |
|
||||
| "Add Myself" button | Available | Not available | Not available |
|
||||
| Toast on settings update | Yes (`'Envelope updated'`) | No | No |
|
||||
| PDF already attached | Yes (1 item) | No (0 items, must upload) | Yes (1 item) |
|
||||
| Delete confirmation dialog | Yes (`'Delete'` button) | No (immediate) | No (immediate) |
|
||||
| DB persistence timing | Immediate (autosaved) | After `persistEmbeddedEnvelope()` | After `persistEmbeddedEnvelope()` |
|
||||
| Persist button label | N/A | `'Create Document'` / `'Create Template'` | `'Update Document'` / `'Update Template'` |
|
||||
|
||||
## Available Fixture Helpers
|
||||
|
||||
### From `fixtures/envelope-editor.ts`
|
||||
|
||||
**Locator helpers** (return Playwright Locators):
|
||||
|
||||
- `getEnvelopeEditorSettingsTrigger(root)` - Settings gear button
|
||||
- `getEnvelopeItemTitleInputs(root)` - Title inputs for envelope items
|
||||
- `getEnvelopeItemDragHandles(root)` - Drag handles for reordering items
|
||||
- `getEnvelopeItemRemoveButtons(root)` - Remove buttons for items
|
||||
- `getEnvelopeItemDropzoneInput(root)` - File input for PDF upload
|
||||
- `getRecipientEmailInputs(root)` - Email inputs for recipients
|
||||
- `getRecipientNameInputs(root)` - Name inputs for recipients
|
||||
- `getRecipientRows(root)` - Full recipient row fieldsets
|
||||
- `getRecipientRemoveButtons(root)` - Remove buttons for recipients
|
||||
- `getSigningOrderInputs(root)` - Signing order number inputs
|
||||
|
||||
**Action helpers**:
|
||||
|
||||
- `addEnvelopeItemPdf(root, fileName?)` - Upload a PDF to the dropzone
|
||||
- `clickEnvelopeEditorStep(root, stepId)` - Navigate to a step: `'upload'`, `'addFields'`, `'preview'`
|
||||
- `clickAddMyselfButton(root)` - Click "Add Myself" (native only)
|
||||
- `clickAddSignerButton(root)` - Click "Add Signer"
|
||||
- `setRecipientEmail(root, index, email)` - Fill recipient email
|
||||
- `setRecipientName(root, index, name)` - Fill recipient name
|
||||
- `setRecipientRole(root, index, roleLabel)` - Set role via combobox
|
||||
- `assertRecipientRole(root, index, roleLabel)` - Assert role value
|
||||
- `toggleSigningOrder(root, enabled)` - Toggle signing order switch
|
||||
- `toggleAllowDictateSigners(root, enabled)` - Toggle dictate signers switch
|
||||
- `setSigningOrderValue(root, index, value)` - Set signing order number
|
||||
- `persistEmbeddedEnvelope(surface)` - Click Create/Update button for embedded flows
|
||||
|
||||
### From `fixtures/generic.ts`
|
||||
|
||||
- `expectTextToBeVisible(page, text)` - Assert text visible on page
|
||||
- `expectTextToNotBeVisible(page, text)` - Assert text not visible
|
||||
- `expectToastTextToBeVisible(page, text)` - Assert toast message visible
|
||||
|
||||
## External ID Pattern
|
||||
|
||||
Every test uses an `externalId` (e.g., `e2e-feature-${nanoid()}`) set via the settings dialog. This unique ID is then used in Prisma queries to reliably locate the envelope in the database for assertions. This is critical because multiple tests run in parallel.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all envelope editor tests
|
||||
npm run test:dev -w @documenso/app-tests -- --grep "Envelope Editor V2"
|
||||
|
||||
# Run a specific test file
|
||||
npm run test:dev -w @documenso/app-tests -- e2e/envelope-editor-v2/envelope-recipients.spec.ts
|
||||
|
||||
# Run with UI
|
||||
npm run test-ui:dev -w @documenso/app-tests -- e2e/envelope-editor-v2/
|
||||
|
||||
# Run specific test by name
|
||||
npm run test:dev -w @documenso/app-tests -- --grep "documents/<id>: add myself"
|
||||
```
|
||||
|
||||
## Checklist When Writing a New Test
|
||||
|
||||
1. Create the spec file in `packages/app-tests/e2e/envelope-editor-v2/`
|
||||
2. Import `TEnvelopeEditorSurface` and the three opener functions
|
||||
3. Import `persistEmbeddedEnvelope` if you need DB assertions for embedded flows
|
||||
4. Define a `FlowResult` type for data passed between flow and assertion
|
||||
5. Define `TEST_VALUES` constants for test data
|
||||
6. Write `updateExternalId` helper (or reuse the pattern)
|
||||
7. Write the `runXxxFlow` function handling embedded vs native differences
|
||||
8. Write the `assertXxxPersistedInDatabase` function using Prisma
|
||||
9. Create four `test.describe` blocks: `'document editor'`, `'template editor'`, `'embedded create'`, `'embedded edit'`
|
||||
10. Place tests inside the appropriate describe block for each surface
|
||||
11. For embedded create tests, add a PDF via `addEnvelopeItemPdf` before the flow
|
||||
12. For embedded tests, call `persistEmbeddedEnvelope(surface)` before DB assertions
|
||||
13. Use `surface.isEmbedded` to branch on behavioral differences (toasts, "Add Myself", etc.)
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Missing `persistEmbeddedEnvelope`**: Embedded flows don't autosave. You MUST call this before any DB assertions.
|
||||
- **PDF required for embedded create**: Embedded create starts with 0 items. Upload a PDF before navigating to fields.
|
||||
- **Toast assertions in embedded**: Don't assert toasts for settings updates in embedded mode (they don't appear).
|
||||
- **Parallel test isolation**: Always use a unique `externalId` via `nanoid()` so parallel tests don't collide.
|
||||
- **Navigation verification**: Navigate away from and back to the current step to verify UI state persistence (the editor may re-render).
|
||||
- **Delete confirmation**: Native surfaces show a confirmation dialog for item deletion; embedded surfaces delete immediately.
|
||||
+28
-1
@@ -35,6 +35,8 @@ 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.
|
||||
@@ -143,16 +145,33 @@ 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: Leave blank to allow users to signup through /signup page.
|
||||
# OPTIONAL: Set to "true" to disable all signup methods (email, Google, Microsoft, OIDC, including the organisation OIDC portal).
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP=
|
||||
# OPTIONAL: Set to "true" to disable email/password signup only.
|
||||
NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=
|
||||
# OPTIONAL: Set to "true" to block new-account creation through Google. Existing linked users can still sign in.
|
||||
NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP=
|
||||
# OPTIONAL: Set to "true" to block new-account creation through Microsoft. Existing linked users can still sign in.
|
||||
NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=
|
||||
# OPTIONAL: Set to "true" to block new-account creation through OIDC (including the organisation portal).
|
||||
NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=
|
||||
# OPTIONAL: Comma-separated list of email domains allowed to sign up (e.g., example.com,acme.org).
|
||||
NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=
|
||||
# OPTIONAL: Set to true to use internal webapp url in browserless requests.
|
||||
NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false
|
||||
|
||||
@@ -171,6 +190,14 @@ 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"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Config files
|
||||
*.config.js
|
||||
*.config.cjs
|
||||
|
||||
# Statically hosted javascript files
|
||||
apps/*/public/*.js
|
||||
apps/*/public/*.cjs
|
||||
scripts/
|
||||
@@ -1,16 +0,0 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['@documenso/eslint-config'],
|
||||
rules: {
|
||||
'@next/next/no-img-element': 'off',
|
||||
'no-unreachable': 'error',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
},
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ['apps/*/'],
|
||||
},
|
||||
},
|
||||
ignorePatterns: ['lingui.config.ts', 'packages/lib/translations/**/*.js'],
|
||||
};
|
||||
@@ -12,6 +12,10 @@ runs:
|
||||
with:
|
||||
node-version: ${{ inputs.node_version }}
|
||||
|
||||
- name: Enable corepack
|
||||
shell: bash
|
||||
run: corepack enable npm
|
||||
|
||||
- name: Cache npm
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
|
||||
+4
-1
@@ -1,5 +1,8 @@
|
||||
'apps: web':
|
||||
- apps/web/**
|
||||
- apps/remix/**
|
||||
|
||||
'type: documentation':
|
||||
- apps/docs/**
|
||||
|
||||
'version bump 👀':
|
||||
- '**/package.json'
|
||||
|
||||
@@ -63,6 +63,7 @@ CLAUDE.md
|
||||
|
||||
# scripts
|
||||
scripts/output*
|
||||
scripts/bench-*
|
||||
|
||||
# license
|
||||
.documenso-license.json
|
||||
@@ -70,3 +71,6 @@ scripts/output*
|
||||
|
||||
# tmp
|
||||
tmp/
|
||||
|
||||
# opencode
|
||||
.opencode/package-lock.json
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
legacy-peer-deps = true
|
||||
prefer-dedupe = true
|
||||
prefer-dedupe = true
|
||||
min-release-age = 7
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
description: Generate markdown documentation for a module or feature
|
||||
argument-hint: <topic-or-feature>
|
||||
---
|
||||
|
||||
You are generating documentation for the Documenso project.
|
||||
|
||||
## Your Task
|
||||
|
||||
Load and follow the skill at `.agents/skills/create-documentation/SKILL.md`. It contains the complete instructions for writing documentation including:
|
||||
|
||||
- Documentation structure and file locations
|
||||
- MDX format and Fumadocs components
|
||||
- Audience-specific patterns (Users, Developers, Self-Hosters)
|
||||
- Navigation (`meta.json`) updates
|
||||
- Writing style guidelines
|
||||
|
||||
## Context
|
||||
|
||||
The topic or feature to document is: `$ARGUMENTS`
|
||||
|
||||
## Begin
|
||||
|
||||
1. **Read the skill** at `.agents/skills/create-documentation/SKILL.md`
|
||||
2. **Read WRITING_STYLE.md** for tone and formatting conventions
|
||||
3. **Follow the skill instructions** to create comprehensive documentation
|
||||
4. **Use TodoWrite** to track your progress throughout
|
||||
@@ -7,69 +7,17 @@ You are creating a new justification file in the `.agents/justifications/` direc
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
|
||||
2. **Gather content** - Collect or generate the justification content
|
||||
3. **Create the file** - Use the create-justification script to generate the file
|
||||
Load and follow the skill at `.agents/skills/create-justification/SKILL.md`. It contains the complete instructions for creating justification files including:
|
||||
|
||||
## Usage
|
||||
- Unique three-word ID generation
|
||||
- Frontmatter format with date and title
|
||||
- Script usage (`scripts/create-justification.ts`)
|
||||
|
||||
The script will automatically:
|
||||
- Generate a unique three-word ID (e.g., `swift-emerald-river`)
|
||||
- Create frontmatter with current date and formatted title
|
||||
- Save the file as `{id}-{slug}.md` in `.agents/justifications/`
|
||||
## Context
|
||||
|
||||
## Creating the File
|
||||
|
||||
### Option 1: Direct Content
|
||||
|
||||
If you have the content ready, run:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-justification.ts "$ARGUMENTS" "Your justification content here"
|
||||
```
|
||||
|
||||
### Option 2: Multi-line Content (Heredoc)
|
||||
|
||||
For multi-line content, use heredoc:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-justification.ts "$ARGUMENTS" << HEREDOC
|
||||
Your multi-line
|
||||
justification content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
### Option 3: Pipe Content
|
||||
|
||||
You can also pipe content:
|
||||
|
||||
```bash
|
||||
echo "Your content" | npx tsx scripts/create-justification.ts "$ARGUMENTS"
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
The created file will have:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Justification Title
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
The title is automatically formatted from the slug (e.g., `architecture-decision` → `Architecture Decision`).
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Use descriptive slugs in kebab-case (e.g., `tech-stack-choice`, `api-design-rationale`)
|
||||
- Include clear reasoning and context for the decision
|
||||
- The unique ID ensures no filename conflicts
|
||||
- Files are automatically dated for organization
|
||||
The justification slug and optional content: `$ARGUMENTS`
|
||||
|
||||
## Begin
|
||||
|
||||
Create a justification file using the slug from `$ARGUMENTS` and appropriate content documenting the reasoning or justification.
|
||||
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
|
||||
|
||||
@@ -7,70 +7,17 @@ You are creating a new plan file in the `.agents/plans/` directory.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
|
||||
2. **Gather content** - Collect or generate the plan content
|
||||
3. **Create the file** - Use the create-plan script to generate the file
|
||||
Load and follow the skill at `.agents/skills/create-plan/SKILL.md`. It contains the complete instructions for creating plan files including:
|
||||
|
||||
## Usage
|
||||
- Unique three-word ID generation
|
||||
- Frontmatter format with date and title
|
||||
- Script usage (`scripts/create-plan.ts`)
|
||||
|
||||
The script will automatically:
|
||||
## Context
|
||||
|
||||
- Generate a unique three-word ID (e.g., `happy-blue-moon`)
|
||||
- Create frontmatter with current date and formatted title
|
||||
- Save the file as `{id}-{slug}.md` in `.agents/plans/`
|
||||
|
||||
## Creating the File
|
||||
|
||||
### Option 1: Direct Content
|
||||
|
||||
If you have the content ready, run:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-plan.ts "$ARGUMENTS" "Your plan content here"
|
||||
```
|
||||
|
||||
### Option 2: Multi-line Content (Heredoc)
|
||||
|
||||
For multi-line content, use heredoc:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-plan.ts "$ARGUMENTS" << HEREDOC
|
||||
Your multi-line
|
||||
plan content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
### Option 3: Pipe Content
|
||||
|
||||
You can also pipe content:
|
||||
|
||||
```bash
|
||||
echo "Your content" | npx tsx scripts/create-plan.ts "$ARGUMENTS"
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
The created file will have:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Plan Title
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
The title is automatically formatted from the slug (e.g., `my-feature` → `My Feature`).
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Use descriptive slugs in kebab-case (e.g., `user-authentication`, `api-integration`)
|
||||
- Include clear, actionable plan content
|
||||
- The unique ID ensures no filename conflicts
|
||||
- Files are automatically dated for organization
|
||||
The plan slug and optional content: `$ARGUMENTS`
|
||||
|
||||
## Begin
|
||||
|
||||
Create a plan file using the slug from `$ARGUMENTS` and appropriate content for the planning task.
|
||||
1. **Read the skill** at `.agents/skills/create-plan/SKILL.md`
|
||||
2. **Create the plan file** using the slug from `$ARGUMENTS` and appropriate content
|
||||
|
||||
@@ -7,69 +7,17 @@ You are creating a new scratch file in the `.agents/scratches/` directory.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Determine the slug** - Use `$ARGUMENTS` as the file slug (kebab-case recommended)
|
||||
2. **Gather content** - Collect or generate the scratch content
|
||||
3. **Create the file** - Use the create-scratch script to generate the file
|
||||
Load and follow the skill at `.agents/skills/create-scratch/SKILL.md`. It contains the complete instructions for creating scratch files including:
|
||||
|
||||
## Usage
|
||||
- Unique three-word ID generation
|
||||
- Frontmatter format with date and title
|
||||
- Script usage (`scripts/create-scratch.ts`)
|
||||
|
||||
The script will automatically:
|
||||
- Generate a unique three-word ID (e.g., `calm-teal-cloud`)
|
||||
- Create frontmatter with current date and formatted title
|
||||
- Save the file as `{id}-{slug}.md` in `.agents/scratches/`
|
||||
## Context
|
||||
|
||||
## Creating the File
|
||||
|
||||
### Option 1: Direct Content
|
||||
|
||||
If you have the content ready, run:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-scratch.ts "$ARGUMENTS" "Your scratch content here"
|
||||
```
|
||||
|
||||
### Option 2: Multi-line Content (Heredoc)
|
||||
|
||||
For multi-line content, use heredoc:
|
||||
|
||||
```bash
|
||||
npx tsx scripts/create-scratch.ts "$ARGUMENTS" << HEREDOC
|
||||
Your multi-line
|
||||
scratch content
|
||||
goes here
|
||||
HEREDOC
|
||||
```
|
||||
|
||||
### Option 3: Pipe Content
|
||||
|
||||
You can also pipe content:
|
||||
|
||||
```bash
|
||||
echo "Your content" | npx tsx scripts/create-scratch.ts "$ARGUMENTS"
|
||||
```
|
||||
|
||||
## File Format
|
||||
|
||||
The created file will have:
|
||||
|
||||
```markdown
|
||||
---
|
||||
date: 2026-01-13
|
||||
title: Scratch Title
|
||||
---
|
||||
|
||||
Your content here
|
||||
```
|
||||
|
||||
The title is automatically formatted from the slug (e.g., `quick-notes` → `Quick Notes`).
|
||||
|
||||
## Guidelines
|
||||
|
||||
- Use descriptive slugs in kebab-case (e.g., `exploration-ideas`, `temporary-notes`)
|
||||
- Scratch files are for temporary notes, explorations, or ideas
|
||||
- The unique ID ensures no filename conflicts
|
||||
- Files are automatically dated for organization
|
||||
The scratch slug and optional content: `$ARGUMENTS`
|
||||
|
||||
## Begin
|
||||
|
||||
Create a scratch file using the slug from `$ARGUMENTS` and appropriate content for notes or exploration.
|
||||
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
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
---
|
||||
description: Generate MDX documentation for a module or feature
|
||||
argument-hint: <module-path-or-feature>
|
||||
---
|
||||
|
||||
You are creating proper MDX documentation for a module or feature in Documenso using Nextra.
|
||||
|
||||
## Your Task
|
||||
|
||||
1. **Identify the scope** - What does `$ARGUMENTS` refer to? (file, directory, or feature name)
|
||||
2. **Read the source code** - Understand the public API, types, and behavior
|
||||
3. **Read existing docs** - Check if there's documentation to update or reference
|
||||
4. **Write comprehensive documentation** - Create or update MDX docs in the appropriate location
|
||||
5. **Update navigation** - Add entry to `_meta.js` if creating a new page
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
Create documentation in the appropriate location:
|
||||
|
||||
- **Developer docs**: `apps/documentation/pages/developers/`
|
||||
- **User docs**: `apps/documentation/pages/users/`
|
||||
|
||||
### File Format
|
||||
|
||||
All documentation files must be `.mdx` files with frontmatter:
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Page Title
|
||||
description: Brief description for SEO and meta tags
|
||||
---
|
||||
|
||||
# Page Title
|
||||
|
||||
Content starts here...
|
||||
```
|
||||
|
||||
### Navigation
|
||||
|
||||
Each directory should have a `_meta.js` file that defines the navigation structure:
|
||||
|
||||
```javascript
|
||||
export default {
|
||||
index: 'Introduction',
|
||||
'feature-name': 'Feature Name',
|
||||
'another-feature': 'Another Feature',
|
||||
};
|
||||
```
|
||||
|
||||
If creating a new page, add it to the appropriate `_meta.js` file.
|
||||
|
||||
### Documentation Format
|
||||
|
||||
````mdx
|
||||
---
|
||||
title: <Module|Feature Name>
|
||||
description: Brief description of what this does and when to use it
|
||||
---
|
||||
|
||||
# <Module|Feature Name>
|
||||
|
||||
Brief description of what this module/feature does and when to use it.
|
||||
|
||||
## Installation
|
||||
|
||||
If there are specific packages or imports needed:
|
||||
|
||||
```bash
|
||||
npm install @documenso/package-name
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```jsx
|
||||
// Minimal working example
|
||||
import { Component } from '@documenso/package';
|
||||
|
||||
const Example = () => {
|
||||
return <Component />;
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Component/Function Name
|
||||
|
||||
Description of what it does.
|
||||
|
||||
#### Props/Parameters
|
||||
|
||||
| Prop/Param | Type | Description |
|
||||
| ---------- | -------------------- | ------------------------- |
|
||||
| prop | `string` | Description of the prop |
|
||||
| optional | `boolean` (optional) | Optional prop description |
|
||||
|
||||
#### Example
|
||||
|
||||
```jsx
|
||||
import { Component } from '@documenso/package';
|
||||
|
||||
<Component prop="value" optional={true} />;
|
||||
```
|
||||
|
||||
### Types
|
||||
|
||||
#### `TypeName`
|
||||
|
||||
```typescript
|
||||
type TypeName = {
|
||||
property: string;
|
||||
optional?: boolean;
|
||||
};
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Common Use Case
|
||||
|
||||
```jsx
|
||||
// Full working example
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
```jsx
|
||||
// More complex example
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [Link to related documentation](/developers/path)
|
||||
- [Another related page](/users/path)
|
||||
````
|
||||
|
||||
## Guidelines
|
||||
|
||||
### Content Quality
|
||||
|
||||
- **Be accurate** - Verify behavior by reading the code
|
||||
- **Be complete** - Document all public API surface
|
||||
- **Be practical** - Include real, working examples
|
||||
- **Be concise** - Don't over-explain obvious things
|
||||
- **Be user-focused** - Write for the target audience (developers or users)
|
||||
|
||||
### Code Examples
|
||||
|
||||
- Use appropriate language tags: `jsx`, `tsx`, `typescript`, `bash`, `json`
|
||||
- Show imports when not obvious
|
||||
- Include expected output in comments where helpful
|
||||
- Progress from simple to complex
|
||||
- Use real examples from the codebase when possible
|
||||
|
||||
### Formatting
|
||||
|
||||
- Always include frontmatter with `title` and `description`
|
||||
- Use proper markdown headers (h1 for title, h2 for sections)
|
||||
- Use tables for props/parameters documentation (matching existing style)
|
||||
- Use code fences with appropriate language tags
|
||||
- Use Nextra components when appropriate:
|
||||
- `<Callout type="info">` for notes
|
||||
- `<Steps>` for step-by-step instructions
|
||||
- Use relative links for internal documentation (e.g., `/developers/embedding/react`)
|
||||
|
||||
### Nextra Components
|
||||
|
||||
You can import and use Nextra components:
|
||||
|
||||
```jsx
|
||||
import { Callout, Steps } from 'nextra/components';
|
||||
|
||||
<Callout type="info">
|
||||
This is an informational note.
|
||||
</Callout>
|
||||
|
||||
<Steps>
|
||||
<Steps.Step>First step</Steps.Step>
|
||||
<Steps.Step>Second step</Steps.Step>
|
||||
</Steps>
|
||||
```
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Include types inline so docs don't get stale
|
||||
- Reference source file locations for complex behavior
|
||||
- Keep examples up-to-date with the codebase
|
||||
- Update `_meta.js` when adding new pages
|
||||
|
||||
## Process
|
||||
|
||||
1. **Explore the code** - Read source files to understand the API
|
||||
2. **Identify the audience** - Is this for developers or users?
|
||||
3. **Check existing docs** - Look for similar pages to match style
|
||||
4. **Draft the structure** - Outline sections before writing
|
||||
5. **Write content** - Fill in each section with frontmatter
|
||||
6. **Add examples** - Create working code samples
|
||||
7. **Update navigation** - Add to `_meta.js` if needed
|
||||
8. **Review** - Read through for clarity and accuracy
|
||||
|
||||
## Begin
|
||||
|
||||
Analyze `$ARGUMENTS`, read the relevant source code, check existing documentation patterns, and create comprehensive MDX documentation following the Documenso documentation style.
|
||||
@@ -1 +0,0 @@
|
||||
../../.agents/skills/agent-browser
|
||||
@@ -1,20 +0,0 @@
|
||||
node_modules
|
||||
.next
|
||||
public
|
||||
**/**/node_modules
|
||||
**/**/.next
|
||||
**/**/public
|
||||
packages/lib/translations/**/*.js
|
||||
|
||||
*.lock
|
||||
*.log
|
||||
*.test.ts
|
||||
|
||||
.gitignore
|
||||
.npmignore
|
||||
.prettierignore
|
||||
.DS_Store
|
||||
.eslintignore
|
||||
|
||||
# Docs MDX - Prettier strips indentation from code blocks inside components
|
||||
apps/docs/content/**/*.mdx
|
||||
Vendored
+18
-6
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": "explicit"
|
||||
"source.fixAll.biome": "explicit",
|
||||
"source.organizeImports.biome": "explicit"
|
||||
},
|
||||
"eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"],
|
||||
"javascript.preferences.importModuleSpecifier": "non-relative",
|
||||
"javascript.preferences.useAliasesForRenames": false,
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
@@ -15,8 +15,20 @@
|
||||
"[prisma]": {
|
||||
"editor.defaultFormatter": "Prisma.prisma"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "vscode.html-language-features"
|
||||
},
|
||||
"prisma.pinToPrisma6": true
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"prisma.pinToPrisma6": true,
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
- `npm run test:e2e` - Run E2E tests with Playwright
|
||||
- `npm run test:dev -w @documenso/app-tests` - Run single E2E test in dev mode
|
||||
- `npm run test-ui:dev -w @documenso/app-tests` - Run E2E tests with UI
|
||||
- `npm run format` - Format code with Prettier
|
||||
- `npm run format` - Format code with Biome
|
||||
- `npm run dev` - Start development server for Remix app
|
||||
|
||||
**Important:** Do not run `npm run build` to verify changes unless explicitly asked. Builds take a long time (~2 minutes). Use `npx tsc --noEmit` for type checking specific packages if needed.
|
||||
|
||||
+1
-2
@@ -65,8 +65,6 @@ Documenso is an open-source document signing platform built as a **monorepo** us
|
||||
| Package | Description |
|
||||
| ---------------------------- | ------------------------- |
|
||||
| `@documenso/app-tests` | E2E tests (Playwright) |
|
||||
| `@documenso/eslint-config` | Shared ESLint config |
|
||||
| `@documenso/prettier-config` | Shared Prettier config |
|
||||
| `@documenso/tailwind-config` | Shared Tailwind config |
|
||||
| `@documenso/tsconfig` | Shared TypeScript configs |
|
||||
|
||||
@@ -235,6 +233,7 @@ 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`
|
||||
|
||||
@@ -182,6 +182,9 @@ git clone https://github.com/<your-username>/documenso
|
||||
- Optional: Seed the database using `npm run prisma:seed -w @documenso/prisma` to create a test user and document.
|
||||
- Optional: Create your own signing certificate.
|
||||
- To generate your own using these steps and a Linux Terminal or Windows Subsystem for Linux (WSL), see **[Create your own signing certificate](./SIGNING.md)**.
|
||||
- Optional: Configure job provider for document reminders.
|
||||
- The default local job provider does not support scheduled jobs required for document reminders.
|
||||
- To enable reminders, set `NEXT_PRIVATE_JOBS_PROVIDER=inngest` and provide `NEXT_PRIVATE_INNGEST_EVENT_KEY` in your `.env` file.
|
||||
|
||||
### Run in Gitpod
|
||||
|
||||
|
||||
+1
-1
@@ -10,4 +10,4 @@
|
||||
"baseDir": "src",
|
||||
"uiLibrary": "radix-ui",
|
||||
"commands": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ ISO 27001 is an international standard for managing information security, specif
|
||||
|
||||
## HIPAA
|
||||
|
||||
<Callout type="warn">Status: [Planned](https://github.com/documenso/backlog/issues/25)</Callout>
|
||||
<Callout type="info">Status: [Compliant](https://documen.so/trust)</Callout>
|
||||
|
||||
The HIPAA (Health Insurance Portability and Accountability Act) is a U.S. law designed to protect patient health information's privacy and security and improve the healthcare system's efficiency and effectiveness.
|
||||
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
{
|
||||
"title": "Concepts",
|
||||
"pages": [
|
||||
"document-lifecycle",
|
||||
"recipient-roles",
|
||||
"field-types",
|
||||
"signing-workflow",
|
||||
"signing-certificates"
|
||||
]
|
||||
"pages": ["document-lifecycle", "recipient-roles", "field-types", "signing-workflow", "signing-certificates"]
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
---
|
||||
title: Developer Mode
|
||||
description: Advanced development tools for debugging field coordinates and integrating with the Documenso API.
|
||||
description: Advanced development tools for debugging field IDs, recipient IDs, coordinates and integrating with the Documenso API.
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Developer mode provides additional tools and features to help you integrate and debug Documenso.
|
||||
|
||||
## Field Coordinates
|
||||
## Field Information
|
||||
|
||||
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.
|
||||
When enabled, developer mode displays the following information for each field:
|
||||
|
||||
To enable field coordinates, add the `devmode=true` query parameter to the editor URL.
|
||||
- **Field ID** - The unique identifier of the field
|
||||
- **Recipient ID** - The ID of the recipient assigned to the field
|
||||
- **Pos X / Pos Y** - The position of the field on the page
|
||||
- **Width / Height** - The dimensions of the field
|
||||
|
||||
To enable developer mode, add the `devmode=true` query parameter to the editor URL.
|
||||
|
||||
```bash
|
||||
# Legacy editor
|
||||
|
||||
@@ -269,6 +269,10 @@ Returns the full document object including recipients, fields, and envelope item
|
||||
|
||||
Create a new document with optional recipients and fields in a single request.
|
||||
|
||||
<Callout type="info">
|
||||
This endpoint automatically scans uploaded PDFs for [placeholder patterns](/docs/users/documents/advanced/pdf-placeholders) like `{"{{signature, r1}}"}` and creates fields at those locations.
|
||||
</Callout>
|
||||
|
||||
```
|
||||
POST /envelope/create
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
@@ -473,6 +473,10 @@ Instead of specifying exact coordinates, you can position fields using placehold
|
||||
|
||||
This approach is useful when generating PDFs programmatically or using templates with consistent layouts.
|
||||
|
||||
<Callout type="info">
|
||||
Placeholder support is only available in `envelope.*` endpoints. `POST /template/use` does not support placeholder parsing.
|
||||
</Callout>
|
||||
|
||||
See the [PDF Placeholders](/docs/users/documents/advanced/pdf-placeholders) guide for the full placeholder format reference, including supported field types, recipient identifiers, and field options.
|
||||
|
||||
---
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
{
|
||||
"title": "API Reference",
|
||||
"pages": [
|
||||
"documents",
|
||||
"recipients",
|
||||
"fields",
|
||||
"templates",
|
||||
"teams",
|
||||
"rate-limits",
|
||||
"versioning",
|
||||
"developer-mode"
|
||||
]
|
||||
"pages": ["documents", "recipients", "fields", "templates", "teams", "rate-limits", "versioning", "developer-mode"]
|
||||
}
|
||||
|
||||
@@ -240,6 +240,10 @@ Returns the full template object including recipients, fields, and metadata.
|
||||
|
||||
Create a new document using a template. This is the primary way to use templates programmatically.
|
||||
|
||||
<Callout type="info">
|
||||
This endpoint does not support [PDF placeholder parsing](/docs/users/documents/advanced/pdf-placeholders). Use `POST /envelope/create` for placeholder-based field positioning.
|
||||
</Callout>
|
||||
|
||||
```
|
||||
|
||||
POST /template/use
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Authoring
|
||||
description: Embed document, template, and envelope creation directly in your application.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
In addition to embedding signing, Documenso supports embedded authoring. It allows your users to create and edit documents, templates, and envelopes without leaving your application.
|
||||
|
||||
<Callout type="warn">
|
||||
Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
|
||||
also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing).
|
||||
Contact sales for access.
|
||||
</Callout>
|
||||
|
||||
## Versions
|
||||
|
||||
Embedded authoring is available in two versions:
|
||||
|
||||
- **[V1 Authoring](/docs/developers/embedding/authoring/v1)** — Works with V1 Documents and Templates.
|
||||
- **[V2 Authoring](/docs/developers/embedding/authoring/v2)** — Works with Envelopes, which are the unified model for documents and templates.
|
||||
|
||||
### Comparison
|
||||
|
||||
| Aspect | V1 | V2 |
|
||||
| --- | --- | --- |
|
||||
| Entity model | Documents and Templates (separate) | Envelopes (unified, can be documents or templates) |
|
||||
| API compatibility | V1 Documents/Templates API | V2 Envelopes API |
|
||||
| Customization | 6 simple boolean flags | Rich structured settings with sections (general, settings, actions, envelope items, recipients) |
|
||||
|
||||
---
|
||||
|
||||
## Presign Tokens
|
||||
|
||||
Before using any authoring component, obtain a presign token from your backend:
|
||||
|
||||
```
|
||||
POST /api/v2/embedding/create-presign-token
|
||||
```
|
||||
|
||||
This endpoint requires your Documenso API key. The token has a default expiration of 1 hour.
|
||||
|
||||
See the [API documentation](https://openapi.documenso.com/reference#tag/embedding) for full details.
|
||||
|
||||
<Callout type="warn">
|
||||
Presign tokens should be created server-side. Never expose your API key in client-side code.
|
||||
</Callout>
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [V1 Authoring](/docs/developers/embedding/authoring/v1) — Create and edit documents and templates using V1 components
|
||||
- [V2 Authoring](/docs/developers/embedding/authoring/v2) — Create and edit envelopes using V2 components
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) — Customize the appearance of embedded components
|
||||
- [SDKs](/docs/developers/embedding/sdks) — Framework-specific SDK documentation
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"title": "Authoring",
|
||||
"pages": ["v1", "v2"]
|
||||
}
|
||||
+9
-15
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: Authoring
|
||||
description: Embed document and template creation directly in your application.
|
||||
title: V1 Authoring
|
||||
description: Embed V1 document and template creation directly in your application.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
In addition to embedding signing, Documenso supports embedded authoring. It allows your users to create and edit documents and templates without leaving your application.
|
||||
V1 authoring components allow your users to create and edit documents and templates using the V1 Documents and Templates API without leaving your application.
|
||||
|
||||
<Callout type="warn">
|
||||
Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
|
||||
@@ -15,7 +15,7 @@ In addition to embedding signing, Documenso supports embedded authoring. It allo
|
||||
|
||||
## Components
|
||||
|
||||
The SDK provides four authoring components:
|
||||
The SDK provides four V1 authoring components:
|
||||
|
||||
| Component | Purpose |
|
||||
| ----------------------- | ----------------------- |
|
||||
@@ -24,24 +24,16 @@ The SDK provides four authoring 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
|
||||
|
||||
Before using any authoring component, obtain a presign token from your backend:
|
||||
All authoring components require a **presign token** for authentication. See the [Authoring overview](/docs/developers/embedding/authoring) for details on obtaining presign tokens.
|
||||
|
||||
```
|
||||
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.
|
||||
A presigned token is NOT an API token
|
||||
</Callout>
|
||||
|
||||
---
|
||||
@@ -147,8 +139,9 @@ 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 overrides (Platform Plan) |
|
||||
| `cssVars` | `object` | No | [CSS variable](/docs/developers/embedding/css-variables) overrides (Platform Plan) |
|
||||
| `darkModeDisabled` | `boolean` | No | Disable dark mode (Platform Plan) |
|
||||
| `language` | `string` | No | Set the UI language. See [Supported Languages](https://github.com/documenso/documenso/tree/main/packages/lib/constants/locales.ts) |
|
||||
| `className` | `string` | No | CSS class for the iframe |
|
||||
| `features` | `object` | No | Feature toggles for the authoring experience |
|
||||
|
||||
@@ -301,6 +294,7 @@ Pass extra props to the iframe for testing experimental features:
|
||||
## See Also
|
||||
|
||||
- [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props
|
||||
- [V2 Authoring](/docs/developers/embedding/authoring/v2) - V2 envelope authoring
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
- [Documents API](/docs/developers/api/documents) - Create documents via API
|
||||
- [Templates API](/docs/developers/api/templates) - Create templates via API
|
||||
@@ -0,0 +1,344 @@
|
||||
---
|
||||
title: V2 Authoring
|
||||
description: Embed envelope creation and editing directly in your application.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
V2 authoring components allow your users to create and edit envelopes without leaving your application. Envelopes are the unified model for documents and templates in the V2 API.
|
||||
|
||||
<Callout type="warn">
|
||||
Embedded authoring is included with [Enterprise](https://documen.so/enterprise-cta) plans. It is
|
||||
also available as a paid add-on for the [Platform Plan](https://documen.so/platform-cta-pricing).
|
||||
Contact sales for access.
|
||||
</Callout>
|
||||
|
||||
## Components
|
||||
|
||||
The SDK provides two V2 authoring components:
|
||||
|
||||
| Component | Purpose |
|
||||
| ---------------------- | ------------------------ |
|
||||
| `EmbedCreateEnvelope` | Create new envelopes |
|
||||
| `EmbedUpdateEnvelope` | Edit existing envelopes |
|
||||
|
||||
---
|
||||
|
||||
## Presign Tokens
|
||||
|
||||
All authoring components require a **presign token** for authentication. See the [Authoring overview](/docs/developers/embedding/authoring) for details on obtaining presign tokens.
|
||||
|
||||
<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 Authoring 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 authoring 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 authoring provides rich, structured feature toggles organized into sections. Pass a partial configuration to customize the authoring experience — any omitted fields will use their defaults.
|
||||
|
||||
```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 authoring 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 authoring:
|
||||
|
||||
| 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 authoring 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
|
||||
|
||||
- [Authoring Overview](/docs/developers/embedding/authoring) - V1 vs V2 comparison and presign tokens
|
||||
- [V1 Authoring](/docs/developers/embedding/authoring/v1) - V1 document and template authoring
|
||||
- [Embedding Overview](/docs/developers/embedding) - Signing embed concepts and props
|
||||
- [CSS Variables](/docs/developers/embedding/css-variables) - Customize appearance
|
||||
@@ -59,6 +59,7 @@ 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
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ 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. |
|
||||
@@ -175,6 +176,7 @@ 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. |
|
||||
|
||||
@@ -13,7 +13,7 @@ All webhook events share a common structure:
|
||||
{
|
||||
"event": "DOCUMENT_COMPLETED",
|
||||
"payload": {
|
||||
// Document data with recipients
|
||||
// Document or template data with recipients
|
||||
},
|
||||
"createdAt": "2024-04-22T11:52:18.277Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
@@ -33,14 +33,13 @@ All webhook events share a common structure:
|
||||
|
||||
| Field | Type | Description |
|
||||
| ---------------- | --------- | ------------------------------------------------------ |
|
||||
| `id` | number | Document ID |
|
||||
| `id` | number | Document or template 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 title |
|
||||
| `title` | string | Document or template 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 |
|
||||
@@ -50,45 +49,50 @@ 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) |
|
||||
| `Recipient` | array | List of recipient objects |
|
||||
| `recipients` | array | List of recipient objects |
|
||||
| `Recipient` | array | List of recipient objects (legacy, same as recipients) |
|
||||
|
||||
### 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` |
|
||||
| `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 |
|
||||
| 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 |
|
||||
|
||||
### 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 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 |
|
||||
| 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 |
|
||||
|
||||
---
|
||||
|
||||
@@ -98,7 +102,7 @@ These events track the document through its lifecycle.
|
||||
|
||||
### `document.created`
|
||||
|
||||
Triggered when a new document is uploaded.
|
||||
Triggered when a new document is created.
|
||||
|
||||
**Event name:** `DOCUMENT_CREATED`
|
||||
|
||||
@@ -114,7 +118,6 @@ Triggered when a new document is uploaded.
|
||||
"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,
|
||||
@@ -131,11 +134,35 @@ Triggered when a new document is uploaded.
|
||||
"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,
|
||||
@@ -145,7 +172,8 @@ Triggered when a new document is uploaded.
|
||||
"name": "John Doe",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
@@ -182,7 +210,6 @@ 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,
|
||||
@@ -199,11 +226,35 @@ 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,
|
||||
@@ -213,7 +264,8 @@ The document status changes to `PENDING` and recipients have `sendStatus: "SENT"
|
||||
"name": "John Doe",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"signedAt": null,
|
||||
"authOptions": null,
|
||||
"signingOrder": 1,
|
||||
@@ -230,6 +282,106 @@ 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.
|
||||
@@ -250,7 +402,6 @@ 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",
|
||||
@@ -267,12 +418,15 @@ 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
|
||||
},
|
||||
"Recipient": [
|
||||
"recipients": [
|
||||
{
|
||||
"id": 50,
|
||||
"documentId": 10,
|
||||
@@ -281,7 +435,8 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
"name": "Jane Smith",
|
||||
"token": "vbT8hi3jKQmrFP_LN1WcS",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"signedAt": "2024-04-22T11:51:10.055Z",
|
||||
"authOptions": {
|
||||
"accessAuth": null,
|
||||
@@ -302,7 +457,54 @@ The document status changes to `COMPLETED` and `completedAt` is set.
|
||||
"name": "John Doe",
|
||||
"token": "HkrptwS42ZBXdRKj1TyUo",
|
||||
"documentDeletedAt": null,
|
||||
"expired": 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,
|
||||
"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,
|
||||
@@ -335,53 +537,17 @@ The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` cont
|
||||
"event": "DOCUMENT_REJECTED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"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,
|
||||
"title": "contract.pdf",
|
||||
"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": [
|
||||
"recipients": [
|
||||
{
|
||||
"id": 52,
|
||||
"documentId": 10,
|
||||
"templateId": null,
|
||||
"email": "signer@example.com",
|
||||
"name": "John Doe",
|
||||
"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",
|
||||
"signedAt": "2024-04-22T11:48:07.569Z",
|
||||
"rejectionReason": "I do not agree with the terms",
|
||||
"readStatus": "OPENED",
|
||||
"signingStatus": "REJECTED",
|
||||
"sendStatus": "SENT"
|
||||
@@ -395,7 +561,9 @@ The recipient's `signingStatus` changes to `REJECTED` and `rejectionReason` cont
|
||||
|
||||
### `document.cancelled`
|
||||
|
||||
Triggered when the document owner cancels a pending document.
|
||||
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.
|
||||
|
||||
**Event name:** `DOCUMENT_CANCELLED`
|
||||
|
||||
@@ -411,7 +579,6 @@ Triggered when the document owner cancels a pending document.
|
||||
"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,
|
||||
@@ -428,11 +595,35 @@ Triggered when the document owner cancels a pending document.
|
||||
"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,
|
||||
@@ -442,7 +633,8 @@ Triggered when the document owner cancels a pending document.
|
||||
"name": "John Doe",
|
||||
"token": "XkKx1HCs6Znm2UBJA2j6o",
|
||||
"documentDeletedAt": null,
|
||||
"expired": null,
|
||||
"expiresAt": null,
|
||||
"expirationNotifiedAt": null,
|
||||
"signedAt": null,
|
||||
"authOptions": { "accessAuth": null, "actionAuth": null },
|
||||
"signingOrder": 1,
|
||||
@@ -459,147 +651,127 @@ Triggered when the document owner cancels a pending document.
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
### `document.reminder.sent`
|
||||
|
||||
## Recipient Events
|
||||
Triggered when a reminder email is sent to a recipient who has not yet completed their action.
|
||||
|
||||
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`.
|
||||
**Event name:** `DOCUMENT_REMINDER_SENT`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_OPENED",
|
||||
"event": "DOCUMENT_REMINDER_SENT",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"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,
|
||||
"title": "contract.pdf",
|
||||
"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": [
|
||||
"recipients": [
|
||||
{
|
||||
"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": "OPENED",
|
||||
"readStatus": "NOT_OPENED",
|
||||
"signingStatus": "NOT_SIGNED",
|
||||
"sendStatus": "SENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-04-22T11:50:26.174Z",
|
||||
"createdAt": "2024-04-23T09:00:00.000Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
|
||||
### `document.signed`
|
||||
---
|
||||
|
||||
Triggered when a recipient signs the document.
|
||||
## Template Events
|
||||
|
||||
**Event name:** `DOCUMENT_SIGNED`
|
||||
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.
|
||||
|
||||
The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated.
|
||||
### `template.created`
|
||||
|
||||
Triggered when a new template is created.
|
||||
|
||||
**Event name:** `TEMPLATE_CREATED`
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "DOCUMENT_SIGNED",
|
||||
"event": "TEMPLATE_CREATED",
|
||||
"payload": {
|
||||
"id": 10,
|
||||
"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"
|
||||
}
|
||||
]
|
||||
"title": "My Template",
|
||||
"status": "DRAFT",
|
||||
"templateId": 10,
|
||||
"source": "TEMPLATE",
|
||||
"recipients": []
|
||||
},
|
||||
"createdAt": "2024-04-22T11:52:18.577Z",
|
||||
"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",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
}
|
||||
```
|
||||
@@ -608,15 +780,28 @@ The recipient's `signingStatus` changes to `SIGNED` and `signedAt` is populated.
|
||||
|
||||
## Event Summary
|
||||
|
||||
| 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 |
|
||||
### 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"` |
|
||||
|
||||
---
|
||||
|
||||
@@ -652,19 +837,22 @@ 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':
|
||||
// Handle signature
|
||||
const signer = payload.Recipient.find((r) => r.signingStatus === 'SIGNED');
|
||||
console.log(`${signer?.name} signed document ${payload.id}`);
|
||||
console.log(`Signature added to document ${payload.id}`);
|
||||
break;
|
||||
case 'DOCUMENT_REJECTED':
|
||||
// Handle rejection
|
||||
const rejecter = payload.Recipient.find((r) => r.signingStatus === 'REJECTED');
|
||||
const rejecter = payload.recipients.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 when documents are signed, completed, or updated.
|
||||
description: Receive real-time notifications for document and template events.
|
||||
---
|
||||
|
||||
## How Webhooks Work
|
||||
@@ -9,6 +9,8 @@ description: Receive real-time notifications when documents are signed, complete
|
||||
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
|
||||
@@ -41,7 +43,15 @@ description: Receive real-time notifications when documents are signed, complete
|
||||
"payload": {
|
||||
"id": 123,
|
||||
"title": "Contract",
|
||||
"status": "COMPLETED"
|
||||
"status": "COMPLETED",
|
||||
"completedAt": "2024-01-15T10:30:00.000Z",
|
||||
"recipients": [
|
||||
{
|
||||
"id": 1,
|
||||
"email": "signer@example.com",
|
||||
"signingStatus": "SIGNED"
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAt": "2024-01-15T10:30:00.000Z",
|
||||
"webhookEndpoint": "https://your-endpoint.com/webhook"
|
||||
|
||||
@@ -220,11 +220,17 @@ 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 |
|
||||
| `DOCUMENT_OPENED` | A recipient opens the document for the first time |
|
||||
| `DOCUMENT_SIGNED` | A recipient signs the document |
|
||||
| `DOCUMENT_COMPLETED` | All recipients have signed the document |
|
||||
| `DOCUMENT_RECIPIENT_COMPLETED` | A recipient completes their required action |
|
||||
| `DOCUMENT_COMPLETED` | All recipients have completed their actions |
|
||||
| `DOCUMENT_REJECTED` | A recipient rejects the document |
|
||||
| `DOCUMENT_CANCELLED` | The document owner cancels 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 |
|
||||
|
||||
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,9 +250,15 @@ 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)) {
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
---
|
||||
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
|
||||
@@ -224,11 +224,44 @@ For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/con
|
||||
|
||||
## Feature Flags
|
||||
|
||||
| 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` |
|
||||
| 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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -248,20 +281,40 @@ AI features must also be enabled in organisation/team settings after configurati
|
||||
|
||||
## Background Jobs
|
||||
|
||||
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.
|
||||
Documenso supports multiple background job providers for processing emails, documents, webhooks, and scheduled tasks.
|
||||
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------ | ------- |
|
||||
| `NEXT_PRIVATE_JOBS_PROVIDER` | Jobs provider: `local` (PostgreSQL-based queue) or `inngest` (managed service) | `local` |
|
||||
### Provider Selection
|
||||
|
||||
### Inngest Configuration
|
||||
| Variable | Description | Default |
|
||||
| ---------------------------- | -------------------------------------------------------------------------------------- | ------- |
|
||||
| `NEXT_PRIVATE_JOBS_PROVIDER` | Jobs provider: `local` (PostgreSQL), `bullmq` (Redis), or `inngest` (managed service) | `local` |
|
||||
|
||||
| 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 |
|
||||
### 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).
|
||||
|
||||
---
|
||||
|
||||
@@ -271,6 +324,8 @@ Documenso uses a PostgreSQL-based job queue by default. Jobs (email delivery, do
|
||||
| ----------------------------- | -------------------------------------------- | ------- |
|
||||
| `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.
|
||||
|
||||
---
|
||||
@@ -326,6 +381,14 @@ 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,6 +5,7 @@
|
||||
"database",
|
||||
"email",
|
||||
"storage",
|
||||
"background-jobs",
|
||||
"signing-certificate",
|
||||
"telemetry",
|
||||
"advanced"
|
||||
|
||||
@@ -154,8 +154,15 @@ PORT=3000
|
||||
# Signing certificate (see Signing Certificate section)
|
||||
NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password
|
||||
|
||||
# Disable public signups
|
||||
# Signup restrictions (optional)
|
||||
# Master switch — disables every signup method
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP=false
|
||||
# Per-method switches (optional). Each disables brand-new account creation through that method.
|
||||
# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP=true
|
||||
# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP=true
|
||||
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=true
|
||||
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=true
|
||||
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org
|
||||
```
|
||||
|
||||
<Callout type="info">Generate secure secrets using: `openssl rand -base64 32`</Callout>
|
||||
@@ -251,7 +258,11 @@ Navigate to the signup page and create your account. Verify your email address
|
||||
<Callout type="info">
|
||||
All accounts created through signup are regular user accounts. Admin access must be granted
|
||||
directly in the database. Once your accounts are set up, consider disabling public signups by
|
||||
setting `NEXT_PUBLIC_DISABLE_SIGNUP=true`.
|
||||
setting `NEXT_PUBLIC_DISABLE_SIGNUP=true`. For finer control, use the per-method switches
|
||||
`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP`, `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP`,
|
||||
`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`, `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP`, or restrict
|
||||
signups to specific email domains with
|
||||
`NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS`.
|
||||
</Callout>
|
||||
|
||||
## Managing Services
|
||||
|
||||
@@ -100,7 +100,12 @@ See [Email Configuration](/docs/self-hosting/configuration/email) for other tran
|
||||
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for the signing certificate | - |
|
||||
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | Base64-encoded `.p12` certificate (alternative to file path) | - |
|
||||
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Document storage: `database` or `s3` | `database` |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public signups | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch — disable all signup methods | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google OAuth | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP` | Block new accounts via Microsoft OAuth | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal) | `false` |
|
||||
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
|
||||
|
||||
For the complete list, see [Environment Variables](/docs/self-hosting/configuration/environment).
|
||||
|
||||
|
||||
@@ -153,8 +153,13 @@ NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@yourdomain.com
|
||||
| Variable | Description | Default |
|
||||
| --------------------------------- | ---------------------------------- | ------- |
|
||||
| `PORT` | Application port | `3000` |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Disable public signups | `false` |
|
||||
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - |
|
||||
| `NEXT_PUBLIC_DISABLE_SIGNUP` | Master switch — disable all signup methods | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNUP` | Disable email/password signup only | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNUP` | Block new accounts via Google OAuth | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP`| Block new accounts via Microsoft OAuth | `false` |
|
||||
| `NEXT_PUBLIC_DISABLE_OIDC_SIGNUP` | Block new accounts via OIDC (incl. organisation portal)| `false` |
|
||||
| `NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS` | Comma-separated list of allowed signup email domains | |
|
||||
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - |
|
||||
| `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` |
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -85,9 +85,13 @@ See [Storage Configuration](/docs/self-hosting/configuration/storage) for setup
|
||||
|
||||
### Background Jobs
|
||||
|
||||
Documenso processes background jobs (email delivery, document processing) using a PostgreSQL-based queue. No additional services like Redis are required: the job queue is built into the application and uses your existing database.
|
||||
Documenso processes background jobs (email delivery, document processing) using a PostgreSQL-based queue by default. No additional services are required: the job queue is built into the application and uses your existing database.
|
||||
|
||||
For high-throughput deployments, Documenso optionally supports [Inngest](https://www.inngest.com/) as an alternative job provider. Set `NEXT_PRIVATE_JOBS_PROVIDER=inngest` and configure `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY`. Most self-hosted instances do not need this.
|
||||
For production deployments that need higher throughput or more reliable job processing, Documenso supports [BullMQ](https://docs.bullmq.io/) as an alternative provider. BullMQ requires a **Redis** instance (v6.2+). Set `NEXT_PRIVATE_JOBS_PROVIDER=bullmq` and configure `NEXT_PRIVATE_REDIS_URL`.
|
||||
|
||||
For managed/cloud deployments, [Inngest](https://www.inngest.com/) is also supported as a job provider. Set `NEXT_PRIVATE_JOBS_PROVIDER=inngest` and configure `INNGEST_EVENT_KEY` and `INNGEST_SIGNING_KEY`.
|
||||
|
||||
See [Background Jobs Configuration](/docs/self-hosting/configuration/background-jobs) for full details.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -144,19 +144,13 @@ See [Storage Configuration](/docs/self-hosting/configuration/storage) for full s
|
||||
|
||||
---
|
||||
|
||||
## Background Jobs Don't Need Redis
|
||||
## Background Jobs
|
||||
|
||||
Documenso uses a PostgreSQL-based job queue by default. No Redis, no external message broker. The job system uses your existing database to store and process background tasks like email delivery and document processing.
|
||||
Documenso uses a PostgreSQL-based job queue by default (`local` provider). No Redis or external message broker is required for basic deployments.
|
||||
|
||||
For high-throughput deployments, Documenso optionally supports [Inngest](https://www.inngest.com/) as an alternative job provider:
|
||||
For production workloads, consider switching to **Inngest** (managed) or **BullMQ** (self-hosted with Redis) for better reliability and throughput.
|
||||
|
||||
```bash
|
||||
NEXT_PRIVATE_JOBS_PROVIDER=inngest
|
||||
INNGEST_EVENT_KEY=your-event-key
|
||||
INNGEST_SIGNING_KEY=your-signing-key
|
||||
```
|
||||
|
||||
Most self-hosted instances do not need Inngest.
|
||||
See [Background Jobs Configuration](/docs/self-hosting/configuration/background-jobs) for setup instructions and provider comparison.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -24,4 +24,14 @@ description: Advanced document features including PDF placeholders, AI detection
|
||||
description="Control who can see documents within a team."
|
||||
href="/docs/users/documents/advanced/document-visibility"
|
||||
/>
|
||||
<Card
|
||||
title="Recipient Expiration"
|
||||
description="Set a signing deadline so document links expire after a configurable period."
|
||||
href="/docs/users/documents/advanced/recipient-expiration"
|
||||
/>
|
||||
<Card
|
||||
title="Signing Reminders"
|
||||
description="Automatically email recipients who have not yet signed on a configurable schedule."
|
||||
href="/docs/users/documents/advanced/signing-reminders"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
"pdf-placeholders",
|
||||
"ai-detection",
|
||||
"default-recipients",
|
||||
"document-visibility"
|
||||
"document-visibility",
|
||||
"recipient-expiration",
|
||||
"signing-reminders"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
---
|
||||
title: Recipient Expiration
|
||||
description: Set a signing deadline for recipients so document links expire after a configurable period.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Overview
|
||||
|
||||
Recipient expiration lets you set a deadline for how long recipients have to sign a document after it is sent. Once the deadline passes, the recipient can no longer access the signing link and the document owner is notified.
|
||||
|
||||
This is useful when:
|
||||
|
||||
- A business deal is contingent on being signed within a specific time frame
|
||||
- A document is no longer relevant after a certain date
|
||||
- You want to ensure recipients act promptly rather than leaving documents unsigned indefinitely
|
||||
|
||||
Expiration is tracked **per recipient**, not per document. If one recipient's deadline passes, other recipients can still sign. The document stays in a pending state so the owner can decide whether to resend or cancel.
|
||||
|
||||
## Default Behaviour
|
||||
|
||||
Every organisation has a default expiration period of **3 months**. This means that when you send a document, each recipient has 3 months from the time the document is sent to complete their signing.
|
||||
|
||||
You can change this default at the organisation or team level, or override it per document.
|
||||
|
||||
## Settings Cascade
|
||||
|
||||
Expiration settings follow a three-level cascade: **Organisation → Team → Document**. Each level can override the one above it.
|
||||
|
||||
<Tabs items={['Organisation', 'Team', 'Document']}>
|
||||
<Tab value="Organisation">
|
||||
|
||||
Sets the default for all teams in the organisation. Options are a **custom duration** or **never expires**.
|
||||
|
||||
To configure, navigate to **Organisation Settings > Preferences > Document** and find **Default Envelope Expiration**.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Team">
|
||||
|
||||
Overrides the organisation default for documents created within this team. Options are a **custom duration**, **never expires**, or **inherit from organisation**.
|
||||
|
||||
New teams default to **inherit from organisation**.
|
||||
|
||||
To configure, navigate to **Team Settings > Preferences > Document** and find **Default Envelope Expiration**.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Document">
|
||||
|
||||
Overrides the team or organisation default for a single document. Options are a **custom duration** or **never expires**.
|
||||
|
||||
If you do not change the expiration when editing a document, the team or organisation default applies.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Set Expiration for a Document
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
### Open the document settings
|
||||
|
||||
In the document editor, open the **Settings** dialog and go to the **General** tab.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Configure the expiration
|
||||
|
||||
Find the **Expiration** field. Choose one of:
|
||||
|
||||
- **Custom duration** — enter a number and select a unit (days, weeks, months, or years)
|
||||
- **Never expires** — the recipient can sign at any time
|
||||
|
||||
If you leave it unchanged, the team or organisation default applies.
|
||||
|
||||

|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Send the document
|
||||
|
||||
When you send the document, the expiration deadline is calculated from that moment. For example, if you set a 7-day expiration and send the document on March 1st, the recipient has until March 8th to sign.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
You cannot change the expiration period after the document has been sent. To extend a recipient's
|
||||
deadline, resend the document to them — this resets the clock.
|
||||
</Callout>
|
||||
|
||||
## Set a Default Expiration Period
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
### Navigate to document preferences
|
||||
|
||||
Go to **Organisation Settings > Preferences > Document** (or **Team Settings > Preferences > Document** for team-level overrides).
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Configure the default
|
||||
|
||||
Find **Default Envelope Expiration** and choose:
|
||||
|
||||
- **Custom duration** — enter a number and unit
|
||||
- **Never expires** — no deadline for recipients
|
||||
- **Inherit from organisation** (team level only) — use whatever the organisation has configured
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Save
|
||||
|
||||
Click **Save** to apply. New documents created after this change use the updated default.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
Changing the default expiration does not affect documents that have already been sent. Only new
|
||||
documents use the updated setting.
|
||||
</Callout>
|
||||
|
||||
## What Happens When a Recipient Expires
|
||||
|
||||
When a recipient's signing deadline passes:
|
||||
|
||||
1. The recipient can no longer access the signing link. They see a message explaining that the signing deadline has expired and to contact the document owner.
|
||||
2. The document owner receives an email notification with a link to view the document.
|
||||
3. An audit log entry is created recording the expiration.
|
||||
4. The document remains in a **pending** state — other recipients who have not expired can still sign.
|
||||
|
||||

|
||||
|
||||
## Resending to Extend a Deadline
|
||||
|
||||
If a recipient's deadline has passed (or is about to), you can resend the document to them. Resending recalculates the expiration from the current time, effectively extending the deadline.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
### Open the document
|
||||
|
||||
Navigate to the document page and find the recipient whose deadline has expired. Expired recipients are marked with an **Expired** badge.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Resend
|
||||
|
||||
Click the resend option for the recipient. This sends a new signing link and resets the expiration clock based on the document's configured expiration period.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Expiration Options Reference
|
||||
|
||||
| Unit | Example | Description |
|
||||
| ------ | --------------- | ------------------------------------------- |
|
||||
| Days | 7 days | Recipient has 7 days from when the document is sent |
|
||||
| Weeks | 2 weeks | Recipient has 2 weeks from when the document is sent |
|
||||
| Months | 3 months | Recipient has 3 months from when the document is sent (default) |
|
||||
| Years | 1 year | Recipient has 1 year from when the document is sent |
|
||||
|
||||
You can also set expiration to **never expires**, which means the signing link remains valid indefinitely.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Send Documents](/docs/users/documents/send) - Send documents for signing
|
||||
- [Document Preferences](/docs/users/organisations/preferences/document) - Configure default document settings
|
||||
- [Add Recipients](/docs/users/documents/add-recipients) - Add signers and other recipients to a document
|
||||
@@ -0,0 +1,195 @@
|
||||
---
|
||||
title: Signing Reminders
|
||||
description: Automatically email recipients who have not yet signed on a configurable schedule.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
|
||||
## Overview
|
||||
|
||||
Signing reminders automatically email recipients who have not completed their signing. You configure when the first reminder goes out and how often it repeats — Documenso handles the rest until the recipient signs or the document leaves a pending state.
|
||||
|
||||
This is useful when:
|
||||
|
||||
- You want recipients nudged without having to track them down manually
|
||||
- A deadline is approaching and you want to escalate gradually
|
||||
- You send a high volume of documents and cannot chase each one
|
||||
|
||||
Reminders are tracked **per recipient**, not per document. Each unsigned recipient is on their own schedule based on when they were emailed.
|
||||
|
||||
This is different from the **Resend** action covered in [Send Documents](/docs/users/documents/send), which is a one-off manual nudge. Reminders are scheduled and recurring.
|
||||
|
||||
## Default Behaviour
|
||||
|
||||
Every **newly created** organisation starts with reminders **enabled**:
|
||||
|
||||
- First reminder sent **5 days** after the recipient is emailed
|
||||
- Repeats **every 2 days** until the recipient signs
|
||||
|
||||
You can change this default at the organisation or team level, or override it per document.
|
||||
|
||||
<Callout type="info">
|
||||
Organisations created **before this feature shipped** have reminders left blank (no default) so
|
||||
recipients of in-flight or future documents are not unexpectedly sent reminders. To enable
|
||||
reminders for an existing organisation, configure **Default Signing Reminders** under
|
||||
**Organisation Settings > Preferences > Document**.
|
||||
</Callout>
|
||||
|
||||
## Settings Cascade
|
||||
|
||||
Reminder settings follow a three-level cascade: **Organisation → Team → Document**. Each level can override the one above it, or inherit from it.
|
||||
|
||||
<Tabs items={['Organisation', 'Team', 'Document']}>
|
||||
<Tab value="Organisation">
|
||||
|
||||
Sets the default for all teams in the organisation. Options are **Enabled** (set when the first reminder fires and how often it repeats) or **No reminders**.
|
||||
|
||||
To configure, navigate to **Organisation Settings > Preferences > Document** and find **Default Signing Reminders**.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Team">
|
||||
|
||||
Overrides the organisation default for documents created within this team. Options are **Enabled**, **No reminders**, or **Inherit from organisation**.
|
||||
|
||||
New teams default to **Inherit from organisation**.
|
||||
|
||||
To configure, navigate to **Team Settings > Preferences > Document** and find **Default Signing Reminders**.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Document">
|
||||
|
||||
Overrides the team or organisation default for a single document. Options are **Enabled**, **No reminders**, or **Inherit from organisation**.
|
||||
|
||||
If you do not change reminders when editing a document, the team or organisation default applies.
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Set Reminders for a Document
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
### Open the document settings
|
||||
|
||||
In the document editor, open the **Settings** dialog and go to the **Reminders** tab.
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Choose a mode
|
||||
|
||||
- **Enabled** — Documenso sends reminders on the schedule you configure below
|
||||
- **No reminders** — no automatic reminders for this document
|
||||
- **Inherit from organisation** — use the team or organisation default
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Configure the schedule
|
||||
|
||||
When **Enabled**, set:
|
||||
|
||||
- **Send first reminder after** — a number and unit (days, weeks, or months) measured from when the recipient is first emailed
|
||||
- **Then repeat every** — either **Custom interval** (a number and unit) or **Don't repeat** (only one reminder is ever sent)
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Send the document
|
||||
|
||||
The first reminder is scheduled when the recipient receives the initial signing email. Subsequent reminders are scheduled from the time the previous reminder was sent.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
Editing reminder settings on a document that is already pending recalculates the next reminder for
|
||||
every unsigned recipient immediately.
|
||||
</Callout>
|
||||
|
||||
## Set a Default Reminder Schedule
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
### Navigate to document preferences
|
||||
|
||||
Go to **Organisation Settings > Preferences > Document** (or **Team Settings > Preferences > Document** for team-level overrides).
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Configure the default
|
||||
|
||||
Find **Default Signing Reminders** and choose:
|
||||
|
||||
- **Enabled** — set the first-reminder delay and repeat interval
|
||||
- **No reminders** — disable reminders by default
|
||||
- **Inherit from organisation** (team level only) — use whatever the organisation has configured
|
||||
|
||||
</Step>
|
||||
<Step>
|
||||
|
||||
### Save
|
||||
|
||||
Click **Save** to apply. New documents created after this change use the updated default. Documents already in flight are unaffected unless you edit their reminder settings directly.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## What Happens When a Reminder Fires
|
||||
|
||||
When a recipient's next reminder time arrives:
|
||||
|
||||
1. Documenso sends the reminder email — same template as the original signing request, but the subject and preview are prefixed with **"Reminder:"**.
|
||||
2. An audit log entry is created for the recipient (an `EMAIL_SENT` entry of type `REMINDER`).
|
||||
3. The `document.reminder.sent` webhook fires. See [webhook events](/docs/developers/webhooks/events).
|
||||
4. The next reminder is scheduled based on **Then repeat every**, or no further reminder is scheduled if you chose **Don't repeat**.
|
||||
|
||||
Reminders stop automatically when the recipient signs, declines, the recipient's signing deadline passes, or the document leaves the pending state (completed, rejected, or deleted).
|
||||
|
||||
## When Reminders Are Skipped
|
||||
|
||||
A configured reminder will not be sent in the following cases:
|
||||
|
||||
- The recipient is a **CC** — CCs are notified once and never reminded
|
||||
- The recipient's **signing deadline has passed** — see [Recipient Expiration](/docs/users/documents/advanced/recipient-expiration). Resending the document refreshes the deadline and resumes reminders.
|
||||
- The document uses **manual link distribution** — Documenso never emails recipients for these documents
|
||||
- The envelope's email settings have **signing request emails disabled**
|
||||
- The recipient has not yet been emailed (for example, an unreached step in a sequential workflow)
|
||||
|
||||
<Callout type="info">
|
||||
Reminders stop automatically **30 days after the recipient was first emailed**, regardless of the
|
||||
repeat interval. This hard cap prevents runaway reminder chains for recipients who never sign and
|
||||
have no expiration set. If you need a different stop condition, set a shorter repeat interval,
|
||||
use **Don't repeat**, or configure [recipient expiration](/docs/users/documents/advanced/recipient-expiration).
|
||||
</Callout>
|
||||
|
||||
<Callout type="info">
|
||||
Reminders are dispatched by a background sweep that runs every 15 minutes, so the actual send time
|
||||
may be up to ~15 minutes after the scheduled time.
|
||||
</Callout>
|
||||
|
||||
## Reminder Options Reference
|
||||
|
||||
| Setting | Options | Notes |
|
||||
| --------------------------- | ---------------------------------------- | -------------------------------------------------------------------- |
|
||||
| **Mode** | Enabled / No reminders / Inherit | Inherit is only available at the team and document levels |
|
||||
| **Send first reminder after** | 1+ days, weeks, or months | Measured from when the recipient receives the initial signing email. Reminders past 30 days from that moment are skipped. |
|
||||
| **Then repeat every** | Custom interval (1+ days/weeks/months) or Don't repeat | Custom interval keeps reminding until the recipient signs or the 30-day cap is reached |
|
||||
|
||||
The organisation default out of the box is **first reminder after 5 days, repeating every 2 days**, which falls well inside the 30-day cap.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Send Documents](/docs/users/documents/send) - Send documents and trigger a one-off resend
|
||||
- [Recipient Expiration](/docs/users/documents/advanced/recipient-expiration) - Set a hard signing deadline
|
||||
- [Document Preferences](/docs/users/organisations/preferences/document) - Configure default document settings
|
||||
- [Webhook Events](/docs/developers/webhooks/events) - Subscribe to `document.reminder.sent`
|
||||
@@ -22,6 +22,37 @@ Organisation members have different permission levels that determine what they c
|
||||
organisation.
|
||||
</Callout>
|
||||
|
||||
## Transferring Organisation Ownership
|
||||
|
||||
Organisation ownership cannot be transferred through the regular organisation settings. Only a Documenso instance administrator can transfer ownership through the admin panel.
|
||||
|
||||
If you are using Documenso Cloud, contact support to request an ownership transfer. If you are self-hosting, an instance administrator can follow the steps below.
|
||||
|
||||
The target user must already be a member of the organisation.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
Navigate to **Admin > Organisations** and select the organisation.
|
||||
</Step>
|
||||
<Step>
|
||||
In the **Organisation Members** table, find the target member and click **Update role**.
|
||||
</Step>
|
||||
<Step>
|
||||
Select **Owner** from the role dropdown and click **Update**.
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
After the transfer:
|
||||
|
||||
- The new owner is promoted to Admin if they previously held a lower role (Manager or Member).
|
||||
- The previous owner retains their Admin role and remains a member of the organisation.
|
||||
- Only one user can be the owner at a time.
|
||||
|
||||
<Callout type="warn">
|
||||
The current owner cannot be demoted below Admin. Transfer ownership to another member first.
|
||||
</Callout>
|
||||
|
||||
## Team Member Roles
|
||||
|
||||
Teams have three roles with different permission levels:
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
{
|
||||
"title": "Organisations",
|
||||
"pages": [
|
||||
"overview",
|
||||
"create-team",
|
||||
"members",
|
||||
"groups",
|
||||
"email-domains",
|
||||
"preferences",
|
||||
"single-sign-on",
|
||||
"billing"
|
||||
]
|
||||
"pages": ["overview", "create-team", "members", "groups", "email-domains", "preferences", "single-sign-on", "billing"]
|
||||
}
|
||||
|
||||
@@ -32,6 +32,8 @@ To access the preferences, navigate to either the organisation or teams settings
|
||||
| **Include the Signing Certificate** | Whether the signing certificate is embedded in signed PDFs. The certificate is always available separately from the logs page. |
|
||||
| **Include the Audit Logs** | Whether the audit logs are embedded in the document when downloaded. The audit logs are always available separately from the logs page. |
|
||||
| **Default Recipients** | Recipients that are automatically added to new documents. Can be overridden per document. |
|
||||
| **Default Envelope Expiration** | How long recipients have to sign before the signing link expires. See [recipient expiration](/docs/users/documents/advanced/recipient-expiration). |
|
||||
| **Default Signing Reminders** | When and how often to email recipients who have not yet signed. See [signing reminders](/docs/users/documents/advanced/signing-reminders). |
|
||||
| **Delegate Document Ownership** | Allow team API tokens to delegate document ownership to another team member. |
|
||||
| **AI Features** | Enable AI-powered features such as automatic recipient detection. Only shown if AI features are configured on the instance. |
|
||||
|
||||
|
||||
@@ -134,6 +134,13 @@ Leave empty to allow any domain authenticated by your identity provider.
|
||||
team.
|
||||
</Callout>
|
||||
|
||||
### Allow Personal Organisations
|
||||
|
||||
Controls whether users signing in via SSO for the first time also receive their own personal organisation in addition to joining your organisation.
|
||||
|
||||
- **Enabled**: New SSO users get a personal organisation where they can create and manage their own documents independently.
|
||||
- **Disabled**: New SSO users only join your organisation and do not receive a personal organisation.
|
||||
|
||||
## User Provisioning
|
||||
|
||||
When a user signs in through your SSO portal for the first time:
|
||||
|
||||
@@ -135,7 +135,9 @@ Additional options that apply to all documents created from this template:
|
||||
|
||||
## Template Visibility
|
||||
|
||||
All templates are created in a team context. Team members can see, edit, delete, and use the templates in that team. See [Organisations](/docs/users/organisations) to learn about creating and managing organisations.
|
||||
All templates are created in a team context. By default, templates are **Private** and only visible to members of the owning team.
|
||||
|
||||
If your organisation has multiple teams, you can set a template's type to **Organisation** to share it across all teams. See [Organisation Templates](/docs/users/templates/organisation-templates) for details.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ description: Create reusable document templates for common signing workflows.
|
||||
description="Create documents from your templates."
|
||||
href="/docs/users/templates/use"
|
||||
/>
|
||||
<Card
|
||||
title="Organisation Templates"
|
||||
description="Share templates across all teams in your organisation."
|
||||
href="/docs/users/templates/organisation-templates"
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
---
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "Templates",
|
||||
"pages": ["create", "use"]
|
||||
"pages": ["create", "use", "organisation-templates"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
---
|
||||
title: Organisation Templates
|
||||
description: Share templates across all teams in your organisation so any team can create documents from them.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
|
||||
## Overview
|
||||
|
||||
Organisation templates are templates shared across all teams within the same organisation. Any team in the organisation can browse and use them to create documents, but only the owning team can edit or delete them.
|
||||
|
||||
This is useful when you have standardised documents that multiple teams need to use, such as company-wide NDAs, onboarding agreements, or compliance forms.
|
||||
|
||||
## Requirements
|
||||
|
||||
The Organisation template type is available when your organisation has **two or more teams**. If your organisation has only one team, the option does not appear.
|
||||
|
||||
## Template Types
|
||||
|
||||
| Type | Who can see it | Who can edit it | Who can use it |
|
||||
| ---------------- | ------------------------- | ----------------- | --------------------------- |
|
||||
| **Private** | Members of the owning team | Owning team | Owning team |
|
||||
| **Organisation** | All teams in the org | Owning team only | All teams in the org |
|
||||
| **Public** | Anyone with the link | Owning team | Anyone via direct link |
|
||||
|
||||
## Set a Template as Organisation
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Open template settings
|
||||
|
||||
Navigate to **Templates**, open the template you want to share, and click **Edit Template** to open the editor. Then open the settings dialog.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Change the template type
|
||||
|
||||
In the **Template type** dropdown, select **Organisation**. This option only appears if your organisation has at least two teams.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Save
|
||||
|
||||
Click **Save** to apply the change. The template is now visible to all teams in your organisation.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
You can also set the template type to Organisation when creating a new template. The type dropdown appears in the template settings step.
|
||||
|
||||
## Browse Organisation Templates
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
### Open the templates page
|
||||
|
||||
Navigate to **Templates** in the sidebar.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Switch to the Organisation tab
|
||||
|
||||
Click the **Organisation** tab above the template list. This tab only appears for non-personal organisations.
|
||||
|
||||
The Organisation tab shows all organisation templates from every team in your organisation, including your own.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
Templates from other teams display the owning team's name next to the template type.
|
||||
|
||||
## Use an Organisation Template
|
||||
|
||||
Any team member in the organisation can create documents from an organisation template, even if the template belongs to a different team.
|
||||
|
||||
{/* prettier-ignore */}
|
||||
<Steps>
|
||||
<Step>
|
||||
|
||||
Find the template in the **Organisation** tab or click through from the template detail page.
|
||||
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
|
||||
Click **Use Template** and fill in the recipient details. The document is created under your team, not the template's owning team.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
See [Use Templates](/docs/users/templates/use) for details on creating documents from templates.
|
||||
|
||||
## Editing and Permissions
|
||||
|
||||
Only members of the team that owns the template can edit or delete it. When viewing an organisation template from another team:
|
||||
|
||||
- The **Edit Template**, **Direct Link**, and **Bulk Send** controls are hidden
|
||||
- The recipients section is read-only
|
||||
- The **Use Template** button is available
|
||||
|
||||
To modify a template owned by another team, contact that team's members or ask an organisation admin to make changes.
|
||||
|
||||
## Visibility
|
||||
|
||||
Organisation templates respect the same visibility settings as other templates. A template's visibility determines which team roles can access it:
|
||||
|
||||
| Visibility | Who can access |
|
||||
| --------------------- | --------------------------------------- |
|
||||
| **Everyone** | All team members (Admin, Manager, Member) |
|
||||
| **Manager and above** | Admins and Managers only |
|
||||
| **Admin** | Admins only |
|
||||
|
||||
This applies to both the owning team and other teams in the organisation. A Member-role user on any team cannot see an organisation template set to Admin visibility.
|
||||
|
||||
## Reverting to Private
|
||||
|
||||
To stop sharing a template across the organisation, change the template type back to **Private** in the template settings. The template will only be visible to the owning team. Documents already created from the template are not affected.
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Create Templates](/docs/users/templates/create) - Build reusable templates
|
||||
- [Use Templates](/docs/users/templates/use) - Create documents from templates
|
||||
- [Organisations](/docs/users/organisations) - Managing organisations and teams
|
||||
@@ -16,7 +16,7 @@
|
||||
"fumadocs-ui": "16.5.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"mermaid": "^11.12.2",
|
||||
"next": "16.1.6",
|
||||
"next": "16.2.4",
|
||||
"next-plausible": "^3.12.5",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.4",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 596 KiB After Width: | Height: | Size: 117 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 571 KiB After Width: | Height: | Size: 126 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
@@ -1,5 +1,5 @@
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default function Layout({ children }: LayoutProps<'/'>) {
|
||||
return <HomeLayout {...baseOptions()}>{children}</HomeLayout>;
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { BookOpenIcon, CodeIcon, FileTextIcon, GithubIcon, ServerIcon, ShieldCheckIcon, UserIcon } from 'lucide-react';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
|
||||
import {
|
||||
BookOpenIcon,
|
||||
CodeIcon,
|
||||
FileTextIcon,
|
||||
GithubIcon,
|
||||
ServerIcon,
|
||||
ShieldCheckIcon,
|
||||
UserIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Documenso Docs',
|
||||
description:
|
||||
@@ -22,21 +13,21 @@ export default function HomePage() {
|
||||
<main className="mx-auto max-w-4xl px-4 py-12">
|
||||
{/* Hero */}
|
||||
<div className="mb-16 pt-6 text-center">
|
||||
<h1 className="mb-4 text-4xl font-bold tracking-tight">Documenso Documentation</h1>
|
||||
<p className="text-fd-muted-foreground mx-auto mb-8 max-w-2xl text-lg">
|
||||
The open-source document signing platform. Send documents for signatures, integrate with
|
||||
your apps, or self-host with full control.
|
||||
<h1 className="mb-4 font-bold text-4xl tracking-tight">Documenso Documentation</h1>
|
||||
<p className="mx-auto mb-8 max-w-2xl text-fd-muted-foreground text-lg">
|
||||
The open-source document signing platform. Send documents for signatures, integrate with your apps, or
|
||||
self-host with full control.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<Link
|
||||
href="/docs/users"
|
||||
className="bg-documenso text-fd-primary-foreground hover:bg-documenso-dark/90 inline-flex items-center gap-2 rounded-lg px-5 py-2.5 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-documenso px-5 py-2.5 font-medium text-fd-primary-foreground text-sm transition-colors hover:bg-documenso-dark/90"
|
||||
>
|
||||
Get Started
|
||||
</Link>
|
||||
<a
|
||||
href="https://github.com/documenso/documenso"
|
||||
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-5 py-2.5 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center gap-2 rounded-lg border bg-fd-background px-5 py-2.5 font-medium text-sm transition-colors hover:bg-fd-accent"
|
||||
>
|
||||
<GithubIcon className="size-4" />
|
||||
View on GitHub
|
||||
@@ -48,64 +39,60 @@ export default function HomePage() {
|
||||
<div className="mb-16 grid gap-4 md:grid-cols-3">
|
||||
<Link
|
||||
href="/docs/users"
|
||||
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
|
||||
className="group relative flex flex-col rounded-xl border bg-fd-card p-6 transition-all hover:border-fd-primary/50 hover:shadow-md"
|
||||
>
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-emerald-500/10 text-emerald-600 dark:text-emerald-400">
|
||||
<UserIcon className="size-6" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-lg font-semibold">User Guide</h2>
|
||||
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
|
||||
<h2 className="mb-2 font-semibold text-lg">User Guide</h2>
|
||||
<p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
|
||||
Send documents, create templates, and manage your team using the web application.
|
||||
</p>
|
||||
<span className="text-fd-primary text-sm font-medium">Get started →</span>
|
||||
<span className="font-medium text-fd-primary text-sm">Get started →</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/docs/developers"
|
||||
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
|
||||
className="group relative flex flex-col rounded-xl border bg-fd-card p-6 transition-all hover:border-fd-primary/50 hover:shadow-md"
|
||||
>
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-blue-500/10 text-blue-600 dark:text-blue-400">
|
||||
<CodeIcon className="size-6" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-lg font-semibold">Developer Guide</h2>
|
||||
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
|
||||
Integrate document signing into your applications with the REST API, webhooks, and
|
||||
embedding.
|
||||
<h2 className="mb-2 font-semibold text-lg">Developer Guide</h2>
|
||||
<p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
|
||||
Integrate document signing into your applications with the REST API, webhooks, and embedding.
|
||||
</p>
|
||||
<span className="text-fd-primary text-sm font-medium">View API docs →</span>
|
||||
<span className="font-medium text-fd-primary text-sm">View API docs →</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/docs/self-hosting"
|
||||
className="group bg-fd-card hover:border-fd-primary/50 relative flex flex-col rounded-xl border p-6 transition-all hover:shadow-md"
|
||||
className="group relative flex flex-col rounded-xl border bg-fd-card p-6 transition-all hover:border-fd-primary/50 hover:shadow-md"
|
||||
>
|
||||
<div className="mb-4 flex size-12 items-center justify-center rounded-lg bg-purple-500/10 text-purple-600 dark:text-purple-400">
|
||||
<ServerIcon className="size-6" />
|
||||
</div>
|
||||
<h2 className="mb-2 text-lg font-semibold">Self-Hosting Guide</h2>
|
||||
<p className="text-fd-muted-foreground mb-4 flex-1 text-sm">
|
||||
<h2 className="mb-2 font-semibold text-lg">Self-Hosting Guide</h2>
|
||||
<p className="mb-4 flex-1 text-fd-muted-foreground text-sm">
|
||||
Deploy your own Documenso instance with Docker, Kubernetes, or Railway.
|
||||
</p>
|
||||
<span className="text-fd-primary text-sm font-medium">Deploy now →</span>
|
||||
<span className="font-medium text-fd-primary text-sm">Deploy now →</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Quick Start & Core Concepts */}
|
||||
<div className="mb-16 grid gap-8 md:grid-cols-2">
|
||||
<div className="bg-fd-card/50 rounded-xl border p-6">
|
||||
<div className="rounded-xl border bg-fd-card/50 p-6">
|
||||
<h3 className="mb-4 flex items-center gap-2 font-semibold">
|
||||
<BookOpenIcon className="text-fd-muted-foreground size-5" />
|
||||
<BookOpenIcon className="size-5 text-fd-muted-foreground" />
|
||||
Quick Start
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="mb-2 text-sm font-medium">Send your first document</h4>
|
||||
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
|
||||
<h4 className="mb-2 font-medium text-sm">Send your first document</h4>
|
||||
<ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
|
||||
<li>
|
||||
<Link
|
||||
href="/docs/users/getting-started/create-account"
|
||||
className="text-fd-primary hover:underline"
|
||||
>
|
||||
<Link href="/docs/users/getting-started/create-account" className="text-fd-primary hover:underline">
|
||||
Create an account
|
||||
</Link>
|
||||
</li>
|
||||
@@ -120,8 +107,8 @@ export default function HomePage() {
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-sm font-medium">Integrate with the API</h4>
|
||||
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
|
||||
<h4 className="mb-2 font-medium text-sm">Integrate with the API</h4>
|
||||
<ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
|
||||
<li>
|
||||
<Link
|
||||
href="/docs/developers/getting-started/authentication"
|
||||
@@ -141,8 +128,8 @@ export default function HomePage() {
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="mb-2 text-sm font-medium">Deploy self-hosted</h4>
|
||||
<ol className="text-fd-muted-foreground list-inside list-decimal space-y-1 text-sm">
|
||||
<h4 className="mb-2 font-medium text-sm">Deploy self-hosted</h4>
|
||||
<ol className="list-inside list-decimal space-y-1 text-fd-muted-foreground text-sm">
|
||||
<li>
|
||||
<Link
|
||||
href="/docs/self-hosting/getting-started/requirements"
|
||||
@@ -164,36 +151,36 @@ export default function HomePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-fd-card/50 rounded-xl border p-6">
|
||||
<div className="rounded-xl border bg-fd-card/50 p-6">
|
||||
<h3 className="mb-4 flex items-center gap-2 font-semibold">
|
||||
<BookOpenIcon className="text-fd-muted-foreground size-5" />
|
||||
<BookOpenIcon className="size-5 text-fd-muted-foreground" />
|
||||
Core Concepts
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Link
|
||||
href="/docs/concepts/document-lifecycle"
|
||||
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
|
||||
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="mb-1 font-medium">Document Lifecycle</div>
|
||||
<div className="text-fd-muted-foreground text-xs">Draft to completed</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs/concepts/recipient-roles"
|
||||
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
|
||||
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="mb-1 font-medium">Recipient Roles</div>
|
||||
<div className="text-fd-muted-foreground text-xs">Signers and approvers</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs/concepts/field-types"
|
||||
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
|
||||
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="mb-1 font-medium">Field Types</div>
|
||||
<div className="text-fd-muted-foreground text-xs">Signatures and inputs</div>
|
||||
</Link>
|
||||
<Link
|
||||
href="/docs/concepts/signing-certificates"
|
||||
className="bg-fd-background hover:border-fd-primary/50 rounded-lg border p-3 text-sm transition-colors"
|
||||
className="rounded-lg border bg-fd-background p-3 text-sm transition-colors hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="mb-1 font-medium">Signing Certificates</div>
|
||||
<div className="text-fd-muted-foreground text-xs">Digital verification</div>
|
||||
@@ -206,7 +193,7 @@ export default function HomePage() {
|
||||
<div className="mb-16 grid gap-4 md:grid-cols-2">
|
||||
<Link
|
||||
href="/docs/compliance"
|
||||
className="bg-fd-card/50 hover:border-fd-primary/50 flex items-start gap-4 rounded-xl border p-5 transition-all"
|
||||
className="flex items-start gap-4 rounded-xl border bg-fd-card/50 p-5 transition-all hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-amber-500/10 text-amber-600 dark:text-amber-400">
|
||||
<ShieldCheckIcon className="size-5" />
|
||||
@@ -221,7 +208,7 @@ export default function HomePage() {
|
||||
|
||||
<Link
|
||||
href="/docs/policies"
|
||||
className="bg-fd-card/50 hover:border-fd-primary/50 flex items-start gap-4 rounded-xl border p-5 transition-all"
|
||||
className="flex items-start gap-4 rounded-xl border bg-fd-card/50 p-5 transition-all hover:border-fd-primary/50"
|
||||
>
|
||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-slate-500/10 text-slate-600 dark:text-slate-400">
|
||||
<FileTextIcon className="size-5" />
|
||||
@@ -236,22 +223,22 @@ export default function HomePage() {
|
||||
</div>
|
||||
|
||||
{/* Community CTA */}
|
||||
<div className="from-fd-primary/5 to-fd-primary/10 rounded-xl border bg-gradient-to-r p-8 text-center">
|
||||
<h3 className="mb-2 text-lg font-semibold">Join the Community</h3>
|
||||
<p className="text-fd-muted-foreground mb-6 text-sm">
|
||||
<div className="rounded-xl border bg-gradient-to-r from-fd-primary/5 to-fd-primary/10 p-8 text-center">
|
||||
<h3 className="mb-2 font-semibold text-lg">Join the Community</h3>
|
||||
<p className="mb-6 text-fd-muted-foreground text-sm">
|
||||
Documenso is open source. Contribute, ask questions, or share feedback.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<a
|
||||
href="https://github.com/documenso/documenso"
|
||||
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center gap-2 rounded-lg border bg-fd-background px-4 py-2 font-medium text-sm transition-colors hover:bg-fd-accent"
|
||||
>
|
||||
<GithubIcon className="size-4" />
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://documen.so/discord"
|
||||
className="bg-fd-background hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg border px-4 py-2 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center gap-2 rounded-lg border bg-fd-background px-4 py-2 font-medium text-sm transition-colors hover:bg-fd-accent"
|
||||
>
|
||||
<svg className="size-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
||||
@@ -260,7 +247,7 @@ export default function HomePage() {
|
||||
</a>
|
||||
<a
|
||||
href="https://app.documenso.com/signup"
|
||||
className="bg-documenso text-fd-primary-foreground hover:bg-documenso/90 inline-flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-documenso px-4 py-2 font-medium text-fd-primary-foreground text-sm transition-colors hover:bg-documenso/90"
|
||||
>
|
||||
Try Documenso
|
||||
</a>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
import { source } from '@/lib/source';
|
||||
|
||||
export const { GET } = createFromSource(source, {
|
||||
// https://docs.orama.com/docs/orama-js/supported-languages
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import { LLMCopyButton, ViewOptions } from '@/components/ai/page-actions';
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import { getMDXComponents } from '@/mdx-components';
|
||||
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
|
||||
|
||||
const gitConfig = {
|
||||
user: 'documenso',
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { cn } from '@/lib/cn';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { getFilteredPageTree, source } from '@/lib/source';
|
||||
import type * as PageTree from 'fumadocs-core/page-tree';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { CodeIcon, ServerIcon, UserIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useMemo } from 'react';
|
||||
import { cn } from '@/lib/cn';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { getFilteredPageTree, source } from '@/lib/source';
|
||||
|
||||
const ROOT_SECTIONS = [
|
||||
{
|
||||
@@ -44,7 +42,9 @@ function getFirstPageUrl(children: PageTree.Node[]): string | undefined {
|
||||
}
|
||||
if (child.type === 'folder' && child.children.length > 0) {
|
||||
const url = getFirstPageUrl(child.children);
|
||||
if (url) return url;
|
||||
if (url) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
@@ -69,13 +69,8 @@ function SectionSwitcher({ activeSection }: { activeSection: string | null }) {
|
||||
>
|
||||
<Icon className={cn('mt-0.5 size-4 shrink-0', isActive ? 'text-fd-primary' : '')} />
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-medium">{section.label}</span>
|
||||
<span
|
||||
className={cn(
|
||||
'text-xs',
|
||||
isActive ? 'text-fd-muted-foreground' : 'text-fd-muted-foreground/70',
|
||||
)}
|
||||
>
|
||||
<span className="font-medium text-sm">{section.label}</span>
|
||||
<span className={cn('text-xs', isActive ? 'text-fd-muted-foreground' : 'text-fd-muted-foreground/70')}>
|
||||
{section.subtitle}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/shadcn.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
@import "tailwindcss";
|
||||
@import "fumadocs-ui/css/shadcn.css";
|
||||
@import "fumadocs-ui/css/preset.css";
|
||||
|
||||
@theme {
|
||||
/* Brand utility colors */
|
||||
@@ -43,13 +43,11 @@
|
||||
--sidebar-border: hsl(223.8136 0.0001% 89.8161%);
|
||||
--sidebar-ring: hsl(223.8136 0% 63.0163%);
|
||||
--font-sans:
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
@@ -103,13 +101,11 @@
|
||||
--sidebar-border: hsl(223.8136 0% 15.5096%);
|
||||
--sidebar-ring: hsl(223.8136 0% 32.1993%);
|
||||
--font-sans:
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
|
||||
--font-mono:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||
monospace;
|
||||
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
|
||||
"Segoe UI Symbol", "Noto Color Emoji";
|
||||
--font-serif: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
--radius: 0.5rem;
|
||||
--shadow-x: 0;
|
||||
--shadow-y: 1px;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import type { Metadata } from 'next';
|
||||
import { Inter } from 'next/font/google';
|
||||
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import PlausibleProvider from 'next-plausible';
|
||||
|
||||
import './global.css';
|
||||
@@ -16,8 +15,7 @@ export const metadata: Metadata = {
|
||||
template: '%s | Documenso Docs',
|
||||
default: 'Documenso Docs',
|
||||
},
|
||||
description:
|
||||
'The official documentation for Documenso, the open-source document signing platform.',
|
||||
description: 'The official documentation for Documenso, the open-source document signing platform.',
|
||||
openGraph: {
|
||||
siteName: 'Documenso Docs',
|
||||
type: 'website',
|
||||
|
||||
@@ -3,20 +3,20 @@ import Link from 'next/link';
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="mx-auto flex max-w-xl flex-col items-center justify-center px-4 py-32 text-center">
|
||||
<h1 className="text-4xl font-bold tracking-tight">Page not found</h1>
|
||||
<p className="text-fd-muted-foreground mt-4 text-lg">
|
||||
<h1 className="font-bold text-4xl tracking-tight">Page not found</h1>
|
||||
<p className="mt-4 text-fd-muted-foreground text-lg">
|
||||
The page you are looking for may have moved. Our documentation was recently restructured.
|
||||
</p>
|
||||
<div className="mt-8 flex flex-wrap justify-center gap-3">
|
||||
<Link
|
||||
href="/docs/users"
|
||||
className="bg-documenso text-fd-primary-foreground hover:bg-documenso/90 inline-flex items-center rounded-lg px-5 py-2.5 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center rounded-lg bg-documenso px-5 py-2.5 font-medium text-fd-primary-foreground text-sm transition-colors hover:bg-documenso/90"
|
||||
>
|
||||
Browse documentation
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="bg-fd-background hover:bg-fd-accent inline-flex items-center rounded-lg border px-5 py-2.5 text-sm font-medium transition-colors"
|
||||
className="inline-flex items-center rounded-lg border bg-fd-background px-5 py-2.5 font-medium text-sm transition-colors hover:bg-fd-accent"
|
||||
>
|
||||
Go to homepage
|
||||
</Link>
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
const loadAssets = async () => {
|
||||
const [logoBuffer, interRegularData, interSemiBoldData, interBoldData] = await Promise.all([
|
||||
readFile(fileURLToPath(new URL('../../../../../public/logo.png', import.meta.url))),
|
||||
readFile(
|
||||
fileURLToPath(new URL('../../../../../public/fonts/inter-regular.ttf', import.meta.url)),
|
||||
),
|
||||
readFile(
|
||||
fileURLToPath(new URL('../../../../../public/fonts/inter-semibold.ttf', import.meta.url)),
|
||||
),
|
||||
readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-regular.ttf', import.meta.url))),
|
||||
readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-semibold.ttf', import.meta.url))),
|
||||
readFile(fileURLToPath(new URL('../../../../../public/fonts/inter-bold.ttf', import.meta.url))),
|
||||
]);
|
||||
|
||||
@@ -40,104 +35,100 @@ export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...
|
||||
const { logoSrc, fonts } = await loadAssets();
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'white',
|
||||
padding: '60px 80px',
|
||||
fontFamily: 'Inter',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
{/* Green accent bar */}
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '6px',
|
||||
backgroundColor: '#6DC947',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Top: Logo */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={logoSrc} alt="Documenso" height="28" />
|
||||
<span
|
||||
style={{
|
||||
color: '#D4D4D8',
|
||||
fontSize: '28px',
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
|
|
||||
</span>
|
||||
<span style={{ color: '#71717A', fontSize: '20px', fontWeight: 400 }}>Docs</span>
|
||||
</div>
|
||||
|
||||
{/* Middle: Title + description */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'white',
|
||||
padding: '60px 80px',
|
||||
fontFamily: 'Inter',
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
gap: '16px',
|
||||
}}
|
||||
>
|
||||
{/* Green accent bar */}
|
||||
<div
|
||||
<h1
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: '6px',
|
||||
backgroundColor: '#6DC947',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Top: Logo */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '16px',
|
||||
color: '#18181B',
|
||||
fontSize: page.data.title.length > 40 ? '48px' : '56px',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.15,
|
||||
letterSpacing: '-0.025em',
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={logoSrc} alt="Documenso" height="28" />
|
||||
<span
|
||||
{page.data.title}
|
||||
</h1>
|
||||
{page.data.description && (
|
||||
<p
|
||||
style={{
|
||||
color: '#D4D4D8',
|
||||
fontSize: '28px',
|
||||
color: '#71717A',
|
||||
fontSize: '22px',
|
||||
fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
|
|
||||
</span>
|
||||
<span style={{ color: '#71717A', fontSize: '20px', fontWeight: 400 }}>Docs</span>
|
||||
</div>
|
||||
|
||||
{/* Middle: Title + description */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
gap: '16px',
|
||||
}}
|
||||
>
|
||||
<h1
|
||||
style={{
|
||||
color: '#18181B',
|
||||
fontSize: page.data.title.length > 40 ? '48px' : '56px',
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.15,
|
||||
letterSpacing: '-0.025em',
|
||||
lineHeight: 1.4,
|
||||
margin: 0,
|
||||
maxWidth: '900px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{page.data.title}
|
||||
</h1>
|
||||
{page.data.description && (
|
||||
<p
|
||||
style={{
|
||||
color: '#71717A',
|
||||
fontSize: '22px',
|
||||
fontWeight: 400,
|
||||
lineHeight: 1.4,
|
||||
margin: 0,
|
||||
maxWidth: '900px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 2,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
}}
|
||||
>
|
||||
{page.data.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom: URL */}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ color: '#A1A1AA', fontSize: '16px', fontWeight: 400 }}>
|
||||
docs.documenso.com{page.url}
|
||||
</span>
|
||||
</div>
|
||||
{page.data.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
|
||||
{/* Bottom: URL */}
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ color: '#A1A1AA', fontSize: '16px', fontWeight: 400 }}>docs.documenso.com{page.url}</span>
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { cn } from '@/lib/cn';
|
||||
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/ui/popover';
|
||||
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';
|
||||
import { Check, ChevronDown, Copy, ExternalLinkIcon, MessageCircleIcon } from 'lucide-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
const cache = new Map<string, string>();
|
||||
|
||||
@@ -21,7 +20,9 @@ export function LLMCopyButton({
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [checked, onClick] = useCopyButton(async () => {
|
||||
const cached = cache.get(markdownUrl);
|
||||
if (cached) return navigator.clipboard.writeText(cached);
|
||||
if (cached) {
|
||||
return navigator.clipboard.writeText(cached);
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
@@ -48,7 +49,7 @@ export function LLMCopyButton({
|
||||
buttonVariants({
|
||||
color: 'secondary',
|
||||
size: 'sm',
|
||||
className: '[&_svg]:text-fd-muted-foreground gap-2 [&_svg]:size-3.5',
|
||||
className: 'gap-2 [&_svg]:size-3.5 [&_svg]:text-fd-muted-foreground',
|
||||
}),
|
||||
)}
|
||||
onClick={onClick}
|
||||
@@ -74,8 +75,7 @@ export function ViewOptions({
|
||||
githubUrl: string;
|
||||
}) {
|
||||
const items = useMemo(() => {
|
||||
const fullMarkdownUrl =
|
||||
typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading';
|
||||
const fullMarkdownUrl = typeof window !== 'undefined' ? new URL(markdownUrl, window.location.origin) : 'loading';
|
||||
const q = `Read ${fullMarkdownUrl}, I want to ask questions about it.`;
|
||||
|
||||
return [
|
||||
@@ -96,12 +96,7 @@ export function ViewOptions({
|
||||
q,
|
||||
})}`,
|
||||
icon: (
|
||||
<svg
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg role="img" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>OpenAI</title>
|
||||
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z" />
|
||||
</svg>
|
||||
@@ -113,12 +108,7 @@ export function ViewOptions({
|
||||
q,
|
||||
})}`,
|
||||
icon: (
|
||||
<svg
|
||||
fill="currentColor"
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<title>Anthropic</title>
|
||||
<path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z" />
|
||||
</svg>
|
||||
@@ -146,7 +136,7 @@ export function ViewOptions({
|
||||
)}
|
||||
>
|
||||
Open
|
||||
<ChevronDown className="text-fd-muted-foreground size-3.5" />
|
||||
<ChevronDown className="size-3.5 text-fd-muted-foreground" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="flex flex-col">
|
||||
{items.map((item) => (
|
||||
@@ -155,11 +145,11 @@ export function ViewOptions({
|
||||
href={item.href}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
className="hover:text-fd-accent-foreground hover:bg-fd-accent inline-flex items-center gap-2 rounded-lg p-2 text-sm [&_svg]:size-4"
|
||||
className="inline-flex items-center gap-2 rounded-lg p-2 text-sm hover:bg-fd-accent hover:text-fd-accent-foreground [&_svg]:size-4"
|
||||
>
|
||||
{item.icon}
|
||||
{item.title}
|
||||
<ExternalLinkIcon className="text-fd-muted-foreground ms-auto size-3.5" />
|
||||
<ExternalLinkIcon className="ms-auto size-3.5 text-fd-muted-foreground" />
|
||||
</a>
|
||||
))}
|
||||
</PopoverContent>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useId, useRef, useState } from 'react';
|
||||
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect, useId, useRef, useState } from 'react';
|
||||
|
||||
export const Mermaid = ({ chart }: { chart: string }) => {
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { docs } from 'fumadocs-mdx:collections/server';
|
||||
import type * as PageTree from 'fumadocs-core/page-tree';
|
||||
import { type InferPageType, loader } from 'fumadocs-core/source';
|
||||
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
|
||||
import { docs } from 'fumadocs-mdx:collections/server';
|
||||
|
||||
// See https://fumadocs.dev/docs/headless/source-api for more info
|
||||
export const source = loader({
|
||||
@@ -30,9 +30,7 @@ export function getFilteredPageTree(rootName: string): PageTree.Root {
|
||||
// Find the main section folder
|
||||
const rootFolder = fullTree.children.find(
|
||||
(child): child is PageTree.Folder =>
|
||||
child.type === 'folder' &&
|
||||
typeof child.name === 'string' &&
|
||||
child.name.toLowerCase() === rootName.toLowerCase(),
|
||||
child.type === 'folder' && typeof child.name === 'string' && child.name.toLowerCase() === rootName.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!rootFolder) {
|
||||
@@ -42,9 +40,7 @@ export function getFilteredPageTree(rootName: string): PageTree.Root {
|
||||
// Find shared section folders
|
||||
const sharedFolders = fullTree.children.filter(
|
||||
(child): child is PageTree.Folder =>
|
||||
child.type === 'folder' &&
|
||||
typeof child.name === 'string' &&
|
||||
SHARED_SECTIONS.includes(child.name.toLowerCase()),
|
||||
child.type === 'folder' && typeof child.name === 'string' && SHARED_SECTIONS.includes(child.name.toLowerCase()),
|
||||
);
|
||||
|
||||
// Create separator for main section
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Mermaid } from '@/components/mdx/mermaid';
|
||||
import * as TabsComponents from 'fumadocs-ui/components/tabs';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
import { Mermaid } from '@/components/mdx/mermaid';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function getMDXComponents(components?: MDXComponents): any {
|
||||
|
||||
+6
-22
@@ -2,11 +2,7 @@
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"target": "ESNext",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -20,12 +16,8 @@
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"fumadocs-mdx:collections/*": [
|
||||
".source/*"
|
||||
]
|
||||
"@/*": ["./src/*"],
|
||||
"fumadocs-mdx:collections/*": [".source/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
@@ -33,14 +25,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@ export async function GET(request: Request) {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=7200',
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user