diff --git a/.env.example b/.env.example
index cfd45e9ee2..86eec374f9 100644
--- a/.env.example
+++ b/.env.example
@@ -48,7 +48,7 @@ NEXT_PRIVATE_DATABASE_URL="postgres://keepcontracts:password@127.0.0.1:54320/kee
NEXT_PRIVATE_DIRECT_DATABASE_URL="postgres://keepcontracts:password@127.0.0.1:54320/keepcontracts"
# [[SIGNING]]
-# The transport to use for document signing. Available options: local (default) | gcloud-hsm
+# The transport to use for document signing. Available options: local (default) | gcloud-hsm | csc
NEXT_PRIVATE_SIGNING_TRANSPORT="local"
# OPTIONAL: The passphrase to use for the local file-based signing transport.
NEXT_PRIVATE_SIGNING_PASSPHRASE=
@@ -70,6 +70,14 @@ NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_FILE_PATH=
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS=
# OPTIONAL: The Google Secret Manager path to retrieve the certificate for the gcloud-hsm signing transport.
NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH=
+# OPTIONAL: The base URL of the Cloud Signature Consortium (CSC) provider for the csc signing transport.
+NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL=
+# OPTIONAL: The OAuth client ID registered with the CSC provider for the csc signing transport.
+NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID=
+# OPTIONAL: The OAuth client secret registered with the CSC provider for the csc signing transport.
+NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET=
+# OPTIONAL: Default signature level for envelopes created on a CSC instance when the caller doesn't specify one. Available options: AES (default) | QES. Explicit AES/QES requests always pass through unchanged.
+NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL=
# OPTIONAL: Comma-separated list of timestamp authority URLs for PDF signing (enables LTV and archival timestamps).
NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=
# OPTIONAL: Contact info to embed in PDF signatures. Defaults to the webapp URL.
@@ -95,7 +103,7 @@ NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID="documenso"
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY="password"
# [[SMTP]]
-# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | mailchannels
+# OPTIONAL: Defines the transport to use for sending emails. Available options: smtp-auth (default) | smtp-api | resend | mailchannels
NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth"
# OPTIONAL: Defines the host to use for sending emails.
NEXT_PRIVATE_SMTP_HOST="127.0.0.1"
@@ -172,6 +180,20 @@ NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=
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 disable all signin methods (email, Google, Microsoft, OIDC).
+NEXT_PUBLIC_DISABLE_SIGNIN=
+# OPTIONAL: Set to "true" to disable email/password signin only. Also closes /forgot-password and /reset-password.
+NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN=
+# OPTIONAL: Set to "true" to hide the Google signin button.
+NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN=
+# OPTIONAL: Set to "true" to hide the Microsoft signin button.
+NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN=
+# OPTIONAL: Set to "true" to hide the OIDC signin button.
+NEXT_PUBLIC_DISABLE_OIDC_SIGNIN=
+# OPTIONAL: When OIDC is the only enabled signin transport, /signin auto-redirects
+# to the OIDC provider (rendering only a spinner). Set to "true" to disable this
+# and keep showing the signin page.
+NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT=
# OPTIONAL: Set to true to use internal webapp url in browserless requests.
NEXT_PUBLIC_USE_INTERNAL_URL_BROWSERLESS=false
diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml
index 4fcde0ea30..ceee6933e6 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.yml
+++ b/.github/ISSUE_TEMPLATE/bug-report.yml
@@ -34,7 +34,7 @@ body:
label: Browser [e.g., Chrome, Firefox]
- type: input
attributes:
- label: Version [e.g., 2.0.1]
+ label: Version [e.g., 2.13.0]
- type: checkboxes
attributes:
label: Please check the boxes that apply to this issue report.
@@ -44,4 +44,3 @@ body:
- label: I have included relevant environment information.
- label: I have included any relevant screenshots.
- label: I understand that this is a voluntary contribution and that there is no guarantee of resolution.
- - label: I want to work on creating a PR for this issue if approved
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..4be27fb496
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,11 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Security vulnerability
+ url: https://github.com/documenso/documenso/security/advisories/new
+ about: Please report security vulnerabilities privately via GitHub Security Advisories. Do not open a public issue.
+ - name: Questions & Discussions
+ url: https://github.com/documenso/documenso/discussions
+ about: Ask questions, share ideas, and discuss Documenso with the community.
+ - name: Discord
+ url: https://documen.so/discord
+ about: Chat with the community and the team.
diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml
index ffb788c23c..ab21e88281 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.yml
+++ b/.github/ISSUE_TEMPLATE/feature-request.yml
@@ -33,4 +33,3 @@ body:
- label: I have explained the use case or scenario for this feature.
- label: I have included any relevant technical details or design suggestions.
- label: I understand that this is a suggestion and that there is no guarantee of implementation.
- - label: I want to work on creating a PR for this issue if approved
diff --git a/.github/ISSUE_TEMPLATE/improvement.yml b/.github/ISSUE_TEMPLATE/improvement.yml
index de2983b678..424d54a53b 100644
--- a/.github/ISSUE_TEMPLATE/improvement.yml
+++ b/.github/ISSUE_TEMPLATE/improvement.yml
@@ -15,17 +15,6 @@ body:
description: 'Are there any additional context or information that might be relevant to the improvement suggestion.'
validations:
required: false
- - type: dropdown
- id: assignee
- attributes:
- label: 'Do you want to work on this improvement?'
- multiple: false
- options:
- - 'No'
- - 'Yes'
- default: 0
- validations:
- required: true
- type: checkboxes
attributes:
label: 'Please check the boxes that apply to this improvement suggestion.'
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 66602d12bd..4e6151b345 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,3 +1,14 @@
+
+
## Description
diff --git a/.github/PULL_REQUEST_TEMPLATE/test-addition.md b/.github/PULL_REQUEST_TEMPLATE/test-addition.md
deleted file mode 100644
index f93c814931..0000000000
--- a/.github/PULL_REQUEST_TEMPLATE/test-addition.md
+++ /dev/null
@@ -1,40 +0,0 @@
----
-name: Test Addition
-about: Submit a new test, either unit or end-to-end (E2E), for review and inclusion
----
-
-## Description
-
-
-
-
-## Related Issue
-
-
-
-
-## Test Details
-
-
-
-
-- Test Name: Name of the test
-- Type: [Unit / E2E]
-- Description: Brief description of what the test checks
-- Inputs: What inputs the test uses (if applicable)
-- Expected Output: What output or behavior the test expects
-
-## Checklist
-
-
-
-
-- [ ] I have written the new test and ensured it works as intended.
-- [ ] I have added necessary documentation to explain the purpose of the test.
-- [ ] I have followed the project's testing guidelines and coding style.
-- [ ] I have addressed any review feedback from previous submissions, if applicable.
-
-## Additional Notes
-
-
-
diff --git a/.github/workflows/first-interaction.yml b/.github/workflows/first-interaction.yml
index 5f53eb280c..a4d7c8a54e 100644
--- a/.github/workflows/first-interaction.yml
+++ b/.github/workflows/first-interaction.yml
@@ -1,13 +1,10 @@
name: 'Welcome New Contributors'
on:
- pull_request:
- types: ['opened']
issues:
types: ['opened']
permissions:
- pull-requests: write
issues: write
jobs:
@@ -20,10 +17,7 @@ jobs:
- uses: actions/first-interaction@v1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- pr-message: |
- Thank you for creating your first Pull Request and for being a part of the open signing revolution! 💚🚀
- Feel free to hop into our community in [Discord](https://documen.so/discord)
issue-message: |
Thank you for opening your first issue and for being a part of the open signing revolution!
- One of our team members will review it and get back to you as soon as it possible 💚
+ One of our team members will review it and get back to you as soon as possible 💚
Meanwhile, please feel free to hop into our community in [Discord](https://documen.so/discord)
diff --git a/.github/workflows/issue-assignee-check.yml b/.github/workflows/issue-assignee-check.yml
deleted file mode 100644
index de53564ecb..0000000000
--- a/.github/workflows/issue-assignee-check.yml
+++ /dev/null
@@ -1,62 +0,0 @@
-name: 'Issue Assignee Check'
-
-on:
- issues:
- types: ['assigned']
-
-permissions:
- issues: write
-
-jobs:
- countIssues:
- if: ${{ github.event.issue.assignee }} && github.event.action == 'assigned' && github.event.sender.type == 'User'
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 2
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '18'
-
- - name: Install Octokit
- run: npm install @octokit/rest@18
-
- - name: Check Assigned User's Issue Count
- id: parse-comment
- uses: actions/github-script@v6
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const { Octokit } = require("@octokit/rest");
- const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
-
- const username = context.payload.issue.assignee.login;
- console.log(`Username Extracted: ${username}`);
-
- const { data: issues } = await octokit.issues.listForRepo({
- owner: context.repo.owner,
- repo: context.repo.repo,
- assignee: username,
- state: 'open'
- });
-
- const issueCount = issues.length;
- console.log(`Issue Count For ${username}: ${issueCount}`);
-
- if (issueCount > 3) {
- let issueCountMessage = `### 🚨 Documenso Police 🚨`;
- issueCountMessage += `\n@${username} has ${issueCount} open issues assigned already. Consider whether this issue should be assigned to them or left open for another contributor.`;
-
- await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- body: issueCountMessage,
- headers: {
- 'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`,
- }
- });
- }
diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml
deleted file mode 100644
index 67dc32f34b..0000000000
--- a/.github/workflows/pr-review-reminder.yml
+++ /dev/null
@@ -1,63 +0,0 @@
-name: 'PR Review Reminder'
-
-on:
- pull_request:
- types: ['opened', 'ready_for_review']
-
-permissions:
- pull-requests: write
-
-jobs:
- checkPRs:
- if: ${{ github.event.pull_request.user.login }} && github.event.action == ('opened' || 'ready_for_review')
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 2
- - name: Set up Node.js
- uses: actions/setup-node@v4
- with:
- node-version: '18'
-
- - name: Install Octokit
- run: npm install @octokit/rest@18
-
- - name: Check user's PRs awaiting review
- id: parse-prs
- uses: actions/github-script@v5
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- const { Octokit } = require("@octokit/rest");
- const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
-
- const username = context.payload.pull_request.user.login;
- console.log(`Username Extracted: ${username}`);
-
- const { data: pullRequests } = await octokit.pulls.list({
- owner: context.repo.owner,
- repo: context.repo.repo,
- state: 'open',
- sort: 'created',
- direction: 'asc',
- });
-
- const userPullRequests = pullRequests.filter(pr => pr.user.login === username && (pr.state === 'open' || pr.state === 'ready_for_review'));
- const prCount = userPullRequests.length;
- console.log(`PR Count for ${username}: ${prCount}`);
-
- if (prCount > 3) {
- let prReminderMessage = `🚨 @${username} has ${prCount} pull requests awaiting review. Please consider reviewing them when possible. 🚨`;
-
- await octokit.request('POST /repos/{owner}/{repo}/issues/{issue_number}/comments', {
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.payload.pull_request.number,
- body: prReminderMessage,
- headers: {
- 'Authorization': `token ${{ secrets.GITHUB_TOKEN }}`,
- }
- });
- }
diff --git a/.github/workflows/semantic-pull-requests.yml b/.github/workflows/semantic-pull-requests.yml
index 76a1b2f42c..0dab3392d6 100644
--- a/.github/workflows/semantic-pull-requests.yml
+++ b/.github/workflows/semantic-pull-requests.yml
@@ -16,24 +16,6 @@ jobs:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- - name: Check PR creator's previous activity
- id: check_activity
- run: |
- CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login')
- ACTIVITY=$(curl -s "https://api.github.com/search/commits?q=author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count')
- if [ "$ACTIVITY" -eq 0 ]; then
- echo "::set-output name=is_new::true"
- else
- echo "::set-output name=is_new::false"
- fi
-
- - name: Count PRs created by user
- id: count_prs
- run: |
- CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login')
- PR_COUNT=$(curl -s "https://api.github.com/search/issues?q=type:pr+is:open+author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count')
- echo "::set-output name=pr_count::$PR_COUNT"
-
- uses: amannn/action-semantic-pull-request@v5
id: lint_pr_title
env:
@@ -44,8 +26,6 @@ jobs:
with:
header: pr-title-lint-error
message: |
- Hey There! and thank you for opening this pull request! 📝👋🏼
-
We require pull request titles to follow the [Conventional Commits Spec](https://www.conventionalcommits.org/en/v1.0.0/) and it looks like your proposed title needs to be adjusted.
Details:
@@ -53,10 +33,3 @@ jobs:
```
${{ steps.lint_pr_title.outputs.error_message }}
```
-
- - if: ${{ steps.lint_pr_title.outputs.error_message == null && steps.check_activity.outputs.is_new == 'false' && steps.count_prs.outputs.pr_count < 2}}
- uses: marocchino/sticky-pull-request-comment@v2
- with:
- header: pr-title-lint-error
- message: |
- Thank you for following the naming conventions for pull request titles! 💚🚀
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index a18e33f878..c9c12ce59c 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -21,4 +21,4 @@ jobs:
stale-pr-message: 'This PR has not seen activitiy for a while. It will be closed in 30 days unless further activity is detected.'
close-pr-message: 'This PR has been closed because of inactivity.'
exempt-pr-labels: 'WIP,on-hold,needs review'
- exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,assigned,needs triage'
+ exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,status: assigned,status: triage'
diff --git a/.gitignore b/.gitignore
index d886dc24af..62e6ac0ce5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,3 +79,6 @@ tmp/
deploy/gcp/config.sh
deploy/gcp/*.p12
deploy/gcp/*.pem
+
+# local dev signing certificate — never commit
+certs/
diff --git a/.gitpod.yml b/.gitpod.yml
index 261f8c96b0..883ac5bb3f 100644
--- a/.gitpod.yml
+++ b/.gitpod.yml
@@ -31,8 +31,7 @@ vscode:
extensions:
- aaron-bond.better-comments
- bradlc.vscode-tailwindcss
- - dbaeumer.vscode-eslint
- - esbenp.prettier-vscode
+ - biomejs.biome
- mikestead.dotenv
- unifiedjs.vscode-mdx
- GitHub.vscode-pull-request-github
diff --git a/.npmrc b/.npmrc
index cbc6b6537f..75baad7f05 100644
--- a/.npmrc
+++ b/.npmrc
@@ -1,3 +1,3 @@
legacy-peer-deps = true
prefer-dedupe = true
-min-release-age = 7
+# min-release-age = 7
diff --git a/.well-known/security.txt b/.well-known/security.txt
index 1a3f685e5c..f96fce0f05 100644
--- a/.well-known/security.txt
+++ b/.well-known/security.txt
@@ -1,7 +1,14 @@
-# General Issues
-Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
+# Report security vulnerabilities privately via GitHub Security Advisories (preferred).
+Contact: https://github.com/documenso/documenso/security/advisories/new
-# Report critical issues privately to let us take appropriate action before publishing.
+# Alternatively, report critical issues privately by email.
Contact: mailto:security@documenso.com
+
+# Security policy
+Policy: https://github.com/documenso/documenso/security/policy
+
+# General (non-security) issues
+Contact: https://github.com/documenso/documenso/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml
+
Preferred-Languages: en
-Canonical: https://documenso.com/.well-known/security.txt
\ No newline at end of file
+Canonical: https://documenso.com/.well-known/security.txt
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..7260cdfe28
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,122 @@
+# Contributing to Documenso
+
+> **We are no longer accepting external pull requests.**
+>
+> Aside from a small group of trusted contributors we reach out to directly, we no longer merge external PRs. New pull requests will usually be closed with a request to open an issue instead. This is a security decision, not a judgement on your work. Read [Why We're Pausing External Pull Requests](https://documenso.com/blog/why-we-re-pausing-external-pull-requests) for the full reasoning.
+>
+> Documenso stays open source. You can still read, audit, run, and fork the code. The best way to contribute is through detailed issues.
+
+## How to contribute now
+
+The most useful contribution is a detailed issue. Treat it like a spec. The more detail, the better:
+
+- The problem you're trying to solve, and who it affects
+- How you expect the feature or change to behave
+- Edge cases, constraints, and anything you've already considered
+- Examples, mockups, or references where they help
+
+Before opening an issue, search [existing issues](https://github.com/documenso/documenso/issues) and [discussions](https://github.com/documenso/documenso/discussions) for related items. If a proposal is detailed and fits where Documenso is heading, we'll pick it up and build against it.
+
+For security vulnerabilities, do not open a public issue. Follow our [Security Policy](./SECURITY.md) instead.
+
+---
+
+The sections below are for trusted contributors working with us directly, and for anyone running Documenso locally or maintaining a fork.
+
+## English only PRs and Issues
+
+Please write all issues, pull requests, and related comments in English so maintainers and the wider contributor community can follow the discussion.
+
+## Taking issues
+
+Before taking an issue, ensure that:
+
+- The issue has been assigned the public label
+- The issue is clearly defined and understood
+- No one has been assigned to the issue
+- No one has expressed intention to work on it
+
+You can then:
+
+1. Comment on the issue with your intention to work on it
+2. Begin work on the issue
+
+Always feel free to ask questions or seek clarification on the issue.
+
+## Developing
+
+The development branch is main. All pull requests should be made against this branch. If you need help getting started, [join us on Discord](https://documen.so/discord).
+
+1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your
+ own GitHub account and then
+ [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device.
+2. Create a new branch:
+
+- Create a new branch (include the issue id and something readable):
+
+ ```sh
+ git checkout -b feat/doc-999-somefeature-that-rocks
+ ```
+
+3. See the [Developer Setup](https://github.com/documenso/documenso/blob/main/README.md#developer-setup) for more setup details.
+
+## Building
+
+> **Note**
+> Please ensure you can make a full production build before pushing code or creating PRs.
+
+You can build the project with:
+
+```bash
+npm run build
+```
+
+## AI-Assisted Development with OpenCode
+
+We use [OpenCode](https://opencode.ai) for AI-assisted development. OpenCode provides custom commands and skills to help maintain consistency and streamline common workflows.
+
+OpenCode works with most major AI providers (Anthropic, OpenAI, Google, etc.) or you can use [Zen](https://opencode.ai/zen) for optimized coding models. Configure your preferred provider in the OpenCode settings.
+
+> **Important**: All AI-generated code must be thoroughly reviewed by the contributor before submitting a PR. You are responsible for understanding and validating every line of code you submit. If we detect that contributors are simply throwing AI-generated code over the wall without proper review, they will be blocked from the repository.
+
+### Getting Started
+
+1. Install OpenCode (see [opencode.ai](https://opencode.ai) for other install methods):
+ ```bash
+ curl -fsSL https://opencode.ai/install | bash
+ ```
+2. Configure your AI provider (or use Zen for optimized models)
+3. Run `opencode` in the project root
+
+### Available Commands
+
+Use these commands in OpenCode by typing the command name:
+
+| Command | Description |
+| ------------------------------ | -------------------------------------------------------- |
+| `/implement ` | Implement a spec from `.agents/plans/` autonomously |
+| `/continue ` | Continue implementing a spec from a previous session |
+| `/interview ` | Deep-dive interview to flesh out a spec or design |
+| `/document ` | Generate MDX documentation for a module or feature |
+| `/commit` | Create a conventional commit for staged changes |
+| `/create-plan ` | Create a new plan file in `.agents/plans/` |
+| `/create-scratch ` | Create a scratch file for notes in `.agents/scratches/` |
+| `/create-justification ` | Create a justification file in `.agents/justifications/` |
+
+### Typical Workflow
+
+1. **Create a plan**: Use `/create-plan my-feature` to draft a spec for a new feature
+2. **Flesh out the spec**: Use `/interview .agents/plans/.md` to refine requirements
+3. **Implement**: Use `/implement .agents/plans/.md` to build the feature
+4. **Continue if needed**: Use `/continue .agents/plans/.md` to pick up where you left off
+5. **Commit**: Use `/commit` to create a conventional commit
+
+### Agent Files
+
+The `.agents/` directory stores AI-generated artifacts:
+
+- **`.agents/plans/`** - Feature specs and implementation plans
+- **`.agents/scratches/`** - Temporary notes and explorations
+- **`.agents/justifications/`** - Decision rationale and technical justifications
+
+These files use a unique ID format (`{word}-{word}-{word}-{slug}.md`) to prevent conflicts.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000000..7c672b928f
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,38 @@
+# Security Policy
+
+We take the security of Documenso seriously. As a platform trusted with legally binding documents, the safety of the project and the people who rely on it is a priority for us. We're grateful to the security researchers who help keep it that way. If you've found an issue, we'd genuinely like to hear about it.
+
+## Reporting a Vulnerability
+
+Report security vulnerabilities privately. Do not open a public issue, discussion, or pull request for security reports.
+
+We accept reports through two channels, in order of preference:
+
+1. **GitHub Security Advisories (preferred)**. Use the [private vulnerability reporting form](https://github.com/documenso/documenso/security/advisories/new). This is our primary channel and lets us triage and work with you on a fix.
+2. **Email**. If you cannot use GitHub Security Advisories, email [security@documenso.com](mailto:security@documenso.com).
+
+Include the affected version, a clear description, steps to reproduce, and the potential impact.
+
+## Triage and Response
+
+We triage reports as we have availability. We read every report we receive, and we appreciate the time and effort it takes to put one together.
+
+We also run [Codex](https://openai.com/codex/) security analysis across the codebase. If Codex has already reported the issue you're sending us, we may close your report as a duplicate. Please don't take this as a reflection on your work; it just means our automated tooling happened to surface the same thing first.
+
+## Scope
+
+This policy covers vulnerabilities in the Documenso application code in this repository.
+
+The items below are out of scope and will not be accepted. They are deployment, infrastructure, and configuration concerns that belong with the operator's firewall, network, and environment setup, not the application:
+
+- Server-Side Request Forgery (SSRF) and related network-egress concerns
+- DNS rebinding and other DNS-level issues
+- Rate limiting, denial of service, and volumetric attacks
+- TLS and certificate configuration, HTTP security headers, and other reverse-proxy or web-server configuration
+- Findings that depend on insecure self-hosted infrastructure or misconfiguration
+
+If you're unsure whether something is in scope, report it privately anyway and we'll happily take a look.
+
+## Supported Versions
+
+Security fixes are applied to the latest release. Run the most recent version of Documenso.
diff --git a/SIGNING.md b/SIGNING.md
index f794bc9ba6..9aed0759a5 100644
--- a/SIGNING.md
+++ b/SIGNING.md
@@ -1,67 +1,9 @@
-# Creating your own signing certificate
+# Signing Certificate
-For the digital signature of your documents you need a signing certificate in .p12 format (public and private key). You can buy one (not recommended for dev) or use the steps to create a self-signed one:
+Documenso needs a signing certificate to digitally sign documents. For full, up-to-date instructions on generating, converting, and configuring a certificate, see the official documentation:
-1. Generate a private key using the OpenSSL command. You can run the following command to generate a 2048-bit RSA key:
+- [Signing Certificate](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate): Overview and all certificate options
+- [Local Certificate](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate/local): Generate a self-signed `.p12` certificate with OpenSSL
+- [Google Cloud HSM](https://docs.documenso.com/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm): Sign using Google Cloud KMS
- `openssl genrsa -out private.key 2048`
-
-2. Generate a self-signed certificate using the private key. You can run the following command to generate a self-signed certificate:
-
- `openssl req -new -x509 -key private.key -out certificate.crt -days 365`
-
- This will prompt you to enter some information, such as the Common Name (CN) for the certificate. Make sure you enter the correct information. The `-days` parameter sets the number of days for which the certificate is valid.
-
-3. Combine the private key and the self-signed certificate to create the p12 certificate. You can run the following commands to do this:
-
- ```bash
- # Set certificate password securely (won't appear in command history)
- read -s -p "Enter certificate password: " CERT_PASS
- echo
-
- # Create the p12 certificate using the environment variable
- openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt \
- -password env:CERT_PASS \
- -keypbe PBE-SHA1-3DES \
- -certpbe PBE-SHA1-3DES \
- -macalg sha1
- ```
-
-4. **IMPORTANT**: A certificate password is required to prevent signing failures. Make sure to use a strong password (minimum 4 characters) when prompted. Certificates without passwords will cause "Failed to get private key bags" errors during document signing.
-
-5. Place the certificate `/apps/remix/resources/certificate.p12` (If the path does not exist, it needs to be created)
-
-## Docker
-
-> We are still working on the publishing of docker images, in the meantime you can follow the steps below to create a production ready docker image.
-
-Want to create a production ready docker image? Follow these steps:
-
-- cd into `docker` directory
-- Make `build.sh` executable by running `chmod +x build.sh`
-- Run `./build.sh` to start building the docker image.
-- Publish the image to your docker registry of choice (or) If you prefer running the image from local, run the below command
-
-```
-docker run -d --restart=unless-stopped -p 3000:3000 -v documenso:/app/data --name documenso documenso:latest
-```
-
-Command Breakdown:
-
-- `-d` - Let's you run the container in background
-- `-p` - Passes down which ports to use. First half is the host port, Second half is the app port. You can change the first half anything you want and reverse proxy to that port.
-- `-v` - Volume let's you persist the data
-- `--name` - Name of the container
-- `documenso:latest` - Image you have built
-
-## Deployment
-
-We support a variety of deployment methods, and are actively working on adding more. Stay tuned for updates!
-
-## Railway
-
-[](https://railway.com/deploy/DjrRRX?referralCode=EZR3s0&utm_medium=integration&utm_source=template&utm_campaign=generic)
-
-## Render
-
-[](https://render.com/deploy?repo=https://github.com/documenso/documenso)
+For deploying Documenso with Docker, see the [Docker Deployment](https://docs.documenso.com/docs/self-hosting/deployment/docker) and [Docker Compose](https://docs.documenso.com/docs/self-hosting/deployment/docker-compose) guides.
diff --git a/apps/docs/README.md b/apps/docs/README.md
index 9b7bba9e09..770d324171 100644
--- a/apps/docs/README.md
+++ b/apps/docs/README.md
@@ -1,45 +1,16 @@
-# docs
+# @documenso/docs
-This is a Next.js application generated with
-[Create Fumadocs](https://github.com/fuma-nama/fumadocs).
+The Documenso documentation site, built with [Next.js](https://nextjs.org/) and [Fumadocs](https://fumadocs.dev/). Published at [docs.documenso.com](https://docs.documenso.com).
-Run development server:
+Content lives under `content/docs/` as MDX. See [WRITING_STYLE.md](../../WRITING_STYLE.md) for the documentation writing conventions.
```bash
-npm run dev
-# or
-pnpm dev
-# or
-yarn dev
+# From the monorepo root
+npm run dev --filter=@documenso/docs
```
-Open http://localhost:3000 with your browser to see the result.
+## Structure
-## Explore
-
-In the project, you can see:
-
-- `lib/source.ts`: Code for content source adapter, [`loader()`](https://fumadocs.dev/docs/headless/source-api) provides the interface to access your content.
-- `lib/layout.shared.tsx`: Shared options for layouts, optional but preferred to keep.
-
-| Route | Description |
-| ------------------------- | ------------------------------------------------------ |
-| `app/(home)` | The route group for your landing page and other pages. |
-| `app/docs` | The documentation layout and pages. |
-| `app/api/search/route.ts` | The Route Handler for search. |
-
-### Fumadocs MDX
-
-A `source.config.ts` config file has been included, you can customise different options like frontmatter schema.
-
-Read the [Introduction](https://fumadocs.dev/docs/mdx) for further details.
-
-## Learn More
-
-To learn more about Next.js and Fumadocs, take a look at the following
-resources:
-
-- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js
- features and API.
-- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
-- [Fumadocs](https://fumadocs.dev) - learn about Fumadocs
+- `content/docs/`: Documentation pages (MDX).
+- `lib/source.ts`: Content source adapter.
+- `lib/layout.shared.tsx`: Shared layout options.
diff --git a/apps/docs/content/docs/developers/api/rate-limits.mdx b/apps/docs/content/docs/developers/api/rate-limits.mdx
index ecd50558e7..95b0a68fe3 100644
--- a/apps/docs/content/docs/developers/api/rate-limits.mdx
+++ b/apps/docs/content/docs/developers/api/rate-limits.mdx
@@ -11,9 +11,14 @@ Documenso enforces rate limits on all API endpoints to ensure service stability.
## HTTP Rate Limits
-**Limit:** 100 requests per minute per IP address
+**Limit:** 1000 requests per minute per IP address
**Response:** 429 Too Many Requests
+
+ This is the global per-IP ceiling. Your organisation may have its own rate limits configured below
+ this value, in which case you can be rate-limited before reaching the global limit.
+
+
### Rate Limit Response
```json
@@ -65,3 +70,4 @@ When you exceed a resource limit:
- [Authentication](/docs/developers/getting-started/authentication) - API authentication guide
- [API Versioning](/docs/developers/api/versioning) - API version management
- [First API Call](/docs/developers/getting-started/first-api-call) - Getting started with the API
+- [Organisation Limits](/docs/self-hosting/configuration/organisation-limits) - Admins: set per-organisation resource quotas and rate limits (the HTTP rate limit above is separate and not admin-settable)
diff --git a/apps/docs/content/docs/developers/examples/common-workflows.mdx b/apps/docs/content/docs/developers/examples/common-workflows.mdx
index fe7887d5bc..e0447d7c48 100644
--- a/apps/docs/content/docs/developers/examples/common-workflows.mdx
+++ b/apps/docs/content/docs/developers/examples/common-workflows.mdx
@@ -472,7 +472,7 @@ Send the same document to multiple recipients in parallel. Useful for policy ack
distributeDocument: true
- Process in batches with a short delay to respect rate limits (e.g. 100 requests/minute)
+ Process in batches with a short delay to respect rate limits (e.g. 1000 requests/minute)
@@ -638,8 +638,8 @@ done
- The API allows 100 requests per minute. For large batches, implement rate limiting with delays
- between requests to avoid hitting limits.
+ The API allows 1000 requests per minute (your organisation may have its own lower limit). For large
+ batches, implement rate limiting with delays between requests to avoid hitting limits.
---
diff --git a/apps/docs/content/docs/developers/getting-started/first-api-call.mdx b/apps/docs/content/docs/developers/getting-started/first-api-call.mdx
index 5ae0a6c67e..665a90afd5 100644
--- a/apps/docs/content/docs/developers/getting-started/first-api-call.mdx
+++ b/apps/docs/content/docs/developers/getting-started/first-api-call.mdx
@@ -483,7 +483,7 @@ The API returns standard HTTP status codes and JSON error responses:
### Handling Rate Limits
-The API allows 100 requests per minute per IP address. When rate limited, wait at least 60 seconds before retrying:
+The API allows 1000 requests per minute per IP address. Your organisation may have its own lower rate limits. When rate limited, wait at least 60 seconds before retrying:
```javascript
async function fetchWithRetry(url, options, maxRetries = 3) {
diff --git a/apps/docs/content/docs/developers/local-development/index.mdx b/apps/docs/content/docs/developers/local-development/index.mdx
index 5714a018bb..3092da4152 100644
--- a/apps/docs/content/docs/developers/local-development/index.mdx
+++ b/apps/docs/content/docs/developers/local-development/index.mdx
@@ -15,16 +15,17 @@ Pick the one that fits your needs the best.
## Tech Stack
-- [Typescript](https://www.typescriptlang.org/) - Language
-- [React Router](https://reactrouter.com/) - Framework
+- [TypeScript](https://www.typescriptlang.org/) - Language
+- [React Router v7](https://reactrouter.com/) - Framework
+- [Hono](https://hono.dev/) - Server
- [Prisma](https://www.prisma.io/) - ORM
-- [Tailwind](https://tailwindcss.com/) - CSS
-- [shadcn/ui](https://ui.shadcn.com/) - Component Library
+- [Tailwind CSS](https://tailwindcss.com/) - CSS
+- [shadcn/ui](https://ui.shadcn.com/) + [Radix UI](https://www.radix-ui.com/) - Component Library
- [react-email](https://react.email/) - Email Templates
+- [Lingui](https://lingui.dev/) - Internationalization
- [tRPC](https://trpc.io/) - API
-- [@documenso/pdf-sign](https://www.npmjs.com/package/@documenso/pdf-sign) - PDF Signatures
-- [React-PDF](https://github.com/wojtekmaj/react-pdf) - Viewing PDFs
-- [PDF-Lib](https://github.com/Hopding/pdf-lib) - PDF manipulation
+- [@libpdf/core](https://www.npmjs.com/package/@libpdf/core) - PDF Signing and Manipulation
+- [pdf.js](https://mozilla.github.io/pdf.js/) - Viewing PDFs
- [Stripe](https://stripe.com/) - Payments
diff --git a/apps/docs/content/docs/policies/enterprise-edition.mdx b/apps/docs/content/docs/policies/enterprise-edition.mdx
index a611db5cd4..f821e73ec8 100644
--- a/apps/docs/content/docs/policies/enterprise-edition.mdx
+++ b/apps/docs/content/docs/policies/enterprise-edition.mdx
@@ -76,6 +76,8 @@ The Enterprise Edition is required when you:
4. Restart your Documenso instance
5. Verify the license is active in the **Admin Panel** under the **Stats** section
+ See [Apply Your License Key](/docs/self-hosting/configuration/license) for the full walkthrough, including how to enable individual features once licensed.
+
@@ -197,7 +199,7 @@ See [Support](/docs/policies/support) for complete support options.
1. Sign the Enterprise license agreement
2. Receive license key and access credentials
3. Deploy using [self-hosting guides](/docs/self-hosting) or access Documenso Cloud
- 4. Configure Enterprise features with support assistance
+ 4. Apply the key — see [Apply Your License Key](/docs/self-hosting/configuration/license) — and configure Enterprise features with support assistance
@@ -238,6 +240,7 @@ See [Support](/docs/policies/support) for complete support options.
## Related
+- [Apply Your License Key](/docs/self-hosting/configuration/license) - Step-by-step license activation
- [Community Edition](/docs/policies/community-edition) - AGPL-3.0 open-source license
- [Licenses](/docs/policies/licenses) - Complete licensing overview and FAQ
- [Support](/docs/policies/support) - Support channels and response times
diff --git a/apps/docs/content/docs/policies/fair-use.mdx b/apps/docs/content/docs/policies/fair-use.mdx
index 0c4de348d5..98d94dd5fb 100644
--- a/apps/docs/content/docs/policies/fair-use.mdx
+++ b/apps/docs/content/docs/policies/fair-use.mdx
@@ -41,12 +41,17 @@ When a limit is reached, requests return a `429 Too Many Requests` response with
| Action | Limit | Window |
| --- | --- | --- |
-| API requests (v1 and v2) | 100 requests | 1 minute |
+| API requests (v1 and v2) | 1000 requests | 1 minute |
| File uploads | 20 requests | 1 minute |
| AI features | 3 requests | 1 minute |
Authentication endpoints (login, signup, password reset, etc.) are also rate-limited to protect against abuse.
+
+ The API request limit above is the global per-IP ceiling. Individual organisations also have their
+ own rate limits, which may be configured below this value.
+
+
Rate limits may vary by plan. Enterprise plans can include higher or custom limits. Contact
[sales](https://documen.so/sales) for details.
diff --git a/apps/docs/content/docs/self-hosting/configuration/email.mdx b/apps/docs/content/docs/self-hosting/configuration/email.mdx
index bc7c729fab..d1c03e6cd6 100644
--- a/apps/docs/content/docs/self-hosting/configuration/email.mdx
+++ b/apps/docs/content/docs/self-hosting/configuration/email.mdx
@@ -278,7 +278,9 @@ Test your email configuration by creating an account or resetting a password. Th
### Using a Test SMTP Server
-For development or testing, use a local SMTP server like [Mailhog](https://github.com/mailhog/MailHog) or [Mailpit](https://github.com/axllent/mailpit):
+For development or testing, use a local SMTP server like [Inbucket](https://www.inbucket.org/), [Mailpit](https://github.com/axllent/mailpit), or [Mailhog](https://github.com/mailhog/MailHog). The default development setup (`docker/development/compose.yml`) already runs Inbucket, with its web UI on port 9000 and SMTP on port 2500.
+
+To run one standalone instead:
```bash
# Using Docker
diff --git a/apps/docs/content/docs/self-hosting/configuration/environment.mdx b/apps/docs/content/docs/self-hosting/configuration/environment.mdx
index 1d57b50626..bc4acd1a6e 100644
--- a/apps/docs/content/docs/self-hosting/configuration/environment.mdx
+++ b/apps/docs/content/docs/self-hosting/configuration/environment.mdx
@@ -86,6 +86,21 @@ Callback URL: `https:///api/auth/callback/microsoft`
| `NEXT_PRIVATE_OIDC_SKIP_VERIFY` | `false` | Skip email verification for OIDC accounts |
| `NEXT_PRIVATE_OIDC_PROMPT` | `login` | OIDC prompt parameter. Set to empty string to omit |
+### Webhooks
+
+| Variable | Default | Description |
+| --------------------------------------- | ------- | ------------------------------------------------------------------------ |
+| `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` | - | Comma-separated hostnames or IPs allowed to resolve to private addresses |
+
+Before delivering a webhook, Documenso checks whether the target resolves to a
+private or loopback address and blocks it if so. This check is best-effort and
+fails open. Use `NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS` to allow specific
+internal hosts, for example when delivering to a service on your own network:
+
+```bash
+NEXT_PRIVATE_WEBHOOK_SSRF_BYPASS_HOSTS="hooks.internal.example,10.0.0.5"
+```
+
---
## Email Configuration
@@ -186,9 +201,9 @@ Documenso requires a certificate to digitally sign documents.
### Transport Selection
-| Variable | Description | Default |
-| -------------------------------- | ---------------------------------------- | ------- |
-| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Signing backend: `local` or `gcloud-hsm` | `local` |
+| Variable | Description | Default |
+| -------------------------------- | ------------------------------------------------- | ------- |
+| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Signing backend: `local`, `gcloud-hsm`, or `csc` | `local` |
### Local Signing
@@ -210,11 +225,36 @@ Documenso requires a certificate to digitally sign documents.
| `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_CERT_CHAIN_CONTENTS` | Base64-encoded certificate chain |
| `NEXT_PRIVATE_SIGNING_GCLOUD_HSM_SECRET_MANAGER_CERT_PATH` | Google Secret Manager path for certificate retrieval |
+### Cloud Signature Consortium (CSC)
+
+Routes signing through a third-party Trust Service Provider for Advanced and Qualified Electronic Signatures (AES/QES). Instance-wide; set `NEXT_PRIVATE_SIGNING_TRANSPORT=csc` to enable. See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup walkthrough.
+
+CSC mode requires an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Without a valid license, the instance will refuse to start in `csc` mode.
+
+| Variable | Description | Default |
+| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ------- |
+| `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | Base URL of the CSC provider's API | |
+| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID` | OAuth client ID registered with the CSC provider | |
+| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET` | OAuth client secret registered with the CSC provider | |
+| `NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` | Default legal tier for new envelopes when the caller doesn't specify one. `AES` or `QES`. Explicit requests pass through. | `AES` |
+
+The OAuth callback URL registered with the CSC provider is fixed at `${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback` — register this exact URL with the TSP.
+
+#### Derived Public Variables
+
+The following client-visible variable is **derived automatically** from the private transport at server startup. Do not set it manually — any value set in the environment is overwritten on boot.
+
+| Variable | Derived from | Value |
+| ------------------------------------- | -------------------------------------------------- | ------------------------------------------------- |
+| `NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC` | `NEXT_PRIVATE_SIGNING_TRANSPORT === 'csc'` | `'true'` when CSC mode is active, else `'false'` |
+
+The authoring UI uses this flag to gate features that AES/QES envelopes cannot support (parallel signing, assistant role, dictate next signer). Deriving it from the private transport prevents the client-side flag from drifting from the real server-side configuration.
+
### Signature Options
| Variable | Description | Default |
| ------------------------------------------- | ----------------------------------------------------------- | ---------- |
-| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | Comma-separated timestamp authority URLs for LTV signatures | |
+| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | Comma-separated timestamp authority URLs for LTV signatures. Optional for `local` / `gcloud-hsm` (signatures omit the timestamp when unset). **Required** when `NEXT_PRIVATE_SIGNING_TRANSPORT=csc` — the instance refuses to start without it. See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes#timestamp-authority-resolution). | |
| `NEXT_PUBLIC_SIGNING_CONTACT_INFO` | Contact info embedded in PDF signatures | Webapp URL |
| `NEXT_PRIVATE_USE_LEGACY_SIGNING_SUBFILTER` | Use `adbe.pkcs7.detached` instead of `ETSI.CAdES.detached` | `false` |
@@ -232,6 +272,12 @@ For detailed certificate setup, see [Signing Certificate](/docs/self-hosting/con
| `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_DISABLE_SIGNIN` | Master switch. Disable all signin methods application-wide | `false` |
+| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin. Also closes `/forgot-password` and `/reset-password` | `false` |
+| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` |
+| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN` | Hide the Microsoft signin button | `false` |
+| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` |
+| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable the automatic `/signin` redirect when OIDC is the only enabled transport | `false` |
| `NEXT_PUBLIC_POSTHOG_KEY` | PostHog API key for analytics and feature flags | |
| `NEXT_PUBLIC_FEATURE_BILLING_ENABLED` | Enable billing features | `false` |
@@ -263,6 +309,44 @@ NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true"
NEXT_PUBLIC_DISABLE_SIGNUP="true"
```
+### Sign-in Restrictions
+
+You can control which methods are available for users to sign in with the following environment variables:
+
+- **`NEXT_PUBLIC_DISABLE_SIGNIN`** (master switch): Set to `true` to block all signin methods (email/password, Google, Microsoft, OIDC). Hides every signin entry point on `/signin` and rejects email/password signin server-side with a `SIGNIN_DISABLED` error.
+- **`NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN`**: Set to `true` to disable email/password signin only. The email/password form is hidden, the `/forgot-password` and `/reset-password` pages redirect to `/signin`, and the corresponding server endpoints reject requests. SSO signin is unaffected.
+- **`NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN`**, **`NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN`**, **`NEXT_PUBLIC_DISABLE_OIDC_SIGNIN`**: Set to `true` to hide the matching SSO button on the signin page. Useful when an SSO provider is kept configured for account linking but not advertised as a signin entry point.
+
+These flags are opt-in: when none are set, signin behaviour is unchanged from a stock Documenso instance.
+
+```bash
+# Allow only OIDC signin (e.g. enterprise SSO-only)
+NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true"
+NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true"
+NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true"
+
+# Or disable signin entirely
+NEXT_PUBLIC_DISABLE_SIGNIN="true"
+```
+
+### OIDC Auto-redirect
+
+When OIDC is the only enabled signin transport on your instance, `/signin` automatically redirects users straight to the OIDC provider instead of showing the signin form. The page renders a spinner while the redirect happens. No extra configuration is required — disabling every other signin method is enough to trigger it.
+
+- **`NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT`**: Set to `true` to opt out of the automatic redirect and keep rendering the signin page even when OIDC is the only enabled transport.
+
+The redirect only triggers when OIDC is configured and email/password, Google, and Microsoft signin are all disabled. If any other transport remains enabled, the signin form is shown as normal.
+
+```bash
+# OIDC-only signin: disabling all other methods auto-redirects to the provider
+NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true"
+NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true"
+NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true"
+
+# Opt out of the auto-redirect while still OIDC-only
+# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT="true"
+```
+
---
## AI Features
@@ -359,11 +443,11 @@ Telemetry collects only: app version, installation ID, and node ID. No personal
## Enterprise Features
-These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance.
+These variables require an active [Enterprise Edition](/docs/policies/enterprise-edition) license. Obtain a license key from [license.documenso.com](https://license.documenso.com) and set it below to unlock enterprise features such as SSO, embed editor, and 21 CFR Part 11 compliance. See [Apply Your License Key](/docs/self-hosting/configuration/license) for step-by-step setup.
| Variable | Description |
| ------------------------------------ | ------------------------------------------------ |
-| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features |
+| `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` | License key for enterprise features — see [Apply Your License Key](/docs/self-hosting/configuration/license) for how to apply it |
| `NEXT_PRIVATE_STRIPE_API_KEY` | Stripe API key for billing |
| `NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET` | Stripe webhook secret |
| `NEXT_PRIVATE_SES_ACCESS_KEY_ID` | AWS SES access key for email domain verification |
@@ -406,6 +490,16 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password"
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP="true"
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP="true"
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS="example.com,acme.org"
+
+# Sign-in restrictions (optional)
+# NEXT_PUBLIC_DISABLE_SIGNIN="true"
+# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN="true"
+# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN="true"
+# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN="true"
+# NEXT_PUBLIC_DISABLE_OIDC_SIGNIN="true"
+
+# Opt out of the automatic OIDC redirect when OIDC is the only enabled transport (optional)
+# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT="true"
```
---
@@ -416,4 +510,5 @@ NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password"
- [Email Configuration](/docs/self-hosting/configuration/email) - Configure email delivery
- [Storage Configuration](/docs/self-hosting/configuration/storage) - Set up S3 storage
- [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) - Configure document signing
+- [Organisation Limits](/docs/self-hosting/configuration/organisation-limits) - Set per-organisation document, email, and API limits from the admin panel
- [Troubleshooting](/docs/self-hosting/maintenance/troubleshooting) - Common configuration issues
diff --git a/apps/docs/content/docs/self-hosting/configuration/index.mdx b/apps/docs/content/docs/self-hosting/configuration/index.mdx
index 0e80e0efc6..de5830a4d0 100644
--- a/apps/docs/content/docs/self-hosting/configuration/index.mdx
+++ b/apps/docs/content/docs/self-hosting/configuration/index.mdx
@@ -29,6 +29,11 @@ description: Configure your self-hosted Documenso instance with environment vari
description="Digital signature certificate setup."
href="/docs/self-hosting/configuration/signing-certificate"
/>
+
## Required Configuration
diff --git a/apps/docs/content/docs/self-hosting/configuration/license.mdx b/apps/docs/content/docs/self-hosting/configuration/license.mdx
new file mode 100644
index 0000000000..eea50c1545
--- /dev/null
+++ b/apps/docs/content/docs/self-hosting/configuration/license.mdx
@@ -0,0 +1,107 @@
+---
+title: Apply Your License Key
+description: Activate your Enterprise license key to unlock enterprise features on your self-hosted instance.
+---
+
+import { Accordion, Accordions } from 'fumadocs-ui/components/accordion';
+import { Callout } from 'fumadocs-ui/components/callout';
+import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
+
+A license key activates the Enterprise features available to your self-hosted instance, such as CSC signing, SSO, embed white-labelling, and 21 CFR Part 11 compliance.
+
+
+ The license key applies to your **whole instance**, not an individual user account. There's one
+ key per deployment.
+
+
+## Prerequisites
+
+- An active Enterprise license key — contact [sales](https://documen.so/enterprise) to set up an
+ Enterprise subscription, then copy your key from [license.documenso.com](https://license.documenso.com).
+ See [Enterprise Edition](/docs/policies/enterprise-edition) for details.
+- A running self-hosted Documenso instance that you're able to restart
+
+## Step 1: Set the environment variable
+
+Set `NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY` to your license key.
+
+
+
+
+Add the variable to your `.env` file (or directly under `environment:` in `compose.yml`):
+
+```bash
+NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here
+```
+
+Then apply it:
+
+```bash
+docker compose up -d
+```
+
+
+
+
+```bash
+docker run -d \
+ --name documenso \
+ -e NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here \
+ documenso/documenso:latest
+```
+
+
+
+
+If you're running Documenso directly (not in a container), add the variable to your `.env` file:
+
+```bash
+NEXT_PRIVATE_DOCUMENSO_LICENSE_KEY=your-license-key-here
+```
+
+
+
+
+## Step 2: Restart the instance
+
+The license key is only read once, at process startup. Setting the variable in a running container or shell has no effect until the process restarts.
+
+```bash
+# Docker Compose
+docker compose restart documenso
+
+# Docker
+docker restart documenso
+```
+
+On startup, Documenso validates the key against the Documenso license server and caches the result locally for future startups, so a brief license-server outage won't lock you out.
+
+## What the license enables
+
+A valid license doesn't turn every enterprise feature on everywhere — activation depends on the feature:
+
+- **CSC signing** activates instance-wide automatically once the license is active and CSC transport is configured. See [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) for the full setup.
+- **SSO, embed white-labelling, 21 CFR Part 11, and similar** are provisioned per organisation. Follow each feature's own guide to configure it once the license is active.
+
+## Troubleshooting
+
+
+
+ - Confirm the key is present in the environment the running process actually reads — `docker
+ exec` into the container and check `env | grep LICENSE` if unsure.
+ - Confirm the instance was fully restarted after the variable was set, not just reloaded.
+ - Re-copy the key to rule out truncation or accidental whitespace.
+
+
+ Instance-wide features (like CSC signing) also need their own configuration — an active license
+ alone isn't enough. Check that feature's guide to confirm the required settings are in place.
+ Per-organisation features additionally need to be provisioned for the organisation that's using
+ them.
+
+
+
+## See Also
+
+- [Environment Variables](/docs/self-hosting/configuration/environment) - Complete configuration reference
+- [Enterprise Edition](/docs/policies/enterprise-edition) - What's included and how to purchase a license
+- [CSC / QES Signing](/docs/self-hosting/configuration/signing-certificate/csc-qes) - Enable CSC-based signing
diff --git a/apps/docs/content/docs/self-hosting/configuration/meta.json b/apps/docs/content/docs/self-hosting/configuration/meta.json
index 32b92f8536..6cc250f648 100644
--- a/apps/docs/content/docs/self-hosting/configuration/meta.json
+++ b/apps/docs/content/docs/self-hosting/configuration/meta.json
@@ -2,12 +2,14 @@
"title": "Configuration",
"pages": [
"environment",
+ "license",
"database",
"email",
"storage",
"background-jobs",
"signing-certificate",
"telemetry",
+ "organisation-limits",
"advanced"
]
}
diff --git a/apps/docs/content/docs/self-hosting/configuration/organisation-limits.mdx b/apps/docs/content/docs/self-hosting/configuration/organisation-limits.mdx
new file mode 100644
index 0000000000..c225975e5f
--- /dev/null
+++ b/apps/docs/content/docs/self-hosting/configuration/organisation-limits.mdx
@@ -0,0 +1,111 @@
+---
+title: Organisation Limits
+description: View and set per-organisation document, email, and API limits on a self-hosted Documenso instance using the admin panel's subscription claims.
+---
+
+import { Callout } from 'fumadocs-ui/components/callout';
+
+Per-organisation limits — document, email, and API usage, plus feature toggles and team/member caps — are controlled by **subscription claims**. You configure them in the admin panel, not through environment variables.
+
+There are three distinct kinds of limit:
+
+| Limit | Caps | Admin-settable |
+| ---------------------- | ------------------------------------------------- | ----------------------- |
+| Resource quota | Documents, emails, and API requests **per month** | Yes — per claim and org |
+| Resource rate limit | The same resources over a short window (e.g. `1h`) | Yes — per claim and org |
+| Global HTTP rate limit | API requests per IP (1000/min, hardcoded) | No — see [Limitations](#limitations) |
+
+## Prerequisites
+
+- A running self-hosted Documenso instance.
+- An account with the **`ADMIN`** role — an account-level role, separate from organisation and team roles. New accounts are created with the `USER` role only. Grant the first admin by adding `ADMIN` to that user's `roles` directly in the database; after that, an existing admin can grant the role to others under **Admin Panel > Users > _(user)_ > Roles > Update user**.
+
+Open the admin panel at `/admin`. The sidebar sections used below are **Claims**, **Organisations**, and **Organisation Stats**.
+
+## Viewing usage
+
+**One organisation:** open **Admin Panel > Organisations** and select it. The **Organisation usage** section shows the current period's document, email, and API usage against its quotas.
+
+**All organisations:** open **Admin Panel > Organisation Stats** to sort and filter monthly usage. Filter by **claim** and by **period** (a UTC calendar month, shown as `YYYY-MM`), and switch between **Show usage**, **Show usage with quotas**, and **Show daily averages**.
+
+
+ Usage counts **attempts**, not only successful actions. A request that exceeds a quota is still counted before it is rejected, so displayed usage can read higher than the number of actions that succeeded.
+
+
+## Subscription claims
+
+A subscription claim is a named bundle of limits and feature flags (for example `Free`, `Individual`, `Teams`, `Platform`, or `Enterprise`). Claims are **templates**: when an organisation is created it receives a private copy of its claim and reads from that copy afterwards. Editing a claim template therefore affects organisations created later, not existing ones — to change an existing organisation, [edit it directly](#change-limits-for-one-organisation).
+
+### Claim fields
+
+Under **Admin Panel > Claims** (`/admin/claims`), each claim has:
+
+| Field | Controls |
+| ----------------------- | --------------------------------------------------------------------------------- |
+| **Name** | The claim's display name. |
+| **Team Count** | Teams allowed. `0` = unlimited. |
+| **Member Count** | Members allowed. `0` = unlimited. |
+| **Envelope Item Count** | Uploaded files allowed per envelope. Minimum `1`. |
+| **Recipient Count** | Recipients allowed per document. `0` = unlimited. |
+| **Feature Flags** | Feature toggles (see [Feature flags](#feature-flags)). |
+| **Limits** | Monthly quota and rate-limit windows for Documents, Emails, and API. |
+| **Email transport** | Transport the claim uses. *Default (system mailer)* uses the instance default. |
+
+### Quotas and rate limits
+
+The **Limits** section has a column for **Documents**, **Emails**, and **API**, each with two controls:
+
+- **Monthly quota** — how many of that resource are allowed per calendar month. An **empty** field is unlimited; **`0`** blocks the resource entirely.
+- **Rate limit windows** — optional short-window caps, each a duration and a maximum. A window is a number and a unit (`s`, `m`, `h`, `d`), such as `5m`, `1h`, or `24h`, and must be unique within the resource.
+
+
+ Quotas and counts use opposite conventions for "unlimited": an **empty** quota is unlimited (and `0` blocks the resource), whereas `0` in the **Team**, **Member**, and **Recipient Count** fields means unlimited.
+
+
+### Feature flags
+
+The **Feature Flags** section toggles capabilities such as Unlimited documents, Branding, Hide Documenso branding, Email domains, Embed authoring, Embed signing, White label for embed authoring/signing, 21 CFR, HIPAA, Authentication portal, Allow Legacy Envelopes, Signing reminders, QES signing, and Disable emails.
+
+Some flags are Enterprise features. If your license does not include one, it is marked and cannot be enabled (you can still turn it off). See [Enterprise Edition](/docs/policies/enterprise-edition).
+
+### Create or edit a claim template
+
+1. Go to **Admin Panel > Claims**.
+2. Select **New claim**, or select an existing claim to edit it.
+3. Set the counts, feature flags, and the **Limits** section.
+4. Save. Changes apply to organisations created afterwards, not existing ones.
+
+### Change limits for one organisation
+
+To change limits for an existing organisation, edit it directly rather than its claim template.
+
+1. Go to **Admin Panel > Organisations** and open the organisation.
+2. Adjust its quota, rate-limit, feature-flag, or email-transport fields.
+3. Save. Changes take effect immediately.
+
+The organisation also shows the **Inherited subscription claim** it was created from.
+
+## Usage reset
+
+Monthly quota usage is keyed to the **UTC calendar month**. There is no scheduled reset job — when the month rolls over, the new period's counter starts at `0`.
+
+## Limitations
+
+The **global HTTP rate limit is not configurable.** Documenso enforces a hardcoded **1000 requests per minute per IP address** on its API endpoint groups (`/api/v1`, `/api/v2`, and the tRPC API are limited separately), returning `429 Too Many Requests`. It is a per-IP safeguard applied at the HTTP layer — not per-organisation, not stored on any claim, and not adjustable from the admin panel. See [Rate Limits](/docs/developers/api/rate-limits).
+
+## Troubleshooting
+
+| Symptom | Cause and fix |
+| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
+| An organisation hit its limit unexpectedly | Usage counts rejected over-quota attempts. Compare usage against the quota under **Organisation Stats > Show usage with quotas**. |
+| A resource is blocked entirely, not just capped | The **Monthly quota** is `0`, which blocks the resource. Leave it empty for unlimited. |
+| Emails are not sending for an organisation | Check whether the **Disable emails** flag is enabled on the organisation's claim — it blocks all emails regardless of quota. |
+| A claim template edit had no effect | Template edits are not retroactive. Edit the organisation directly under **Admin Panel > Organisations**. |
+
+---
+
+## See Also
+
+- [Environment Variables](/docs/self-hosting/configuration/environment) - All configuration options
+- [Rate Limits](/docs/developers/api/rate-limits) - The global HTTP API rate limit (separate from claims)
+- [Enterprise Edition](/docs/policies/enterprise-edition) - Features unlocked by license flags
diff --git a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/csc-qes.mdx b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/csc-qes.mdx
new file mode 100644
index 0000000000..4998baaf74
--- /dev/null
+++ b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/csc-qes.mdx
@@ -0,0 +1,213 @@
+---
+title: CSC (AES / QES)
+description: Configure Cloud Signature Consortium signing for Advanced and Qualified Electronic Signatures via a third-party Trust Service Provider.
+---
+
+import { Callout } from 'fumadocs-ui/components/callout';
+import { Step, Steps } from 'fumadocs-ui/components/steps';
+
+The `csc` signing transport routes signatures through a third-party Trust Service Provider (TSP) using the [Cloud Signature Consortium API v1.0.4.0](https://cloudsignatureconsortium.org/). Each recipient authenticates directly with the TSP (Strong Customer Authentication) and the TSP returns a per-recipient signature bound to the document hash. Documenso assembles the resulting PAdES signature inside the PDF.
+
+This transport enables **Advanced Electronic Signatures (AES)** and **Qualified Electronic Signatures (QES)** under eIDAS. See [Signature Levels](/docs/compliance/signature-levels) for the legal framework.
+
+
+ CSC mode is **instance-wide**: one CSC provider per Documenso install. All envelopes created
+ while the instance runs in `csc` mode use AES or QES. Switching `NEXT_PRIVATE_SIGNING_TRANSPORT`
+ is a one-way operational migration — see [Switching Transports](#switching-transports).
+
+
+
+ CSC mode requires an active [Enterprise Edition](/docs/policies/enterprise-edition) license. The
+ instance refuses to start in `csc` mode without it.
+
+
+## Prerequisites
+
+{/* prettier-ignore */}
+
+
+
+### A TSP account
+
+Establish a relationship with a CSC-compatible Trust Service Provider. The TSP issues qualified or advanced certificates to your signers, holds the private keys in its HSM, and exposes a CSC v1.0.4.0-compliant API.
+
+
+
+
+### OAuth client credentials
+
+Register Documenso as an OAuth client with the TSP. You will receive a client ID and client secret, and must supply Documenso's callback URL when registering:
+
+```
+${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback
+```
+
+The callback URL is fixed — Documenso derives it from `NEXT_PUBLIC_WEBAPP_URL` and the route mount path. There is no env var to override it; ensuring the registered URL matches your instance's webapp URL exactly is the operator's responsibility.
+
+
+
+
+### Enterprise Edition license
+
+CSC mode is gated by the `instanceCscSigning` license flag. Without a valid Enterprise license, the transport refuses to start (`CSC_UNLICENSED`). See [Apply Your License Key](/docs/self-hosting/configuration/license) to activate one.
+
+
+
+
+### S3 storage (strongly recommended)
+
+CSC produces multiple `DocumentData` rows per envelope item (one per recipient signature, plus the materialised and source rows). Database-backed storage base64-inflates each row by ~33% and is impractical at meaningful PDF sizes. Configure [S3 storage](/docs/self-hosting/configuration/storage) before enabling CSC.
+
+
+
+
+## Environment Variables
+
+| Variable | Description | Default |
+| ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | ------- |
+| `NEXT_PRIVATE_SIGNING_TRANSPORT` | Set to `csc` | |
+| `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` | Base URL of the CSC provider's API | |
+| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID` | OAuth client ID registered with the CSC provider | |
+| `NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET` | OAuth client secret registered with the CSC provider | |
+| `NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` | Default legal tier for new envelopes when the caller does not specify one. `AES` or `QES`. Explicit requests always pass through. | `AES` |
+| `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` | **Required.** Comma-separated RFC 3161 TSA URLs. Always used for B-LTA archival timestamps at seal time, and also serves as the B-T sign-time fallback when the TSP does not expose `signatures/timestamp`. The instance refuses to start in CSC mode without it. See [Timestamp Authority Resolution](#timestamp-authority-resolution). | |
+
+
+ `NEXT_PUBLIC_SIGNING_TRANSPORT_IS_CSC` is set automatically from
+ `NEXT_PRIVATE_SIGNING_TRANSPORT` at server startup. Do not set it manually — see
+ [Environment Variables](/docs/self-hosting/configuration/environment#derived-public-variables).
+
+
+## Configuration Example
+
+```bash
+NEXT_PRIVATE_SIGNING_TRANSPORT=csc
+NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL=https://api.example-tsp.com/csc/v1
+NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_ID=documenso-prod
+NEXT_PRIVATE_SIGNING_CSC_OAUTH_CLIENT_SECRET=...
+NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL=QES
+NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY=http://timestamp.example.com
+```
+
+Register `${NEXT_PUBLIC_WEBAPP_URL}/api/csc/oauth/callback` (e.g. `https://sign.example.com/api/csc/oauth/callback`) as the OAuth callback URL with the TSP.
+
+## Default Signature Level
+
+`NEXT_PRIVATE_SIGNING_CSC_SIGNATURE_LEVEL` selects the legal tier applied to envelopes that do not specify one explicitly. It is a default, not a capability gate: callers may still create AES or QES envelopes explicitly regardless of this setting.
+
+| Configured value | Caller passes nothing | Caller passes `AES` | Caller passes `QES` |
+| ---------------- | --------------------- | ------------------- | ------------------- |
+| `AES` (default) | Envelope is `AES` | Envelope is `AES` | Envelope is `QES` |
+| `QES` | Envelope is `QES` | Envelope is `AES` | Envelope is `QES` |
+
+Any value other than `AES` or `QES` causes the instance to refuse to start. This prevents silent qualified-to-advanced downgrades from a typo.
+
+## Timestamp Authority Resolution
+
+AES/QES envelopes use TSA-attested timestamps in two distinct phases. Resolution differs per phase.
+
+### Sign time — PAdES B-T per recipient
+
+Each recipient's CMS embeds a signature timestamp (CMS unsigned attribute) so proven time is bound to the recipient's signature itself. Resolution order:
+
+1. If the TSP advertises `signatures/timestamp` in its `info` response (CSC §11.10), the TSP endpoint is used. The call is authorised with **this recipient's** service-scope bearer token — the same one authorising the `signatures/signHash` call alongside it.
+2. Otherwise, the first URL from `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is used (RFC 3161 over HTTP).
+
+Selection is made at boot from the discovered transport, not at runtime; there is no try-then-fall-through. If the chosen source fails, the recipient's sign attempt fails.
+
+### Seal time — PAdES B-LTA archival
+
+The seal-document job emits a single archival `/DocTimeStamp` over the fully-signed envelope (plus DSS for the existing signatures and the timestamp's own chain). This phase is **env-only**: the first URL from `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is always used.
+
+The archival anchor is the operator's long-term trust anchor and SHOULD point at a dedicated qualified archival TSA (e.g. DigiCert) independent of the per-recipient TSP. We deliberately do not fall back to the TSP at seal time: archive longevity should not be coupled to a TSP that may rotate or revoke, and the seal-document job has no recipient context to carry a service-scope bearer.
+
+### Boot-time guard
+
+The instance refuses to start in CSC mode unless `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is set (`CSC_PROVIDER_NO_TSA` at transport construction). The env var is required unconditionally — even when the TSP advertises its own `signatures/timestamp`, seal-time B-LTA archival uses the env TSA. Catching this at boot prevents the failure mode where an envelope signs successfully at B-T and then hangs in `WAITING_FOR_SIGNATURE_COMPLETION` when the seal job throws.
+
+## Switching Transports
+
+`NEXT_PRIVATE_SIGNING_TRANSPORT` is a one-way operational migration. Existing envelopes route per the `signatureLevel` column they were created with — the runtime branching looks at the envelope, not the env var. After a switch:
+
+- Envelopes already at `SES` continue to use the new transport for sealing, but the new transport's signer must produce SES-compatible signatures (only `local` and `gcloud-hsm` qualify).
+- Envelopes already at `AES` / `QES` will fail at sign or seal time if the new transport is not `csc`.
+
+Plan migrations during a quiet window with no in-flight envelopes.
+
+## Behavioural Notes
+
+CSC mode changes a number of envelope-authoring behaviours that operators should communicate to users.
+
+### Mutation lock at distribution
+
+For AES/QES envelopes, all authoring routes refuse mutations once the envelope leaves DRAFT. This locks the PDF before any recipient begins Strong Customer Authentication, closing the PDF-swap window that would otherwise allow an owner to replace the PDF between view and sign and break the legal "what you see is what you sign" guarantee.
+
+In practice: edit envelope, recipients, fields, and items freely while DRAFT; once sent, no changes are accepted (including from the API).
+
+### Sequential signing only
+
+Parallel signing produces conflicting incremental updates over the same base PDF, breaking the per-recipient `/ByteRange` invariant. The signing order is forced to `SEQUENTIAL` on AES/QES envelopes — at the schema layer, at send time, and in the UI (the parallel-signing toggle is hidden).
+
+### Assistant role and Dictate Next Signer disabled
+
+Both features modify the recipient set after the envelope is sent, which is incompatible with the AES/QES mutation lock. They are hidden in the UI and rejected at the server schema layer.
+
+### Sidecar PDFs at download
+
+The signed PDF must remain byte-identical to what each recipient's TSP signature authorised — Documenso cannot decorate it after signing. Audit logs and the Certificate of Completion are generated on demand and delivered as separate PDFs:
+
+- `GET /sign/{token}/download` returns the signed PDF only (or a ZIP for multi-item envelopes).
+- `GET /sign/{token}/download?version=bundle` returns a ZIP containing the signed PDFs, audit log PDF, and Certificate of Completion.
+- The completion email attaches all three.
+
+## Recipient Flow
+
+For context when supporting end users, here is what a recipient experiences on an AES/QES envelope:
+
+1. Opens the email link, lands on the signing page.
+2. Documenso redirects to the TSP for Strong Customer Authentication (first visit only; cached for the session lifetime).
+3. Fills fields as normal.
+4. Clicks Sign → redirected to the TSP for a second authentication round (issues a per-document Signature Activation Data token).
+5. Returns to Documenso; the signing call completes within ~15 seconds.
+6. Sees the standard completion screen.
+
+If the TSP returns no eligible credentials for the recipient (e.g. they have not enrolled), they see a blocking page directing them to enrol with the TSP and retry.
+
+## Error Codes
+
+CSC-specific error codes surfaced through the standard error channels:
+
+| Code | Meaning | Recovery |
+| -------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------- |
+| `CSC_UNLICENSED` | License flag absent at transport-create | Operator: enable Enterprise Edition, restart |
+| `CSC_PROVIDER_INFO_FAILED` | `info` discovery failed at startup | Operator: check TSP availability and `NEXT_PRIVATE_SIGNING_CSC_PROVIDER_BASE_URL` |
+| `CSC_PROVIDER_NO_TSA` | `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` is unset | Operator: configure `NEXT_PRIVATE_SIGNING_TIMESTAMP_AUTHORITY` |
+| `CSC_CREDENTIAL_LIST_EMPTY`| TSP returned no credentials for the user | Recipient: enrol with the TSP |
+| `CSC_CERT_INVALID` | Certificate refused at credential validation | Recipient: contact the TSP |
+| `CSC_ALGORITHM_REFUSED` | Signature algorithm fails policy | Operator/recipient: TSP does not meet policy (see below) |
+| `CSC_SAD_EXPIRED_PRE_SIGN` | Signature Activation Data expired before signing | Recipient: retry from Sign |
+| `CSC_TSP_TIMEOUT` | 15-second synchronous timeout reached | Recipient: retry (idempotent — the TSP enforces single-use SAD binding) |
+| `CSC_EMBED_FAILED` | Sign-time digest diverged from prep capture | Recipient: retry from Sign |
+| `CSC_BASE_DOCUMENT_MUTATED`| Document data changed between prep and sign | Operator: investigate (structural guard violation) |
+| `CSC_INSTANCE_MODE_MISMATCH`| Envelope created with wrong level for transport | Caller: use a level matching the instance transport |
+| `CSC_REQUEST_FAILED` | TSP HTTP transport failure — network error, non-2xx, or malformed response | Operator: check TSP availability; carries the TSP HTTP status and error in the message |
+
+## Algorithm Policy
+
+Documenso refuses TSP credentials that do not meet the following minimums, at the OAuth callback boundary and again at sign time:
+
+| Class | Allowed | Refused |
+| ----- | ---------------------------------- | ------------------------------------------------------ |
+| RSA | `key.len >= 2048` | Missing `key.len`, `key.len < 2048` |
+| ECDSA | P-256, P-384, P-521 | Missing `key.curve`, P-192, P-224, other curves |
+| Hash | SHA-256, SHA-384, SHA-512 | SHA-1, MD5 |
+| Other | — | DSA |
+
+This is the union of CSC v1.0.4.0 §11.5 requirements and current cryptographic guidance.
+
+## Related
+
+- [Signature Levels](/docs/compliance/signature-levels) — AES / QES legal framework
+- [Signing Certificate](/docs/self-hosting/configuration/signing-certificate) — overview of all signing transports
+- [Environment Variables](/docs/self-hosting/configuration/environment) — full env reference
+- [Enterprise Edition](/docs/policies/enterprise-edition) — license requirements
diff --git a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/index.mdx b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/index.mdx
index 89a12a3270..a2d5a0a7cc 100644
--- a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/index.mdx
+++ b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/index.mdx
@@ -24,6 +24,11 @@ Self-hosted Documenso instances require a signing certificate. You can generate
description="Hardware-based key protection with Google Cloud KMS."
href="/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm"
/>
+
+
A self-signed certificate is sufficient for most use cases where your industry has no special signing regulations.
@@ -79,6 +84,18 @@ For organisations requiring hardware-based key protection, Documenso supports Go
See [Google Cloud HSM](/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm) for setup instructions.
+
+
+
+For Advanced and Qualified Electronic Signatures under eIDAS, Documenso integrates with third-party Trust Service Providers via the Cloud Signature Consortium API. Each recipient authenticates directly with the TSP, which holds the private key and issues the signature.
+
+- Per-recipient identity verification by an accredited TSP
+- Legally equivalent to a handwritten signature within the EU (QES)
+- Requires an [Enterprise Edition](/docs/policies/enterprise-edition) license
+- Instance-wide setting; one CSC provider per Documenso install
+
+See [CSC (AES / QES)](/docs/self-hosting/configuration/signing-certificate/csc-qes) for setup instructions.
+
diff --git a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/meta.json b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/meta.json
index d37038af32..b52f83f9e2 100644
--- a/apps/docs/content/docs/self-hosting/configuration/signing-certificate/meta.json
+++ b/apps/docs/content/docs/self-hosting/configuration/signing-certificate/meta.json
@@ -1,4 +1,4 @@
{
"title": "Signing Certificate",
- "pages": ["...index", "local", "google-cloud-hsm", "timestamp-server", "troubleshooting"]
+ "pages": ["...index", "local", "google-cloud-hsm", "csc-qes", "timestamp-server", "troubleshooting"]
}
diff --git a/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx b/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx
index b3590a7f2f..84e2281157 100644
--- a/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx
+++ b/apps/docs/content/docs/self-hosting/deployment/docker-compose.mdx
@@ -163,6 +163,19 @@ NEXT_PUBLIC_DISABLE_SIGNUP=false
# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNUP=true
# NEXT_PUBLIC_DISABLE_OIDC_SIGNUP=true
# NEXT_PRIVATE_ALLOWED_SIGNUP_DOMAINS=example.com,acme.org
+
+# Signin restrictions (optional)
+# Master switch — disables every signin method
+# NEXT_PUBLIC_DISABLE_SIGNIN=true
+# Per-method switches (optional). Each disables that signin path.
+# NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN=true
+# NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN=true
+# NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN=true
+# NEXT_PUBLIC_DISABLE_OIDC_SIGNIN=true
+
+# When OIDC is the only enabled transport, /signin auto-redirects to the provider.
+# Set this to opt out and keep showing the signin page (optional).
+# NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT=true
```
Generate secure secrets using: `openssl rand -base64 32`
diff --git a/apps/docs/content/docs/self-hosting/deployment/docker.mdx b/apps/docs/content/docs/self-hosting/deployment/docker.mdx
index 7d6d4b9cd9..68508e7678 100644
--- a/apps/docs/content/docs/self-hosting/deployment/docker.mdx
+++ b/apps/docs/content/docs/self-hosting/deployment/docker.mdx
@@ -112,6 +112,12 @@ See [Email Configuration](/docs/self-hosting/configuration/email) for other tran
| `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_PUBLIC_DISABLE_SIGNIN` | Master switch — disable all signin methods | `false` |
+| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin only | `false` |
+| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` |
+| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN` | Hide the Microsoft signin button | `false` |
+| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` |
+| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable auto-redirect to OIDC when it is the only transport | `false` |
For the complete list, see [Environment Variables](/docs/self-hosting/configuration/environment).
diff --git a/apps/docs/content/docs/self-hosting/deployment/kubernetes.mdx b/apps/docs/content/docs/self-hosting/deployment/kubernetes.mdx
index d8adb8ac9e..e7a18490fd 100644
--- a/apps/docs/content/docs/self-hosting/deployment/kubernetes.mdx
+++ b/apps/docs/content/docs/self-hosting/deployment/kubernetes.mdx
@@ -235,7 +235,7 @@ spec:
```
- Pin to a specific image tag (e.g., `documenso/documenso:1.5.0`) in production instead of `latest`
+ Pin to a specific image tag (e.g., `documenso/documenso:`) in production instead of `latest`
to ensure predictable deployments.
diff --git a/apps/docs/content/docs/self-hosting/deployment/manual.mdx b/apps/docs/content/docs/self-hosting/deployment/manual.mdx
index bdd51fd683..d6dc4fda5b 100644
--- a/apps/docs/content/docs/self-hosting/deployment/manual.mdx
+++ b/apps/docs/content/docs/self-hosting/deployment/manual.mdx
@@ -14,8 +14,8 @@ import { Step, Steps } from 'fumadocs-ui/components/steps';
## Prerequisites
-- Node.js 20 or later
-- npm
+- Node.js 22 or later
+- npm 11 or later
- PostgreSQL 14 or later
- A Linux server (for systemd service setup)
diff --git a/apps/docs/content/docs/self-hosting/deployment/railway.mdx b/apps/docs/content/docs/self-hosting/deployment/railway.mdx
index 81392a37d0..501edca1c9 100644
--- a/apps/docs/content/docs/self-hosting/deployment/railway.mdx
+++ b/apps/docs/content/docs/self-hosting/deployment/railway.mdx
@@ -159,6 +159,12 @@ NEXT_PRIVATE_SMTP_FROM_ADDRESS=noreply@yourdomain.com
| `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_PUBLIC_DISABLE_SIGNIN` | Master switch — disable all signin methods | `false` |
+| `NEXT_PUBLIC_DISABLE_EMAIL_PASSWORD_SIGNIN` | Disable email/password signin only | `false` |
+| `NEXT_PUBLIC_DISABLE_GOOGLE_SIGNIN` | Hide the Google signin button | `false` |
+| `NEXT_PUBLIC_DISABLE_MICROSOFT_SIGNIN`| Hide the Microsoft signin button | `false` |
+| `NEXT_PUBLIC_DISABLE_OIDC_SIGNIN` | Hide the OIDC signin button | `false` |
+| `NEXT_PUBLIC_DISABLE_OIDC_AUTO_REDIRECT` | Disable auto-redirect to OIDC when it is the only transport | `false` |
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for signing certificate | - |
| `DOCUMENSO_DISABLE_TELEMETRY` | Disable anonymous telemetry | `false` |
diff --git a/apps/docs/content/docs/self-hosting/getting-started/quick-start.mdx b/apps/docs/content/docs/self-hosting/getting-started/quick-start.mdx
index b0cc80ff82..9f913c7661 100644
--- a/apps/docs/content/docs/self-hosting/getting-started/quick-start.mdx
+++ b/apps/docs/content/docs/self-hosting/getting-started/quick-start.mdx
@@ -124,12 +124,16 @@ docker compose -f docker/development/compose.yml exec database \
The quick start setup runs the following containers:
-| Container | Purpose | Port |
-| ----------- | -------------------------------- | ----- |
-| `documenso` | Main application | 3000 |
-| `database` | PostgreSQL database | 54320 |
-| `maildev` | Local email testing server | 2500 |
-| `minio` | S3-compatible storage (optional) | 9000 |
+| Container | Purpose | Port |
+| ----------- | ------------------------------------ | ----------------------------- |
+| `documenso` | Main application | 3000 |
+| `database` | PostgreSQL database | 54320 |
+| `inbucket` | Local email testing server | 9000 (web UI), 2500 (SMTP) |
+| `redis` | Cache and background job queue | 63790 |
+| `minio` | S3-compatible storage | 9002 (API), 9001 (console) |
+| `gotenberg` | Document conversion (optional) | 3005 |
+
+The local email server is [Inbucket](https://www.inbucket.org/). Open its web UI at [http://localhost:9000](http://localhost:9000) to view emails Documenso sends during development. For your own deployment you can use any SMTP-compatible mailserver, such as Inbucket, [Mailpit](https://github.com/axllent/mailpit), or [Mailhog](https://github.com/mailhog/MailHog).
## Useful Commands
diff --git a/apps/docs/content/docs/self-hosting/getting-started/requirements.mdx b/apps/docs/content/docs/self-hosting/getting-started/requirements.mdx
index 13b2b75abd..c64bd081e6 100644
--- a/apps/docs/content/docs/self-hosting/getting-started/requirements.mdx
+++ b/apps/docs/content/docs/self-hosting/getting-started/requirements.mdx
@@ -141,8 +141,8 @@ If building from source (not using Docker images):
| Requirement | Version |
| ----------- | ------- |
-| Node.js | 18+ |
-| npm | 8+ |
+| Node.js | 22+ |
+| npm | 11+ |
---
@@ -169,7 +169,7 @@ Documenso runs on:
| MySQL/MariaDB | PostgreSQL-specific features required |
| SQLite | Not suitable for production workloads |
| MongoDB | Relational database required |
-| Node.js < 18 | Modern JavaScript features required |
+| Node.js < 22 | Modern JavaScript features required |
---
diff --git a/apps/docs/content/docs/self-hosting/getting-started/tips.mdx b/apps/docs/content/docs/self-hosting/getting-started/tips.mdx
index 7fe640ab39..f4ff0e0a5f 100644
--- a/apps/docs/content/docs/self-hosting/getting-started/tips.mdx
+++ b/apps/docs/content/docs/self-hosting/getting-started/tips.mdx
@@ -92,7 +92,7 @@ Use a specific version tag in production:
```bash
# Good — predictable, reproducible
-docker pull documenso/documenso:1.8.0
+docker pull documenso/documenso:
# Risky — may pull breaking changes
docker pull documenso/documenso:latest
diff --git a/apps/docs/content/docs/self-hosting/index.mdx b/apps/docs/content/docs/self-hosting/index.mdx
index 4263d30d40..99e4ecb01c 100644
--- a/apps/docs/content/docs/self-hosting/index.mdx
+++ b/apps/docs/content/docs/self-hosting/index.mdx
@@ -27,6 +27,14 @@ import { Callout } from 'fumadocs-ui/components/callout';
Please see all the [requirements](/docs/self-hosting/getting-started/requirements) before proceeding.
+
+ **You are responsible for your own network security.** Documenso applies best-effort, non-exhaustive
+ checks to outbound requests such as webhooks, but these are not a complete SSRF mitigation and they
+ fail open. A self-hosted instance can reach internal addresses on your network. Restricting outbound
+ traffic, egress filtering, and blocking access to internal services and cloud metadata endpoints is
+ your responsibility through your firewall and network configuration.
+
+
---
## Deployment Options
@@ -133,7 +141,7 @@ See the [Quick Start guide](/docs/self-hosting/getting-started/quick-start) for
Self-hosted Documenso includes full core functionality under the AGPL-3.0 license. If you need enterprise features such as SSO, embed editor white label, or 21 CFR Part 11 compliance, you can activate them with a license key.
-See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison.
+See [Enterprise Edition](/docs/policies/enterprise-edition) for details and [Licenses](/docs/policies/licenses) for a comparison. Already have a key? See [Apply Your License Key](/docs/self-hosting/configuration/license).
---
diff --git a/apps/docs/content/docs/self-hosting/maintenance/upgrades.mdx b/apps/docs/content/docs/self-hosting/maintenance/upgrades.mdx
index 4ac7879cfb..832bb0088e 100644
--- a/apps/docs/content/docs/self-hosting/maintenance/upgrades.mdx
+++ b/apps/docs/content/docs/self-hosting/maintenance/upgrades.mdx
@@ -165,10 +165,10 @@ See [Backups](/docs/self-hosting/maintenance/backups) for automated backup strat
### Pull the new image
```bash
-docker pull documenso/documenso:1.6.0
+docker pull documenso/documenso:
```
-Replace `1.6.0` with your target version.
+Replace `` with your target version.
@@ -189,7 +189,7 @@ docker run -d \
-p 3000:3000 \
--env-file .env \
-v /path/to/cert.p12:/opt/documenso/cert.p12:ro \
- documenso/documenso:1.6.0
+ documenso/documenso:
```
@@ -223,14 +223,14 @@ Edit `compose.yml` or your `.env` file to specify the new version:
```yaml
services:
documenso:
- image: documenso/documenso:1.6.0
+ image: documenso/documenso:
```
Or if using environment variable substitution:
```bash
# In .env
-DOCUMENSO_VERSION=1.6.0
+DOCUMENSO_VERSION=
```
```yaml
@@ -283,7 +283,7 @@ Edit the deployment directly:
```bash
kubectl set image deployment/documenso \
- documenso=documenso/documenso:1.6.0 \
+ documenso=documenso/documenso: \
-n documenso
```
@@ -295,7 +295,7 @@ spec:
spec:
containers:
- name: documenso
- image: documenso/documenso:1.6.0
+ image: documenso/documenso:
```
Then apply:
@@ -421,12 +421,12 @@ To run migrations manually before upgrading:
```bash
# Pull the new image
-docker pull documenso/documenso:1.6.0
+docker pull documenso/documenso:
# Run migrations only
docker run --rm \
-e NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@host:5432/documenso" \
- documenso/documenso:1.6.0 \
+ documenso/documenso: \
npx prisma migrate deploy
```
@@ -516,7 +516,7 @@ docker run -d \
-p 3000:3000 \
--env-file .env \
-v /path/to/cert.p12:/opt/documenso/cert.p12:ro \
- documenso/documenso:1.5.0
+ documenso/documenso:
```
diff --git a/apps/docs/package.json b/apps/docs/package.json
index 76aecb7160..da99666791 100644
--- a/apps/docs/package.json
+++ b/apps/docs/package.json
@@ -3,7 +3,7 @@
"version": "0.0.0",
"private": true,
"scripts": {
- "build": "next build",
+ "build": "NEXT_IGNORE_INCORRECT_LOCKFILE=true next build",
"dev": "next dev",
"start": "next start",
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit",
@@ -29,7 +29,7 @@
"@types/node": "^25.1.0",
"@types/react": "^19.2.10",
"@types/react-dom": "^19.2.3",
- "postcss": "^8.5.14",
+ "postcss": "^8.5.19",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
}
diff --git a/apps/docs/src/app/global.css b/apps/docs/src/app/global.css
index 3b6bc40348..18a51c0582 100644
--- a/apps/docs/src/app/global.css
+++ b/apps/docs/src/app/global.css
@@ -83,7 +83,7 @@
--accent: hsl(0 0% 27.8431%);
--accent-foreground: hsl(207 60% 67.451%);
--destructive: hsl(0 86.5979% 61.9608%);
- --destructive-foreground: hsl(0 87.6289% 19.0196%);
+ --destructive-foreground: hsl(0 0% 98.0392%);
--border: hsl(0 0% 27.8431%);
--input: hsl(0 0% 27.8431%);
--ring: hsl(207 60% 36%);
diff --git a/apps/remix/Dockerfile b/apps/remix/Dockerfile
deleted file mode 100644
index 207bf937e3..0000000000
--- a/apps/remix/Dockerfile
+++ /dev/null
@@ -1,22 +0,0 @@
-FROM node:20-alpine AS development-dependencies-env
-COPY . /app
-WORKDIR /app
-RUN npm ci
-
-FROM node:20-alpine AS production-dependencies-env
-COPY ./package.json package-lock.json /app/
-WORKDIR /app
-RUN npm ci --omit=dev
-
-FROM node:20-alpine AS build-env
-COPY . /app/
-COPY --from=development-dependencies-env /app/node_modules /app/node_modules
-WORKDIR /app
-RUN npm run build
-
-FROM node:20-alpine
-COPY ./package.json package-lock.json /app/
-COPY --from=production-dependencies-env /app/node_modules /app/node_modules
-COPY --from=build-env /app/build /app/build
-WORKDIR /app
-CMD ["npm", "run", "start"]
\ No newline at end of file
diff --git a/apps/remix/README.md b/apps/remix/README.md
index e0d20664ec..6824c05b10 100644
--- a/apps/remix/README.md
+++ b/apps/remix/README.md
@@ -1,100 +1,14 @@
-# Welcome to React Router!
+# @documenso/remix
-A modern, production-ready template for building full-stack React applications using React Router.
+The main Documenso web application. Built with [React Router v7](https://reactrouter.com/) and served by a [Hono](https://hono.dev/) server.
-[](https://stackblitz.com/github/remix-run/react-router-templates/tree/main/default)
+This package is part of the Documenso monorepo and is not meant to be run standalone. Use the root scripts instead.
-## Features
-
-- 🚀 Server-side rendering
-- ⚡️ Hot Module Replacement (HMR)
-- 📦 Asset bundling and optimization
-- 🔄 Data loading and mutations
-- 🔒 TypeScript by default
-- 🎉 TailwindCSS for styling
-- 📖 [React Router docs](https://reactrouter.com/)
-
-## Getting Started
-
-### Installation
-
-Install the dependencies:
-
-```bash
-npm install
-```
-
-### Development
-
-Start the development server with HMR:
+- Local development: see the [root README](../../README.md) and the [Local Development docs](https://docs.documenso.com/docs/developers/local-development).
+- Self-hosting and deployment: see the [Self-Hosting docs](https://docs.documenso.com/docs/self-hosting).
+- Architecture overview: see [ARCHITECTURE.md](../../ARCHITECTURE.md).
```bash
+# From the monorepo root
npm run dev
```
-
-Your application will be available at `http://localhost:5173`.
-
-## Building for Production
-
-Create a production build:
-
-```bash
-npm run build
-```
-
-## Deployment
-
-### Docker Deployment
-
-This template includes three Dockerfiles optimized for different package managers:
-
-- `Dockerfile` - for npm
-- `Dockerfile.pnpm` - for pnpm
-- `Dockerfile.bun` - for bun
-
-To build and run using Docker:
-
-```bash
-# For npm
-docker build -t my-app .
-
-# For pnpm
-docker build -f Dockerfile.pnpm -t my-app .
-
-# For bun
-docker build -f Dockerfile.bun -t my-app .
-
-# Run the container
-docker run -p 3000:3000 my-app
-```
-
-The containerized application can be deployed to any platform that supports Docker, including:
-
-- AWS ECS
-- Google Cloud Run
-- Azure Container Apps
-- Digital Ocean App Platform
-- Fly.io
-- Railway
-
-### DIY Deployment
-
-If you're familiar with deploying Node applications, the built-in app server is production-ready.
-
-Make sure to deploy the output of `npm run build`
-
-```
-├── package.json
-├── package-lock.json (or pnpm-lock.yaml, or bun.lockb)
-├── build/
-│ ├── client/ # Static assets
-│ └── server/ # Server-side code
-```
-
-## Styling
-
-This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer.
-
----
-
-Built with ❤️ using React Router.
diff --git a/apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx b/apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx
new file mode 100644
index 0000000000..6205b1341c
--- /dev/null
+++ b/apps/remix/app/components/dialogs/branding-preferences-reset-dialog.tsx
@@ -0,0 +1,119 @@
+import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@documenso/ui/primitives/dialog';
+import { Trans } from '@lingui/react/macro';
+import { useState } from 'react';
+
+export type BrandingPreferencesResetDialogProps = {
+ hasAdvancedBranding: boolean;
+ isSubmitting: boolean;
+ onReset: () => Promise;
+ trigger?: React.ReactNode;
+};
+
+export const BrandingPreferencesResetDialog = ({
+ hasAdvancedBranding,
+ isSubmitting,
+ onReset,
+ trigger,
+}: BrandingPreferencesResetDialogProps) => {
+ const [open, setOpen] = useState(false);
+ const [isResetting, setIsResetting] = useState(false);
+
+ const isLoading = isSubmitting || isResetting;
+
+ const handleResetToDefaults = async () => {
+ setIsResetting(true);
+
+ try {
+ await onReset();
+ setOpen(false);
+ } catch {
+ // The submit handler surfaces its own error toast. Keep the dialog open
+ // so the user can retry.
+ } finally {
+ setIsResetting(false);
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx b/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
deleted file mode 100644
index 694f2d56aa..0000000000
--- a/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
-import { FolderType } from '@documenso/lib/types/folder-type';
-import { formatDocumentsPath } from '@documenso/lib/utils/teams';
-import { trpc } from '@documenso/trpc/react';
-import { Button } from '@documenso/ui/primitives/button';
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from '@documenso/ui/primitives/dialog';
-import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
-import { Input } from '@documenso/ui/primitives/input';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { msg } from '@lingui/core/macro';
-import { useLingui } from '@lingui/react';
-import { Trans } from '@lingui/react/macro';
-import type * as DialogPrimitive from '@radix-ui/react-dialog';
-import { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react';
-import { useEffect, useState } from 'react';
-import { useForm } from 'react-hook-form';
-import { useNavigate } from 'react-router';
-import { z } from 'zod';
-
-import { useCurrentTeam } from '~/providers/team';
-
-export type DocumentMoveToFolderDialogProps = {
- documentId: number;
- open: boolean;
- onOpenChange: (open: boolean) => void;
- currentFolderId?: string;
-} & Omit;
-
-const ZMoveDocumentFormSchema = z.object({
- folderId: z.string().nullable().optional(),
-});
-
-type TMoveDocumentFormSchema = z.infer;
-
-export const DocumentMoveToFolderDialog = ({
- documentId,
- open,
- onOpenChange,
- currentFolderId,
- ...props
-}: DocumentMoveToFolderDialogProps) => {
- const { _ } = useLingui();
- const { toast } = useToast();
-
- const navigate = useNavigate();
- const team = useCurrentTeam();
-
- const [searchTerm, setSearchTerm] = useState('');
-
- const form = useForm({
- resolver: zodResolver(ZMoveDocumentFormSchema),
- defaultValues: {
- folderId: currentFolderId,
- },
- });
-
- const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFoldersInternal.useQuery(
- {
- parentId: currentFolderId,
- type: FolderType.DOCUMENT,
- },
- {
- enabled: open,
- },
- );
-
- const { mutateAsync: updateDocument } = trpc.document.update.useMutation();
-
- useEffect(() => {
- if (!open) {
- form.reset();
- setSearchTerm('');
- } else {
- form.reset({ folderId: currentFolderId });
- }
- }, [open, currentFolderId, form]);
-
- const onSubmit = async (data: TMoveDocumentFormSchema) => {
- try {
- await updateDocument({
- documentId,
- data: {
- folderId: data.folderId ?? null,
- },
- });
-
- const documentsPath = formatDocumentsPath(team.url);
-
- if (data.folderId) {
- await navigate(`${documentsPath}/f/${data.folderId}`);
- } else {
- await navigate(documentsPath);
- }
-
- toast({
- title: _(msg`Document moved`),
- description: _(msg`The document has been moved successfully.`),
- variant: 'default',
- });
-
- onOpenChange(false);
- } catch (err) {
- const error = AppError.parseError(err);
-
- if (error.code === AppErrorCode.NOT_FOUND) {
- toast({
- title: _(msg`Error`),
- description: _(msg`The folder you are trying to move the document to does not exist.`),
- variant: 'destructive',
- });
-
- return;
- }
-
- if (error.code === AppErrorCode.UNAUTHORIZED) {
- toast({
- title: _(msg`Error`),
- description: _(msg`You are not allowed to move this document.`),
- variant: 'destructive',
- });
-
- return;
- }
-
- toast({
- title: _(msg`Error`),
- description: _(msg`An error occurred while moving the document.`),
- variant: 'destructive',
- });
- }
- };
-
- const filteredFolders = folders?.data.filter((folder) =>
- folder.name.toLowerCase().includes(searchTerm.toLowerCase()),
- );
-
- return (
-
- );
-};
diff --git a/apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx b/apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx
new file mode 100644
index 0000000000..9adebb36e9
--- /dev/null
+++ b/apps/remix/app/components/dialogs/document-preferences-reset-dialog.tsx
@@ -0,0 +1,141 @@
+import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@documenso/ui/primitives/dialog';
+import { Trans } from '@lingui/react/macro';
+import { useState } from 'react';
+
+export type DocumentPreferencesResetDialogProps = {
+ isSubmitting: boolean;
+ onReset: () => Promise;
+ showAiFeatures?: boolean;
+ showDocumentVisibility?: boolean;
+ showIncludeSenderDetails?: boolean;
+};
+
+export const DocumentPreferencesResetDialog = ({
+ isSubmitting,
+ onReset,
+ showAiFeatures = false,
+ showDocumentVisibility = false,
+ showIncludeSenderDetails = false,
+}: DocumentPreferencesResetDialogProps) => {
+ const [open, setOpen] = useState(false);
+ const [isResetting, setIsResetting] = useState(false);
+
+ const isLoading = isSubmitting || isResetting;
+
+ const handleResetToDefaults = async () => {
+ setIsResetting(true);
+
+ try {
+ await onReset();
+ setOpen(false);
+ } catch {
+ // The submit handler surfaces its own error toast. Keep the dialog open
+ // so the user can retry.
+ } finally {
+ setIsResetting(false);
+ }
+ };
+
+ return (
+
+ );
+};
diff --git a/apps/remix/app/components/dialogs/document-resend-dialog.tsx b/apps/remix/app/components/dialogs/document-resend-dialog.tsx
deleted file mode 100644
index 493d07138d..0000000000
--- a/apps/remix/app/components/dialogs/document-resend-dialog.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-import { useSession } from '@documenso/lib/client-only/providers/session';
-import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
-import { AppError } from '@documenso/lib/errors/app-error';
-import type { TRecipientLite } from '@documenso/lib/types/recipient';
-import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
-import type { Document } from '@documenso/prisma/types/document-legacy-schema';
-import { trpc as trpcReact } from '@documenso/trpc/react';
-import { cn } from '@documenso/ui/lib/utils';
-import { Button } from '@documenso/ui/primitives/button';
-import { Checkbox } from '@documenso/ui/primitives/checkbox';
-import {
- Dialog,
- DialogClose,
- DialogContent,
- DialogFooter,
- DialogHeader,
- DialogTitle,
- DialogTrigger,
-} from '@documenso/ui/primitives/dialog';
-import { DropdownMenuItem } from '@documenso/ui/primitives/dropdown-menu';
-import { Form, FormControl, FormField, FormItem, FormLabel } from '@documenso/ui/primitives/form/form';
-import { useToast } from '@documenso/ui/primitives/use-toast';
-import { zodResolver } from '@hookform/resolvers/zod';
-import { msg } from '@lingui/core/macro';
-import { useLingui } from '@lingui/react';
-import { Trans } from '@lingui/react/macro';
-import { SigningStatus, type Team, type User } from '@prisma/client';
-import { History } from 'lucide-react';
-import { useState } from 'react';
-import { useForm, useWatch } from 'react-hook-form';
-import * as z from 'zod';
-
-import { useCurrentTeam } from '~/providers/team';
-import { getDistributeErrorMessage } from '~/utils/toast-error-messages';
-
-import { StackAvatar } from '../general/stack-avatar';
-
-const FORM_ID = 'resend-email';
-
-export type DocumentResendDialogProps = {
- document: Pick & {
- user: Pick;
- recipients: TRecipientLite[];
- team: Pick | null;
- };
- recipients: TRecipientLite[];
-};
-
-export const ZResendDocumentFormSchema = z.object({
- recipients: z.array(z.number()).min(1, {
- message: 'You must select at least one item.',
- }),
-});
-
-export type TResendDocumentFormSchema = z.infer;
-
-export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => {
- const { user } = useSession();
- const team = useCurrentTeam();
-
- const { toast } = useToast();
- const { _ } = useLingui();
-
- const [isOpen, setIsOpen] = useState(false);
- const isOwner = document.userId === user.id;
- const isCurrentTeamDocument = team && document.team?.url === team.url;
-
- const isDisabled =
- (!isOwner && !isCurrentTeamDocument) ||
- document.status !== 'PENDING' ||
- !recipients.some((r) => r.signingStatus === SigningStatus.NOT_SIGNED);
-
- const { mutateAsync: resendDocument } = trpcReact.document.redistribute.useMutation();
-
- const form = useForm({
- resolver: zodResolver(ZResendDocumentFormSchema),
- defaultValues: {
- recipients: [],
- },
- });
-
- const {
- handleSubmit,
- formState: { isSubmitting },
- } = form;
-
- const selectedRecipients = useWatch({
- control: form.control,
- name: 'recipients',
- });
-
- const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => {
- try {
- await resendDocument({ documentId: document.id, recipients });
-
- toast({
- title: _(msg`Document re-sent`),
- description: _(msg`Your document has been re-sent successfully.`),
- duration: 5000,
- });
-
- setIsOpen(false);
- } catch (err) {
- const error = AppError.parseError(err);
- const errorMessage = getDistributeErrorMessage(error.code);
-
- toast({
- title: _(errorMessage.title),
- description: _(errorMessage.description),
- variant: 'destructive',
- duration: 7500,
- });
- }
- };
-
- return (
-
- );
-};
diff --git a/apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx b/apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx
new file mode 100644
index 0000000000..8cb90cfa03
--- /dev/null
+++ b/apps/remix/app/components/dialogs/envelope-cancel-dialog.tsx
@@ -0,0 +1,134 @@
+import { trpc as trpcReact } from '@documenso/trpc/react';
+import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from '@documenso/ui/primitives/dialog';
+import { Label } from '@documenso/ui/primitives/label';
+import { Textarea } from '@documenso/ui/primitives/textarea';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+import { Trans, useLingui } from '@lingui/react/macro';
+import { useEffect, useState } from 'react';
+
+export type EnvelopeCancelDialogProps = {
+ id: string;
+ title: string;
+ trigger?: React.ReactNode;
+ onCancel?: () => Promise | void;
+};
+
+export const EnvelopeCancelDialog = ({ id, title, trigger, onCancel }: EnvelopeCancelDialogProps) => {
+ const { toast } = useToast();
+ const { t } = useLingui();
+ const trpcUtils = trpcReact.useUtils();
+
+ const [open, setOpen] = useState(false);
+ const [reason, setReason] = useState('');
+
+ const { mutateAsync: cancelEnvelope, isPending } = trpcReact.envelope.cancel.useMutation({
+ onSuccess: async () => {
+ toast({
+ title: t`Document cancelled`,
+ description: t`"${title}" has been successfully cancelled`,
+ duration: 5000,
+ });
+
+ await trpcUtils.document.findDocumentsInternal.invalidate();
+
+ await onCancel?.();
+
+ setOpen(false);
+ },
+ onError: () => {
+ toast({
+ title: t`Something went wrong`,
+ description: t`This document could not be cancelled at this time. Please try again.`,
+ variant: 'destructive',
+ duration: 7500,
+ });
+ },
+ });
+
+ useEffect(() => {
+ if (open) {
+ setReason('');
+ }
+ }, [open]);
+
+ return (
+
+ );
+};
diff --git a/apps/remix/app/components/dialogs/envelope-delete-dialog.tsx b/apps/remix/app/components/dialogs/envelope-delete-dialog.tsx
index 1130a14504..aea4c766c8 100644
--- a/apps/remix/app/components/dialogs/envelope-delete-dialog.tsx
+++ b/apps/remix/app/components/dialogs/envelope-delete-dialog.tsx
@@ -166,7 +166,7 @@ export const EnvelopeDeleteDialog = ({
))
- .with(P.union(DocumentStatus.COMPLETED, DocumentStatus.REJECTED), () => (
+ .with(P.union(DocumentStatus.COMPLETED, DocumentStatus.REJECTED, DocumentStatus.CANCELLED), () => (
By deleting this document, the following will occur:
diff --git a/apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx b/apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
index 7a3b95c9a4..d85249adf8 100644
--- a/apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
+++ b/apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
@@ -3,12 +3,13 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/org
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError } from '@documenso/lib/errors/app-error';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
+import { hasOverlappingFields } from '@documenso/lib/utils/fields-overlap';
import { getRecipientsWithMissingFields } from '@documenso/lib/utils/recipients';
import { zEmail } from '@documenso/lib/utils/zod';
import { trpc, trpc as trpcReact } from '@documenso/trpc/react';
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
import { cn } from '@documenso/ui/lib/utils';
-import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
+import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
Dialog,
@@ -32,7 +33,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { DocumentDistributionMethod, DocumentStatus, EnvelopeType } from '@prisma/client';
import { AnimatePresence, motion } from 'framer-motion';
-import { InfoIcon } from 'lucide-react';
+import { AlertTriangleIcon, InfoIcon } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
@@ -138,6 +139,27 @@ export const EnvelopeDistributeDialog = ({
});
}, [recipientsWithIndex, envelope.authOptions]);
+ /**
+ * Whether any fields significantly overlap each other. This is surfaced as a
+ * non-blocking warning since overlapping fields still allow sending, but can
+ * complicate the signing process or cause fields to behave unexpectedly.
+ */
+ const hasOverlappingEnvelopeFields = useMemo(
+ () =>
+ hasOverlappingFields(
+ envelope.fields.map((field) => ({
+ id: field.id,
+ envelopeItemId: field.envelopeItemId,
+ page: field.page,
+ positionX: Number(field.positionX),
+ positionY: Number(field.positionY),
+ width: Number(field.width),
+ height: Number(field.height),
+ })),
+ ),
+ [envelope.fields],
+ );
+
const invalidEnvelopeCode = useMemo(() => {
if (recipientsMissingSignatureFields.length > 0) {
return 'MISSING_SIGNATURES';
@@ -206,6 +228,11 @@ export const EnvelopeDistributeDialog = ({
};
useEffect(() => {
+ // Default the distribution method tab to the envelope's configured setting.
+ if (isOpen && envelope.documentMeta) {
+ setValue('meta.distributionMethod', envelope.documentMeta.distributionMethod);
+ }
+
// Resync the whole envelope if the envelope is mid saving.
if (isOpen && (isAutosaving || autosaveError)) {
void handleSync();
@@ -235,6 +262,24 @@ export const EnvelopeDistributeDialog = ({