Bento docs
Architecture

How the modules talk, and how they split apart

Fourteen modules share one build and never import each other. They exchange integration events and call each other through versioned capability contracts. The same build runs as one process or as several services behind an edge, depending on configuration.

Go and NestJSChecked against the current v1 readiness tracker, 16 October 2026

At a glance

15modules, each with its own schema
0cross-module imports allowed
45capability contracts
118event schemas

Modules

Each module owns a PostgreSQL schema and connects with its own database role, which can reach nothing else. Other modules learn about its data only through events or a capability. Every module is built the same way (Module design); each one is described in Domains.

IdentityPassword/OAuth/passkey sign-in, MFA, sessions and recovery.identity
AccountWorkspaces, memberships, invitations and their lifecycle.account
AuthorizationRoles, assignments and permission checks.authz
AuditHash-chained history of significant actions.audit
UsageCounters, reservations, ledger and limits.usage
FilesUploads, folders, trash and storage accounting.files
BillingSubscriptions and payment-provider state.billing
EntitlementsPlans, features and numeric limits.entitlements
NotificationsInbox, preferences and durable email delivery.notifications
PrivacyPersonal data exports across every module.privacy
AITasks, retrieval, assistant conversations and MCP.ai
ChatWorkspace conversations, messages and read cursors.chat
RealtimePushes workspace events to open connections.realtime
IntegrationsAPI credentials, webhooks and MCP connections.integrations

Who talks to whom

Capabilities are synchronous calls from a caller to a provider, with a two-second deadline, and they fail closed. Events are asynchronous, carried by each module's outbox, and delivered at least once. Select a module to trace its links.

Capability (sync, 2 s, fail closed)

Selected relationships from the Go backend's capability clients and event consumers, including all fifteen modules. This is a reading map, not an exhaustive subscription or runtime route inventory. Privacy gathers supported personal-data sections through *.personal_data.v1 capabilities; see Domains for each module's reference.

Capability contracts

The table shows selected examples; the complete, versioned catalog is in contracts/capabilities/. A capability is named after the module that provides it. It is an external protocol, not shared code: the provider implements a handler, the caller implements a client behind its own port, and the composition root connects them. The call runs in-process when the provider is local and over HTTP when it is not. The JSON is the same either way. Over HTTP, each call carries a service assertion signed with the calling service's own Ed25519 key, and each capability lists the callers it accepts.

CapabilityCallersAnswers
identity.session.v1Edge, RealtimeThe active identity for a session token, or none.
identity.session_authentication.v1PrivacyThe session's identity and when it last signed in, for recent-sign-in checks.
identity.email.v1AccountAn identity's normalized sign-in email.
identity.profiles.v1Account, Audit, Realtime, Integrations, NotificationsDisplay names and emails for up to 100 identities.
account.shared_ownerships.v1IdentityWorkspaces the identity owns that have other members. Self-deletion is refused while any exist.
authorization.permission_check.v1Account, Identity, Audit, Entitlements, Files, Usage, AI, Realtime, Billing, Integrations, Notifications, Operations, ChatWhether the principal holds a permission in a scope. Always false in a closed or purged workspace.
entitlements.account.v1Account, Files, Usage, AI, IntegrationsFeature grants and numeric limits. A missing limit is unconfigured, never unlimited.
files.read.v1AIA five-minute download link for one available file.
files.describe.v1AIBounded file metadata and availability used by AI file selection and retrieval.
usage.reserve.v1 · commit · releaseAIHold, settle or return metered usage around a unit of work.
ai.run_access.v1RealtimeWhether the principal may see one AI run.
<module>.personal_data.v1PrivacyOne page of the personal data a module holds about a principal, for an export.

Fail closed

A slow or failing provider is a dependency failure, never an answer. An unreachable Identity is a 503, not an anonymous request, and a failed permission check is not a denial. See Errors. Only a successful negative answer may deny, hide or refuse.

Split deployment

BENTO_MODULES chooses which modules a process runs. This is the layout the split end-to-end suite runs for both backends: Account and Files in one service, the rest in another, and an edge in front. The suite also runs a Realtime-only service behind its own edge. The monolith is the same build with no split settings.

