Deployment profiles
Engram runs in two contexts: a single-operator host that wants persistence
without an auth stack, and a multi-tenant server with real infrastructure.
Instead of two builds or a thicket of feature flags, one binary reads
DEPLOYMENT_PROFILE and resolves a ProfileCapabilities record
(packages/config/src/profile.ts) that every module consults at boot.
Both profiles run on the same backing service — PostgreSQL with the pgvector extension, the only external dependency. What differs is whether the multi-tenant auth/organization stack is wired in.
The ladder
Section titled “The ladder”| Capability | lite |
standard |
|---|---|---|
requiresDatabase |
yes | yes |
persistent |
yes | yes |
multiTenant |
no | yes |
lite— single-user local durability on Postgres. The auth/organization stack is not wired: no OAuth sessions, no per-agent API-key enforcement, no rate limiting. For a personal, always-on server.standard— the default. The same Postgres storage plus the full multi-tenant stack: authentication, per-agent API keys, organizations, and rate limiting.
The default is standard; the config loader fails fast at boot when
DATABASE_URL is missing, rather than failing at first use.
The value
enterpriseis a deprecated alias forstandard(it maps to the same capabilities), kept so existing deployments keep booting. The oldmemoryprofile — zero external services, in-process adapters — was removed: every profile now runs on Postgres.
Capabilities drive wiring, not scattered ifs
Section titled “Capabilities drive wiring, not scattered ifs”AppModule.forRoot() consults the capability record once. Because both
profiles set requiresDatabase, PrismaModule and AuthModule are always
imported; AuthModule then gates its OAuth sessions, rate limiting, and the
multi-tenant boot fail-safe on multiTenant internally. Services take
their optional dependencies with @Optional() injection, so the same
MemoryService code path works whether or not the auth stack is present.
The alternative — checking process.env.DEPLOYMENT_PROFILE at each call
site — is exactly the drift-prone pattern this design avoids: capability
checks happen at composition time, in one place.
The tool surface is capability-filtered
Section titled “The tool surface is capability-filtered”MemoryController.getMcpTools() builds the MCP manifest through a
capability-aware filter, so a profile never advertises a tool it cannot
serve. Because both profiles are Postgres-backed, every tool — including the
async reindex queue (queue_reindex_memories, get_reindex_status,
cancel_reindex_job, retry_reindex_job), consolidate_corpus, and the
Postgres-only export_memories / import_agent_memory — is currently served
in both. The filter hook is retained so a future profile can exclude tools
again without re-plumbing the call site.
Multi-tenancy is a profile property
Section titled “Multi-tenancy is a profile property”Only standard sets multiTenant: true, and that flag is load-bearing for
security: it is what arms the
boot-time auth fail-safe
— a multi-tenant server over streamable-http refuses to start unauthenticated.
lite is exempt because there is no cross-tenant data to protect.
Moving up the ladder
Section titled “Moving up the ladder”Profiles are a ladder, not a fork: start on lite for a personal server, and
move to standard when multi-tenancy or scale does. Both share identical
Postgres storage, so the move is a configuration change — enabling the auth
stack — not a data migration. The runbook lives in
Migrate lite to standard.