YouBothAgent▾
You — Business rules and flows you own. Read these yourself.
Both — Know the idea; your agent follows the details.
Agent — Conventions and references your agent follows. Look up as needed.
CLI Reference▾
AkanJS Reference▾
UI Reference▾

akanjs/signal

akanjs/signal declares the API boundary around a service: what may be called, by whom, over which transport. You import it in *.signal.ts files and in the srvkit/ files that hold guards and middleware.
What It Exports
endpoint
Declares the calls a module exposes: queries, mutations and WebSocket endpoints.
internal
Declares work the server starts itself: schedules, lifecycle hooks, queue jobs, resolved fields.
slice
Declares the list queries a client store loads, and guards the generated CRUD endpoints.
PublicNoneguard
Guards. They run before the handler and decide whether the call may go on.
ReqResIpWs
Internal arguments: handler arguments the server fills in, such as the request.
middlewareLoggingTimeout
Middleware wraps every endpoint call. The two built-ins are registered by default.
McpProgress
Reports progress from inside a long MCP tool call.
SignalRegistryserverSignal
Look up registered signals, and publish or enqueue from a service.
SignalContext
The per-call context guards and middleware receive: transport, arguments and caller.
Four Endpoint Kinds
The endpoint builder hands you four kinds, and each kind fixes its transport:
Kind
HTTP
WebSocket
Request and response
query
✓
Reads, over GET. The only kind that may declare cache.
mutation
✓
Writes, over POST. The method option moves it to PATCH, PUT or DELETE.
Realtime
pubsub
✓
The client subscribes to a room, and the server publishes into it.
message
✓
The client sends one message and gets one answer back.
✓travels over itdoes not
An MCP prompt is not an endpoint kind. It is declared on a page with page().prompt(name, description).

Public / None / guard

A guard decides whether a call may run, before its handler does. Every custom endpoint names its own guards array, and every guard in it must pass.
Public
Always passes. Use it on slice reads such as get:, never as a mutation's only guard.
None
Always refuses the call.
guard(name)
A base class with static name filled in and scope preset to "account".
Guard
The interface: canPass(context) returns a boolean, or a promise of one.
GuardScope
"account" or "resource": what the guard needs to reach a verdict.
Account Or Resource
Every guard carries a static scope, which says whether the verdict needs the call's arguments:
scope
Reads arguments
Checked for MCP listing
GuardScope
"account"
✓
Reads only the caller, through context.get("account").
"resource"
✓
Reads the call's arguments via context.getArg(name), so it is judged only at call time.
✓yesno
  • Role checks are "account". In libs/shared, Every, Admin and Person are "account"; Owner, SelfOrAdmin and any Can<Verb><Model> guard are "resource".
  • With implements Guard, declare it yourself. guard(name) presets "account", so a guard built on it that reads arguments overrides it with "resource".
Writing A Guard
Guards live in srvkit/guards.ts, one class each:
apps/koyo/srvkit/guards.ts
Then name them on each endpoint. A pubsub room is not covered by slice guards, so it declares its own:
apps/koyo/lib/notice/notice.signal.ts
  • Same guard, every transport. Guards run on HTTP and WebSocket calls alike, so read the caller with context.get("account") instead of branching on the transport.
  • In order, and the first refusal wins. Guards run in the order declared, and the first one that refuses stops the call.
  • Slice guards stop at CRUD. A slice's guards cover only its generated query and mutation endpoints; each pubsub and message declares its own.
  • Keep static name. fetch serializes guard names, and the API explorer filters on them.

McpProgress

McpProgress reports how far a long MCP tool call has got, so the agent's client can show it. Call it wherever the work happens; nothing has to be passed down.
McpProgress.report(progress, option?)(progress: number, option?: McpProgressOption) => void
Sends one progress notification for the call running on this stack.
option.totalnumber
Optional. The denominator the client renders; omit it when the amount of work is unknown.
option.messagestring
Optional. One short line on the current step; the user reads it, so write prose.
McpProgress.streamingboolean
true only while a client is streaming, so a costly message can be skipped.
A service that imports rows reports after each one:
apps/koyo/lib/task/task.service.ts
  • A no-op outside a stream. Over plain HTTP, a WebSocket or in a test, report does nothing, so the same code runs unchanged.
  • Reachable from any depth. It rides AsyncLocalStorage, so a service, an adapter or a loop several frames down reports without a channel parameter.
  • The client opts in. A call streams only when the request sent both Accept: text/event-stream and a _meta.progressToken.
  • The response switches on the first report. Only then does the server answer with SSE; a call that never reports gets an ordinary response.

