The backend-for-frontend (BFF) tier is the runtime seam between a product
surface and the domain orchestration behind it. A surface — the consumer web
shell, the mobile app, an Unreal client, an admin console — never talks to a
domain library or a database directly; it talks to a BFF, and the BFF is the one
place that authenticates the session, decides what that principal is allowed to
see, fans the request out across however many domains a screen needs, folds the
results into one shaped envelope, and emits the cross-cutting governance
(idempotency, residency, abuse, budget) every write must pass through. It is
shared rather than per-product for the same reason the rest of the platform is:
the rules for "is this caller authenticated, entitled, in-region, within budget,
and replay-safe" are identical across nine products, so they live in one gateway
codebase instead of nine. The primary gateway is @oshun/bff
(apps/oshun/bff), a Fastify TypeScript service of ~1,780 TypeScript files
(~1,055 of them non-test); its src/app.ts is an 83 KB composition root that
decorates a shared adapter bundle and wires roughly 460 app.register(...)
route-module plugins over the ~490 files in apps/oshun/bff/src/routes/.
This page is the BFF slab of the layered model in
the platform overview: the box labelled "BFF & gateway" that
sits directly under the product surfaces and directly above
domain orchestration. It documents what a BFF is
responsible for, the real routing and module structure of apps/oshun/bff/src,
and — candidly — how the four gateways differ in maturity. Three are real, of
very different depth (@oshun/bff at ~1,780 files, @lilith/bff at ~300,
@kalika/bff at 8), and one — apps/urania/bff — is a scaffold with no source
yet. The page labels those distinctions rather than implying uniform depth, in
keeping with the repository's no-stub culture and the docs center's implemented
/ spec-only / provider-gated convention.
What ships, honestly#
The Oshun gateway is real and load-bearing. Auth resolution, domain-scope
authorization, the typed domain-adapter bundle with circuit breakers, the
cross-domain aggregators with partial-failure envelopes, server-side
idempotency, residency enforcement, abuse protection, entitlement evaluation,
and the agentic governance gate are all implemented TypeScript with sibling
tests — not placeholders. The composition is deliberately built around
injectable seams that default fail-soft: createApp(options) accepts a
durable admin store bundle, an audit-events store, a Redis idempotency store, an
LLM synthesizer, and an account-deletion runner; when any is omitted the gateway
falls back to an in-memory or extractive default so dev and test boot without a
database, and apps/oshun/bff/src/server.ts (the process entry) is where the
real Redis/Postgres-backed implementations are injected. That is an honest seam,
not a fabricated success: an absent dependency degrades loudly or stays
in-memory, it does not pretend.
Two honest nuances matter. First, the domain adapters are a hybrid: tara,
veritas, and arete front real downstream HTTP services (/v1/oshun/*
facades) with retry and circuit-breaker policy, while nyx, nisaba, and
metis default to in-process adapters because their real sources
(ephemeris, the passage corpus, the tutor stores) run inside the BFF — each is
flippable to HTTP with an env var (OSHUN_NYX_ADAPTER=http, etc.). Second,
gateway maturity is genuinely uneven across products, covered below. Where a
behaviour is environment-gated (signed JWTs in production, durable stores when a
database is present) the page says so.
The gateway tier at a glance#
| Gateway | Path | Source .ts |
Shape | Maturity |
|---|---|---|---|---|
@oshun/bff |
apps/oshun/bff |
~1,780 | The deep, production-shaped gateway: ~460 route plugins, typed adapters, full middleware stack | Implemented, deep |
@lilith/bff |
apps/lilith/bff |
~300 | Response-shaping + synthesis gateway fronting the Isis/Sophia capability domains | Implemented |
@kalika/bff |
apps/kalika/bff |
8 | Small domain-scoped gateway: compute/agents/notebooks proxy + realtime workbench | Implemented, thin |
urania/bff |
apps/urania/bff |
0 | Directory exists with node_modules only — no src |
Scaffold |
The rest of this page is mostly about @oshun/bff, the gateway every V1 surface
depends on; the others are characterized honestly at the end.
What a BFF is responsible for#
Session and auth enforcement#
Auth is the gateway's first job and lives in
apps/oshun/bff/src/middleware/authz.ts. createAuthPreHandler()
(authz.ts:99) is a Fastify preHandler that requires an
Authorization: Bearer <token> header (else 401 missing_bearer_token),
resolves the token, and decorates the request with an OshunAuthContext
({ userId, scopes, exp, sessionId?, homeZone?, tenantId? }). Token resolution
in resolveBffAuthToken (authz.ts:192) accepts three credential shapes:
- Dev tokens —
dev.<base64url-json>carrying{ sub, scopes, exp }. Allowed only outside production and only whenOSHUN_BFF_ALLOW_DEV_TOKENSis notfalse. - Tenant session tokens —
tenant.<base64url-json>with a mandatorytidclaim, sharing the dev gate until signed tenant sessions land. - Signed HS256 JWTs — verified in
verifySignedJwtAuthToken(authz.ts:323) with a constant-time HMAC comparison (timingSafeEqual,authz.ts:573), issuer/audience matching, andexp/nbfchecks against a configurable clock tolerance. The secret must be at leastMIN_HMAC_SECRET_LENGTH = 32chars.
The production posture is fail-closed by construction: isProductionRuntime
(authz.ts:522) keys off NODE_ENV / OSHUN_ENV / RUNTIME_ENV, and in
production unsigned dev./tenant. tokens are rejected (dev_tokens_disabled)
and a missing signed-JWT secret yields signed_auth_not_configured rather than
a silent downgrade. The full identity/session/consent model these tokens ride on
is detailed in Auth & Identity.
Domain-scope authorization#
Authentication answers "who"; authorization answers "what". A protected domain
route additionally requires the principal to hold either the wildcard domain:*
scope or the specific domain:{domainId} scope.
createDomainAuthorizationPreHandler() (authz.ts:124) validates the route's
domainId against isKnownDomain from @oshun/domain-registry (400 on an
unknown domain), then checks scope membership and returns
403 domain_scope_missing when absent. Per-domain route files inline the same
check — routes/tara.ts's requireTara returns the userId or replies
401 missing_auth_context / 403 domain_scope_missing before any adapter is
touched. This is the same fail-closed shape the
Sophia grounding routes use; nothing
reaches a domain library until both gates pass.
Composing domain calls for a surface#
The defining BFF responsibility is composition: a single screen needs several
domains, and the gateway is what calls them in parallel and assembles the
result. The composition substrate is the typed adapter bundle decorated onto the
Fastify instance as app.domainAdapters (app.ts:565), built by
createDomainServiceAdapters in the 78 KB
apps/oshun/bff/src/adapters/domain-service-adapters.ts. Each HTTP adapter
wraps a DomainServiceCircuitBreaker (domain-service-adapters.ts:946) and a
fetchWithTimeout with a per-domain retry policy; probeDomainHealth (:2470)
normalizes heterogeneous upstream /healthz-style payloads into one
shell-facing ok | degraded | down model and short-circuits when the breaker is
open. Every outbound call injects the tenant header
(injectTenantHeaderIntoHeaders) and residency-routing headers
(injectResidencyRoutingHeadersIntoHeaders) so downstream services inherit the
caller's tenant and home zone.
The cross-domain aggregators are where composition is most visible.
routes/home.ts builds a HomeRoutePayload by fanning fetchDomainHighlights
/ fetchDomainContinue / fetchDomainFavorites across only the domains the
principal is scoped for (resolveAuthorizedDomains), then records a
per-domain status
(domainStatus: Record<DomainId, 'ok' | 'degraded' | 'forbidden'>) and a
structured errors list. A failure in one domain does not fail the request: the
envelope carries partial and partialFailure booleans, and the route emits
x-oshun-partial-response, x-oshun-partial-failure-count, and
x-oshun-degraded-domains response headers (per the BFF README) so the surface
can render what succeeded and badge what degraded. The same pattern backs
/continue, /activity, /library, and /search.
A concrete read path, GET /v1/tara/recommended, shows the whole shape in one
handler (routes/tara.ts):
app.get('/v1/tara/recommended', { preHandler }, async (request, reply) => {
const userId = requireTara(request, reply); // 401/403 fail-closed
if (userId === null) return;
const q = (request.query ?? {}) as {
limit?: string;
category?: string;
intensity?: string;
};
await guarded(reply, async () => ({
// 502 domain_unavailable on adapter throw
recommended: await app.domainAdapters.tara.getRecommendedSessions({
userId,
limit: optionalInt(q.limit),
category: q.category,
intensity: q.intensity,
}),
}));
});
guarded() wraps the adapter call, sets cache-control: no-store, and turns
any thrown adapter error into 502 domain_unavailable rather than leaking an
upstream stack trace. The domain library it calls — domain-tara in this case —
is the next layer down, documented in
Domain Orchestration.
Response shaping#
Composition produces raw domain results; shaping makes them a surface's
contract. In @oshun/bff shaping means cursor pagination envelopes
(highlightsLimit, continueCursor, …), short-lived private response caching
with x-oshun-cache: hit|miss and cache-control headers, revisioned profile
and preferences envelopes that clients patch incrementally, and the
partial-failure trace envelope (correlation id, route, degraded domains and
stages) attached to every aggregator response. @lilith/bff takes shaping
further with a dedicated response-shaping.ts service that strips and reformats
fields per client type (web | ios | android | desktop | unknown) detected
from headers — the same domain payload arrives leaner on a phone than on the
web. Shaping is also where the BFF refuses to over-serve: an adapter read is
no-store, an aggregator read is short-TTL private cache, and a write is never
cached.
Governance and budget gates#
A write must clear several cross-cutting gates the gateway owns. These are the "governance/budget" layer, and they are real:
- Idempotency (
middleware/idempotency.ts) — server-side, per ARCHITECTURE §5, opt-in via anIdempotency-Keyheader on unsafe methods. The first keyed write takes a tenant-scoped lock; a replay with the same key and request fingerprint returns the stored envelope verbatim withIdempotent-Replayed: true(the handler never re-runs); a concurrent duplicate gets409 idempotency_in_progress(idempotency.ts:324); the same key with a different body gets422 idempotency_key_reuse(:330).server.tsinjects aRedisBffIdempotencyStorefor cross-instance replay safety; dev/test use a per-instance in-memory store. - Residency (
middleware/residency-guard.ts, V1-PRIV-018) — wraps the canonicalResidencyEnforcementService. A route declares itsartifactTypeand target zone; the guard resolves the principal'shomeZoneclaim and either admits the transfer or replies403with the policy'sacceptableMechanisms, emitting a canonical audit event either way. It runs after auth (it refuses a request with noauthContext). The residency, migration, and deletion machinery underneath is detailed in Persistence & Data. - Entitlements (
middleware/entitlements.ts) — resolves the authoritative tier from the principal's persisted plan (free | pro | premium), falling back to thex-oshun-tierheader only outside production and to lowest privilege otherwise, so a production client can no longer self-assertpro. Suspensions (x-oshun-suspended-domains) only ever restrict and are honored everywhere; per-domain overrides can elevate and are dev-only.evaluateDomainAccessfrom@oshun/auth-clientturns that into a per-domain access decision. - Abuse protection (
middleware/abuse-protection.ts) — a fixed-window / block limiter (default 60 requests / 60 s, 5-minute block) returning429withretry-after, applied to entitlement, bootstrap, and per-domain routes. - Agentic budget gate (
agentic/agentic-governance-gate.ts) — the budget control plane for orchestrated content runs.createAgenticStudioGovernanceGatedelegates to the literal@oshun/agentic-studioprimitives:admit(node)callsadmitToolCall(real kill-switchdecideExecution+ real budgetcheckBudget) andrecordDispatch(node)callsconsumeBudget(the meter mutation threaded across dispatches). Admission outcomes areadmit | killed | throttled | run-terminal, so a kill-switch armed on a content family stops both the BFF's generator-tool catalog and the orchestrator's routing.
High-risk routes additionally read device-integrity headers
(x-oshun-device-platform, x-oshun-device-integrity,
x-oshun-device-attested-at).
The routing and module structure of apps/oshun/bff/src#
The gateway is organized as a composition root plus a flat route surface plus a typed adapter bundle plus a middleware stack:
app.ts—createApp(options)is the factory. It builds (or accepts) the domain adapters viacreateDefaultDomainAdapters()(app.ts:1149), decoratesdomainAdapters,bffProbes, andsophiaAnswerSynthesizer, registers CORS with the explicit verb set, then installs the request-lifecycle middleware in order —registerRequestTracing,registerTenantContext,registerResidencyRoutingContext,registerBffIdempotency(app.ts:591-596) — before the ~460 route plugins. The middleware order is deliberate: idempotency runs after tenant/residency resolve so keys are tenant-scoped.server.ts— the process entry (not the factory). It builds the durable admin stores (buildDurableAdminStores,server.ts:259), the durable-backed audit-events store, the Redis idempotency store, and the real account-deletion runner, passes them intocreateApp(...)(:542), and callsapp.listen(...)(:1143). This is the seam where in-memory dev defaults become production-grade backends.routes/(~490 files) — the HTTP surface: per-domain customer routes (tara.ts,nisaba.ts,veritas.ts,nyx.ts,arete.ts,metis.ts,sophia.ts, …), the cross-domain aggregators (home.ts,continue.ts,activity.ts,library.ts,search.ts), the large admin and Studio surface (admin-*.ts,admin-studio-*.ts), V6 catalog/tier-routing, and the consumer-shell envelopes (profile, preferences, notifications). Across all ofsrcthese declare on the order of 1,400 HTTP handlers.adapters/(16 files) —domain-service-adapters.ts(the typed HTTP clients + circuit breakers), the in-process adapters' read-adapter registries (tara-read-adapters.ts,sophia-read-adapters.ts, …), andmock-domain-service-adapters.tsforUSE_MOCK_ADAPTERS=true.middleware/—authz,entitlements,idempotency,abuse-protection,residency-guard,residency-routing,tenant,tracing,device-integrity, with Redis-backed variants for the stateful ones.- Per-domain subdirectories —
sophia/,nisaba/,metis/,tara/,agentic/,isis/,nyx/, etc., hold the BFF-resident logic that is not a thin proxy (the Sophia extractive answer composer, the in-process Nisaba corpus retriever, the agentic governance gate and tool catalog).
A request, traced end to end#
No step in that trace reinvents identity, validation, or partial-failure
handling: auth and tenancy come from middleware, the contract shape is the same
Zod type the domain library validates against (see Contracts),
and the failure mode is a typed 502/partial envelope rather than a leaked
upstream error.
The other gateways, honestly#
@lilith/bff (~300 files) — implemented, different lineage. This is a real
Fastify gateway built on @lilith/service-lib (its own
integratedSecurityPlugin
setupSecurityAndAuth+PERMISSION_LEVELS), with 40 route files undersrc/routes/. Its specialty is not breadth of customer domains but response shaping and synthesis:response-shaping.tstailors payloads per client type,synthesis-engine.tsdoes consensus/theme extraction for comparative views (and routes ComfyUI generation jobs), andcapability-clients.tsproxies the Isis (generation) and Sophia (knowledge) capability domains so a browser never receives their credentials — the client constructs real@isis/client/@sophia/clientSDK instances from env and returnsnull(degrade to503) when no endpoint is configured. It is a genuine gateway, simply scoped to a different product than@oshun/bff.
@kalika/bff (8 source files) — implemented, thin. buildKalikaBffApp
(apps/kalika/bff/src/app.ts) is a coherent but small gateway for a math /
research-notebook product: an auth hook (dev tokens + JWT via
KALIKA_BFF_JWT_*), three upstream service clients (compute / agents /
notebooks), a realtime WebSocket hub, an in-memory session store, and a
/api/v1/workbench aggregator that fans out with Promise.all + a
settleForWorkbench partial-failure wrapper. Its error handler maps an
UpstreamServiceError to 502. It is real, but its in-memory sessions and
three-upstream scope make it scaffold-tier next to the Oshun gateway — the right
honest label.
apps/urania/bff — scaffold. The directory contains only node_modules and
no src: zero TypeScript source files. It is a placeholder for a future
gateway, not a running one, and the overview's layered diagram should be read
with that in mind.
Edge cases and failure modes#
- A degraded domain never fails the screen. Aggregators return
partial: truewith a per-domaindomainStatusandx-oshun-partial-*headers; a single-domain read returns502 domain_unavailableviaguarded()rather than a500or a leaked upstream error. - An open circuit breaker short-circuits.
probeDomainHealthreturns a builtunavailablehealth without an outbound call when the breaker is open, and reportscircuitStateso readiness can reflect it. - A replayed write happens exactly once. Same idempotency key + fingerprint
→ the stored envelope; same key mid-flight →
409; same key, different body →422. Keyless writes are unchanged (opt-in). - Cross-zone transfer is refused, not silently routed. The residency guard
replies
403with acceptable mechanisms and emits an audit event; a request with noauthContextis401, never an unscoped transfer. - Production refuses unsigned credentials. Dev/tenant tokens are rejected
and a missing JWT secret yields
signed_auth_not_configured— fail-loud over fake-trust. - A client cannot self-elevate entitlements. In production the tier comes
from the persisted plan; the
x-oshun-tierheader is ignored for elevation and honored only for suspension (restriction).
How this connects to the rest of the platform#
The BFF is the only layer that touches all the others. It authenticates with the
identity model in Auth & Identity, composes the service
layer in Domain Orchestration, validates every
payload against the Zod schemas in Contracts, reads and writes
through the residency-aware foundations in
Persistence & Data, and emits logging, tracing, and
metrics through the @oshun/* packages catalogued in
Shared Libraries — including @oshun/traefik-config,
which generates the gateway config this runtime tier sits behind. Read top-down
it is the surface's single door; read bottom-up it is the one place the
platform's shared rules are enforced before a domain ever runs.
Related#
- The Shared Platform — the platform framing and layered model this page fills in.
- Domain Orchestration —
libs/oshun, the domain libraries the BFF composes behind every surface. - Contracts — the Zod vocabulary the gateway validates payloads against at the boundary.
- Auth & Identity — the identity, session, consent, and authorization model behind the BFF's auth middleware.
- Persistence & Data — the residency,
idempotency-store, and deletion-runner backends
server.tsinjects. - Shared Libraries — the
@oshun/*infrastructure (@oshun/http-client,@oshun/traefik-config,@oshun/data-residency) the BFF composes.