diff --git a/.github/actions/node-install/action.yml b/.github/actions/node-install/action.yml
index b01a28740..fb208e916 100644
--- a/.github/actions/node-install/action.yml
+++ b/.github/actions/node-install/action.yml
@@ -2,7 +2,7 @@ name: 'Setup node'
inputs:
node_version:
required: false
- default: v22.x
+ default: v24.x
runs:
using: 'composite'
diff --git a/README.md b/README.md
index a1ed84f30..ad4d7c5b9 100644
--- a/README.md
+++ b/README.md
@@ -107,7 +107,7 @@ Contact us if you are interested in our Enterprise plan for large organizations
To run Documenso locally, you will need
-- Node.js (v22 or above)
+- Node.js (v24 or above)
- Postgres SQL Database
- Docker (optional)
diff --git a/apps/docs/content/docs/developers/api/teams.mdx b/apps/docs/content/docs/developers/api/teams.mdx
index 99d708b41..0d869c56c 100644
--- a/apps/docs/content/docs/developers/api/teams.mdx
+++ b/apps/docs/content/docs/developers/api/teams.mdx
@@ -95,7 +95,7 @@ Documents created with a team token belong to that team:
```bash
curl -X POST "https://app.documenso.com/api/v2/envelope/create" \
- -H "Authorization: api_team_xxxxxxxxxxxxxxxx" \
+ -H "Authorization: api_xxxxxxxxxxxxxxxx" \
-H "Content-Type: multipart/form-data" \
-F 'payload={
"type": "DOCUMENT",
@@ -157,11 +157,11 @@ Retrieve all documents belonging to the team:
```bash
# List all team documents
curl -X GET "https://app.documenso.com/api/v2/envelope" \
- -H "Authorization: api_team_xxxxxxxxxxxxxxxx"
+ -H "Authorization: api_xxxxxxxxxxxxxxxx"
# Filter by status
curl -X GET "https://app.documenso.com/api/v2/envelope?status=PENDING" \
- -H "Authorization: api_team_xxxxxxxxxxxxxxxx"
+ -H "Authorization: api_xxxxxxxxxxxxxxxx"
````
@@ -191,7 +191,7 @@ Templates created with a team token are shared across the team.
```bash
curl -X POST "https://app.documenso.com/api/v2/template/create" \
- -H "Authorization: api_team_xxxxxxxxxxxxxxxx" \
+ -H "Authorization: api_xxxxxxxxxxxxxxxx" \
-H "Content-Type: multipart/form-data" \
-F 'payload={
"title": "NDA Template",
@@ -269,7 +269,7 @@ console.log('Created team template:', template.id);
```bash
curl -X GET "https://app.documenso.com/api/v2/template" \
- -H "Authorization: api_team_xxxxxxxxxxxxxxxx"
+ -H "Authorization: api_xxxxxxxxxxxxxxxx"
````
diff --git a/apps/docs/content/docs/self-hosting/deployment/docker.mdx b/apps/docs/content/docs/self-hosting/deployment/docker.mdx
index 68508e767..d8ba9c70a 100644
--- a/apps/docs/content/docs/self-hosting/deployment/docker.mdx
+++ b/apps/docs/content/docs/self-hosting/deployment/docker.mdx
@@ -102,7 +102,7 @@ See [Email Configuration](/docs/self-hosting/configuration/email) for other tran
| Variable | Description | Default |
| ------------------------------------------- | -------------------------------------------------------------- | ------------------------- |
| `PORT` | Port the application listens on | `3000` |
-| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` | Path to signing certificate inside container | `/opt/documenso/cert.p12` |
+| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` | Path to signing certificate inside container — set to the volume-mount path (e.g. `/opt/documenso/cert.p12`). Only Docker Compose defaults this; plain `docker run` must set it explicitly | - |
| `NEXT_PRIVATE_SIGNING_PASSPHRASE` | Passphrase for the signing certificate | - |
| `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` | Base64-encoded `.p12` certificate (alternative to file path) | - |
| `NEXT_PUBLIC_UPLOAD_TRANSPORT` | Document storage: `database` or `s3` | `database` |
@@ -136,6 +136,7 @@ docker run -d \
-e NEXT_PUBLIC_WEBAPP_URL="https://sign.example.com" \
-e NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000" \
-e NEXT_PRIVATE_DATABASE_URL="postgresql://user:password@db-host:5432/documenso" \
+ -e NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH="/opt/documenso/cert.p12" \
-e NEXT_PRIVATE_SIGNING_PASSPHRASE="your-certificate-password" \
-e NEXT_PRIVATE_SMTP_TRANSPORT="smtp-auth" \
-e NEXT_PRIVATE_SMTP_HOST="smtp.example.com" \
@@ -154,6 +155,12 @@ A signing certificate is required for document signing. You have two options for
- **Volume mount** — mount a `.p12` file from the host into the container at `/opt/documenso/cert.p12` (shown above). This is the simplest approach for small to moderate deployments.
- **Base64-encoded contents** — set `NEXT_PRIVATE_SIGNING_LOCAL_FILE_CONTENTS` with the base64-encoded certificate string. Use this when file mounting is not available (e.g., Railway, Vercel).
+
+ Plain `docker run` deployments must set `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH` explicitly. This
+ prevents production deployments from accidentally using the insecure example certificate.
+ Docker Compose sets the file path for you.
+
+
For production deployments that require Adobe Approved Trust List recognition, consider using a [Google Cloud HSM](/docs/self-hosting/configuration/signing-certificate/google-cloud-hsm) or another external HSM.
@@ -178,6 +185,7 @@ NEXT_PUBLIC_WEBAPP_URL=https://sign.example.com
NEXT_PRIVATE_INTERNAL_WEBAPP_URL=http://localhost:3000
NEXT_PRIVATE_DATABASE_URL=postgresql://user:password@db-host:5432/documenso
NEXT_PRIVATE_DIRECT_DATABASE_URL=postgresql://user:password@db-host:5432/documenso
+NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12
NEXT_PRIVATE_SIGNING_PASSPHRASE=your-certificate-password
NEXT_PRIVATE_SMTP_TRANSPORT=smtp-auth
NEXT_PRIVATE_SMTP_HOST=smtp.example.com
@@ -203,6 +211,12 @@ docker run -d \
Documenso provides health check endpoints for monitoring:
+
+ If a certificate is mounted but signing fails, ensure `NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH`
+ explicitly points to its path inside the container. Production does not use the development
+ example certificate as a fallback.
+
+
| Endpoint | Purpose |
| ------------------------- | -------------------------------------------------------------- |
| `/api/health` | Checks database connectivity and certificate status |
diff --git a/apps/docs/content/docs/self-hosting/deployment/manual.mdx b/apps/docs/content/docs/self-hosting/deployment/manual.mdx
index d6dc4fda5..70f7da640 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 22 or later
-- npm 11 or later
+- Node.js 24 or later
+- npm 11.17 or later
- PostgreSQL 14 or later
- A Linux server (for systemd service setup)
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 c64bd081e..b75069906 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 | 22+ |
-| npm | 11+ |
+| Node.js | 24+ |
+| npm | 11.17+ |
---
@@ -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 < 22 | Modern JavaScript features required |
+| Node.js < 24 | Modern JavaScript features required |
---
diff --git a/apps/docs/content/docs/users/organisations/preferences/document.mdx b/apps/docs/content/docs/users/organisations/preferences/document.mdx
index 1f4e82c08..d81252325 100644
--- a/apps/docs/content/docs/users/organisations/preferences/document.mdx
+++ b/apps/docs/content/docs/users/organisations/preferences/document.mdx
@@ -34,7 +34,7 @@ To access the preferences, navigate to either the organisation or teams settings
| **Default Recipients** | Recipients that are automatically added to new documents. Can be overridden per document. |
| **Default Envelope Expiration** | How long recipients have to sign before the signing link expires. See [recipient expiration](/docs/users/documents/advanced/recipient-expiration). |
| **Default Signing Reminders** | When and how often to email recipients who have not yet signed. See [signing reminders](/docs/users/documents/advanced/signing-reminders). |
-| **Delegate Document Ownership** | Allow team API tokens to delegate document ownership to another team member. |
+| **Delegate Document Ownership** | By default, documents created with a team API token are owned by the user who created the token. Enable this setting to let supported API requests assign ownership to another team member. |
| **AI Features** | Enable AI-powered features such as automatic recipient detection. Only shown if AI features are configured on the instance. |
Document visibility, language, and signature settings can be overridden per document.
diff --git a/apps/remix/Dockerfile.bun b/apps/remix/Dockerfile.bun
deleted file mode 100644
index 973038e8a..000000000
--- a/apps/remix/Dockerfile.bun
+++ /dev/null
@@ -1,25 +0,0 @@
-FROM oven/bun:1 AS dependencies-env
-COPY . /app
-
-FROM dependencies-env AS development-dependencies-env
-COPY ./package.json bun.lockb /app/
-WORKDIR /app
-RUN bun i --frozen-lockfile
-
-FROM dependencies-env AS production-dependencies-env
-COPY ./package.json bun.lockb /app/
-WORKDIR /app
-RUN bun i --production
-
-FROM dependencies-env AS build-env
-COPY ./package.json bun.lockb /app/
-COPY --from=development-dependencies-env /app/node_modules /app/node_modules
-WORKDIR /app
-RUN bun run build
-
-FROM dependencies-env
-COPY ./package.json bun.lockb /app/
-COPY --from=production-dependencies-env /app/node_modules /app/node_modules
-COPY --from=build-env /app/build /app/build
-WORKDIR /app
-CMD ["bun", "run", "start"]
\ No newline at end of file
diff --git a/apps/remix/Dockerfile.pnpm b/apps/remix/Dockerfile.pnpm
deleted file mode 100644
index 57916afc2..000000000
--- a/apps/remix/Dockerfile.pnpm
+++ /dev/null
@@ -1,26 +0,0 @@
-FROM node:20-alpine AS dependencies-env
-RUN npm i -g pnpm
-COPY . /app
-
-FROM dependencies-env AS development-dependencies-env
-COPY ./package.json pnpm-lock.yaml /app/
-WORKDIR /app
-RUN pnpm i --frozen-lockfile
-
-FROM dependencies-env AS production-dependencies-env
-COPY ./package.json pnpm-lock.yaml /app/
-WORKDIR /app
-RUN pnpm i --prod --frozen-lockfile
-
-FROM dependencies-env AS build-env
-COPY ./package.json pnpm-lock.yaml /app/
-COPY --from=development-dependencies-env /app/node_modules /app/node_modules
-WORKDIR /app
-RUN pnpm build
-
-FROM dependencies-env
-COPY ./package.json pnpm-lock.yaml /app/
-COPY --from=production-dependencies-env /app/node_modules /app/node_modules
-COPY --from=build-env /app/build /app/build
-WORKDIR /app
-CMD ["pnpm", "start"]
\ No newline at end of file
diff --git a/apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx b/apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
index 7e381c82f..af03b216d 100644
--- a/apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
+++ b/apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
@@ -1,4 +1,7 @@
+import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
+import type { TBulkSendCsvError } from '@documenso/lib/server-only/template/validate-bulk-send-csv';
import { trpc } from '@documenso/trpc/react';
+import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
import {
@@ -15,9 +18,11 @@ 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 { Plural, Trans } from '@lingui/react/macro';
import { File as FileIcon, Upload, X } from 'lucide-react';
+import { useState } from 'react';
import { useForm } from 'react-hook-form';
+import { match } from 'ts-pattern';
import { z } from 'zod';
import { useCurrentTeam } from '~/providers/team';
@@ -29,6 +34,8 @@ const ZBulkSendFormSchema = z.object({
type TBulkSendFormSchema = z.infer;
+type TBulkSendValidationError = TBulkSendCsvError | { type: 'UPLOAD_ERROR'; code: string };
+
export type TemplateBulkSendDialogProps = {
templateId: number;
recipients: Array<{ email: string; name?: string | null }>;
@@ -42,6 +49,9 @@ export const TemplateBulkSendDialog = ({ templateId, recipients, trigger, onSucc
const team = useCurrentTeam();
+ const [open, setOpen] = useState(false);
+ const [validationError, setValidationError] = useState(null);
+
const form = useForm({
resolver: zodResolver(ZBulkSendFormSchema),
defaultValues: {
@@ -51,6 +61,20 @@ export const TemplateBulkSendDialog = ({ templateId, recipients, trigger, onSucc
const { mutateAsync: uploadBulkSend } = trpc.template.uploadBulkSend.useMutation();
+ const onOpenChange = (value: boolean) => {
+ if (form.formState.isSubmitting) {
+ return;
+ }
+
+ setOpen(value);
+
+ if (!value) {
+ setValidationError(null);
+
+ form.reset();
+ }
+ };
+
const onDownloadTemplate = () => {
const headers = recipients.flatMap((_, index) => [`recipient_${index + 1}_email`, `recipient_${index + 1}_name`]);
@@ -71,36 +95,44 @@ export const TemplateBulkSendDialog = ({ templateId, recipients, trigger, onSucc
};
const onSubmit = async (values: TBulkSendFormSchema) => {
+ setValidationError(null);
+
try {
const csv = await values.file.text();
- await uploadBulkSend({
+ const result = await uploadBulkSend({
templateId,
teamId: team?.id,
csv: csv,
sendImmediately: values.sendImmediately,
});
+ if (!result.success) {
+ setValidationError(result.error);
+
+ return;
+ }
+
toast({
title: _(msg`Success`),
description: _(msg`Your bulk send has been initiated. You will receive an email notification upon completion.`),
});
+ setOpen(false);
form.reset();
+
onSuccess?.();
} catch (err) {
console.error(err);
- toast({
- title: _(msg`Error`),
- description: _(msg`Failed to upload CSV. Please check the file format and try again.`),
- variant: 'destructive',
- });
+ const error = AppError.parseError(err);
+
+ setValidationError({ type: 'UPLOAD_ERROR', code: error.code });
}
};
return (
-