Remote / foldkit-remote-server

Sixty seconds: answer one Entity read

Start with the same Entity the client selects:

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

ts
// 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:

ts
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 unavailable

privateNotes 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.