Skip to content

Auth & Isolation

engram enforces per-actor memory isolation backed by OIDC bearer tokens. When authentication is enabled, each caller sees and mutates only their own records. When it is disabled, all callers share a single anonymous bucket.

Set --oidc-issuer (or its env equivalent ENGRAM_OIDC_ISSUER) to the OIDC issuer URL. This is the only configuration required to enable bearer-token enforcement.

Terminal window
engram serve \
--oidc-issuer https://idp.example/application/o/engram/ \
--oidc-audience engram

The MCP bearer-token lane’s serve flags that have both a --flag and an ENGRAM_* env equivalent (the web-UI lane’s flags are in the console section below):

Flag Env Default
--listen-addr ENGRAM_LISTEN_ADDR :8080
--oidc-issuer ENGRAM_OIDC_ISSUER (unset — auth disabled)
--oidc-audience ENGRAM_OIDC_AUDIENCE (unset — audience not checked)
--oidc-resource-metadata ENGRAM_OIDC_RESOURCE_METADATA (unset)

Storage and embedding are configured via env-only variables (ENGRAM_QDRANT_ADDR, ENGRAM_QDRANT_COLLECTION, ENGRAM_OPENAI_BASE_URL, ENGRAM_OPENAI_API_KEY, ENGRAM_EMBED_MODEL, ENGRAM_EMBED_DIM) — these do not have --flag equivalents.

On every MCP request, engram verifies:

  • Signature — validated against the issuer’s JWKS endpoint
  • Issuer — must match --oidc-issuer
  • Expiry — expired tokens are rejected
  • Audience — checked only when --oidc-audience is set

The verified identity is extracted from the token (email address preferred, then username, then subject) and recorded as the record’s actor field.

When --oidc-issuer is not set, validation is disabled. Every request is accepted. The server logs a loud warning at startup so this state is never silently open. All callers share a single anonymous bucket (owner == "").


engram authenticates over two independent lanes, each verifying the issuer and token signature on its own (and the audience where configured):

  • MCP bearer lane — agents forward an OIDC bearer token (issued to a public PKCE client). Verified against ENGRAM_OIDC_ISSUER; the audience is checked only when ENGRAM_OIDC_AUDIENCE is set.
  • Web console login lane — the operator console runs the OIDC authorization-code flow as a confidential client. It verifies ID tokens against its own issuer and pins the audience to the client ID.

The console lane is configured by these env vars (all have --flag equivalents) and only activates when its credentials are present (or forced via ENGRAM_UI_ENABLED=true):

Flag Env Purpose
--ui-enabled ENGRAM_UI_ENABLED "" implies-from-creds, "true" forces on, "false" hard off
--ui-issuer ENGRAM_UI_ISSUER Console OIDC issuer — empty defaults to ENGRAM_OIDC_ISSUER
--oidc-client-id ENGRAM_OIDC_CLIENT_ID Confidential-client ID
--oidc-client-secret ENGRAM_OIDC_CLIENT_SECRET Confidential-client secret
--ui-redirect-url ENGRAM_UI_REDIRECT_URL Auth-code callback URL
--ui-cookie-key ENGRAM_UI_COOKIE_KEY 32-byte AES-256 session-cookie key

Split-issuer (per-application IdP) topology

Section titled “Split-issuer (per-application IdP) topology”

On an IdP that mints a distinct issuer per application (for example Authentik’s default issuer_mode), the security-preferred split — agents on a public PKCE client and the console on a confidential client, i.e. two apps with two different iss values — needs the two lanes to trust different issuers. Set ENGRAM_UI_ISSUER to the console app’s issuer; the MCP bearer lane keeps ENGRAM_OIDC_ISSUER (the agents’ app):

Terminal window
engram serve \
--oidc-issuer https://idp.example/application/o/engram-agents/ \
--ui-issuer https://idp.example/application/o/engram-console/ \
--oidc-client-id engram-console \
--oidc-client-secret "$SECRET" \
--ui-redirect-url https://engram.example/auth/callback \
--ui-cookie-key "$COOKIE_KEY"

When ENGRAM_UI_ISSUER is unset, the console lane reuses ENGRAM_OIDC_ISSUER, so single-application deployments need no extra configuration. An enabled console with neither issuer set is a fail-fast startup error.


Each authenticated caller is identified by the value of the configured owner claim, stored as the record’s owner. The owner claim is set via ENGRAM_OWNER_CLAIM / --owner-claim (default email). Authentication fails closed when the claim is absent from the token. When the claim is email, email_verified is also required.

Caller type What is readable
Authenticated (owner claim present) Own records plus records where visibility == "shared"
Anonymous (no issuer, or auth disabled) Only ownerless records (owner == "") — cannot read shared records

The shared read grant explicitly requires an authenticated caller with a resolved owner claim. Anonymous callers cannot read other actors’ shared records even when visibility is set to "shared".

Writes (update, delete, set_visibility) always require ownership:

  • Authenticated callers: must be the record’s owner
  • Anonymous callers: may only mutate records where owner == ""

Sharing (visibility == "shared") grants read only — it never grants write access to any other caller.

  • A record that exists but is not readable by the caller returns not found (ownership never leaks across actors)
  • An unknown or nil subject fails closed, returning zero results rather than over-returning
  • A validated token with a missing or absent owner claim is rejected (fails closed rather than collapsing to anonymous)

Records written before per-actor isolation was introduced carry no owner key (distinct from an empty-string owner). Once the new binary starts, these pre-isolation records are invisible to every read and cannot be cleared by delete_all.

The server logs a startup warning when owner-less records exist. Claim them with migrate-remap-owner (migrate-set-owner is a deprecated alias for --from-missing):

Terminal window
# Backfill pre-isolation records that have no owner set
engram migrate-remap-owner --from-missing --to <owner-claim-value>
# Re-stamp records that were written when owner was derived from sub
# (e.g. after switching ENGRAM_OWNER_CLAIM from "sub" to "email")
engram migrate-remap-owner --from <old-sub-value> --to <email-value>

Both forms accept --dry-run to preview changes and --timeout to cap the operation. The command is idempotent — re-running when no matching records remain reports 0.

Note: changing ENGRAM_OWNER_CLAIM (including upgrading from an older binary that always used sub) invalidates existing web-console session cookies. Users see a one-time re-login prompt on their next console visit.

Records written while authenticated carry a non-empty owner. If you remove --oidc-issuer, callers fall into the anonymous bucket and can no longer read those records — including ones marked shared (the shared read grant requires an authenticated caller). The records are not lost and not deleted; they become readable again once authentication is re-enabled.

migrate-remap-owner --from-missing only backfills pre-isolation records (those missing an owner key entirely). It requires a non-empty --to, so it is safe to re-run.