Architecture
The platform shape one layer up — public packages vs the private substrate, the ports-and-adapters seam, and the Convex backend.
The platform is a set of published npm packages over a private substrate,
separated by a ports-and-adapters seam. The authoritative contract is
canon/architecture.md;
this page is the summary.
Public packages vs the private substrate
Five packages publish to npm. Everything else is "private": true and never
publishes — private packages never appear in a consumer's node_modules;
published artifacts inline the internal code they need at pack time.
| Public (published) | Private (never published) |
|---|---|
@capxul/sdk | @capxul/backend |
@capxul/sdk-react | @capxul/wire |
@capxul/sdk-testing | @capxul/types |
@capxul/mcp | @capxul/errors |
@capxul/sandbox | @capxul/config |
@capxul/observability | |
@capxul/contracts |
The layer stack
Consumers call method bundles and receive Promise<CapxulResult<T>>.
Internally, ports are Effect Context.Tags, adapters are Effect Layers, and
domain code is an Effect program that depends only on ports:
| Layer | Owns |
|---|---|
| Public SDK surface | createCapxulClient, method bundles, CapxulResult, CapxulError |
| Domain core | Effect programs for auth, provisioning, identity, credentials, smart accounts |
| Ports | Context.Tag declarations and service interfaces |
| Adapters | Layer implementations of ports — external libraries and platform IO |
| Backend | Convex functions, BetterAuth, Resend, AgentMail, tenancy guards |
The load-bearing constraint: the domain core does not import Convex, BetterAuth, Resend, AgentMail, Openfort, browser storage, the Node filesystem, or network clients directly. All platform IO enters through an adapter behind a port.
The ports-and-adapters seam
A port is a Context.Tag plus a service interface; an adapter is a
Layer that provides it. Each port has a spec in
canon/ports/
and the full list lives in the
port catalog.
The catalog groups them into three families:
- Domain ports —
IdentityPort,SmartAccountPort,AccountReadPort,SubAccountPort,SafeDeploymentPort,CredentialsPort,BootstrapPort. - Platform ports —
AuthClientPort,AuthCachePort,EvmSignerPort,ClockPort,EnvPort,TelemetryPort. - Primitive ports —
ConvexCallPort,TransportPort.
Every port is proven by a single conformance matrix that runs the same tests against its hermetic and real adapters — see the test contract.
Where the backend runs
The backend is serverless Convex — there is no Docker image and no local
database. @capxul/backend owns the Convex functions, BetterAuth (auth and
OTP), Resend (email delivery), AgentMail (test inboxes), and the tenancy
guards. Every public query or mutation that returns user data must call
requireConsentedUser(userId, appId) before reading or returning it. The SDK
reaches Convex only through the ConvexCallPort / TransportPort adapters —
never by importing the generated Convex API.
The public-surface rule
Consumers import @capxul/sdk and @capxul/sdk-react — nothing else. The
backend, @capxul/wire, and the Convex function APIs are private surface, and
internal SDK subpaths (@capxul/sdk/production, _internal, adapter seams)
are not part of the published contract.
Public SDK methods return Promise<CapxulResult<T>>:
export type CapxulResult<T> =
| { readonly ok: true; readonly value: T }
| { readonly ok: false; readonly error: CapxulError };They do not return raw values, thrown exceptions, actors, state machines, or
Effect programs. Internal Effect error variants are mapped to CapxulError
before any value crosses the boundary. The rule itself is
canon/rules/public-surface.md;
the error shape is covered in
Conventions.