Remote / foldkit-remote-server
Sixty seconds: answer one Entity read
Start with the same Entity the client selects:
import { Effect, Schema } from 'effect'
import { Entity, Remote, RemoteRpc } from 'foldkit-remote'
import { RemoteServer } from 'foldkit-remote-server'
const Project = Entity.make(
'Project',
Schema.Struct({
id: Schema.String,
name: Schema.String,
privateNotes: Schema.String,
}),
)Give that Entity one Source:
// Your authentication's principal type; without it `principal` is `unknown`.
type Principal = { readonly isAdmin: boolean }
const rows = new Map([
['p1', { id: 'p1', name: 'Apollo', privateNotes: 'Internal' }],
])
const ProjectSource = RemoteServer.entity<Principal>(Project, {
// Authentication already resolved `principal` before RemoteServer sees it.
// Return only fields this caller may read.
authorize: (principal, fields) =>
fields.filter(field => field !== 'privateNotes' || principal.isAdmin),
read: ({ ids, fields }) => Effect.succeed(ids.flatMap(id => {
const row = rows.get(id)
return row === undefined ? [] : [{
id,
values: Object.fromEntries(Object.entries(row).filter(([field]) => fields.includes(field))),
}]
})),
})
const Server = RemoteServer.make({
entities: [ProjectSource],
})Compile that definition into the Remote RPC handlers for one authenticated principal:
const principal: Principal = { isAdmin: false }
const handlers = RemoteServer.handlers(Server, principal)
const layer = RemoteRpc.toLayer(handlers)RemoteServer.entity and make only describe the server. handlers binds
a principal; RemoteRpc.toLayer provides handlers to Effect RPC. Neither starts
an HTTP listener. In this example the source returns an Effect over local rows;
replace its body with your database or API reader when connecting real data.
A client requirement such as:
Project:p1 [id, name, privateNotes]runs as:
1. Project declares: id, name, privateNotes
2. authorize returns: id, name
3. Source receives: ids=[p1], fields=[id,name]
4. Source returns: { id: 'p1', values: { id: 'p1', name: 'Apollo' } }
5. handler normalizes: Project:p1 { id, name }, settled: privateNotes
6. client records presence for the returned fields, privateNotes as unavailableprivateNotes never reaches read. Authorization happens before application
data access, while the request still has semantic field names. The answer
names the field as settled, so the client stops asking for it; it does not
say why. A Selection naming it reads Failed with an Unavailable error, one
that does not name it is unaffected, and Data.refresh asks again.