Deploy anywhere

On-premises and air-gapped

Everything inside your network with no outbound internet: PostgreSQL and PgBouncer, built-in auth with Keycloak or AD FS, MinIO, SSE, the worker, SMTP, Vault Transit, Valkey, syslog and LDAP.

Everything runs inside your own network: no SaaS, no CDN and no outbound internet. The on-premises reference profile proves it: it runs the stack on a Docker network with no route out, and its smoke test checks that the app can't reach the internet.

On-premises / air-gapped

People and clients

Browser · REST API · MCP · CLI

UserTasks

Web, API and worker · Docker Compose or Kubernetes on your servers

Built in

  • RealtimeSSE
  • SchedulerWorker container
  • SecretsMounted files

Your network (no internet)Your network

  • DatabasePostgreSQL + PgBouncer
  • AuthBuilt-in + Keycloak / AD FS
  • File storageMinIO (S3) or NFS
  • EmailSMTP relay
  • KMSVault / OpenBao Transit
  • CacheValkey
  • Log exportSyslog / Splunk / OTel
  • Directory syncLDAP / Active Directory
Everything runs inside your own network with no outbound internet: PostgreSQL behind PgBouncer, built-in auth with Keycloak or AD FS, MinIO or NFS for files, an SMTP relay, Vault Transit, Valkey, syslog or Splunk for logs and LDAP directory sync. Realtime, scheduling and secrets are built in.

What runs where

AreaServiceProvider idAlternatives
HostingDocker Compose (standalone profile) or Kubernetes, same image—Any container runtime
DatabasePostgreSQL 15+ behind PgBouncer in transaction modepostgresPatroni, CloudNativePG, Crunchy PGO, EDB
AuthBuilt-in passwords, plus Keycloak, Authentik, AD FS, Dex or Zitadel over OIDC or SAMLbuiltinoidc / saml for SSO only; LDAP bind login with directory sync
StorageMinIO or another S3-compatible store (Ceph RGW, SeaweedFS, StorageGRID)s3Local disk or NFS / SMB (local)
RealtimeBuilt-in SSE over LISTEN/NOTIFYsse—
SchedulerThe worker containerworkerOS cron, a systemd timer or a Kubernetes CronJob calling /api/cron/tick (external)
EmailAn SMTP relay (Exchange, Postfix)smtpconsole for demos
SecretsEnvironment and mounted files (<VAR>_FILE, SECRETS_FILES_DIR)envHashiCorp Vault / OpenBao (hashicorp_vault), encrypted in Postgres (db)
KMSENCRYPTION_KEYenvVault / OpenBao Transit (vault_transit) for envelope encryption
CacheValkey or Redisredispostgres (no extra service); KeyDB, Dragonfly; memory for one instance
Log exportSyslog over TLS to rsyslog, syslog-ng or a SIEM; Splunk HEC; an OpenTelemetry Collector; a webhook to Fluent Bit or VectordestinationsSyslog over TCP or UDP on a private network
Directory syncLDAP / Active Directory (LDAPS or StartTLS), or SCIM from an on-premises IdPper workspace—
AI agents (optional)Ollama, vLLM or any OpenAI-compatible endpointagent connector—

Configuration

Start from deploy/compose/standalone.env.example (npm run stack -- init generates the secrets). The parts that make it on-premises:

