Documentation · 10

Self-hosting and operations

A short operator page: the environment variables that matter, what changes when you run more than one host, and what is in the system admin area. The README in the repository is the longer version.

Environment variables

Core

VariableDefaultWhat it is for
DATABASE_URLlocal filePostgres connection string. Required for embeddings and semantic search, and so for everything built on them.
APPFIGURES_API_KEYnoneToken for the review sync.
OLLAMA_BASE_URLhttp://localhost:11434Where embeddings are computed.
EMBEDDING_MODELmxbai-embed-largeEmbedding model id.
EMBEDDING_DIM1024Vector dimension. Must match the model and the schema.
HOST and PORT0.0.0.0, 8000Where the server listens.
ENABLE_SCHEDULERtrueWhether this instance runs the automatic sync.
SYNC_INTERVAL_HOURS6Hours between automatic syncs.

Accounts, mail and access

VariableDefaultWhat it is for
SESSION_SECRETdev fallbackSigns session and flash cookies, and derives the key that protects registered connector secrets. Required in production, and identical on every host.
SITE_URLhttps://userken.comPublic origin used in magic links and emails.
PUBLIC_BASE_URLSITE_URLThe issuer for connector sign-in and the base of the /mcp resource.
SMTP_URLunset, mail is loggedsmtps://user:pass@host:465 or smtp://user:pass@host:587.
MAIL_FROM, MAIL_REPLY_TOuserken addressesHeaders on outgoing mail.
DEV_LOGINfalseEnables a loopback-only development login. Never in production.
APP_ENVunsetproduction marks a deployed instance.
Set the secret before you go live

In production the server refuses to boot without SESSION_SECRET. That is deliberate: a rotating or per-host secret would sign people out at random and would break connectors that have already been approved.

Running more than one host

userken is built to run on two hosts behind a load balancer with one Postgres, no sticky sessions and no shared disk. That shapes two rules.

State lives in Postgres, never on a host. Sessions, magic links, API keys, connector grants, job progress and run heartbeats are all rows. Uploaded files are held in memory for the length of the request and never written to local disk. Any host can serve any request, including the poll that follows a job it did not start.

Scheduled work runs on one host only. The hosts elect a leader through a lease row in Postgres, renewed continuously. The leader runs the syncs and the scheduled regeneration; a secondary serves reads and takes over within a lease if the leader stops. Long-running work such as a survey run or an onboarding job is claimed atomically by whichever host is free, with a heartbeat, so a run whose host dies is picked up rather than stranded.

One deliberate exception: the per-address limit on login attempts is counted per host, so two hosts means twice the ceiling. It is a throttle, not a security boundary.

The system admin area

/app/admin is the operator area inside the app shell, visible to superusers only:

  • System: the overview, including which host holds the lease.
  • Data: sync schedules, and running one now.
  • Embeddings: coverage, and backfilling.
  • Personas: regeneration per app and per category.
  • Catalogue: apps by category, adding an app, theme taxonomies.
  • Sources: connector status, running one now, and the uploads.
  • Accuracy: evaluation runs, starting one, and publishing a result.
  • Jobs: every queued job with its status and step.

An older token-gated dashboard at /admin still exists for the same operations. The app area is the one to use: it authenticates the person rather than passing an operations token through a browser.

Was this page useful?Nothing is sent anywhere. This button only changes what you see.Thanks. Nothing was sent anywhere: this page has no analytics. If something here is wrong or missing, write to [email protected].