Effect RPC Contract Transport
An internal job-control API defined once as an Effect 4 RpcGroup that the server and the client both import, with the transport left swappable. The problem it replaces is the usual internal service: route handlers on one side, a hand-written fetch wrapper on the other, and a types package in the middle that drifts until a field is undefined in production and was a string in the caller's head. Here one file is the contract: each Rpc.make binds a tag to a payload schema, a success schema, and a typed error schema, so the wire shape has a single author and there is no second definition to fall out of sync. Enforcement is two layers and both hold: the handler and client types are derived from the group, so a handler that returns the wrong shape or a call with the wrong payload does not compile, and the schema validates the same shapes at the boundary at runtime as an independent guard, rejecting a request that does not decode before a handler runs and encoding the reply through the success schema on the way out. Declared errors cross the wire as tagged failures: JobNotFound is a TaggedErrorClass, so it travels with its _tag intact and the client matches it with catchTag by name at runtime, while a defect that is not in the schema stays a defect and pages someone rather than arriving as a typed error. The transport is deliberately unnamed in the contract, because HTTP with ndjson, WebSocket, and a Worker MessagePort are the same contract with a different protocol layer chosen in the service file, and the handlers do not move when it changes. Auth is RPC middleware that runs before every handler and provides the authenticated operator, so a handler reads who is calling without re-checking a token. Verified against [email protected], the RPC layer largely Tim Smart's work.
npx shadcn@latest add https://ui.aryank.space/r/effect-rpc-contract-transport.jsonInstalls from ui.aryank.space. To add it by hand, copy the files in Files below, or register the @compronents namespace via the docs.
Server and client both import the same RpcGroup contract, and the transport is provided as a layer: here HTTP. Note the result: job.id, typed end to end, with no fetch wrapper and no types package in the middle.