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▾
akanjs/constant
akanjs/constant is Akan's schema layer. Every .constant.ts file is built from two of its exports: via, which declares a class, and field, the builder via hands you.import { via } from "akanjs/constant";Everything else supports those two. From
getDefault on, the entries are helpers you mostly read rather than call.ExportDescription
Declares every constant class. What you pass decides which class it is.
The builder
via hands you. Each call declares one stored field.field.visualfield.hiddenfield.secret
Variants of
field that keep a value away from agents, the client, or default reads.Declares a field the server computes for each response instead of storing it.
Builds the blank object a new record or form starts from.
DocumentModelDefaultOfQueryOfPurifiedModelProtoFileProtoLightFile
Types for the stored shape, the default shape, a query, a purified value, and a file.
crystalizemakePurify
Turn raw data into model values, and a model back into a checked plain object.
serializedeserialize
Convert values to and from the payload that crosses a boundary.
Finds model classes, refNames and enums at runtime.
Words Used on This Page
TermDescription
refName
The camelCase name a module is registered under, such as
banner.scalar
A value object with no table of its own. Other documents embed it whole.
relation
A field whose type is another database model. It stores that document's id.
projection
A read option that names the fields to load, such as
{ password: true }.The Five Classes of a Module
A database module declares these five classes, always in this order. The banner module is the example throughout this page:
ClassDescription
BannerInput
The fields a caller sends to create or update a banner.
BannerObject
The Input plus fields the server keeps, and
id, createdAt, updatedAt, removedAt.LightBanner
Only the fields a list needs. Display and predicate methods live here.
Banner
The full model: every field, plus the ones
resolve computes.BannerInsight
Numbers about a whole list. It starts with
count and is written even when empty.- The order is fixed.
enumOfclasses go on top, then Input, Object, Light, full and Insight. Write the Insight class even when it is empty. - Shared logic goes on Light. The server and the client both hold the Light class, so
isNew()orcanWrite(user)belongs there, not in a util module. - A scalar is a single class.
via((field) => ({ … }))alone declares it. It lives underlib/__scalar/<name>/.
via
via is one overloaded function, not a namespace: there is no via.model or via.scalar. What you pass decides which class you get.| Arguments |
|---|
| ↳ Declares |
| (field) => ({ … }) |
BannerInput, or a scalar. A lone builder callback is either one. |
| BannerInput, (field) => ({ … }) |
BannerObject: the Input's fields plus the ones you add. |
| BannerObject, ["title", …] as const, (resolve) => ({ … }) |
LightBanner: only the named fields, plus any resolve fields. |
| BannerObject, LightBanner, (resolve) => ({ … }) |
Banner: Object and Light merged, plus any resolve fields. |
| Banner, (field) => ({ … }) |
BannerInsight: count plus the fields you add. |
The banner module from
libs/shared, shortened:libs/shared/lib/banner/banner.constant.ts
- The callback's parameter names the builder. Input, Object and Insight get
field. Light and full getresolve, because they only add computed fields. - Write the Light tuple
as const. The field names Light picks are always a literal tuple. - Pass a lib's class last to extend it. Every form takes more classes after its own arguments, so an app adds fields to a lib's model by passing the lib's class at the end.
field
field is the builder via hands to Input, Object and Insight callbacks. Each call declares one stored field: the type first, the options second.Types
| Write |
|---|
| ↳ Declares |
| field(String) |
One value: String, Boolean, Date, ID, Int, Float or Any. |
| field([String]) |
An array. Brackets nest up to three deep, as in [[Float]]. |
| field(File) |
| A relation to another model's document, stored as its id. |
| field(Coordinate) |
| A scalar, embedded whole inside this document. |
| field(ProductStatus) |
An enum class declared with enumOf. |
| field(Map, { of: String }) |
A map with string keys. of names the value type and is required. |
| field<T>(Any) |
| An open value. The type argument keeps it typed in TypeScript. |
- A number is
IntorFloat.field(Number)does not typecheck. - Bytes are never a field.
BinaryandUploadbelong to signals; store a file asfield(File).
A product input that uses most of them:
apps/myapp/lib/product/product.constant.ts
Options
The second argument is an options object.
nullable, select, enum and meta are not options: .optional(), field.secret, the enumOf type and .meta() set them.Value and Checks
defaultT | ((doc: { id: string }) => T)
The starting value. A function runs again for every record.
validate(value, model) => boolean
Your own check, run by
purify and on every document save. false rejects the value.immutablebooleandefault false
Changing it in a document save throws. Query-level writes skip the check.
of
The value type of a
Map field, such as String or a scalar. Required for Map.visualbooleandefault false
The same as declaring it with
field.visual.accumulatequery object
Insight fields only: the condition this counter counts.
{} counts every match.Search and Relations
text"title" | "desc" | "tag" | "thumb" | "filter"
Adds the field to full-text search in that role.
thumb is kept for display, never matched.cascade"removeRef" | "removeWith" | "removeWithAny"
Removes related documents together. The value says which side follows which.
refstring
The refName an
ID field points at, as in { ref: "org", cascade: "removeWith" }.refPathstring
The field naming which model the id points at: an
enumOf, or a String with removeWithAny.textneeds a string.title,descandtagtakeString;thumbandfilteralso take anIDor a relation. AMapor a nested array takes no role.cascadenames a direction.removeRefgoes on the owner's relation,removeWithon the child's reference to its owner. The wrong one removes the wrong documents.
Docs and Samples Only
These describe the field for the schema docs, the API explorer and
sampleOf(). Nothing enforces them, so put a rule that must hold in validate.minnumber
A lower bound shown in the schema docs.
sampleOf() uses it as the sample.maxnumber
An upper bound, used the same way as
min.minlengthnumber
A shortest length for the schema docs. On an array,
purify does check the item count.maxlengthnumber
A longest length, handled the same way as
minlength.type"email" | "password" | "url"
Makes
sampleOf() produce a realistic email, password or URL.exampleT
A sample value for the schema docs and the API explorer's example requests.
refType"child" | "parent" | "relation"
A label the schema docs show on a relation.
Chained Methods
MethodDescription
.optional()
Allows
null. Without a default, the field starts at null..meta(obj)
Attaches free-form metadata. A summary counter uses it to name the list it counts.


