Remote / foldkit-remote-server
Over fetch (Workers)
serveFetch from foldkit-remote-server/fetch is that route as a value: a
(request: Request, env: Env) => Promise<Response> for a Worker's fetch,
or any other runtime that speaks Request/Response. Each request is one
Effect: layer is built from the environment, then resolvePrincipal says
who is asking, then the request is answered as them. The resolver may need
what layer provides (a session store, an auth service):
import { serveFetch } from 'foldkit-remote-server/fetch'
export default {
fetch: serveFetch({
server,
resolvePrincipal: request =>
Effect.succeed({ actorId: request.headers.get('x-actor') ?? 'anon' }),
layer: env => databaseLayer(drizzle(env.DB)),
}),
}Only POSTs to path (default /remote) are answered; anything else is a 404. A body that is not JSON, or not a request, is a 400 that reaches no
handler; a handler's failure a 500 with its message.
A resolver refuses the caller by failing with
RemoteServer.unauthenticated(reason?): a 401 whose body is
{ error, unauthenticated: { reason? } }, as answer's 403 carries
forbidden, and whose reason is the only thing the caller learns. Anything
else this side breaks, the resolver's defects and the layer included, is a
500 that says nothing of it. A visitor is not a refusal: resolve one to the
principal your Sources treat as nobody, and refuse only a credential that was
sent and no longer holds.
headers: principal => ({ … }) adds headers to every answer to that
principal, a live stream's included; a refusal answered no one and carries
none. foldkit-auth's AuthServer.answered uses it to say whom each answer
was for.
revalidate: { every, key? } asks resolvePrincipal again, of the same
request, every every while a live stream is open. A refusal ends the
stream with Unauthenticated; a principal whose key differs from the one
it opened with ends it as a live error, which the client opens again after,
as the new principal. key is who a principal is, as liveHub's key:
with guards, key: Policy.keyOf. Without revalidate a stream serves the
principal it opened with, a revoked session included, until it closes.
A body that opens live ({ operation: 'live', payload }) is answered with
the stream as text/event-stream: one data: frame per LiveChange, then
a terminal event: error frame carrying the failure's message when the
stream fails rather than ends. A payload no live stream speaks is a 400 that
subscribes to nothing. Remote.httpWithLive is the other end; a client that
disconnects interrupts the subscription, as closing the RPC stream would.
The stream moves on its own even when nothing changes: a : ping comment opens
it, and another follows every liveHeartbeat (default 15 seconds), so proxies
and worker runtimes do not drop an idle connection. The heartbeat stops with
the events it serves, so a stream with nothing to serve still ends.
live takes a hub whose changed/deleted signals reach these streams. What
calls it must be able to wake the subscriber's fiber: in a worker, one request
cannot wake another's stream, so a hub fed from a second request delivers
nothing and ends the first stream. Poll the shared store from inside the
subscriber's own request instead — examples/cloudflare's pollLive is the
shape. A hub is right for a long-lived process that owns both sides.
What it does not own
foldkit-remote-server intentionally does not own:
- HTTP/WebSocket transport or serialization;
- authentication protocol or session storage;
- a database connection;
- the client's normalized cache;
- client rendering or Surface activation;
- durable/offline client-authored state.
Those boundaries keep this package an interpreter of the Remote contract rather than another application runtime.
See also
- Server-derived state — the client/server mental model and ownership rules.
foldkit-remote— requirements, normalized client Model, subscriptions, mutations, live reads, and retention.foldkit-remote-drizzle— compile Entity selections and Query windows into Drizzle reads.examples/kitchen-sink— Remote + RemoteServer + Drizzle +liveHubin one executable trace.examples/cloudflare— Remote + Sync + remote-drizzle on D1 in a real worker, with a polling live Source.