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.
Introduction▾
Tutorials▾
Core Concepts▾
Architecture▾

Business Service Architecture

A customer taps Order on the kiosk, and that one button has to do four things: refuse the order when the mango has run out, write the order down, hand back a receipt now, and put the ticket on the kitchen screen before the customer turns away.
One tap, four jobs
One tap on the kiosk reaches the business service, which checks the stock, saves the order, hands back a receipt, and puts a ticket on the kitchen screen.
None of that is drawing a screen. Together it is the business service: the action a request asks for, the business rule behind it, the background job it starts, and the change other screens must be told about.
Every module splits that work across the same three files, and the split never changes. A request arrives at the API port and passes through them in order:
One module, top to bottom
Browser · mobile app · agent
API port8282/api
icecreamOrder.signal.tsendpoint · slice· internal
icecreamOrder.service.tsrules · other services· external APIs
icecreamOrder.document.tsschema · filters· chain methods
Stored data
Each file answers one question, and only that one:
signal.tsThe phone operator
May this caller ask at all?
Takes the call, refuses the ones that should never reach the floor, and hands valid work to the right service.
service.tsThe business owner
What should happen?
Stock rules, payment status, reservation conflicts and external APIs are combined here into one meaningful action.
document.tsThe archive and its rulebook
How is the record stored, and which changes will it accept?
The stored form, the query filters, and the state changes a record will accept. A chain method mutates and returns this; the caller saves.
Words used on this page
guard
A class that decides who may make a call. Every endpoint names its own in the signal file.
generated CRUD
The create, read, update and remove endpoints every model already has. You never write them.
chain method
A state change on the document: it checks, mutates and returns this. The caller saves.
adaptor
A singleton that wraps an outside system, such as a POS terminal, and is plugged into a service.
room
A pubsub channel. Every screen subscribed to it receives what the server publishes there.

Two Actions, End To End

Let's follow two real actions from the counter through the three files:
A customer places an order
Creating an order is generated CRUD, so no endpoint is written for it. What you write is the rule: an order takes stock out of today's inventory.
_preCreate
Staff move it to the next status
This one is not generated. It is a mutation you declare, and only an admin may call it.
processIcecreamOrder
apps/koyo/lib/icecreamOrder/icecreamOrder.signal.ts
  • IcecreamOrderSlice.byStatuses is the order list a screen loads, filtered by status.
  • processIcecreamOrder is one line of delegation, because the decision is not the endpoint's to make.
The decision lives in the service. This is where the order meets a second module, inventory, and where both documents are loaded before either is saved:
apps/koyo/lib/icecreamOrder/icecreamOrder.service.ts
  • _preCreate runs before the generated create saves the order. It hands the stock deduction to inventoryService.
  • processIcecreamOrder loads the order, calls the chain method process(), then saves.
The manager's half is the mirror image: the same three files, a different guard, and no state machine, because refilling today's inventory is allowed whenever an admin asks. Its signal appears in Bounding A Call below.

Endpoint, Slice, Internal

A model's signal file exports exactly three classes, and every module declares all three even when two of them are empty. Which one you reach for depends on one thing: who starts the call.
ClassWho calls it
↳ What goes in it
endpointA user, through fetch.*
query / mutation, or message / pubsub on an open connection
sliceA screen
fetch.initXInY(...) in the route seeds the store; st.do.initXInY() reloads it
internalThe server itself
Schedules such as cron, lifecycle hooks, and queued process jobs
Choosing by what the screen needs
Start from the product behavior, not from the class list. Does the user need an answer now, a live conversation, a broadcast to many screens, or a job that finishes later?
Signal shape choice
What does the screen need?
Answer now
Keep talking while open
Notify many screens
Finish later
Use query or mutation
Use message
Use pubsub
Use process or schedule
query / mutation
The screen asks once and expects one result: load a list, save a form, approve a request, add stock.
message
An open screen keeps a websocket conversation going: device control, a live operation panel, a guided workflow.
pubsub
One business change is pushed into a room that many screens, dashboards, devices or users subscribe to.
process / cron / interval
The work is queued, scheduled, repeated, or tied to the server lifecycle rather than to a caller.

Bounding A Call

Every endpoint takes an option object, and guards is only its first field. The rest say how the call behaves: how long it may take, whether its answer may be reused, whether agents see it.
Here is the manager's half of the shift:
apps/koyo/lib/inventory/inventory.signal.ts
  • getTodaysInventory is a read the whole shop shares: Public, and its answer is reused for a second (cache: 1000).
  • refillTodaysInventory is a write only an admin may make, and a stock provider can make it slow, so it gets a minute (timeout: 60_000).
guardsGuardCls[]
Who may call this. An endpoint that names none runs no check; there is no default policy.
timeoutnumberdefault 30000 (client)
Milliseconds this call may take; the Timeout middleware and the client both enforce it.
cachenumber
Milliseconds the answer may be reused. Only a query taking no internal argument may carry one.
mcpbooleandefault true
false takes the endpoint out of the MCP catalogue without touching its guards.
method"POST" | "PATCH" | "PUT" | "DELETE"default "POST"
The HTTP verb a mutation answers on, for when a foreign wire protocol forces one.

