Architecture
This page is for people working on or evaluating the codebase. For deployment, see Self-hosting. For user-facing flows, see Authentication.
The shape, in one paragraph
Section titled “The shape, in one paragraph”A single Node process runs Express. Behind it: PostgreSQL (org-scoped schemas) + Apache AGE (permission graph) + an MCP/SSE transport from @modelcontextprotocol/sdk. Users authenticate via GitHub OAuth, GitLab OAuth, OIDC, SAML, or API key — all paths mint the same internal JWT, which then flows through scope resolution against the AGE graph on every request. The server hosts five surfaces (MCP/SSE, REST API, admin UI, /me portal, SSO/SCIM endpoints) on a single port behind Caddy.
Top-level layout
Section titled “Top-level layout”src/├── index.ts # Process entry; loads config + starts server├── config.ts # Zod schema for all env vars + provider configs├── server.ts # createApp() — wires every router; the spine│├── auth/ # JWT minting + verification, API key model│ ├── token.ts # generateToken / verifyToken (HS256, dual-secret rotation)│ ├── api-key.ts # plur_sk_... long-lived bearer; SHA-256 at rest│ ├── middleware.ts # createAuthMiddleware: accepts JWT OR API key│ └── types.ts # AuthUser shape│├── admin/ # Admin dashboard + /me portal + SSO│ ├── auth.ts # createAdminAuth (cookie OR header), createUserAuth│ ├── csrf.ts # Double-submit-cookie CSRF middleware│ ├── rbac.ts # 3-tier viewer / editor / admin model│ ├── router.ts # /admin/* routes│ ├── me-router.ts # /me/* routes (portal, API keys, webhooks)│ ├── views.ts # Server-rendered HTML (Outfit font, dark/light)│ ├── queries.ts # Read queries│ ├── oidc/ # OIDC SSO: registry, openid-client wrapper, routes│ └── saml/ # SAML 2.0: registry, @node-saml/node-saml wrapper, routes│├── api/v1.ts # Public REST API + auto-generated OpenAPI spec│├── scim/ # SCIM 2.0 provisioning endpoints│ ├── router.ts # /scim/v2/{Users,Groups,...}│ └── tokens.ts # SCIM-specific bearer tokens│├── webhooks/dispatcher.ts # WebhookDispatcher — HMAC + retry + auto-disable├── audit/write.ts # writeAudit() — never-throws audit helper├── email/sender.ts # nodemailer wrapper (stub mode if no SMTP)│├── db/ # Postgres + AGE│ ├── pool.ts # Single pg.Pool for the process│ ├── migrate.ts # Schema migration runner│ ├── tenant.ts # TenantManager: per-org schema isolation│ ├── graph.ts # GraphLayer: AGE Cypher wrapper, scope resolution│ ├── postgres-store.ts # EngramStore impl backed by Postgres│ └── migrations/*.sql # Numbered migrations│├── permissions/│ ├── resolver.ts # PermissionResolver: scope expansion via graph│ └── validator.ts # validateIdentifier, validateScope, etc.│├── plur/│ ├── enterprise-plur.ts # Per-session Plur instance with scope filter│ └── episode-store.ts # Episodes (session timeline) backed by Postgres│├── mcp/ # MCP-specific layers on top of @plur-ai/mcp│ ├── tool-filter.ts # Which tools are exposed to enterprise users│ ├── permission-wrapper.ts # Pre-call scope check; sets ingest cap│ └── read-sanitizer.ts # Strips dangerous fields from arg + response│├── github/, gitlab/ # OAuth + REST clients + per-org sync into AGE├── middleware/ # security headers, rate limit, session manager├── logging/ # pino logger + audit table writer└── types/express.d.ts # Request.user augmentationRequest flow: three example paths
Section titled “Request flow: three example paths”MCP tool call (Claude Code → SSE → JSON-RPC)
Section titled “MCP tool call (Claude Code → SSE → JSON-RPC)”GET /sse (Authorization: Bearer <jwt>) ↓ requireAuth (JWT or API key) ↓ SessionManager.create() — registers session, returns sessionId ↓ MCP Server initialized with per-session EnterprisePlur instance ← stream of SSE events (endpoint, then JSON-RPC responses)
POST /messages?sessionId=<uuid> (Authorization: Bearer <jwt>) ↓ requireAuth + messageRateLimit ↓ Look up SSE session, find its transport ↓ MCP server dispatches the tool call via setRequestHandler ↓ isToolAllowed(name) — tool-filter ↓ sanitizeToolArgs(args, name) — read-sanitizer (strips llm_*, etc.) ↓ enforceWritePermission() — scope check via PermissionResolver ↓ executeToolOnPlur() — calls plur.learn() / .recall() / etc. ↓ sanitizeToolResponse() — strips storage_root, etc. ↓ writeAudit() (fire-and-forget) ↓ webhookDispatcher.fire() if engram_created/retired/etc. ← tool result streams back via SSE on the existing sessionREST API (curl → JSON)
Section titled “REST API (curl → JSON)”POST /api/v1/engrams (Authorization: Bearer <jwt-or-api-key>) ↓ requireAuth (same dual JWT/API-key path) ↓ permissionResolver.canWrite(user, scope) ↓ Per-request EnterprisePlur instance with user's resolved scopes ↓ plur.learn(statement, { scope, domain, type }) ↓ writeAudit() ← 201 + engram bodyBrowser admin (Brave → HTML)
Section titled “Browser admin (Brave → HTML)”GET /admin/users ↓ createAdminAuth (cookie session OR Bearer header) ↓ rbac.requireRole('admin') ↓ queries.getUsers() ↓ views.renderUsersPage(data) ← 200 + server-rendered HTMLPermission resolution
Section titled “Permission resolution”Every read and write goes through PermissionResolver. Given an authenticated user and a target scope, it walks the AGE graph:
user → memberships → groups → scopes → parent groups → scopes → org → scopes → globalThe result is the set of scopes the user can read; a second pass marks which it can write. The MCP tool-filter and the REST middleware both call this before any engine operation.
Permission is never decided client-side. Scopes coming from the client are validated against the resolved set; mismatches return 403.
What’s NOT here
Section titled “What’s NOT here”PLUR Enterprise is not a generic data platform. Things it deliberately doesn’t do:
- It doesn’t host arbitrary user files — only engrams, episodes, and pack metadata.
- It doesn’t run LLMs server-side — clients bring their own.
- It doesn’t have its own search UI for engrams — the admin dashboard offers list/inspect, not freeform search; recall happens through MCP/REST.
- It doesn’t ship a notification system beyond webhooks and SMTP email.
- It doesn’t replace your IdP — it federates to one (OIDC/SAML/GitHub/GitLab).
If you want any of those, build them on top of the REST API.
Reference
Section titled “Reference”Full source: github.com/plur-ai/enterprise. The architecture doc inside the repo (ARCHITECTURE.md) is the authoritative version of this page.