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.
Workspace▾
App & Library▾
Domain▾
Scalar▾
model.service.ts
<model>.service.ts is where one business action runs from start to finish: load the documents, change them, save, then tell whoever else needs to know.Open it when an action needs more than one document, another service, a background job, an external API, or anything that must stay on the server.
Which file owns the work
The work
document
*.document.ts
service
*.service.ts
signal
*.signal.ts
Changing one document
state change
✓
A chain method such as
story.approve() validates, changes the document and returns this.state precondition
✓
The chain method throws when the document is in the wrong state for the change.
Running a business action
multi-document workflow
✓
Load the documents, call their chain methods, save, then notify.
cross-document rule
✓
A rule that compares several documents throws its
Err here.external API · job · server-only code
✓
Reached through injected adapters, signals and env values.
Exposing it
who may call it
✓
The endpoint's guards decide access.
the endpoint
✓
Its
exec calls one service method and nothing more.✓Belongs hereNot here
Words used on this page
TermDescription
database service
A service bound to one model with
serve(db.<model>, …). It gets that model's methods.plain service
A service with no model, made with
serve("<name>" as const, …).injection builder
The function you pass to
serve(). Each key it returns becomes a property on this.chain method
A document method that changes one document and returns
this, e.g. story.approve().hook
A method such as
_preCreate that runs around create<Model>, update<Model> or remove<Model>.Service Shapes
Every service is a class that extends
serve(…). What you pass to it decides which of three shapes the service takes:Database Service
serve(db.story, …)Bound to one model. It gets
storyModel, the CRUD methods, and fourteen methods per filter.Plain Service
serve("base" as const, …)No model. For runtime coordination, scheduled work, shared server features, or app-level orchestration.
Extended Service
serve(db.user, …, ...user.services)A database service that also mixes in a lib's service for the same model, then adds app-specific behaviour.
A database service, complete with its imports:
apps/koyo/lib/story/story.service.ts
dbis a value import,srva type import.serve()needs the model at runtime; services are only named as types, so the runtime import graph stays lazy.- Injected keys become properties.
actionLogServiceis read asthis.actionLogService. - Methods stay short. Load, call a chain method, then
return await ….save().
A plain service has no model. The framework's own
BaseService is one:pkgs/akanjs/service/base.service.ts
What serve() Gives You
serve() returns a class for you to extend. What that class already carries depends on the first argument:What you get
Database
serve(db.x, …)
Plain
serve("x", …)
From the model
<model>Model
✓
The model adaptor, such as
this.storyModel.get<Model> … remove<Model>
✓
The six CRUD methods listed under Generated Methods.
list<Query> … updateOne<Query>
✓
Fourteen methods for each filter in the document.
_preCreate … _postRemove
✓
Hooks around create, update and remove.
On every service
logger
✓
✓
A Logger named after the class, such as
StoryService.onInit · onDestroy
✓
✓
Run once at boot and once at shutdown.
injected properties
✓
✓
Every key your injection builder returns.
...extendServices
✓
✓
Service classes passed after the builder, mixed in.
✓IncludedNot included
Arguments
db.<model>DatabaseModel
First argument for a database service.
"<name>" as conststring
First argument for a plain service.
option{ enabled?, serverMode? }optional
Goes second when present. See Service Option below.
injectBuilder({ service, use, … }) => ({ … })
Returns the properties to inject. See Injection Builder.
...extendServicesServiceCls[]optional
Mixes in their methods, injections and hooks. See Service Extension.
Service Option
The option decides which processes run the service. A
batch process runs background work and takes no traffic; a federation process serves traffic. The default single process runs as all, so both are on there.enabledboolean | (() => boolean)default true
false leaves the service out. A function runs once, the first time it is read.serverMode"batch" | "federation"
On only where
SERVER_MODE is that value or all. enabled wins when both are set.Generated Methods
A database service gets these without writing them. Their names follow the model name and the filters declared in
<model>.document.ts.Predefined Properties
PropertyDescription
<model>Model
The model adaptor, injected automatically. Call the model's own methods and filters on it.
logger
A Logger named after the service class.
CRUD Methods
MethodDescription
get<Model>(id)
Loads one document by id. Throws when it does not exist.
load<Model>(id?)
Loads one document by id. Returns null when it does not exist or the id is empty.
load<Model>Many(ids)
Loads several documents by id in one batch.
create<Model>(data)
Creates a document through
_preCreate and _postCreate.update<Model>(id, data)
Applies a patch through
_preUpdate and _postUpdate, then returns the document.remove<Model>(id)
Soft-removes (sets
removedAt) through the remove hooks, then runs cascades.Filter Methods
Every filter in the document generates fourteen methods.
<Query> is the filter's key with a capital first letter: filter inRoot gives listInRoot.Reads
MethodDescription
list<Query>(...args, option?)
Lists the matching documents.
listIds<Query>(...args, option?)
Lists the ids of the matching documents.
find<Query>(...args, option?)
Finds one match, or returns null.
findId<Query>(...args, option?)
Finds the id of one match, or returns null.
pick<Query>(...args, option?)
Finds one match. Throws when there is none.
pickId<Query>(...args, option?)
Finds the id of one match. Throws when there is none.
exists<Query>(...args)
Checks for a match. Returns the id of one match, or null.
count<Query>(...args)
Counts the matching documents.
insight<Query>(...args)
Computes the model's insight over the matching documents.
query<Query>(...args)
Returns the query descriptor itself, without running it.
The trailing option.
list and listIds take { sort, skip, limit, sample, select }; find, findId, pick and pickId take the same without limit. The rest take none.Query-level writes
MethodDescription
remove<Query>(...args)
Soft-removes every match in one atomic update.
removeOne<Query>(...args)
Soft-removes the newest match by
createdAt. For at-most-one queries, not for queues.update<Query>(...args).set(patch)
Updates every match atomically. The patch goes in
.set(); the chain alone runs nothing.updateOne<Query>(...args).set(patch)
Updates the newest match by
createdAt. The result has counts, not which row changed.