bash
DB_PROVIDER=postgres
DATABASE_URL=postgresql://usertasks:…@pgbouncer:5432/usertasks
MIGRATION_DATABASE_URL=postgresql://usertasks:…@postgres:5432/usertasks
REALTIME_DATABASE_URL=postgresql://usertasks:…@postgres:5432/usertasks
SCHEDULER_DATABASE_URL=postgresql://usertasks:…@postgres:5432/usertasks
AUTH_PROVIDER=builtin
AUTH_SSO_PROVIDERS=[{"id":"keycloak","type":"oidc","preset":"keycloak","baseUrl":"https://sso.corp.example","realm":"corp","clientId":"usertasks","clientSecret":"secret://keycloak-client-secret"}]
REALTIME_PROVIDER=sse
SCHEDULER_PROVIDER=worker
SCHEDULER_IN_PROCESS=false          # the worker container ticks
STORAGE_PROVIDER=s3
S3_ENDPOINT=https://minio.corp.example:9000
S3_FORCE_PATH_STYLE=true
STORAGE_BUCKET_PREFIX=usertasks-
EMAIL_PROVIDER=smtp
SMTP_HOST=mail.corp.example
SMTP_STARTTLS=required
EMAIL_FROM="UserTasks <tasks@corp.example>"
SECRETS_FILES_DIR=/run/secrets
KMS_PROVIDER=vault_transit          # optional
VAULT_ADDR=https://vault.corp.example:8200
VAULT_TRANSIT_KEY=usertasks
CACHE_PROVIDER=redis
CACHE_REDIS_URL=rediss://valkey.corp.example:6379
NODE_EXTRA_CA_CERTS=/etc/ssl/corp-ca.pem
ALLOW_PRIVATE_EGRESS=true           # integrations, LDAP and log destinations on private addresses

The full walkthrough (images, configuration, first workspace, checks) is On-premises setup.

Air-gapped installs

  • Images. Build or pull on a connected machine (usertasks, postgres:16, MinIO, and optionally PgBouncer, Keycloak and Valkey), then docker save them and push them to the internal registry. Build the usertasks image with the optional drivers you need (INCLUDE_OPTIONAL_DB_DRIVERS=true for Snowflake, Oracle and MongoDB): they can't be downloaded inside the network later, and a finished image can't gain one without a rebuild.
  • No outbound connections. Fonts and scripts are bundled into the image at build time, and the docs site is part of the app (/docs). The reference profile's smoke test fails if the app can reach the internet.
  • Integrations and agents reach only what you allow. Private addresses need ALLOW_PRIVATE_EGRESS=true, and cloud metadata addresses stay blocked.
  • Private CA. Mount it and set NODE_EXTRA_CA_CERTS for the database, SMTP, the IdP, Vault, LDAP and log destinations.
  • Kubernetes. Use the same image: a Job for node admin.cjs db migrate, a Deployment for node server.js, and a Deployment or CronJob for node worker.cjs.

Getting here from Supabase

If you evaluated on Supabase and now move in-house, follow the order in Switching providers: auth, realtime, scheduler, storage, email and secrets first, then the database. Run the switches from a machine that can reach both sides. After the database copy, the install no longer needs any outbound connection.

bash
usertasks-admin switch auth      --to builtin --yes
usertasks-admin switch realtime  --to sse --yes
usertasks-admin switch scheduler --to worker --yes
usertasks-admin switch storage   --to s3 --set endpoint=https://minio.corp.example:9000 --set force_path_style=true \
  --set access_key_id=usertasks --secret secret_access_key=… --yes
usertasks-admin switch email     --to smtp --set smtp_host=mail.corp.example --set from='UserTasks <tasks@corp.example>' --test-to you@corp.example --yes
usertasks-admin switch secrets   --to db --yes
# stop writes, then:
usertasks-admin switch database  --to postgres --target-url 'postgresql://usertasks:…@postgres.corp.example:5432/usertasks' --yes

A new install doesn't need any of this: start directly with the standalone profile (npm run stack -- up).

Trade-offs

  • Cost: hardware and people instead of cloud bills. Nothing is metered.
  • Operations: you run everything: PostgreSQL HA and backups, object storage, the IdP, certificates, upgrades. The reference profile and usertasks-admin doctor give you a tested baseline.
  • Latency: everything sits on your LAN.
  • Compliance: the strongest story. Data never leaves your network, and every component is open source or yours.

Data residency

Your data centre. Backups (pg_dump, object store replication, BACKUP_DIR for switch backups) stay wherever you put them.

Failure domains

Your own infrastructure. Run PostgreSQL with a replica and automated failover (Patroni, CloudNativePG), MinIO in distributed mode, and at least two app instances with a shared cache. The worker elects a leader through the database, so extra worker replicas wait as standbys.