Skip to content

Configuration reference

The API reads its configuration from environment variables. It also loads apps/api/.env if present; real environment variables take precedence. apps/api/.env.example lists the common ones.

Variable Default Required in production Notes
NODE_ENV (development) Set to production production turns on the checks above, disables dev routes and dev sign-in, and changes the TRUST_PROXY default. test is used by the test suite. Anything else means development.
PORT 3001 Port the API listens on.
APP_URL http://localhost:5173 Yes Public URL of the app, used in email links, Stripe return URLs and the Microsoft admin-consent redirect. In production the API serves the app itself, so this is the API’s public URL.
TRUST_PROXY 1 in production, loopback otherwise Express trust proxy. A number means that many proxy hops; otherwise an Express value such as loopback or an IP/subnet list. Determines the client IP that’s logged and rate-limited.
WEB_DIST apps/web/dist Folder of the built web app. If it has no index.html, the API serves only /api.
LOG_LEVEL info Log level for the API’s JSON (pino) logs.
MIGRATE_ON_START Outside production, migrations always run on start. In production they run on start only when this is true. See Database.
Variable Default Required in production Notes
DATABASE_URL postgres://postgres:postgres@localhost:54329/pdfeditor Yes Main database.
AUDIT_DATABASE_URL (uses DATABASE_URL) Connection the API uses to write the activity log; in production point it at the insert-only audit_writer role.
AUDIT_READ_DATABASE_URL (uses AUDIT_DATABASE_URL, else DATABASE_URL) Connection used to read the activity log (reports, exports, verification); point it at audit_reader.
AUDIT_ADMIN_DATABASE_URL Only for npm run migrate: an owner connection to the audit database, needed when AUDIT_DATABASE_URL is insert-only.
DEV_DB_PORT 54329 Only for npm run dev:db.
Variable Default Required in production Notes
SESSION_SECRET a fixed development value Yes, 32+ characters Signs session tokens and the Microsoft admin-consent state. Generate with openssl rand -hex 32.
SIGNING_LINK_SECRET SESSION_SECRET Recommended Derives the personal links in signing emails. Set it to its own random value so you can rotate SESSION_SECRET without affecting signing requests. See the warning below.
SESSION_HOURS 12 How long a sign-in lasts.
Variable Default Required in production Notes
ENTRA_CLIENT_ID Yes Application (client) ID of the app registration.
ENTRA_CLIENT_SECRET Yes Client secret, used for the on-behalf-of exchange. Microsoft sign-in is off unless both ID and secret are set.
ENTRA_APP_ID_URI Yes Application ID URI, api://<app domain>/<client id>. The app requests the scope <ENTRA_APP_ID_URI>/access_as_user.

See Microsoft Entra app registration.

Variable Default Required in production Notes
BILLING_MODE stripe if STRIPE_SECRET_KEY is set, else fake Must be stripe fake simulates checkout (development only).
STRIPE_SECRET_KEY Yes Secret API key.
STRIPE_WEBHOOK_SECRET Yes Signing secret of the webhook endpoint.

See Stripe.

Variable Default Required in production Notes
EMAIL_TRANSPORT mailgun if MAILGUN_API_KEY is set, else console Must be mailgun console keeps emails in memory (see /api/dev/emails).
MAILGUN_API_KEY Yes
MAILGUN_DOMAIN Yes Sending domain, e.g. mg.example.com.
MAILGUN_REGION us eu uses Mailgun’s EU endpoint; any other value uses the US endpoint.
EMAIL_FROM Team EditSign <no-reply@example.com> (from BRAND) Not checked, but set it From address. On-behalf emails use Sender Name via <display name> <address>. Use an address on MAILGUN_DOMAIN.

See Mailgun.

See Custom signing domains.

Variable Default Required in production Notes
CUSTOM_DOMAINS cloudflare if CLOUDFLARE_API_TOKEN is set; else off in production and fake in development cloudflare, off or fake (simulated; not allowed in production).
CLOUDFLARE_API_TOKEN With cloudflare Token with SSL and Certificates: Edit on the zone.
CLOUDFLARE_ZONE_ID With cloudflare Zone that has Cloudflare for SaaS enabled.
CUSTOM_DOMAIN_CNAME_TARGET customers.example.net in development With cloudflare Hostname customers point their CNAME at, e.g. customers.example.com.

The marketing site’s contact form posts to /api/contact, which checks a Cloudflare Turnstile token and emails the message to BRAND.salesEmail or BRAND.supportEmail (with the visitor as Reply-To). Create a Turnstile widget for the marketing site’s hostname in the Cloudflare dashboard; it gives you a site key (for the marketing build, PUBLIC_TURNSTILE_SITE_KEY) and a secret key (for the API).

Variable Default Required in production Notes
TURNSTILE_SECRET_KEY Cloudflare’s always-pass test secret outside production Yes Secret key of the Turnstile widget.
MARKETING_URL http://localhost:4321 outside production Yes Origin of the marketing site, e.g. https://www.example.com. Only this origin may call /api/contact from a browser. Separate several with commas.
Variable Default Notes
MAX_UPLOAD_MB 25 Largest PDF accepted when sending for signature and when saving to OneDrive or Teams.
SIGNING_EXPIRY_DAYS 30 Expiry for requests that don’t specify one. The app always sends one (7–90 days, chosen by the sender), so this applies to API clients that omit it.
Variable Default Notes
DEV_LOGIN true Shows the Development sign-in panel. Always off in production.
API_URL http://localhost:3001 Read by the web app’s Vite dev server: where to proxy /api.
Variable Used by Notes
SUPER_ADMIN_PASSWORD create-super-admin Password for the new super admin, instead of a prompt.
APP_URL, ENTRA_CLIENT_ID, ENTRA_APP_ID_URI, MARKETING_URL, TEAMS_APP_ID, TEAMS_APP_VERSION teams:package See Teams app package.
STRIPE_SECRET_KEY stripe:setup

These are read at build time:

Variable Site Default
PUBLIC_SITE_URL docs (apps/docs) https://docs.example.com
PUBLIC_APP_URL docs, marketing http://localhost:5173
PUBLIC_MARKETING_URL docs http://localhost:4321
PUBLIC_SITE_URL marketing (apps/marketing) https://www.example.com
PUBLIC_DOCS_URL marketing http://localhost:4322
PUBLIC_TURNSTILE_SITE_KEY marketing Cloudflare’s always-pass test site key (shows a “For testing only” notice)