Most frameworks hide this. FrontierJS names each handoff, because a boundary you can name is a boundary you can reason about — and every one of these is a real function you can open. Here is a single POST /api/leads, end to end.
HTTP arrives
transport/http.ts
The server matches a route. If an Authorization header is present, IAuth.verifySession(token) resolves it to a session — the inbound auth bridge.
bridge.toContext()
transport/bridge.ts
THE transport-to-service boundary. The HTTP request becomes a Context: body to ctx.data, path captures to ctx.route, filters to ctx.query.
parseDirectives()
transport/bridge.ts
$limit and $orderBy are split off into ctx.directives. The $ prefix is transport syntax and nothing past this line ever sees one.
around hooks
core/hooks.ts
The outermost pipeline phase wraps everything that follows, including the failure paths. Timing and tracing live here.
gateAuth()
core/litestone.ts
Derived from the model @@gate. No session where one is required stops here as a 401, before any query is built.
autoValidate()
core/litestone.ts
Derived from the field rules. @email, @length and @gte become a 400 with per-field messages — still before the database is touched.
before hooks
core/service.ts
Your code, finally. Defaults, enrichment, anything that should happen to the data on the way in.
$.db
core/litestone.ts
withLitestoneDb() put a per-request Litestone client here at createApp({ db }) time, already scoped to the caller.
$setAuth(user)
litestone/core/client.js
The Data-boundary checkpoint. Everything past this point knows who is asking — and asSystem() is the single documented way around it.
GatePlugin level check
litestone/plugins/gate.js
The same @@gate, enforced a second time at the data layer. Not redundancy — the API is one door, and the door is not the lock.
buildReadFilter()
litestone/core/plugin.js
@@allow predicates become a WHERE fragment, AND-merged with your own. This is why a policy cannot be skipped by a forgetful caller.
SQL
litestone/core/query.js
One statement, with your filters, the policy, and the pagination already folded in. Then the row comes back.
after hooks
core/service.ts
Your code again, on the way out. Shape it, format it, redact it.
wrapResult()
core/envelope.ts
THE result envelope, one owner. A list keeps { object, data, total }; a single record unwraps. Nothing else in the framework decides this.
announceToChannels()
core/service.ts
The single announcement point. One mutation, fanned out to both the event bus and the declared channel — there is no second path to forget.
WebSocket frame
transport/channels.ts
Subscribers receive "leads created". The gate still applies on the way out, so nobody is pushed a row they could not have fetched.
bridge.toResponse()
transport/bridge.ts
Back across the same boundary. A thrown FrameworkError becomes its status code here; a Litestone AccessDeniedError was normalized to 403 on the way.
The resource updates
sierra/resource/index.js
watchProxy() makes the result reactive, so whatever reads it re-renders. That seam — not a fetch call — is where the UI actually binds.