Query-level writes skip hooks and cascades. Each is one atomic update, so no
_postRemove runs and no cascade follows. When a model has either, remove its documents one at a time with remove<Model>(id).Full-text search
Full-text search is not a method of its own. A filter whose query calls
q.search() generates the same fourteen methods every other filter does:apps/koyo/lib/story/
sort: "relevance"orders the results by match score, best first.- Blank text matches nothing. An empty or whitespace-only search returns no rows, not every row.
Service Extension
When an app declares a module with the same name as a lib module, such as
user from libs/shared, the app's module replaces the lib's. Spread ...user.services into serve() to keep the lib's behaviour and add your own on top.lib/__lib/lib.service.ts exports one such entry for each model the app shares with a lib:apps/koyo/lib/user/user.service.ts
- The lib's methods come along. Everything the lib's
UserServicedefines is callable onthis. - Hooks stack instead of overriding. The lib's
_preCreateruns first, then yours, each receiving the previous result. BothonInithooks run too. - Your injections win a name clash. A key you declare replaces the lib's key of the same name.
- Keep app-only integrations here. Shared behaviour stays in the lib; what only this app needs, such as GitHub sign-in, goes in the app service.
Injection Builder
The function you pass to
serve() is the injection builder. It receives seven helpers (database, service, use, signal, plug, env, memory) and returns an object whose keys become properties on this:apps/koyo/lib/example/example.service.ts
- Injected values are read-only. Only
memory(…, { local: true })stays writable. - The key name is part of the wiring.
service()keys end inService,signal()keys end inSignal, anduse()keys match the name registered inlib/option.ts. - Reach for them in this order.
service()for another module,plug()for an adapter,use()only for a value registered inoption.ts, andenv()for configuration.
Injection Types
Pick the helper by where the value comes from:
HelperDescription
service<T>()
Another service, a lib's included. The key must end in
Service; the rest names the target.use<T>()
A value registered with
option.use() in lib/option.ts. The key must match its name.signal<T>()
A server signal, for queueing a background job or publishing an event. Key ends in
Signal.plug(Adaptor)
An
adapt() adapter. If an implementation was applied to that role, you get it instead.env(factory)
A value built at boot from the server env or
process.env. Pass a factory, not env("KEY").memory(ref, opts)
State kept in the cache adaptor, or on the instance with
local: true. See below.database()
This service's own model. A database service already has it as
<model>Model.use() and plug() in real code
The shared lib's file service reaches storage through
use() and IPFS through plug():libs/shared/lib/file/file.service.ts
env() feeding a hook
The factory receives the app's server env, typed as
ModulesOptions, and runs once at boot:apps/koyo/lib/devProject/devProject.service.ts
memory() in detail
memory(ref, opts) gives the service state that outlives one call. Without local, it lives in the app's cache adaptor. Its options:localbooleandefault false
Keep a plain writable value on this instance instead of in the cache; on a
Map, a real Map.default
What a single value reads before its first
set(), else null; a local one starts with it.of
The value type of a
Map memory, a scalar or model class. Required when ref is Map.ttlnumber (ms)
How long each write lives, unless that
set() passes its own { expireAt }.get(stored) => value
Maps the stored value (a Map's entry value) to what code reads. Give it with
set or not at all.set(value) => stored
The inverse of
get: turns what code writes back into the stored value.What
this.x turns out to be depends on how it was declared:Declared asWhat you get
memory(ref, { local: true })
A plain value you read and assign directly.
memory(ref)
An object with three async methods.
memory(Map, { of: ref })
An async key–value map.
All three shapes side by side:
apps/koyo/lib/_runtime/runtime.service.ts
- Store a model, not hand-made JSON.
memory(Map, { of: cnst.OauthClient })serializes through the constant; never encode JSON into aStringmemory yourself. - A memory belongs to the service or adaptor that declares it. Two services may both declare
token; each keeps its own value. - A
Mapread of a missing key isundefined.defaultapplies to a single value only, so guardget(key)with??. Map entries expire one by one, on SQLite and Redis alike. getandsetare not allowed withlocal. A local memory holds the value as it is.
Business Logic Flow
A service method should read like the business action it performs. It can load documents, call their chain methods, work with other services, write logs and queue signals, all in one place.
A like is recorded through another service, then counted by the model:
apps/koyo/lib/story/story.service.ts
A backup moves through several steps, and the slow part runs later as a queued job:
apps/koyo/lib/dbBackup/dbBackup.service.ts
- Load, save, then notify. Load every document the action needs, save, and only then call signals or other services.
- Write
return awaitat the end. Keep theawaiteven where a barereturnwould work. - Mark fire-and-forget with
void. When you deliberately do not wait for a call, writevoidin front of it so the missingawaitreads as intended. - Return
nullorfalsefor "not allowed" or "not found". The signal decides whether that is an error.
Lifecycle Hooks
Hooks run around the service's
create<Model>, update<Model> and remove<Model>, and once at boot and shutdown. Use one when a rule must always run; a one-off business action is a normal method.HookDescription
_preCreate(data)
Runs before
create<Model>. Return the data to create; you may change it._postCreate(doc)
Runs after the document is created. Return the document.
_preUpdate(id, data)
Runs before
update<Model>. Return the patch to apply._postUpdate(doc)
Runs after the update. Return the document.
_preRemove(id)
Runs before
remove<Model>. Check or clean up here; throw to stop the removal._postRemove(doc)
Runs after the soft remove. Return the document.
cascade
A cascade field removes its targets through their services, so their
_postRemove runs too.onInit()
Runs once at boot, after this service's injections are filled in.
onDestroy()
Runs once when the server shuts down.
- Only the service's own writes run these hooks.
create<Model>,update<Model>andremove<Model>go through them; a chain's.save()and the query-level writes do not. - Removal runs in a fixed order:
_preRemove, the soft remove,_postRemove, then cascades.
Here a backup refuses to start twice for the same branch, and a new backup queues its own archive job:
apps/koyo/lib/dbBackup/dbBackup.service.ts


Throw
Err, never new Error. A bare Error reaches the caller as a generic "Internal Server Error". Throw an Err keyed to the module's dictionary, and register the key there as an [en, ko] pair:apps/koyo/lib/dbBackup/dbBackup.dictionary.ts
Practical Rules
- Workflows go in the service. Anything that coordinates several models, services, signals or external APIs is a service method.
- One-document changes go on the document. Write a chain method, then call
.save()from the service when the change must persist. - Name injections by role. Service keys end in
Service, signal keys inSignal. - Wrap external packages in
srvkit/. Write new ones asadapt()classes and inject them withplug();use()is for values already registered inlib/option.ts. - Extend a lib service instead of copying it. Spread
...<model>.servicesfor shared behaviour and keep app-only integrations in the app service. - No circular dependencies. Two services cannot inject each other; move the shared operation into a smaller service or a
srvkit/helper. - Re-check ownership. Check that the caller owns the document even when a guard already gated the call; the two are independent gates.