Skip to main content
Turborepo + pnpm workspaces. The license boundary follows the package boundary.

Layering rules (enforced, not aspirational)

  • Transport layers (REST routes, MCP tools) contain zero business logic — they parse (schema), call packages/core, format. Oversized router files are the anti-pattern to avoid.
  • packages/core never imports express/MCP/Next types.
  • packages/db enforces two-layer tenant isolation: every tenant-scoped query runs inside withTenant() (RLS) and carries an explicit caller-bound user_id predicate — defense in depth, per docs/concepts/data-model.mdx.
  • 50-line function / 500-line file budget. Target, not a lint rule — same as AGENTS.md hard rule 5. Oversized files in core paths are the failure mode the budget is there to prevent.
  • Dependency direction is package.json + review + scripts/check-db-access.sh. CI does not run a dedicated dependency-direction linter.

Server topology note

API and MCP ship as one express app / one service in v1 (a shared service layer makes a separate process split unnecessary at this scale). Split into separate deployables only when load profiles demonstrably diverge — the code structure (thin adapters over core) keeps that a deploy-config change, not a refactor.

Resource-limit composition contract

The public server exposes two optional, billing-neutral resource limits through the injected Limits resolver:
  • maxLiveMemories caps rows where status = 'active' AND valid_to IS NULL.
  • maxActiveMcpClients caps distinct client IDs with at least one non-revoked, unexpired OAuth token.
Omitting either field means unlimited, which is the Apache self-host default. Values are validated as finite, non-negative safe integers before use; invalid injected policy fails closed. The schema contains no plan names or product prices. Live-memory admission is an exact count-and-insert transaction serialized per tenant. Active welcome memories and active imports consume a slot. Archived imports and imports whose valid_to is already set do not. A revision is allowed at the cap because it closes one live row and appends its successor in the same transaction, leaving the live count unchanged. OAuth admission is serialized with account credential lifecycle changes. A first grant for a new distinct client is refused at the cap, while reauthorization of an already-active client remains possible when usage is at or below the cap. If a limit is lowered below current usage, issuance converges without deleting credentials: only the newest allowed client IDs (ordered by their latest live token creation time, then client_id ascending) may reauthorize or rotate. Losing clients cannot leapfrog the retained set with a fresh authorization-code flow, and their current access tokens expire naturally. Dynamic registration and CIMD resolution remain unrestricted; the limit is enforced when a client first receives tokens. Transport contracts are explicit:
  • REST memory creation returns 409 {"error":"resource_limit_exceeded"}.
  • MCP tools return an isError result with reason code resource_limit_exceeded.
  • The onboarding welcome-seed endpoint returns the same HTTP 409 reason.
  • OAuth token issuance returns RFC-compatible HTTP 400 invalid_grant with the safe description Active MCP client limit reached.
@3ngram/server/app exports the literal RESOURCE_LIMITS_ENFORCED = true. Downstream composition roots can require this sentinel before booting, preventing an older public server from silently ignoring the optional fields. It is a runtime compatibility marker, not commercial policy.