Docs

Embedded and headless mode

Run Localization OS embedded inside another product, or as a headless API service driven by scripts, agents, or a host application. What the mode changes, the full environment matrix, the Postgres backend, and the external-cron pair.

Prime invariant

The default, non-embedded install is byte-identical to a normal deploy. Nothing on this page changes behavior unless you explicitly turn embedded mode on, and trust-host-auth (below) adds a second required condition on top of that.

What embedded mode changes#

Turning embedded mode on recomposes the app as an API-only service:

SurfaceNormal serverEmbedded
Web UIservednot mounted (404s)
Static assetsmountednot mounted (404s)
Back officemountednot mounted (404s)
In-process schedulerstartednot started; drive it from outside (below)
Retention purge daemonstartednot started; drive it from outside (below)
Login wallonon: it is the master switch for API role and scope checks
API, webhooks, and machine-readable docsservedserved: the whole machine surface stays live
Health and readiness checksservedserved

An embedded install is a real install, never a test boot, even though it legitimately points at its own data volume. Everything before the daemons in the startup sequence, database setup, identity resolution, applying settings, crash recovery, runs unchanged; embedded still needs all of it.

The login wall staying on is deliberate: the API is always token-authenticated, never session-authenticated, so walling it costs the headless surface nothing, but without the wall a valid token would write at any role.

Environment matrix#

The core variables an embedded deployment sets (not exhaustive: trust-host-auth, below, has its own documented environment variables). Boot-time environment variables are read once at process start; a Setting is stored in the database and read live.

Mode and storage

VariableMeaning
LOC_TMS_EMBEDDEDSet to 1 for the API-only assembly described above. Implies the server flag below.
LOC_TMS_SERVERSet to 1 for real-install posture: login wall on, no seeded accounts. Set it explicitly too, as belt and suspenders.
LOC_TMS_DATA_DIRThe volume holding uploads, keyfiles, and, on SQLite, the database. Defaults to a local folder.
DATABASE_URLSet to a Postgres connection string to use Postgres (below). Unset means zero-config SQLite.

Secrets

The two runtime secrets (a session secret and an encryption key) work the same way as in a normal deploy: pin them from your own secret manager on any container without a persistent volume, since an auto-generated keyfile regenerates on every boot and logs everyone out. In embedded mode, use the LOC_TMS_-prefixed variable names for both; this is deliberate, so an embedded install never silently adopts a same-named secret the host application already exports for its own purposes.

First-admin bootstrap

VariableMeaning
LOC_TMS_ADMIN_EMAILThe admin account's email, seeded by the headless init command.
LOC_TMS_ADMIN_PASSWORDThe admin account's password (12-character minimum). Never echoed.

Bootstrapping the first admin (headless)#

There is no interactive setup page in embedded mode, so seed the admin from the CLI, in-process, no server needed:

loc-tms init --from-env --issue-key --key-scopes "docs keys admin"

This reads the two admin environment variables above, enables the login wall, and prints one API key carrying all three per-surface scopes: the document translation API, string keys and the change-feed API, and the tick and admin verbs. The command is idempotent, so it is safe to leave wired into a container entrypoint; it does nothing once an account exists.

The external-cron pair#

Because the in-process daemons are off, an external scheduler drives the two things they normally do. Both mirror a CLI command, so you can run either remotely or in-process.

The tick. Runs one scheduler pass synchronously: routine housekeeping sweeps, plus any due connector schedules. It requires admin scope and role, but is deliberately not behind the stricter remote-admin lock, since a tick is the routine passage of time, not an irreversible admin action. It returns a count of any jobs it enqueued, never their ids. A tick already in progress returns a busy response rather than double-running. Cron it on whatever cadence the daemon would have used.

The retention purge. The destructive half. It keeps the full set of guards a destructive admin action needs: admin scope and role, the remote-admin lock, and an explicit confirmation value in the request body. It refuses when retention is disabled or the window is zero, and refuses a wrong or missing confirmation, deleting nothing either way. Run it on your own retention cadence, not on every tick.

# tick (frequent)
curl -fsS -X POST -H "X-Loc-Token: $LOC_TMS_ADMIN_TOKEN" \
     http://host:8000/api/admin/tick

# retention purge (on your retention cadence; requires confirmation)
curl -fsS -X POST -H "X-Loc-Token: $LOC_TMS_ADMIN_TOKEN" \
     -H "Content-Type: application/json" -d '{"confirm":"purge"}' \
     http://host:8000/api/admin/retention/purge

Postgres backend (embedded profile)#

Postgres is supported for the embedded and enterprise profile. Select it by setting DATABASE_URL to a Postgres connection string; leave it unset for the zero-config SQLite default. On a Postgres URL, migrations are applied before the app boots.

