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):

ts
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