Public request and assertionCapability over HTTPEvents over NATS JetStream
A browser sends its cookie to the edge. The edge resolves the session with Identity in service A, then forwards the request with a signed principal assertion to service A or B. Service B calls capabilities in service A. Both services exchange events through NATS and use their own Postgres schemas.Browseror clientEdgeBENTO_ROLE=edgeNo databaseService ABENTO_MODULES=identity,authz,…Identity · AuthorizationAudit · Usage · BillingEntitlements · NotificationsPrivacy · AI · RealtimeService BBENTO_MODULES=account,filesAccount · FilesACCOUNT_DATABASE_URL …PostgresA's schemasNATSJetStreamPostgresB's schemas123345
  1. The client sends its session cookie to the edge, the only public entry point.
  2. The edge resolves the session once, through identity.session.v1.
  3. It drops any client-supplied principal header and forwards a signed Ed25519 assertion, valid for 60 seconds.
  4. Service B calls capabilities in A over /internal/capabilities, signing each call with its own service key.
  5. Both services publish and consume events through NATS. Separately, the edge polls each service's GET /internal/routes every 30 seconds to learn which routes it serves.
SettingMonolithService in a splitEdge
BENTO_ROLEserviceserviceedge
BENTO_MODULESunset (all)its modules, e.g. account,filesunused
EVENT_TRANSPORTmemorynats (required)unused
BROADCAST_TRANSPORTmemorynats (required)unused
SERVICE_SIGNING_KEYunsetthis service's Ed25519 keythe edge's own key
SERVICE_KEYSunsetpublic keys of the services that call itpublic keys of its upstreams
CAPABILITY_PROVIDERSunsetidentity=http://a,…identity=http://a
PRINCIPAL_SOURCEsessionassertion + public keys, unless it runs Identitysession + signing key
EDGE_UPSTREAMSunsetunsetservice base URLs

A split refuses to start without NATS for events and broadcasts, without service keys, or without a provider, local or remote, for every capability its modules require.

One request through the edge

Accepting an invitation in the split above. The edge authenticates once. Account never sees the session and reaches other modules only through capabilities.

  1. Client→EdgePOST /v1/accounts/invitations/{id}/accept with the session cookie.
  2. EdgeDrops any X-Bento-Principal header the client sent.
  3. Edge→Identityidentity.session.v1 returns the principal.
  4. Edge→AccountForwards the request with a signed assertion, valid for 60 seconds.
  5. AccountVerifies the assertion with the edge's public key.
  6. Account→Identityidentity.email.v1 confirms the invitee's email.
  7. Account→Entitlementsentitlements.account.v1 checks the member limit.
  8. AccountAdds the membership and its outbox row in one transaction, then returns 200.
  9. Account⇢NATSThe relay publishes account.membership_added.
  10. NATS⇢AuthorizationGrants the role the invitation carries. Its inbox drops a redelivered event.

Rules that keep it splittable

Own schema only

A module reads and writes its own schema. No cross-schema joins, views, triggers or foreign keys.

State and event commit together

The outbox row is written in the state transaction, and each consumer dedupes through its inbox.

Fail closed

Only a successful negative answer may deny, hide or refuse. Anything else is a dependency failure.

Contracts are language-neutral

The Go edge served the Nest services unchanged. Every protocol is specified in contracts/.

Enforced, not just agreed

Import checkers in both backends fail make check and CI on a cross-module import, including in tests. Each schema has its own migration history, starting from0000_<schema>_baseline.sql. See Module design.

In the repository

backend/SERVICE-BOUNDARIES.mdSchemas, roles, migrations and extraction.
backend/EVENT-RULES.mdOutbox, inbox and delivery guarantees.
backend/MODULE-AUTHORING.mdAdding a module that follows the rules.
contracts/capabilities/README.mdEvery capability and its wire protocol.
contracts/edge/README.mdAssertions, route listings and the edge.
backend/ARCHITECTURE-ENFORCEMENT.mdHow the import checkers work.