OPERATIONS

Admin & self-hosting.

This page covers service administration and running your own Contact instance. It is intended for operators, not for end users embedding a form.

Administrator roles

RoleScope
sudoFull service control, including creating super admins and using the management API key.
superService administration: tenants, forms across tenants, and the website contact inbox.
tenantWorkspace administration: own forms, submissions, integrations, and account.

Administrator authentication

People sign in through the app at POST /v1/admin/login, choosing the admin or tenant portal. Machine clients can call the management API with Authorization: Bearer <ADMIN_API_KEY>, which acts as a sudo service key. Never place this key in browser code.

Management API

Tenant, form, submission, and analytics endpoints are documented in the repository’s MANAGEMENT_API.md, and the full API contract is in UNIVERSAL_CONTACT_FORM_BACKEND.md. Public form endpoints never expose integrations.

Self-hosting

The backend is a Fastify service with a PostgreSQL database and a separate delivery worker. Run migrations before starting the API; the API also applies pending migrations on startup under an advisory lock.

cp .env.example .env
docker compose up -d postgres
npm install
npm run migrate
npm run dev

# in a second terminal
npm run worker

Key environment variables

VariablePurpose
DATABASE_URLPostgreSQL connection string.
PUBLIC_BASE_URLCanonical API origin used in generated URLs.
DATA_ENCRYPTION_KEYEncrypts integration secrets; must be shared by app and worker.
ADMIN_API_KEYService key for the management API (min 24 chars).
TERMS_VERSION / PRIVACY_VERSIONLegal versions that trigger re-acceptance.
SMTP_URL / MAIL_*Email delivery and auth codes.
TURNSTILE_SECRET_KEYEnables Turnstile on signup and site contact.
RATE_LIMIT_WINDOW_SECONDS / RATE_LIMIT_MAXSubmission rate limiting (default 60 / 20).
MAX_FILE_SIZE_MB / MAX_FILES_PER_REQUESTUpload limits (default 10 MB / 1).
MAX_TENANT_STORAGE_MBPer-workspace media quota (default 1024 MB).
RETENTION_DAYSSubmission retention window.

Seed an administrator

Set the seed credentials once, then run the seed script. Existing admins are left unchanged.

# .env.docker
SEED_ADMIN_EMAIL=admin@example.com
SEED_ADMIN_PASSWORD=change-me-please

docker compose --env-file .env.docker run --rm app node dist/db/migrate.js
docker compose --env-file .env.docker run --rm app node dist/db/seed-admin.js

Delivery semantics

Delivery is at-least-once, not exactly-once, with up to eight retries. Webhook payloads keep a stable deliveryId so receivers can deduplicate. Back up DATA_ENCRYPTION_KEY — rotating it without re-encrypting stored integrations makes them unreadable.

Verify a deployment

npm test
npm run typecheck
npm run build
npm audit --omit=dev