Guide

Self-hosted backend

The API (apps/api) is a standard NestJS service backed by PostgreSQL. Deploying it yourself means signature records never leave your infrastructure.

Requirements

  • Node.js 20+ and PostgreSQL 14+
  • A GitHub and/or Google OAuth app registered under your own domain — you can't reuse SignFlow's hosted OAuth app, since callback URLs are tied to a specific domain
  • S3-compatible object storage — only if you store signatures as rasterized images. The default SVG-path capture mode stores everything directly in Postgres and needs no object storage at all.

Quick start (Docker Compose)

A minimal starting point — adjust image tags and secrets before running this anywhere but locally.

services:
  api:
    build: ./apps/api
    ports:
      - "3001:3001"
    environment:
      DATABASE_URL: postgres://signflow:signflow@postgres:5432/signflow
      GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID}
      GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET}
      GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID}
      GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET}
      SESSION_SECRET: ${SESSION_SECRET}
      PUBLIC_APP_URL: https://your-domain.example.com
    depends_on:
      - postgres
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: signflow
      POSTGRES_PASSWORD: signflow
      POSTGRES_DB: signflow
    volumes:
      - signflow_pg:/var/lib/postgresql/data

volumes:
  signflow_pg:

Run migrations before first use — npm run db:migrate (adjust to whatever migration tooling apps/api actually ships with).

Add a MinIO (or other S3-compatible) service to this file only if you're using rasterized signature storage — it's not required for the default SVG-path mode.

Environment variables

VariableRequiredDescription
DATABASE_URLYesPostgreSQL connection string
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRETAt least one providerYour own GitHub OAuth App, scoped to your domain
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRETAt least one providerSame, for Google OAuth
SESSION_SECRETYesRandom string used to sign session cookies
PUBLIC_APP_URLYesThe public URL your instance is reachable at — used to build OAuth callback URLs
S3_ENDPOINT / S3_BUCKET / S3_ACCESS_KEY / S3_SECRET_KEYOnly for raster modeNot needed when signatures are stored as SVG path data (the default)

Pointing the SDK at your instance

The widget and API client need to talk to your deployment instead of the hosted default:

const client = new SignClient({
  apiKey: 'pk_live_...',
  baseUrl: 'https://api.your-domain.example.com',
});
baseUrl is optional in SignClient's constructor and defaults to the hosted instance — a self-hosted deployment must always pass it explicitly, or submissions will silently go to the default backend instead of yours.

Reverse proxy & TLS

The API doesn't terminate TLS itself — put it behind a reverse proxy (Caddy, Nginx, or Traefik all work fine) that handles certificates and forwards to the container's port. Make sure PUBLIC_APP_URL matches the externally-reachable HTTPS URL, since OAuth callback verification depends on it matching exactly.

Backups & data ownership

Signature records live in Postgres — back it up the way you would any production database (scheduled pg_dump, or your provider's point-in-time recovery). If you're using object storage for raster assets, back that up independently — it isn't covered by a Postgres backup.

SignFlow doesn't enforce a retention policy — how long you keep records, and your deletion process for right-to-erasure requests, is entirely under your control once self-hosted (see the Security & Compliance guide).

Self-hosting vs. the hosted dashboard

Self-host if data residency, retention control, or infrastructure ownership matters for your use case — you take on running Postgres and the API yourself in exchange for full control.

Use the hosted dashboard flow instead if you'd rather not run infrastructure at all — same API surface, same key model, just managed for you. Both point at the same client SDK; switching later is a matter of changing baseUrl, not rewriting integration code.