Signa is a TypeScript and React document-signing platform built for DocuSeal-compatible signing workflows and API behavior. It uses a NestJS backend, Next.js product and marketing applications, TypeORM, PostgreSQL or SQLite, Redis-backed queues, PDF rendering/signing tooling, mail/SMS delivery, webhooks, Supabase-backed marketing data, and shared contracts in a pnpm monorepo.
This scaffold was created from the official framework CLIs:
- Next.js frontend:
pnpm create next-app@latest frontend --yes - NestJS backend:
pnpm dlx @nestjs/cli new backend --strict --package-manager pnpm --skip-git
Clone the canonical repository:
git clone git@github.com:codeignite-labs/signa.git
cd signaapps/frontend: Next.js App Router frontend.apps/backend: NestJS backend API.apps/marketing: standalone full-stack Next.js marketing site for independent Vercel deployment and Supabase-backed marketing data.packages/signa-react: npm-publishable React embedding package for Signa signing forms and builder workflows.packages/signa-react-native: npm-publishable React Native WebView package for mobile signing flows.packages/shared: shared Zod schemas, contracts, and types.packages/ts-config: shared TypeScript config.
pnpm install
pnpm devDefault local URLs:
- Frontend:
http://localhost:3000 - Backend:
http://localhost:3001/api - Marketing:
http://localhost:3002 - Swagger:
http://localhost:3001/api/docs - Health:
http://localhost:3001/api/health - Bull Board, when enabled:
http://localhost:3001/queues
Frontend and backend run as separate servers in development and production because the frontend uses Next.js server routes. Docker starts both product processes in one app container for simple deployment. The marketing workspace is deployed independently and is not served by the on-prem product container.
Signa ships its user, developer, API, compliance, and deployment documentation
from the standalone apps/marketing deployment. These routes are public and
do not require an authenticated product account:
Production documentation: https://signa-docs.vercel.app
| Route | Purpose |
|---|---|
/docs |
Documentation home and navigation. |
/docs/api |
Curated API onboarding, endpoint contracts, examples, and response shapes. |
/docs/embedding |
React and React Native embedding guidance. |
/docs/webhooks |
Webhook registration, events, retries, and signature verification. |
/guides |
End-to-end signing, template, verification, and deployment guides. |
/resources |
Operational references for teams, branding, integrations, and storage. |
/compliance |
Audit, certificate, PAdES, timestamp, and LTV controls. |
/qualified-electronic-signature |
Signature levels and external qualified trust-provider requirements. |
The generated NestJS OpenAPI explorer remains available at /api/docs. Use
the public API guide for guided
onboarding and /api/docs on a running Signa instance for the authoritative,
machine-generated controller and DTO schema.
packages/signa-react contains the npm-publishable React package for embedding Signa signing forms and builder workflows.
packages/signa-react-native contains the npm-publishable React Native package for mobile apps that embed hosted Signa signing flows through react-native-webview.
Common package commands:
pnpm --filter @signajs/react typecheck
pnpm --filter @signajs/react build
pnpm pack:signa-react
pnpm --filter @signajs/react-native typecheck
pnpm --filter @signajs/react-native build
pnpm pack:signa-react-nativeBasic usage:
import { SignaForm } from "@signajs/react";
export function App() {
return (
<SignaForm
host="https://signa.example.com"
src="https://signa.example.com/s/submitter-slug"
/>
);
}Publishing notes are documented in packages/signa-react/README.md. The current package name is @signajs/react, which requires access to the signajs npm scope. Package releases use Changesets so version bumps, changelog entries, and npm publishing are reviewed in git before release.
React Native setup notes are documented in packages/signa-react-native/README.md. The native package name is @signajs/react-native and also requires access to the signajs npm scope.
Signa has two release lanes:
- Application release: the deployable Signa app is versioned from the private root
package.json, git tags, and Docker image tags. - Package release: public npm packages, currently
@signajs/reactand@signajs/react-native, are versioned and published with Changesets.
Use the root package.json version as the app semver. A production release should be tagged and built with the matching app metadata:
git tag app-v0.1.0
git push origin app-v0.1.0
docker build \
--build-arg APP_VERSION=0.1.0 \
--build-arg APP_COMMIT_SHA=$(git rev-parse HEAD) \
--build-arg APP_BUILD_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") \
-t signa:0.1.0 .APP_VERSION, APP_COMMIT_SHA, and APP_BUILD_TIME are optional runtime metadata. When set, they appear in /api/health, /api/health/live, /api/health/ready, and the root API metadata endpoint. They are intentionally separate from npm package versions because the app is deployed as a Docker artifact, not published to npm.
With Docker Compose, set the same values in .env before building:
APP_VERSION=0.1.0
APP_COMMIT_SHA=<git-sha>
APP_BUILD_TIME=2026-07-11T10:00:00ZCreate a changeset for every user-visible package change:
pnpm changesetChoose the semver level for the changed package, such as @signajs/react or @signajs/react-native:
patch: bug fixes and documentation-only runtime clarifications.minor: backward-compatible props, events, or behavior.major: breaking prop, event, bundle, peer dependency, or runtime-script changes.
When ready to prepare a release PR:
pnpm version:packages
pnpm --filter @signajs/react typecheck
pnpm --filter @signajs/react build
pnpm pack:signa-react
pnpm --filter @signajs/react-native typecheck
pnpm --filter @signajs/react-native build
pnpm pack:signa-react-nativeAfter the version PR is merged and npm credentials are available:
pnpm release:packagesFor CI, prefer npm trusted publishing with provenance enabled so published package tarballs are tied to the GitHub Actions release workflow. The publish workflow should run the same checks, then execute pnpm release:packages with npm provenance/trusted-publisher configuration.
The default image is a complete single-node Signa installation. It serves the
web app and API on one port, stores SQLite, uploads, generated documents,
secrets, and its private Redis data under /data, and needs no configuration
for an initial evaluation.
docker build -t signa .
docker run --name signa -p 3000:3000 -v signa-data:/data signaOpen http://localhost:3000. The first registered user becomes the owner, and
self-service registration closes after that initial account.
Docker Compose provides the same setup:
docker compose up -d --buildThe public documentation includes a copyable installation, health-check, SMTP, storage, backup, and upgrade runbook at Deploy Signa on premise.
Useful endpoints:
- App:
http://localhost:3000 - API docs:
http://localhost:3000/api/docs - Health:
http://localhost:3000/api/health
| Variable | Required | Description |
|---|---|---|
APP_URL |
Yes outside localhost | The single public origin, for example https://signa.example.com. Signa derives frontend, API, signing, storage, and email links from it. |
DATABASE_URL |
No | Leave empty for SQLite. Set a PostgreSQL URL to switch to Postgres. |
JWT_SECRET |
No | Generated once into /data/signa.env when omitted. Supply your own secret through a secrets manager for stateless or multi-instance deployments. |
REGISTRATION_MODE |
No | Defaults to initial_only. Use initial_only for on-prem bootstrap, open for public signup, or disabled to block self-service registration. |
Run behind an HTTPS reverse proxy or load balancer and set:
APP_URL=https://signa.example.comOptional release metadata:
| Variable | Required | Description |
|---|---|---|
APP_VERSION |
No | App semver exposed through health/API metadata. Defaults to the package version fallback. |
APP_COMMIT_SHA |
No | Git commit SHA for the deployed artifact. |
APP_BUILD_TIME |
No | UTC ISO timestamp for the deployed artifact build. |
Signa mirrors DocuSeal's non-multitenant deployment posture: public signup is not treated as a normal on-prem entry point. The default REGISTRATION_MODE=initial_only allows the first owner account to self-register on a fresh database, then closes self-service registration after a user exists. Additional users should be invited or created from Settings.
Registration modes:
| Mode | Behavior |
|---|---|
initial_only |
Default. The first user can register; every later self-service registration and OAuth account creation is blocked. |
open |
Public signup is enabled. Use this only for controlled SaaS/public deployments. |
disabled |
Self-service registration is fully disabled, including the first user. Use this when users are seeded or provisioned externally. |
The product root redirects to /templates. Existing auth guards send signed-out
users to login and preserve authenticated access to the console. The public
marketing site is the separate apps/marketing deployment.
Database selection follows DocuSeal's deployment model:
# SQLite: leave DATABASE_URL empty.
DATABASE_URL=
# PostgreSQL: set one URL.
DATABASE_URL=postgresql://signa:password@database.example.com:5432/signa| Variable | Required | Default | Description |
|---|---|---|---|
SQLITE_DATABASE_PATH |
No | /data/signa.sqlite |
SQLite file path inside the container. |
SQLITE_SYNCHRONIZE |
No | true |
TypeORM synchronize for SQLite quick starts. Disable for controlled production migrations. |
DATABASE_URL |
Optional | empty | PostgreSQL URL. If set, PostgreSQL is used. |
DATABASE_SSL |
Optional | false |
Encrypts PostgreSQL traffic without requiring a custom CA certificate. |
DATABASE_MIGRATIONS_RUN |
Optional | true for PostgreSQL |
Runs TypeORM migrations on container start. |
PostgreSQL example:
DATABASE_URL=postgresql://signa:password@database.example.com:5432/signa
DATABASE_SSL=trueDATABASE_SSL=true encrypts the PostgreSQL connection but does not validate
the database certificate chain. Keep SSL parameters out of DATABASE_URL when
using this flag because PostgreSQL URL parameters can override the driver SSL
configuration. Deployments that require database identity verification should
use a trusted CA configuration instead.
DATABASE_HOST, DATABASE_PORT, DATABASE_USER, DATABASE_PASSWORD, and
DATABASE_NAME remain accepted for backward compatibility, but new
deployments should use DATABASE_URL.
SQLite plus local blob storage is the smallest self-hosted setup. The single
/data volume contains the database, files, generated PDFs, Redis persistence,
and generated application secret:
APP_URL=https://signa.example.comPostgreSQL plus local blob storage keeps documents on the app host but moves relational data to PostgreSQL:
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/signaPostgreSQL plus S3 is the recommended production shape when the app container should be disposable. Keep the S3 bucket private; Signa serves files through its own signed /storage/blobs/... proxy URLs:
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/signa
S3_ATTACHMENTS_BUCKET=signa-production-blobsThat single bucket variable activates S3, matching DocuSeal's deployment
contract. The region defaults to us-east-1. Set AWS_REGION when the bucket
uses another region. On AWS infrastructure, use an EC2 instance profile, ECS
task role, or EKS workload identity and leave static credentials empty. The AWS
SDK default provider chain also supports standard local AWS profiles and
temporary credentials.
For S3-compatible storage such as MinIO, R2, or LocalStack, add:
S3_ATTACHMENTS_BUCKET=signa-documents
S3_ENDPOINT=https://storage.example.comA custom endpoint automatically enables path-style requests. Existing
AWS_S3_BUCKET and AWS_S3_ENDPOINT variables remain supported as aliases.
S3 encrypts new objects at rest by default; use
AWS_S3_SERVER_SIDE_ENCRYPTION only when an explicit upload encryption header
is required by bucket policy.
The runtime role needs only bucket readiness plus object read/write access:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::signa-production-blobs"
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::signa-production-blobs/*"
}
]
}Replace the example bucket name. s3:ListBucket is used by the readiness
probe; Signa does not require a public bucket or public object ACLs.
Use the smallest topology that matches the required availability.
Single-node EC2
This is the closest equivalent to DocuSeal's one-command deployment. Attach an
encrypted EBS volume, mount it for Docker data, and run the Signa image with one
persistent /data volume:
docker run -d \
--name signa \
--restart unless-stopped \
-p 3000:3000 \
-e APP_URL=https://signa.example.com \
-e SMTP_ADDRESS=email-smtp.eu-west-1.amazonaws.com \
-e SMTP_PORT=587 \
-e SMTP_USERNAME=SES_SMTP_USERNAME \
-e SMTP_PASSWORD=SES_SMTP_PASSWORD \
-e "SMTP_FROM=Signa <signing@example.com>" \
-v signa-data:/data \
signaTerminate TLS at an Application Load Balancer, reverse proxy, or managed edge
service and forward traffic to container port 3000. Back up the /data
volume. This shape intentionally keeps SQLite, local storage, and private Redis
on one host and must not be scaled horizontally.
Stateless ECS
Use this topology for multiple application tasks or replaceable containers:
| Concern | AWS service | Signa configuration |
|---|---|---|
| Container | ECR and ECS/Fargate | Expose container port 3000; ALB health check /api/health. |
| Relational data | RDS PostgreSQL | Set one DATABASE_URL; set DATABASE_SSL=true. |
| Documents | Private S3 bucket | Set S3_ATTACHMENTS_BUCKET; grant the ECS task role access to that bucket. |
| Queues and cache | ElastiCache for Redis | Set REDIS_URL; QUEUE_REDIS_URL defaults to the same URL. |
| Amazon SES SMTP | Set the SMTP_* variables. SES SMTP credentials are not IAM access keys. |
|
| Secrets | Secrets Manager or SSM | Inject DATABASE_URL, JWT_SECRET, SMTP credentials, and provider secrets into the task. |
Each ECS task remains disposable because persistent state is held by RDS, S3, and Redis. Use rolling deployments with more than one healthy task only after all three shared services are configured.
Signa follows the same deployment principle documented by DocuSeal: one public application port, SQLite when no database URL is supplied, and a database URL for an external database. AWS recommends task roles for application credentials; do not bake AWS access keys into the image.
| Variable | Required | Default | Description |
|---|---|---|---|
REDIS_URL |
No | private Redis in the container | Set an external Redis URL for multi-instance or managed deployments. |
QUEUE_ENABLED |
No | true in Docker |
Enables BullMQ workers for mail, SMS, webhooks, and async processing. |
QUEUE_REDIS_URL |
No | same as REDIS_URL |
Redis connection used by BullMQ. |
QUEUE_PREFIX |
No | signa |
BullMQ key prefix. |
QUEUE_DEFAULT_ATTEMPTS |
No | 3 |
Default retry attempts. |
QUEUE_BACKOFF_MS |
No | 5000 |
Default retry backoff. |
QUEUE_REMOVE_ON_COMPLETE |
No | 1000 |
Number of completed jobs retained. |
QUEUE_REMOVE_ON_FAIL |
No | 5000 |
Number of failed jobs retained. |
CACHE_NAMESPACE |
No | signa-cache |
Cache namespace. |
CACHE_TTL_MS |
No | 3600000 |
Cache TTL in milliseconds. |
| Variable | Required | Default | Description |
|---|---|---|---|
BULL_BOARD_ENABLED |
No | false |
Enables queue dashboard at BULL_BOARD_ROUTE. |
BULL_BOARD_ROUTE |
No | /queues |
Dashboard route on the backend server. |
BULL_BOARD_USER |
Required if enabled | admin |
Basic-auth username. |
BULL_BOARD_PASS |
Required if enabled | none | Use a strong password. |
Mail delivery becomes active as soon as SMTP_ADDRESS is present. SMTP
authentication is enabled when both username and password are present.
| Variable | Required | Default | Description |
|---|---|---|---|
SMTP_ADDRESS |
Yes | empty | SMTP hostname. Its presence enables delivery. |
SMTP_PORT |
No | 587 |
SMTP port. Use 587 for STARTTLS or 465 for implicit TLS. |
SMTP_USERNAME |
Provider dependent | empty | SMTP username. |
SMTP_PASSWORD |
Provider dependent | empty | SMTP password. |
SMTP_FROM |
Yes | empty | Sender, for example Signa <sign@company.com>. |
SMTP_DOMAIN |
No | empty | Client domain used for SMTP EHLO. |
SMTP_AUTHENTICATION |
No | plain |
SMTP auth method. |
SMTP_REPLY_TO |
No | empty | Optional reply-to address. |
SMTP_ENABLE_STARTTLS |
No | true |
Require STARTTLS on non-implicit-TLS connections. |
SMTP_ENABLE_SSL |
No | false |
Enable implicit TLS. Port 465 enables it automatically. |
SMTP_SSL_VERIFY |
No | true |
Verify the SMTP server certificate. |
SMTP_OPEN_TIMEOUT |
No | 15 |
Connection timeout in seconds. |
SMTP_READ_TIMEOUT |
No | 25 |
Socket timeout in seconds. |
Amazon SES example:
SMTP_ADDRESS=email-smtp.eu-west-1.amazonaws.com
SMTP_PORT=587
SMTP_USERNAME=SES_SMTP_USERNAME
SMTP_PASSWORD=SES_SMTP_PASSWORD
SMTP_FROM=Signa <signing@example.com>
SMTP_ENABLE_STARTTLS=true
SMTP_SSL_VERIFY=trueSES SMTP credentials are distinct from normal AWS access keys. Verify the sender identity in SES and move the account out of the SES sandbox before sending to arbitrary recipients.
Existing MAIL_* variables remain supported as compatibility aliases.
Social login OAuth credentials:
| Variable | Required | Description |
|---|---|---|
GOOGLE_AUTH_CLIENT_ID |
Optional | Google OAuth client ID for Sign in with Google. |
GOOGLE_AUTH_CLIENT_SECRET |
Optional | Google OAuth client secret for backend code exchange. |
GOOGLE_AUTH_REDIRECT_URI |
Optional | Frontend callback URL, for example http://localhost:3000/auth/oauth/google/callback. |
MICROSOFT_AUTH_CLIENT_ID |
Optional | Microsoft Entra application client ID for Microsoft sign-in. |
MICROSOFT_AUTH_CLIENT_SECRET |
Optional | Microsoft Entra client secret for backend code exchange. |
MICROSOFT_AUTH_REDIRECT_URI |
Optional | Frontend callback URL, for example http://localhost:3000/auth/oauth/microsoft/callback. |
Register the exact redirect URLs above with each provider. The backend creates the authorization URL, validates signed state and nonce, exchanges the code server-side, validates the provider ID token, then returns the normal Signa session.
Server-side OAuth credentials:
| Variable | Required | Description |
|---|---|---|
GMAIL_OAUTH_CLIENT_ID |
Optional | Gmail OAuth client ID. |
GMAIL_OAUTH_CLIENT_SECRET |
Optional | Gmail OAuth client secret. |
GMAIL_OAUTH_REDIRECT_URI |
Optional | Frontend Gmail callback URL. |
MICROSOFT_OAUTH_CLIENT_ID |
Optional | Microsoft OAuth client ID. |
MICROSOFT_OAUTH_CLIENT_SECRET |
Optional | Microsoft OAuth client secret. |
MICROSOFT_OAUTH_REDIRECT_URI |
Optional | Frontend Microsoft callback URL. |
Google Drive Picker frontend build-time credentials:
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID |
Optional | Google OAuth client ID exposed to the browser. |
NEXT_PUBLIC_GOOGLE_PICKER_API_KEY |
Optional | Google Picker API key. |
NEXT_PUBLIC_GOOGLE_PICKER_APP_ID |
Optional | Google Cloud project app ID. |
Configure the OAuth client and API key in Google Cloud Console, enable Google Picker API and Google Drive API, then allow the app origin in OAuth and API key restrictions.
| Variable | Required | Description |
|---|---|---|
TWILIO_ACCOUNT_SID |
Optional | Twilio account SID. |
TWILIO_AUTH_TOKEN |
Optional | Twilio auth token. |
TWILIO_VERIFY_SERVICE_SID |
Optional | Twilio Verify service for phone OTP. |
TWILIO_MESSAGING_SERVICE_SID |
Optional | Twilio Messaging Service for SMS invitations. |
TWILIO_FROM_PHONE |
Optional | Fallback Twilio sender number. |
SMS_CALLBACK_SECRET |
Optional | Secret for SMS provider callbacks. |
| Variable | Required | Default | Description |
|---|---|---|---|
WEBHOOK_TIMEOUT_MS |
No | 10000 |
HTTP timeout for webhook deliveries. |
WEBHOOK_MAX_ATTEMPTS |
No | 8 |
Maximum webhook retry attempts. |
WEBHOOK_BACKOFF_MS |
No | 30000 |
Base retry backoff. |
| Variable | Required | Default | Description |
|---|---|---|---|
STORAGE_SERVICE |
No | auto |
auto uses S3 when a bucket is configured, otherwise local disk. Use local or s3 only to force a backend. |
STORAGE_PATH |
No | /storage in Docker |
Local file storage root, used when the active storage backend is local. Existing local blobs remain readable after switching new writes to S3. |
S3_ATTACHMENTS_BUCKET |
Required for S3 | empty | Preferred private S3 bucket variable. Setting it automatically activates S3. |
AWS_REGION |
No | us-east-1 |
AWS region for the S3 bucket. AWS_DEFAULT_REGION is also recognized. |
AWS_ACCESS_KEY_ID |
Optional | empty | Explicit AWS access key. Leave empty on EC2/ECS/EKS/Lambda-style deployments that use IAM roles. |
AWS_SECRET_ACCESS_KEY |
Optional | empty | Explicit AWS secret key. Leave empty when using IAM roles. |
AWS_SESSION_TOKEN |
Optional | empty | Temporary session token for STS credentials. |
S3_ENDPOINT |
Optional | empty | Preferred custom endpoint for S3-compatible providers. It automatically enables path-style requests. |
AWS_S3_BUCKET |
Optional alias | empty | Backward-compatible alias for S3_ATTACHMENTS_BUCKET. |
AWS_S3_PREFIX |
No | empty | Optional object key prefix, for example production/blobs. |
AWS_S3_ENDPOINT |
Optional alias | empty | Backward-compatible alias for S3_ENDPOINT. |
AWS_S3_FORCE_PATH_STYLE |
No | false |
Enable path-style URLs for S3-compatible storage that requires it. |
AWS_S3_SERVER_SIDE_ENCRYPTION |
Optional | empty | Explicit upload encryption header: AES256 or aws:kms. S3 default bucket encryption normally makes this unnecessary. |
ATTACHMENT_INGEST_MAX_BYTES |
No | 10485760 |
Max downloaded attachment size. |
PDF_PREVIEW_MAX_PAGES |
No | 15 |
Max preview pages generated per PDF. |
PDF_PREVIEW_MAX_WIDTH |
No | 1400 |
Preview image width. |
PDF_SIGNATURE_SUBFILTER |
No | pades |
pades uses ETSI.CAdES.detached; adobe uses legacy Adobe detached signatures. |
PDF_TIMESTAMP_REQUIRED |
No | false |
When true, completed PDF signing fails if no RFC3161 /DocTimeStamp can be embedded. Leave false to keep TSA optional. |
PDF_TIMESTAMP_TIMEOUT_MS |
No | 10000 |
Timeout for RFC3161 timestamp server requests. |
PDF_LTV_REQUIRED |
No | false |
When true, completed PDF signing fails if OCSP/CRL evidence cannot be collected and embedded into DSS/VRI. |
PDF_LTV_HTTP_TIMEOUT_MS |
No | 10000 |
Timeout for OCSP/CRL revocation evidence HTTP requests. |
PDF_A_ENABLED |
No | false |
Enables pre-signing PDF/A conversion through an external Ghostscript-compatible binary. |
PDF_A_REQUIRED |
No | false |
When true, completed PDF signing fails if PDF/A conversion fails. |
PDF_A_LEVEL |
No | 2b |
Requested PDF/A level: 1b, 2b, or 3b. |
PDF_A_GHOSTSCRIPT_PATH |
No | gs |
Ghostscript executable used for PDF/A conversion. |
PDF_A_VERAPDF_PATH |
No | verapdf |
Optional veraPDF executable used for PDF/A validation after conversion. |
PDF_A_TIMEOUT_MS |
No | 60000 |
Timeout for PDF/A conversion and validation commands. |
DOCUMENT_CONVERSION_MAX_BYTES |
No | 15728640 |
Max DOCX/HTML conversion input size. |
HTML_TO_PDF_TIMEOUT_MS |
No | 30000 |
HTML-to-PDF render timeout. |
THROTTLE_TTL_MS |
No | 60000 |
API rate-limit window. |
THROTTLE_LIMIT |
No | 120 |
API rate-limit request count. |
Signa separates visible document completion from cryptographic PDF validation. The visible form values are rendered onto the PDF first, then the final bytes are signed, enriched with long-term validation evidence, and optionally timestamped.
- A submitter fills fields in the signing UI.
- The backend normalizes the submitted values and renders them onto the source PDF.
- Supported field values are stamped onto pages before cryptographic signing: text, dates, numbers, checkboxes, radio/select/multiple values, typed/drawn/uploaded signatures, initials, images, and file references where applicable.
- If account settings require a signature ID, Signa stamps signer identity metadata next to the visible signature.
- The generated completed PDF bytes are hashed with SHA-256 and stored with the completed document metadata.
This stage is the visual/legal representation of what the signer saw and accepted. Cryptographic signing starts after this stage so later validation covers the completed document bytes.
- Signa loads the account default signing certificate from encrypted account config. Certificates are stored as PKCS#12/PFX material, with a generated Signa default certificate available for self-hosted setups.
- The backend prepares a PDF signature placeholder with
@signpdf/placeholder-pdf-lib. - The PDF is signed with
@signpdf/signer-p12and@signpdf/signpdf. - By default, signatures use the PAdES-compatible
ETSI.CAdES.detachedSubFilter. SetPDF_SIGNATURE_SUBFILTER=adobeonly for legacyadbe.pkcs7.detachedcompatibility. - The CMS
SignedDatacovers the PDFByteRange, meaning verification can detect changes to signed PDF bytes.
The CMS signing model follows RFC 5652. The certificate chain and revocation model follow RFC 5280. PAdES PDF signature structure follows ETSI EN 319 142-1.
Timestamping is optional unless explicitly required.
- Configure a timestamp server URL in account e-signature settings.
- Signa validates the timestamp server before saving it.
- During completed PDF generation, Signa requests an RFC3161 timestamp token after PAdES signing and DSS/VRI evidence embedding.
- If successful, Signa appends a final
/DocTimeStampsignature dictionary with/SubFilter /ETSI.RFC3161, so the timestamp covers the signed PDF plus embedded validation evidence. - Timestamp metadata is stored with the completed document.
- If
PDF_TIMESTAMP_REQUIRED=true, signing fails when timestamp evidence cannot be embedded.
This allows deployments with stricter evidence requirements to fail closed while keeping normal self-hosted signing usable without a TSA.
Long-term validation is optional unless PDF_LTV_REQUIRED=true.
During signing, Signa attempts to collect revocation evidence for signer certificates:
- Parse the CMS signer certificate chain.
- For Signa autogenerated self-hosted certificates, generate issuer-signed internal CRL evidence and treat the certificate as good when it is absent from the empty CRL.
- For uploaded/external certificates, read Authority Information Access extensions for OCSP URLs.
- Read CRL Distribution Point extensions for CRL URLs.
- Request OCSP evidence first.
- Fall back to CRL evidence when OCSP is unavailable.
- Store raw OCSP/CRL bytes in
pdf_revocation_evidence, keyed by certificate hash, issuer, serial number, and evidence type. - Append a PDF DSS/VRI update after signing without rewriting already signed bytes.
The completed PDF signing order is:
visual stamping -> PAdES CMS signature -> DSS/VRI evidence append -> optional RFC3161 DocTimeStamp
The DSS/VRI append is a low-level incremental PDF update:
/DSSis added to the PDF catalog./Certsstores DER certificate streams./OCSPsstores OCSP response streams./CRLsstores CRL streams./VRIlinks the exact signature hash to the evidence streams.- The update writes a new xref/trailer with
/Prev, preserving the original signed byte ranges.
Evidence status is explicit:
good: revocation evidence validates and does not report revocation.revoked: evidence reports the signer certificate as revoked.unknown: evidence exists but cannot be fully classified.unavailable: revocation endpoints could not be reached.missing: no usable OCSP/CRL evidence was available.
If PDF_LTV_REQUIRED=false, signing continues when evidence is unavailable and the result records ltv_status=missing. If PDF_LTV_REQUIRED=true, signing fails unless good revocation evidence can be collected and embedded. Signa autogenerated certificates satisfy this requirement through their issuer-signed internal CRL evidence.
POST /api/tools/verify accepts a PDF upload and returns checksum, signature, certificate, timestamp, revocation, and LTV status.
Verification performs these checks:
- Parse the PDF and detect signature dictionaries.
- Validate the signature
ByteRangeshape and compute the signed byte-range SHA-256. - Compare the full PDF SHA-256 against completed Signa documents and generated artifact metadata.
- Parse CMS
SignedDatafrom the PDF/Contents. - Validate the CMS signed
messageDigestagainst the PDFByteRangebytes. - Verify the CMS signature over signed attributes.
- Extract signer certificates and classify the chain as trusted, external, expired, invalid, or missing. Trust anchors include the generated Signa root and any account-uploaded root CA certificates from E-Signature settings.
- Detect PAdES SubFilter compatibility.
- Detect RFC3161
/DocTimeStampsignatures. - Read
/DSSand/VRI. - Match the VRI entry to the exact CMS signature hash.
- Parse embedded DER certificates, OCSP responses, and CRLs.
- Validate OCSP/CRL signatures and certificate revocation status where evidence is present.
- Return
ltv_statusasvalid,missing, orinvalid.
DocuSeal-style verification output includes:
- checksum status,
- signature validity,
- trusted or external certificate status,
- certificate chain,
- matched trust anchor and certificate policy errors,
- signer name,
- signing reason,
- signing time,
- PAdES SubFilter status,
- timestamp-signature presence,
- revocation evidence status,
- LTV status.
Implemented:
- RFC 5652 CMS
SignedDataparsing and verification with PKI.js. - RFC 5280 certificate extraction, CA/basic-constraints checks, validity-window checks, and chain classification against Signa plus account-uploaded trust roots.
- RFC 6960 OCSP request/response parsing and status checks where certificate endpoints exist.
- CRL parsing and revoked-certificate checks.
- Issuer-signed internal CRL evidence for Signa autogenerated self-hosted certificates.
- PAdES
ETSI.CAdES.detachedSubFilter signing. - RFC3161 document timestamp append after DSS/VRI embedding.
- Incremental DSS/VRI embedding that preserves signed bytes.
- Verifier-side DSS/VRI matching and evidence validation.
Still tracked as hardening work:
- Optional pre-signing PDF/A conversion through Ghostscript and optional veraPDF validation. This is disabled by default and fails open unless
PDF_A_REQUIRED=true. - More exhaustive OCSP responder-chain validation.
- Full HexaPDF/PDFium-grade flattening for every PDF edge case.
PDF/A compliance is handled before cryptographic signing:
visual stamping -> optional PDF/A conversion/validation -> PAdES CMS signature -> DSS/VRI evidence append -> optional RFC3161 DocTimeStamp
This order is intentional. PDF/A conversion rewrites PDF structure, so it must happen before the final PAdES signature. Signa records conversion and validation metadata on completed-document artifacts. If Ghostscript or veraPDF is not installed and PDF_A_REQUIRED=false, signing continues with conversionStatus=unavailable; regulated deployments should install those binaries and set PDF_A_REQUIRED=true.
References:
- RFC 5652, Cryptographic Message Syntax:
https://www.rfc-editor.org/rfc/rfc5652 - RFC 5280, X.509 PKI certificate and CRL profile:
https://www.rfc-editor.org/rfc/rfc5280 - RFC 6960, Online Certificate Status Protocol:
https://www.rfc-editor.org/rfc/rfc6960 - RFC 3161, Time-Stamp Protocol:
https://www.rfc-editor.org/rfc/rfc3161 - ETSI EN 319 142-1, PAdES PDF Advanced Electronic Signatures:
https://www.etsi.org/deliver/etsi_en/319100_319199/31914201/
| Variable | Required | Default | Description |
|---|---|---|---|
HEALTH_HEAP_LIMIT_MB |
No | 512 |
Heap warning threshold. |
HEALTH_REDIS_REQUIRED |
No | false |
Marks health unhealthy when Redis is unavailable. |
HEALTH_REDIS_TIMEOUT_MS |
No | 1000 |
Redis health timeout. |
API_HEALTH_WINDOW_MS |
No | 300000 |
Runtime observability window. |
API_SLOW_REQUEST_WARN_MS |
No | 1000 |
Slow request warning threshold. |
API_HEALTH_P95_DEGRADED_MS |
No | 1000 |
Degraded p95 threshold. |
API_HEALTH_ERROR_RATE_DEGRADED_PERCENT |
No | 5 |
Degraded error-rate threshold. |
API_HEALTH_MIN_REQUEST_COUNT |
No | 20 |
Minimum requests before degraded error-rate checks. |
These are read when next build runs. Rebuild the Docker image after changing them.
| Variable | Required | Default | Description |
|---|---|---|---|
NEXT_PUBLIC_API_BASE_URL |
No | empty | Empty uses same-origin /api and is recommended. Set only when the API is intentionally hosted on a separate public origin. |
NEXT_PUBLIC_MARKETING_URL |
No | http://localhost:3002 |
Standalone marketing and documentation origin used by legacy product redirects. |
NEXT_PUBLIC_SIGNING_BASE_URL |
No | http://localhost:3000 in Compose |
Public signing-page origin, useful for QR signing links. |
NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID |
Optional | empty | Browser Google OAuth client ID. |
NEXT_PUBLIC_GOOGLE_PICKER_API_KEY |
Optional | empty | Google Picker API key. |
NEXT_PUBLIC_GOOGLE_PICKER_APP_ID |
Optional | empty | Google Picker app ID. |
NEXT_OUTPUT |
Optional | empty | Set to export only for static export experiments. Do not use for the full app. |
apps/marketing is a separate full-stack Next.js application. It is intended
for Vercel deployment and does not ship in the on-prem Signa runtime. Run it
locally with:
Production URL: https://signa-docs.vercel.app
cp apps/marketing/.env.example apps/marketing/.env.local
pnpm dev:marketingThe marketing workspace uses Supabase for newsletter subscriptions and future licensing records:
| Variable | Required | Description |
|---|---|---|
VERCEL_PROJECT_PRODUCTION_URL |
Vercel | System-provided production domain used as the canonical marketing and documentation origin on Vercel. |
NEXT_PUBLIC_MARKETING_URL |
Fallback | Public origin used locally and on non-Vercel deployments. Use https://signa-docs.vercel.app in production. |
NEXT_PUBLIC_APP_URL |
Yes | Product application origin used by sign-in and console links. |
SUPABASE_URL |
Newsletter | Server-side Supabase project URL for marketing data. |
SUPABASE_SECRET_KEY |
Newsletter | Server-only secret used by the newsletter Route Handler. Never expose this value to browsers. |
Newsletter schema changes live in apps/marketing/supabase/migrations. The
subscriber table enables RLS and grants no public table access; only the
validated server Route Handler writes with the secret key. These variables are
not required to compile the static site; missing runtime configuration makes
newsletter requests return a temporary-unavailable response.
Documentation helpfulness responses use the same protected server integration
and the docs_page_feedback migration. The docs footer derives previous and
next destinations from the shared sidebar navigation, preserves each browser's
vote per page, and lets readers revise their response.
The same deployment publishes the indexable /alternatives comparison hub and
focused DocuSeal, Docusign, PandaDoc, Adobe Acrobat Sign, Dropbox Sign, and
SignNow comparison routes. Vercel uses VERCEL_PROJECT_PRODUCTION_URL; other
deployments use NEXT_PUBLIC_MARKETING_URL. Keep that canonical origin aligned
with the public domain so canonical tags, JSON-LD, Open Graph URLs, robots host,
and sitemap entries remain consistent.
The default container includes its private single-node Redis runtime, so mail, webhooks, SMS, and document queues work with the same one-container command:
docker build -t signa .
docker run --name signa \
-p 3000:3000 \
-v signa-data:/data \
signaFor a public host:
docker run --name signa \
-p 3000:3000 \
-e APP_URL=https://signa.example.com \
-v signa-data:/data \
signaFor multi-instance deployment, set DATABASE_URL, REDIS_URL, and S3
variables so every application container is disposable and shares the same
database, queue, and object store.
pnpm install
pnpm build
NODE_ENV=production \
APP_URL=https://signa.example.com \
DATABASE_URL=postgresql://signa:password@database.example.com:5432/signa \
JWT_SECRET=replace-with-at-least-32-random-characters \
pnpm start:backend:prod
INTERNAL_API_URL=http://127.0.0.1:3001 pnpm start:frontendFor production without Docker, run Redis separately and set
QUEUE_ENABLED=true plus REDIS_URL. The frontend exposes the public
same-origin /api route and proxies it to INTERNAL_API_URL, so browsers do
not need direct access to backend port 3001.