FrontierJS

The seams

One request, every seam it crosses.

A single POST /api/leads. Click a step, or walk it with ← →.

Transport HTTP / WS
Framework core Junction
Data layer Litestone
Database SQLite
SQL executesrow(s) returned
Channels (WS)subscribers receive “leads created” — the gate still applies
1

HTTP arrives

transport/http.ts

The server matches the route and, if an Authorization header is present, resolves it to a session through IAuth.verifySession().

Why it matters. This is the only place authentication happens. Everything downstream receives an identity or no identity — never a token to re-check.

2

bridge.toContext()

transport/bridge.ts

The HTTP request becomes a Context: body into ctx.data, path captures into ctx.route, filters into ctx.query.

Why it matters. THE transport-to-service boundary. Cross it once and a service never touches a Request object — which is why the same service answers a WebSocket call unchanged.

3

parseDirectives()

transport/bridge.ts

$limit, $offset, $orderBy and $select are split off into ctx.directives, leaving ctx.query as pure filters.

Why it matters. The $ prefix is transport syntax and dies here. Nothing past this line sees one, so result-shaping has exactly one parser rather than a convention repeated per handler.

4

around hooks

core/hooks.ts

The outermost pipeline phase opens, wrapping everything that follows — including the failure paths.

Why it matters. Because it wraps the errors too, timing and tracing belong here. A before hook cannot see a request that throws in the data layer; an around hook can.

5

gateAuth()

core/litestone.ts

Derived from the model’s @@gate. A request with no session where one is required stops here as a 401.

Why it matters. You did not write this check — the schema did. Add a model and its authorization arrives with it, already at the edge.

6

autoValidate()

core/litestone.ts

Derived from the field rules. @email, @length and @gte become a 400 with per-field messages.

Why it matters. Still before the database is touched. The rule is stated once, on the field, and every entry point enforces it — including direct calls that never went through HTTP.

7

before hooks

core/service.ts

Your code, finally. Defaults, enrichment, anything that should happen to the data on the way in.

Why it matters. Everything above this line was derived. This is the first point where the framework asks you to write something.

8

$.db

core/litestone.ts

withLitestoneDb() put a per-request Litestone client here when the app was created, already scoped to the caller.

Why it matters. Per-request scoping is what lets gates and policies see the user without a single line of wiring in your service.

9

$setAuth(user)

litestone/core/client.js

The Data-boundary checkpoint. Identity is attached to the client for the rest of this request.

Why it matters. One checkpoint, one bypass. asSystem() is the single documented way past it, which makes privileged code searchable rather than scattered.

10

GatePlugin level check

litestone/plugins/gate.js

The same @@gate is enforced a second time, now at the data layer.

Why it matters. Not redundancy. The API is one door, and the door is not the lock — a job, a CLI command or a test reaching the same model gets the same answer.

11

buildReadFilter()

litestone/core/plugin.js

@@allow predicates compile into a WHERE fragment, AND-merged with the filters your query already carried.

Why it matters. This is why a row policy cannot be forgotten. It is not a filter a caller applies — it is part of the statement, added below every caller.

12

SQL

litestone/core/query.js

One statement, with your filters, the policy and the pagination already folded in.

Why it matters. A single round trip. The policy did not become a second query, and pagination did not become a slice in memory.

13

after hooks

core/service.ts

Your code again, on the way out. Shape it, format it, redact it.

Why it matters. The symmetry is deliberate: before and after are the same list in reverse, so a transform in and a transform out live next to each other.

14

wrapResult()

core/envelope.ts

THE result envelope, one owner. A list keeps { object, data, total }; a single record unwraps.

Why it matters. One module decides the response shape for the whole framework, so a client never has to ask which endpoint wraps and which does not.

15

announceToChannels()

core/service.ts

The single announcement point. One mutation fans out to both the event bus and the declared channel.

Why it matters. Announcing in one place is what makes realtime reliable. Two publish paths means one of them eventually gets forgotten in a new code path.

16

bridge.toResponse()

transport/bridge.ts

Back across the same boundary. A thrown FrameworkError becomes its status code here.

Why it matters. Failures cross the boundary the way successes do. A Litestone AccessDeniedError was normalized to 403 on the way, so clients match one contract.

17

HTTP Response

transport/http.ts

The response is sent. In the browser, watchProxy() makes the result reactive and whatever reads it re-renders.

Why it matters. No refetch, no invalidation key. The Resource is the binding, which is why a pushed row and a returned row update the UI the same way.

Step 1 of 17
One requestSame path for HTTP and WebSocket
Named seamsEvery handoff is a real function
Policy at the data layerEnforced where the data lives
Single announcementOne mutation, two fan-outs
Reactive by defaultThe Resource updates the UI
Every name here is a real function — see the bridge index in CLAUDE.md.← Every package