Self-hosted PDF e-signature. Upload a document, add signers — each person gets a unique link to place and sign. When everyone is done, a final PDF with embedded signatures and an audit trail is ready to download.
No external services. No accounts. Just a URL you control.
Upload a PDF and define who needs to sign it. The app generates a private panel link for the owner and a unique signing link for each signer (valid for 48 hours).
Each signer opens their link, draws or types their signature, drags it to position on any page of the document, and submits. When the last signature lands, the final PDF is composed automatically — signatures embedded at their recorded coordinates, audit page appended.
The owner can also sign directly from the panel without a separate link.
Requirements: Node.js 20+ · pnpm
git clone https://github.com/avelor-es/sign
cd sign
pnpm install
pnpm build
node build/index.jsThe app listens on port 3000 (PORT env var overrides it). Run it behind a reverse proxy (nginx, Caddy, Cloudflare Tunnel) for TLS termination.
| Variable | Description |
|---|---|
DATA_DIR |
Directory for the SQLite database and uploaded files. Defaults to ./data. |
TRUST_PROXY |
Set to 1 behind nginx / HAProxy, or cloudflare behind Cloudflare. Controls which header is used for real client IPs (rate limiting). Leave unset for direct exposure. |
ALLOWED_CIDRS |
Comma-separated list of CIDR blocks allowed to upload documents. Leave unset to allow uploads from any IP. Example: 100.64.0.0/10 for Tailscale, 192.168.1.0/24 for a local network. Signing links remain accessible from anywhere regardless of this setting. |
ALLOWED_ORIGINS |
Comma-separated list of additional origins allowed to submit uploads. Use when the app is served under more than one domain — for example a public domain plus a Tailscale domain. The app's own origin is always implicitly allowed. |
PUBLIC_URL |
Public-facing URL used to generate signing links (e.g. https://sign.example.com). Set this when the panel is accessed from a private URL but signing links must point to the public domain. |
ENCRYPTION_KEY |
64-character hex string. When set, PDFs and signature images are encrypted at rest with AES-256-GCM. Generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" |
PORT |
Port to listen on. Defaults to 3000. |
Copy .env.example to .env to get started.
pnpm dev # http://localhost:5173
pnpm check # svelte-check type validation
pnpm build # production build → build/MIT
Avelor