Give a per-record default as a function.
default: dayjs() freezes the moment the module loaded, and a literal {} is one object every record shares. Write () => dayjs() and () => ({}).field.visual / field.hidden / field.secret
Three variants of
field. All three are stored like any other field; they differ only in where the value can go afterwards. Below, Read is a server read with no projection, and Draft is the saved form draft.Field
Read
Page
Agent
MCP
Draft
Search
Sent to the page
field
✓
✓
✓
✓
✓
An ordinary field. Every side reads it.
field.visual
✓
✓
✓
✓
Drawn on the page, stripped from everything an agent reads.
Kept on the server
field.hidden
✓
Server code reads it. The client gets
null.field.secret
Read only through a projection that names it.
✓The value can reach itNever reaches it
A profile with one of each:
apps/myapp/lib/profile/profile.constant.ts
- visual is about cost, not secrecy. A blur placeholder or a rendered HTML body is data the screen needs but no question is answered from. Storage, search, forms and the page are untouched.
- hidden still claims to be a string. Its type says
string, unlike secret'sstring | null, yet the client readsnull. - secret is not even loaded. Only a projection that names it reads it, as in
pickById(id, { password: true }), and then only the named fields come back. The endpoint response leaves it out even then. - Neither takes a
textrole. The search index is plaintext, sotexton hidden or secret is a type error.


