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 MEM_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 four serve flags that have both a --flag and a MEM_* env equivalent:

FlagEnvDefault
--listen-addrMEM_LISTEN_ADDR:8080
--oidc-issuerMEM_OIDC_ISSUER(unset — auth disabled)
--oidc-audienceMEM_OIDC_AUDIENCE(unset — audience not checked)
--oidc-resource-metadataMEM_OIDC_RESOURCE_METADATA(unset)

Storage and embedding are configured via env-only variables (MEM_QDRANT_ADDR, MEM_QDRANT_COLLECTION, MEM_LITELLM_URL, MEM_LITELLM_KEY, MEM_EMBED_MODEL, MEM_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 MEM_OIDC_ISSUER; the audience is checked only when MEM_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 MEM_UI_ENABLED=true):

FlagEnvPurpose
--ui-enabledMEM_UI_ENABLED"" implies-from-creds, "true" forces on, "false" hard off
--ui-issuerMEM_UI_ISSUERConsole OIDC issuer — empty defaults to MEM_OIDC_ISSUER
--oidc-client-idMEM_OIDC_CLIENT_IDConfidential-client ID
--oidc-client-secretMEM_OIDC_CLIENT_SECRETConfidential-client secret
--ui-redirect-urlMEM_UI_REDIRECT_URLAuth-code callback URL
--ui-cookie-keyMEM_UI_COOKIE_KEY32-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 MEM_UI_ISSUER to the console app’s issuer; the MCP bearer lane keeps MEM_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 MEM_UI_ISSUER is unset, the console lane reuses MEM_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 stable OIDC sub claim, stored as the record’s owner. The sub does not change when the user’s email changes, so access is never revoked by an identity provider profile update.

Caller typeWhat is readable
Authenticated (sub present)Own records (owner == sub) 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 sub. 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 (owner == sub)
  • 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 empty sub 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 once with:

Terminal window
engram migrate-set-owner --owner <your-oidc-sub>

The --owner flag also reads from MEM_MIGRATE_OWNER. The command is idempotent — re-running when no owner-less records remain reports 0.

The migrate-set-owner subcommand is implemented in cmd/engram/migrate.go and is registered as a cobra subcommand (Use: "migrate-set-owner").

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 sub). The records are not lost and not deleted; they become readable again once authentication is re-enabled.

migrate-set-owner only backfills pre-isolation records (those missing an owner key entirely). It cannot move owner-stamped records into the anonymous bucket and requires a non-empty --owner, so it is safe to re-run.