Work That Outlives The Request

Some work has no caller waiting for it. Nobody presses a button to make ice cream melt, and nobody asks for last night's orders to be closed. That work goes in the Internal class, and the server starts it itself:
  • interval(10000): served ice cream melts on its own schedule, so every ten seconds the server warns about it.
  • cron("0 4 * * *", …): orders nobody collected are closed out overnight, at 4 a.m.
Both are internal signals, and the batch replica, the server process that runs background jobs, is what runs them.
The same file also gains a pubsub room. It is an endpoint nobody calls, because the server is what publishes into it:
apps/koyo/lib/icecreamOrder/icecreamOrder.signal.ts
Telling screens that are already open
When an order moves to processing, the kitchen screen is already open, and nobody is going to press refresh. So the service publishes the saved order into the room named after its new status, and every screen subscribed to that status appends the ticket.
One publish, every open screen
The business service publishes the processed order once into the room for its status, and every kitchen screen subscribed to that room receives the ticket.
The service reaches its own signal through an injected field, then publishes right after the save:
apps/koyo/lib/icecreamOrder/icecreamOrder.service.ts
A heavy job uses all three at once:
  1. A mutation starts the monthly settlement report and returns the queued record immediately.
  2. An internal process, reached from the service through an injected signal, builds the file.
  3. A slice lets the screen read progress, status, and the download result as the record changes.

What A Service Is Handed

A service never builds the things it needs. It lists them in the builder argument of serve(), and the container hands each one in before any handler runs.
That is what lets a payment provider, a cache backend or a whole sibling module be swapped without editing the business method that uses it.
Handed in, never built
Another module's service, an adaptor, an environment value and shared memory are each handed into the service from outside; the service builds none of them.
service<T extends Service>() => T
Another module's service. The field must end in Service: inventoryService resolves inventory.
plug(adaptor: AdaptorCls) => Adaptor
An adaptor singleton: an adapt() class, or a role that option.applyAdaptor binds.
signal<S>() => S
The module's own signal, to publish a pubsub room or enqueue a process. Ends in Signal.
env(fn: (env) => T) => T
A value read out of the backend environment at wiring time.
memory(ref, opts?) => Store
Runtime state held in the cache adaptor, so every replica sees it. Takes a scalar or model class.
use<T>() => T
Legacy: a constructor-style singleton from lib/option.ts. Write new adapters with adapt().
Writing an adaptor
An adaptor is the unit you plug. It is a class built on adapt(), and it registers itself under the name it is given.
It takes the same injectors as a service minus service and signal. So an adaptor can hold config, another adaptor and shared state, but not business logic:
apps/koyo/srvkit/posTerminal.ts
A database-backed service also receives its own model as this.<refName>Model without declaring anything — serve(db.inventory, …) is what adds it. The worked walk through each injector, including how a role is bound, is on Dependency Injection.

Where An Error Belongs

Refusing an order because the mango ran out and refusing an order from a customer who is not signed in are not the same refusal, and they are not written in the same file. Each layer throws what only it can know:
  • A customer who is not signed in: the guard in signal.ts refuses before anything else runs.
  • The mango ran out: another document forbids it, so service.ts refuses.
  • An order that is not active cannot be processed: the record's own state forbids it, so document.ts refuses.
Which layer refuses
A call arrives
signal.ts guardsmay this caller do this at all?
401 or 403 · the guard returns false
service.tsdoes another document forbid it?
document.tsis this record in a state that allows it?
chain method mutates · caller saves
dictionary .error keytranslated for the caller
A chain method is the smallest version of this. It validates, mutates, and returns this. It never saves, so chains compose and the caller decides when the write happens:
apps/koyo/lib/icecreamOrder/icecreamOrder.document.ts
When failing is not an error
Best-effort code does not throw at all. It returns a plain value, and the caller decides whether that is an error:
  • An adaptor that cannot reach a provider logs and returns null.
  • A guard that cannot load a record warns and returns false.
There are no Result wrappers anywhere in the stack.

The Same Endpoints, For Agents

Every signal is also served to AI agents as an MCP server on POST /mcp, mounted by default. You write nothing extra in a signal file, and there is no per-endpoint switch to turn on.
Instead, exposure follows the guards. The guards are already the authorization decision, and a second switch would only guarantee that endpoints added later stay invisible until somebody remembers them.
Endpoint
Published
Left out
Decided by the guards
guards: [Admin]
✓
An endpoint that declares a real guard is published to agents.
no guards
✓
Refused. A missing guards array now costs visibility as well as authorization.
mutation · [Public]
✓
A mutation whose only guard is Public is refused too.
Decided by the shape
pubsub · message
✓
Refused.
fileUpload: true
✓
A file upload is refused.
Any · Binary
✓
An endpoint returning Any or Binary is refused.
Your own choice
mcp: false
✓
Takes the endpoint off the shelf without touching its guards. Right for a step of a UI-driven state machine: perfectly guarded, still no business of a model.
✓What agents getNot this
The business service you already wrote is therefore the agent surface too: the same guards, the same Err, the same service method. What changes is the cost of the catalogue and who may be on the other end. The wire, the OAuth metadata, the rate limits and the Person guard are on MCP Server.

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