Guard hidden and secret values with
?? or == null. On the client the key is present and null, so === undefined, a destructuring default and an optional-parameter default all miss it.resolve
resolve is the builder Light and full callbacks receive. It declares a field that is never stored: the server computes it each time it builds a response.Declare it on the model:
apps/myapp/lib/order/order.constant.ts
Then compute it with a
resolveField of the same name in the module's Internal class. It receives the stored order, here one with unitPrice and quantity fields:apps/myapp/lib/order/order.signal.ts
- Every
resolvefield needs itsresolveField. The Internal's type lists each one, so leaving one out fails the typecheck. - Optional on both sides.
resolve(Int).optional()pairs withresolveField(Int, { nullable: true }). thisreaches the services. Writeexecwith afunction, as in an endpoint, and callthis.orderServicewhen the value needs a lookup.- No
textrole. A computed value is never in the search index, soresolvedoes not accept one.
getDefault
getDefault builds the blank object a new record or form starts from. You usually call it through the model as Model.getDefault().| Field |
|---|
| ↳ Starts at |
field.hidden · field.secret |
null, always. |
default: () => … |
| What the function returns, run again on each call. |
| An array field |
A fresh copy of its default, or [] without one. |
Any other default |
| That value itself, shared by every object built from it. |
.optional() with no default |
null |
| An embedded scalar |
| That scalar's own default object. |
| A relation |
null |
| Any other type |
The type's empty value, such as "", 0 or false. |
Both ways of calling it, in a test:
libs/shared/lib/banner/banner.test.ts
Model.getDefault()is built once. The first call builds the object and later calls return a shallow copy, so a() => dayjs()default keeps its first value there.- The field map runs every function again.
getDefault(Model[FIELD_META])calls eachdefaultfunction on every call.
DocumentModel / DefaultOf / QueryOf
Type helpers that documents, stores and tests use to name a model's other shapes. Import them as types only:
import type { DefaultOf, DocumentModel, PurifiedModel, QueryOf } from "akanjs/constant";TypeDescription
DocumentModel<T>
The stored shape. Relations become id strings, and a list of them
string[].DefaultOf<T>
What
getDefault() returns. Methods are dropped, and relation fields may be null.QueryOf<T>
An opaque query descriptor, typed
any. A slice's exec returns one.PurifiedModel<T>
What
purify returns. Relations become ids; dates keep the Dayjs type.ProtoFileProtoLightFile
The shape of a
File and a LightFile, for UI code that takes a file prop.

A
QueryOf does not chain. You cannot call .sort() or .limit() on what a slice returns. Pass { sort, page, limit } to the store's init fetch instead.crystalize / purify
These two move a value between raw data and a model instance, in opposite directions. You reach them through the model rather than by name.
crystalize — raw to model
Converts one field's raw value: a date string to
Dayjs, a nested object to its class. The constructor and set() use the same converters.crystalize(field.getProps(), value)purify — model to plain
Checks every field (required, enum,
validate, array length) and turns relations into ids. Returns null when a check fails.Model.purify(value) · makePurify(Model)In practice you build with the constructor and check with the model's purify:
libs/shared/lib/banner/banner.test.ts
purifyis not a named export. Every class carries it as the staticModel.purify;makePurify(Model)builds the same function.- An empty required string fails. A required
StringorIDleft at""does not pass, which is why the firstpurifyabove isnull. - The store runs it before it sends. A generated create or update action purifies the form with the Input class and sends nothing when the result is
null.


Copy a model with
new cnst.X().set(x), never a spread. Date fields are accessors on the prototype, so { ...banner } and Object.keys(banner) leave them out.serialize / deserialize
These convert between runtime values and the payload that crosses a document or transport boundary.
serialize — runtime to payload
Walks the model's fields:
Dayjs becomes Date, and a Map becomes a plain object.deserialize — payload to runtime
Parses each primitive, so a date string becomes
Dayjs, and goes into embedded scalars.CallDescription
serialize(ref, arrDepth, value, type?, opts)
Default
type is "object"; "input" sends relations as ids. opts is { nullable?, key? }.deserialize(ref, arrDepth, value, opts)
opts is { nullable?, key?, enum?, convertFn? }. With enum, a value outside it throws.ConstantRegistry.serializeConstantRegistry.deserialize
The short form,
(ref, value, nullable?). Takes a primitive, Map or model; [Ref] for a list.A date on its way out and back:
libs/shared/lib/banner/banner.test.ts
- To
deserialize, a database model is a relation. Only primitives and scalars are converted; a model's value comes back as given. Build the instance withnew cnst.X(value). - A missing required value throws. Both refuse
nullandundefinedunlessnullableis set or the type isAny.
ConstantRegistry
ConstantRegistry ties model classes to their refName at runtime. Every module's classes, its scalars and its enums are registered here.import { ConstantRegistry } from "akanjs/constant";Static methodDescription
getRefName(Model)
The model's refName. Throws for an unknown class unless
{ allowEmpty: true } is given.getModelName(Model)
The class name for its role, such as
BannerInput or LightBanner.getModelRef(refName, modelType?)
The class for a refName and role. With no role it finds a primitive such as
"Int".getDatabase(refName)getScalar(refName)
A module's registered entry: its five classes, or a scalar's one. Throws unless
allowEmpty.has(Model)
Whether the class is registered.
isFullisLightisObjectisInsightisScalar
Which role a class plays.
serializedeserialize
The short forms from the
serialize / deserialize section above.