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
| Role | Scope |
|---|---|
sudo | Full service control, including creating super admins and using the management API key. |
super | Service administration: tenants, forms across tenants, and the website contact inbox. |
tenant | Workspace 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 workerKey environment variables
| Variable | Purpose |
|---|---|
DATABASE_URL | PostgreSQL connection string. |
PUBLIC_BASE_URL | Canonical API origin used in generated URLs. |
DATA_ENCRYPTION_KEY | Encrypts integration secrets; must be shared by app and worker. |
ADMIN_API_KEY | Service key for the management API (min 24 chars). |
TERMS_VERSION / PRIVACY_VERSION | Legal versions that trigger re-acceptance. |
SMTP_URL / MAIL_* | Email delivery and auth codes. |
TURNSTILE_SECRET_KEY | Enables Turnstile on signup and site contact. |
RATE_LIMIT_WINDOW_SECONDS / RATE_LIMIT_MAX | Submission rate limiting (default 60 / 20). |
MAX_FILE_SIZE_MB / MAX_FILES_PER_REQUEST | Upload limits (default 10 MB / 1). |
MAX_TENANT_STORAGE_MB | Per-workspace media quota (default 1024 MB). |
RETENTION_DAYS | Submission 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.jsDelivery 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