Req / Res / Ip / Ws

Internal arguments are handler arguments the server fills in, not the caller. Declare one with .with(X), and the handler receives it after the declared arguments.
Argument
HTTP
WebSocket
The request
Req
✓
The current Bun request, Bun.BunRequest.
Res
✓
The Response class, for building a reply such as res.json(value).
The caller
Ip
✓
✓
The caller's IP as the nearest proxy recorded it, or null.
The connection
Ws
✓
ws, socketId, subscribe, and the on / off cleanup hooks.
✓availablenot available
Libraries add their own, such as Self, Me and Account from @libs/shared/srvkit. Take the caller from those, never from an id the client sends.
A mutation that reads the raw request body and the caller's IP, and a message handler that cleans up when the socket closes:
apps/koyo/lib/_wallpad/wallpad.signal.ts
  • Required unless nullable. A required internal argument that comes back null refuses the call with 401. Pass { nullable: true } when null is a valid answer, as it is for Ip.
  • A returned Response is sent as is. Serialization is skipped, which is how an endpoint declared Any streams a file back.
  • Cleanup belongs to the call that registered it. on("disconnect" | "unsubscribe", fn) is scoped to the room for a pubsub and to the socket for a message. Register both when it must run either way.
  • socketId names a connection, not a caller. Key per-user state on the account, and never mint an id of your own.

middleware / Middleware

Middleware wraps every endpoint call, before and after the handler. Two are registered by default; write your own with middleware(refName).
MiddlewareActs when
↳ What it does
LoggingAlways.
Writes debug lines around the call, and an error line when it fails.
TimeoutThe endpoint declares timeout in ms.
Rejects with base.error.gatewayTimeout (504) once the time is spent.
An endpoint's { cache: <ms> } is not a middleware. The stored answer is looked up inside the call, after the guards, so a hit reaches only a caller they admitted.
Call Order
From the outside in:
  1. The two defaults: Logging → Timeout.
  2. Middleware a lib/option.ts adds with applyMiddleware(...), such as AccountMiddleware from libs/shared.
  3. The endpoint's own middlewares option.
  4. Guards, then internal arguments, then the cache lookup if the endpoint declares one, then the handler.
Writing One
A middleware that warns about slow calls:
apps/koyo/srvkit/slowCallMiddleware.ts
Register it in one of two places:
lib/option.ts
Every endpoint the server runs, after the two defaults.
middlewares
The endpoint option. Applies to that endpoint only, inside every global middleware.
  • use(env) runs once. The handler it returns serves every call, so set up in use and keep per-call work in the handler.
  • Skipping next() skips the guards. Guards run inside next(), so never answer from a middleware on behalf of a guarded endpoint. For a stored answer, declare { cache: <ms> } on the endpoint instead.
  • refName is the key. Registering a middleware under a refName already taken replaces the earlier one.

SignalRegistry

SignalRegistry finds a module's registered signals by refName at runtime. A refName nothing registered returns undefined.
getDatabase(refName)
A database module's internal, endpoint, slice, server and serializedSignal.
getService(refName)
A service module's internal, endpoint, server and serializedSignal.
Look one up by refName:
apps/koyo/srvkit/signalLookup.ts
Publishing From A Service
Each module also has a server signal, which a service injects with signal<sig.X>(). It turns endpoints and internals into methods:
<pubsubKey>(...roomArgs, data)
One per pubsub endpoint. Publishes data to the room the arguments name.
<processKey>(...args, jobOptions?)
One per process internal. Enqueues a job and returns its AkanJob.
The notice service saves a notice, then publishes it to the noticeAdded room from the guard example:
apps/koyo/lib/notice/notice.service.ts
  • Save first, then notify. A subscriber then never receives a record that failed to save.
  • The room's return model shapes the data. data is serialized with the pubsub endpoint's return model, like any response.
  • The field name picks the signal. noticeSignal resolves to the notice module's server signal, and the Signal suffix is required.

Released under the MIT License

Connect your AI to these docs

MCPhttps://akanjs.com/mcp
Copyright © 2026 Akan.js All rights reserved.System managed bybassman