# docker-compose.yml (excerpt): embedded/headless API service on Postgres + a ticker.
services:
  localization-os:
    image: localization-os:latest
    environment:
      LOC_TMS_EMBEDDED: "1"                 # API-only assembly (implies LOC_TMS_SERVER)
      LOC_TMS_SERVER: "1"                   # explicit real-install signal (belt-and-suspenders)
      LOC_TMS_DATA_DIR: /data               # uploads + keyfiles volume (DB lives in Postgres)
      DATABASE_URL: postgresql+psycopg://locos_app:locos_app@db:5432/locos_app
      # Embedded mode honors only the prefixed spelling (see the secrets note above).
      LOC_TMS_SESSION_SECRET: ${LOC_TMS_SESSION_SECRET}   # pin from your secret manager
      LOC_TMS_ENCRYPTION_KEY: ${LOC_TMS_ENCRYPTION_KEY}
    volumes:
      - loc-data:/data
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8000/ready"]
      interval: 30s

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: locos_app
      POSTGRES_PASSWORD: locos_app          # change me; never publish 5432 to the host
      POSTGRES_DB: locos_app
    volumes:
      - loc-pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U locos_app -d locos_app"]
      interval: 10s
      timeout: 5s
      retries: 5

  ticker:
    # Any cron/sidecar that POSTs the tick on an interval with an admin API token
    # (mint it once via the init command above). NOTE the doubled $$ below:
    # Compose interpolates a single $ on the HOST, so $$ escapes to a literal $ that
    # the CONTAINER shell expands from its own environment instead.
    image: curlimages/curl:latest
    entrypoint: ["sh", "-c", "while true; do
        curl -fsS -X POST -H \"X-Loc-Token: $$LOC_TMS_ADMIN_TOKEN\"
             http://localization-os:8000/api/admin/tick || true;
        sleep 60; done"]
    environment:
      LOC_TMS_ADMIN_TOKEN: ${LOC_TMS_ADMIN_TOKEN}   # an admin-scoped per-user key

volumes:
  loc-data:
  loc-pgdata:

The embedded assembly (which routes are mounted, which daemons are skipped, the wall staying on) and the database backend selection are independent by construction: one keys off the embedded flag, the other off the connection string, and neither depends on the other.

Trust-host-auth (tokenless API, embedded only)#

An embedded install that sits behind a host application whose own boundary already authenticates every caller can let API calls arrive without a per-request token. It is off by default and requires two independent conditions to hold at once: embedded mode itself, and a dedicated setting that can only ever be changed on the host, never over the very API it guards.

When both hold, a tokenless request resolves to a configured trust principal: a real, dedicated user provisioned at boot, whose reach is bounded by an explicit scope grant and role, the same two levers that gate every other API caller. A request that does carry a token is still verified exactly as always; a bad token is still refused. A non-embedded boot, or the setting left off, is byte-identical to a normal deploy.

Security posture

This mode is only for deployments where the host's own boundary already authenticates every caller. Turning it on anywhere the API is directly reachable removes authentication from it.

Two optional, strictly narrowing hardening layers sit on top of this: a shared-secret header your proxy injects on every request, which further restricts trust to just your own proxy and supports rotation without a hard cutover; and an attribution header carrying the end user your proxy already authenticated, recorded for audit purposes only and carrying no authority of its own. Both are dormant unless you deliberately configure them, and a reverse-proxy contract governs both: your proxy must overwrite, not append, these headers, and must strip any copy a client tries to send itself.

Forwarding an end-user identity places it permanently in the audit trail: those rows are append-only by design, so once a forwarded identity has been recorded there is no erasure path for that record. Forward only identities your own retention and privacy policy permits to persist indefinitely.

MCP sidecar wiring#

MCP access is out-of-process by design: the MCP sidecar is an HTTP client of the API, so the app itself needs no MCP-specific work to support an embedded install. To expose an embedded install to MCP clients, run the sidecar with its streaming HTTP transport pointed at the embedded install's API base.

The sidecar's streaming HTTP listener has no authentication of its own, so it must sit behind your own authenticating, TLS-terminating reverse proxy. In multi-user HTTP mode each request carries its own API token, so leave the sidecar's own single-identity token unset on that listener; with it set, every request would collapse onto one identity and all of its scopes. A single-user, local desktop connection is the one path that does use a single fixed token.

Supported configurations#

ConfigurationSupport
SQLiteSupported for standard, local installs: the zero-config default.
PostgresSupported for the embedded and enterprise profile: schema managed by migrations, applied automatically on boot.
Any other database backendUnsupported. Migrations are skipped for a non-Postgres connection string, and no schema or behavior is verified for it.

SQLite remains the sole supported backend for the standard local install; Postgres is documented for the embedded profile. Anything else is out of scope.