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.
General▾
Interface▾
Observability▾
Dependency Injection
A service lists what it needs in the builder of
serve(), and Akan fills each field before the service starts. Business code stays small, and an outside system can be swapped without touching it.Words used on this page
TermDescription
injector
A helper like
service() or plug() inside serve() or adapt(). Each one fills one field.adaptor
A class built with
adapt() that wraps one outside tool, such as storage or a mail API.role
A slot for a built-in adaptor, such as
StorageAdaptorRole. The app decides what fills it.singleton
One instance per server process, shared by everything that injects it.
server env
The object in
env/env.server.<environment>.ts, typed by ModulesOptions in lib/option.ts.Which injector to use
Reach for them in this order; the first that fits is the right one. The marks show where each works.
Injector
serve()
adapt()
Pick in this order: the first that fits wins
service<T>()
✓
Another service's business method.
plug(Class)
✓
✓
A replaceable tool such as storage, a cache or a message API.
use<T>()
✓
✓
A legacy singleton registered in
option.ts. Recognise it; do not write new ones.env(factory)
✓
✓
Runtime configuration, read without passing it through every function.
For one specific job
memory(Type)
✓
✓
A small value that survives between calls.
signal<T>()
✓
A server signal, to publish an event or queue a job. The field name ends in
Signal.✓AvailableNot available
- Destructure only what you use.
({ service, env }) => ({ … })names the injectors this class needs, and nothing else. - Every field is ready before
onInit(). Injected values are filled first, soonInit()can already use them.
Inject Services
When one service needs another service's business method, declare it with
service<T>(). It is clearer than importing the other service and constructing it yourself:apps/koyo/lib/article/article.service.ts
- The field name picks the service.
subscriptionServiceresolves to the service namedsubscription, so the name must end inService. The type argument only adds types. - A lib's service goes through its namespace. From an app it is typed
srv.shared.FileService; the field is stillfileService. - Import
srvas a type.import type * as srv from "../srv"keeps the runtime import graph lazy. - Generated methods come with the service. A database service already has
this.getArticle,this.updateArticleandthis.articleModel.


Two services cannot inject each other. If
ArticleService injects SubscriptionService, the reverse is a circular dependency. Move the shared step into one of them.Adapt And Plug
Use an adaptor for a tool that has behavior of its own and may be replaced later. The service asks for the class or the role; it never builds the client.
1. Declare it in srvkit/
Write the adaptor as an
adapt() class under srvkit/. This one adds image paths on top of whatever storage the app runs:apps/koyo/srvkit/imageStorage.ts
2. Plug it into a service
The service names the class with
plug() and calls it like any field:apps/koyo/lib/article/article.service.ts
plug(Class)is all the registration there is. Nooption.tsentry: the class itself is the token.- One instance per process. Every service that plugs
ImageStorageshares the same object. this.loggeris built in. Never construct aLoggerin an adaptor; put setup work inoverride async onInit().- Give it a name no other adaptor uses. Write the name passed to
adapt()as const, unique across the app and its libs.
3. Swap a built-in role
Framework infrastructure is plugged by role.
plug(StorageAdaptorRole) gets whatever fills that role, and these are the nine roles with their defaults:| Role | Default |
|---|---|
| ↳ Used for | |
| DatabaseAdaptorRole | SqliteDatabase |
| Documents and queries | |
| CacheAdaptorRole | SolidCache |
memory() values and the document cache | |
| StorageAdaptorRole | BlobStorage |
| Uploaded files, on local disk by default | |
| QueueAdaptorRole | SolidQueue |
| Background jobs queued by signals | |
| ScheduleAdaptorRole | Scheduler |
| Cron and interval jobs | |
| LoggingAdaptorRole | ConsoleLogger |
| Writing log lines by level | |
| WebsocketAdaptorRole | SolidPubSub |
| Pubsub rooms for websocket clients | |
| CompressAdaptorRole | JsonCompressor |
| Encoding a typed value to bytes and back | |
| LlmAdaptorRole | OpenaiLlm |
| LLM calls from the in-page agent relay | |
To replace one for the whole app, call
applyAdaptor in lib/option.ts:apps/koyo/lib/option.ts
- The replacement implements the role's interface.
R2Storageis anadapt()class thatimplements StorageAdaptor. - Only these nine roles can be swapped.
applyAdaptorignores any other class, and it is not a way to register an adaptor. - The app has the last word. The app's
option.tsis read after every lib's, so its choice wins. - Defaults follow the database mode. The database stays SQLite in
multiplemode and becomes Postgres inclustermode. Both modes move cache and websocket to Redis, and queue to BullMQ.
Read Environment
env() builds a value from runtime configuration when the service or adaptor starts. Use it when code needs the app's identity, a hostname or a feature flag.| What you need |
|---|
| ↳ Read it with |
| A server env field: hostname, a feature flag, an API option |
env((options: ModulesOptions) => options.hostname) |
| App identity: appName, environment, operationMode |
env(() => getEnv().operationMode) |
| A container variable or a secret |
env(() => process.env.PAYMENT_KEY) |
Add a setting of your own
- Declare the field on
ModulesOptionsinlib/option.ts. - Set it in each
env/env.server.<environment>.tsthat needs it. - Read it with
env()in a service or an adaptor.
Steps 1 and 2 take a few lines each:
apps/koyo/lib/option.ts · apps/koyo/env/env.server.local.ts
Step 3 reads it next to the app's identity:
apps/koyo/lib/article/article.service.ts
- It runs once, at startup. The value is fixed for the life of the process, and the factory may be
async. - Pass a factory.
env()takes a function; there is noenv("KEY")form. - Call
getEnv()inside the factory. At module scope it throws duringakan build, which has no app env to give it.
Tips
- Declare one client, not one per call. Do not create external clients inside every method; declare one
adapt()class andplug()it. service()for business,plug()for infrastructure. Business collaboration goes through services; replaceable infrastructure goes through adaptors.- Inject prepared clients, not raw credentials. Keep secrets in the server env or
process.env, and resolve them inside a function:process.env.X ?? options.x ?? generate(…), never at module scope. adapt()is for singletons only. A per-use value object stays a plain class younewat the call site.
Read next