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/client

akanjs/client is what route files and client code import from the framework: the route chain, navigation, class merging, auth helpers, device access and font declarations.
import { cn, page, router } from "akanjs/client";
pagelayoutrootLayout
The route chain: the one default export of every route file.
PageConfig
What .config() takes: transition, safe area, cache and SSR mode.
router
Moves between routes and adds the locale and basePath for you.
cn
Joins class names and resolves Tailwind conflicts.
ModelPropsModelsProps
Prop types for components that draw one record or a list.
usePagemsgErrfetchsig
Runtime proxies without your app's types. Import the typed ones from @apps/<app>/client.
getCookiegetAccountgetAuthToken
Read cookies, the auth token and the signed-in account.
setAuthinitAuthresetAuth
Save or clear the auth token in fetch, the cookie and storage at once.
resolveServerUrl
Puts a stored /api/… URL on the server's origin when a CSR page is served elsewhere, such as in a desktop app.
Device
Platform, safe area, keyboard, haptics and scroll of the device.
Font
The type of one font entry in rootLayout().fonts([...]).
resolveRouteModuleisRouteDefinition
Used by route loaders. App code never calls them.
  • Most of it runs on the server too. page(), cn, getCookie and getAccount work in server components; router.back(), setCookie and Device need the browser.
  • Typed helpers come from your app. usePage, fetch, msg, Err and sig here do not know your app's types. Import them from @apps/<app>/client to get your own dictionary keys and endpoints.

router

router moves between routes from stores, event handlers and utilities. Give it the app path: it adds the locale and basePath itself.
push(href, { scrollToTop })
Goes to a route and adds a history entry. Browser only; on the server, use redirect().
replace(href)
Goes to a route in place of the current history entry. Browser only; on the server, use redirect().
back()
Goes back one entry. Browser only.
backOrFallback(href?)
Goes back, or replaces with href when there is no history. Defaults to the index path.
refresh()
Renders the current route again. Browser only.
redirect(href, { method, status })
On the server, answers with a redirect (307 by default). In the browser, navigates.
notFound()
Shows the nearest layout's .notFound() view with a 404. Raised once the page has begun streaming, the view still shows but a browser keeps status 200; crawlers and ssr: "block" routes get the 404.
setLang(lang)
Switches the locale and stays on the same route. Browser only.
getPath()
The current route without the locale and basePath. Browser only.
getPrefixedPath(path)
Adds the locale and basePath to a path when the app has a basePath.
navigation()
The last push or replace as a promise. It rejects when the route refused to move.
A store action that opens the record it just created:
apps/myapp/lib/project/project.store.ts
  • Paths are app-internal. Write /profile, not /en/profile. A path that already starts with the locale is accepted too.
  • Redirect from a page. In a page's render, router.redirect("/signin") answers the request with a 307 redirect.

cn

cn is the only class-combining function. It joins class strings and resolves Tailwind conflicts, Akan's semantic tokens included.
A component that adds one conditional class and takes the caller's className last:
apps/myapp/ui/Chip.tsx
  • Only for a condition or a merge. A fixed class string stays a plain string. Reach for cn when a part is conditional or the caller passes className.
  • The caller goes last. The last class wins a conflict, so the incoming className can override the defaults.
  • Tokens resolve too. cn("bg-primary", "bg-open") keeps only bg-open, because the semantic color and radius tokens are registered.
  • No object syntax. Write cond && "x", not { x: cond }. clsx and a raw twMerge are not used.

ModelProps / ModelsProps

ModelProps types a Unit that draws one record: ModelProps<"user", cnst.LightUser> puts the record under user. ModelsProps types a component that draws a list.
ModelProps<"user", cnst.LightUser>
usercnst.LightUserrequired
The record, under the key named by the first type argument.
classNamestring
Classes from the caller.
hrefstring
Where the card links to.
onClick(model) => unknown
Called with the record when it is clicked.
sliceSliceMeta
The slice the record came from.
actionsDataAction[]
"edit", "view", "remove" or an element, shown as row actions.
columnsDataColumn[]
Columns to show when the record is drawn as a table row.
ModelsProps<cnst.LightUser>
initFetchInitForm
How to load the list: page, limit, sort, insight and so on.
queryQuerySetting
Which filter to list with, as { queryKey, args }.
sliceSliceMeta
The slice the list came from.
onClickItem(model) => unknown
Called with the clicked record.
classNamestring
Classes from the caller.
A Unit card that links to the record:
apps/myapp/lib/user/User.Unit.tsx
  • Units and Views take the model. They are server components, so a cnst model prop never has to cross to the browser.
  • Utils and Zones take an id. They are client components: take userId: string and read the model from the store. A current Zone types its init prop with ClientInit from akanjs/fetch.

page / layout / rootLayout

Every route file has exactly one export: a chain that starts with page(), layout() or rootLayout() and ends with .render(). Each route setting is one stage of that chain.
  • page() goes in a page file: <name>.tsx or _index.tsx.
  • layout() goes in a _layout.tsx.
  • rootLayout() goes in the root _layout.tsx of the app or of a basePath.
Stages by chain
Stage
page
layout
rootLayout
Every chain
.param(name, Type)
✓
✓
✓
One [name] segment of the path. A page declares every segment in its path.
.search(name, Type)
✓
✓
✓
One query key. Written as [Type], it is a list.
.config(options)
✓
✓
✓
A PageConfig: transition, safe area, cache and SSR mode.
.head(node | fn)
✓
✓
✓
The <title>, <meta> and <link> tags, written as JSX.
.loading(fn)
✓
✓
✓
Shown while render awaits. It gets path values but no search values.
.render(fn)
✓
✓
✓
The last stage. Draws the route from the typed arguments.
Page only
.prompt(name, desc)
✓
Publishes the screen as an MCP prompt.
Layouts only
.notFound(fn)
✓
✓
What the subtree shows when a page is not found.
.error(fn)
✓
✓
What the subtree shows when rendering throws.
Root layout only
.fonts(Font[])
✓
Fonts the build subsets and preloads.
.theme(name)
✓
The theme the page opens with.
.manifest(obj)
✓
The web app manifest.
.layoutStyle(…)
✓
Full-width web layout, or a centered phone column.
.reconnect(on?)
✓
The connection-lost overlay.
.wsConnect(on?)
✓
Opens the websocket when the page loads.
✓AvailableNot available
What render receives
Each declared argument arrives already typed:
Declared asArrives as
↳ Note
ID · Stringstring
Int · Floatnumber
Booleanboolean
DateDayjs
A day.js date.
cnst.TicketStatus"open" | …
An enumOf class arrives as its value union.
[String]string[]
Search only. Repeat the key: ?tags=a&tags=b.
A page that reads one path segment and two query keys:
apps/myapp/page/project/[projectId]/_index.tsx
  • Every search value is optional. A missing or unreadable one arrives as undefined. A path value that fails its type answers not-found.
  • lang is always there. Every route sits under the locale segment, so every stage receives lang as a string. Never declare it with .param().
  • Render is not a React component. Call usePage(), getSelf() and fetch.* inside it, and make it async only when it awaits.
  • Names are string literals. A [projectId] folder needs .param("projectId", ID) with that literal name; the same goes for .prompt("name", …).

layout / rootLayout Stages

layout() takes every page() stage except .prompt(), and its render also receives children. It may declare only the [x] segments it reads.
layout() adds
.notFound(fn)({ pathname, params, searchParams }) => node
What the subtree shows when a page is not found. Replaces the legacy NotFound export.
.error(fn)({ error, digest, pathname }) => node
What the subtree shows when rendering throws. Replaces the legacy Error export.
rootLayout() adds
These are app-wide settings, so only the root _layout.tsx of an app or a basePath sets them.
.fonts(fonts)Font[]
Fonts to subset and preload. Write the list inline; see Font / createFont below.
.theme(theme)"system" | "css" | string
system follows the OS, css sets no data-theme, and any other name is set as is.
.manifest(manifest)WebAppManifest
The PWA manifest, emitted as a data URL.
.layoutStyle(style)"web" | "mobile"default "web"
mobile centers the app in a column at most 600px wide, and fills a narrower screen.
.reconnect(on = true)booleandefault operationMode === "local"
Shows an overlay while the websocket is disconnected.
.wsConnect(on = true)booleandefault true
Connects the websocket on load. With false, call fetch.instance.connect() before subscribing.
An app's root layout. import "./styles.css"; stays its first line:
apps/myapp/page/_layout.tsx
A nested layout that reads one segment and draws a not-found screen for its subtree:
apps/myapp/page/org/[orgId]/_layout.tsx
  • A theme cookie wins. Once the user picks a theme, the theme cookie overrides .theme() on the next load.
  • No argument means on. .reconnect() and .wsConnect() with no argument are true.

PageConfig

PageConfig is the object .config() takes. It sets how a route enters, how much room it keeps for the device edges, and how the server sends it.
transition"none" | "fade" | "bottomUp" | "stack" | "scaleOut"default by platform
Enter animation. Nested routes use stack on iOS, scaleOut on Android, none elsewhere.
safeAreaboolean | "top" | "bottom" | { top, bottom, android }default on in the app, off on the web
Pads the page for the notch and the home bar.
topInsetnumber | booleandefault 0
Space kept for a fixed top bar, in px. true means 48.
bottomInsetnumber | booleandefault 0
Space kept for a fixed bottom bar, in px. true means 48.
gesturebooleandefault on for nested routes on iOS
Allows swipe-back.
cachebooleandefault true for top-level routes
In the app shell, keeps the page's last render to show again on return.
ssr"stream" | "block"default "stream"
stream sends the shell first; block waits for every section before the first byte.
topSafeAreaColorstringdefault background color
Color painted behind the top safe area.
bottomSafeAreaColorstringdefault background color
Color painted behind the bottom safe area.
devOnlybooleandefault false
Keeps the route out of akan build. It still serves under akan start.
A playground page that slides up, pads for the notch, and never ships to production:
apps/myapp/page/playground.tsx
  • Configs merge down the tree. A layout's config applies to every route under it, and the page's own value wins.
  • devOnly is a literal. Write true or false directly. On a _layout.tsx it drops every route under that folder.
  • block trades speed for a clean error page. With ssr: "block" the Loading fallback never reaches the browser. Use it only where SEO and first paint do not matter.
  • Crawlers always get the whole page. A search engine, an AI crawler or a link preview receives every section already in place, whatever ssr says: it runs no script to reveal a streamed one.

prompt

.prompt(name, description) publishes a page as an MCP prompt, so an agent can open the same screen a person sees. The description is the whole instruction the model gets, in English.
.prompt(name, …)
The prompt name: letters, digits, _ and -, up to 64 characters.
.prompt(…, description)
The whole instruction the model receives. English, and never empty.
.param(name, Type, { desc })
A required prompt argument. Give it a desc.
.search(name, Type, { desc })
An optional prompt argument. A list is typed comma-separated.
A ticket board published as a prompt:
apps/myapp/page/project/[projectId]/board.tsx
What an agent gets back
prompts/get runs the page body under the caller's token and renders nothing. What it answers depends on how the body went:
When
↳ prompts/get answers
The page renders
The description, one resource per fetch.* query, and a Tools for this screen: … line.
A required argument is missing
One message per argument, pointing at the <model>List… tool that finds the id.
Redirect or guard refusal, no token
A 401 challenge, so the client signs in first.
Redirect or guard refusal, with a token
This screen is not available to the signed-in account.
The page answers not-found
No screen exists for these arguments.
  • Data is masked. Each resource is masked by its endpoint's return model and addressed by an akan:// uri. One document travels once, however many queries read it.
  • Only lists are cut. They are trimmed to promptBudget characters, 60,000 by default. Change it with option.setMcp({ promptBudget }) or AKAN_MCP_PROMPT_BUDGET.
  • Tools are the screen's own. The last line names the published tools of the modules the page fetched from, limited to what this caller may see.

resolveRouteModule / isRouteDefinition

Every route loader reads route files through these two functions. App code never calls them; you need them only when you write a tool that loads route files itself.
resolveRouteModule(mod, key, { kind, pattern })
Unfolds a chain's default export into the named-export shape. A legacy module passes through.
isRouteDefinition(value)
True when the value is a page(), layout() or rootLayout() chain.
A script that loads one route file the way the server does:
apps/myapp/script/inspectRoute.ts
  • It returns both shapes. module is what loaders read; definition is set only for a chain module.
  • It checks the file. A named export beside the chain is always rejected. kind also rejects a chain in the wrong kind of file, and pattern rejects a .param() that does not match the path.

Font / createFont

Font is the type of one entry in rootLayout().fonts([...]). The build subsets each font, serves it from /_akan/fonts, and preloads it.
namestringrequired
Family name. It also names the --font-<name> variable and the font-<name> class.
paths{ src, weight, style? }[]required
One file per weight and style. src starts with / and is read from public/.
defaultboolean
Applies this font to the whole app. One font per root layout at most.
subsetsstring[]default ["latin"]
Character sets to keep, such as latin or ks-x-1001 for Korean.
subsetfalse
Skips subsetting. The file is only converted to woff2.
optimizebooleandefault true
false serves the file from src as is, with no build step and no preload.
preloadbooleandefault true
Adds a preload link for each optimized file.
display"auto" | "block" | "swap" | "fallback" | "optional"default "swap"
The CSS font-display value.
variablestringdefault --font-<name>
The CSS variable that holds the font family.
classNamestringdefault font-<name>
The class a default font puts on the app.
A Korean font in two weights, applied to the whole app:
apps/myapp/page/_layout.tsx
  • Paths are public URLs. /fonts/NotoSansKR-Regular.woff2 is read from apps/myapp/public/fonts/. A relative path such as ./font.woff2 is not found.
  • createFont is a shim. createFont and the named factories Noto_Sans_KR, Inter, Roboto and Nanum_Gothic_Coding return null. They keep old font-factory imports loading; they declare nothing.

usePage / msg / Err

Translation, toast messages and the error class. Import them from your app's @apps/<app>/client, where the keys are typed by your dictionary.
usePage()
Returns { l, lang, path }. Works in server components too.
l(key, params?)
Translates a dictionary key such as project.name.
l.trans({ en, ko })
Picks the text for the current locale, falling back to the default locale.
l.rich(key)
Renders a translation that contains HTML tags.
l._(key)
Same as l, without type-checking the key.
msg.success(key, { key, duration, data })
A toast from a dictionary key, 3 seconds by default. info, warning, error, loading too.
new Err(key, data?)
The translated error class. Keys look like <module>.error.<key>; the status is 400.
Err.BadRequestErr.UnauthorizedErr.ForbiddenErr.NotFoundErr.Conflict
Subclasses with their own status: 400, 401, 403, 404 and 409.
usePage() works in a server View, so translated text never needs a client component:
apps/myapp/lib/project/Project.View.tsx
In a store, msg reports a failed check and confirms success:
apps/myapp/lib/project/project.store.ts
  • Validation reports, never throws. On the client, call msg.error(key) and return early.
  • A store action does not catch. A failed fetch.* rejects with the server's Err, and the framework shows it as a toast.
  • One toast per key. option.key replaces an earlier toast with the same key, and data fills the translation's parameters.

fetch / sig

fetch calls the server's endpoints and slices; sig describes each model's signal so a store can be built from it. Import both from your app: @apps/<app>/client in UI, ../useClient inside lib/.
fetch.<endpoint>(...args)
Calls one generated or custom endpoint, such as fetch.user(id).
fetch.init<Model><Suffix>(...args)
Loads a slice's list and insight in a route, for a Zone's init prop.
fetch.view<Model>(id)fetch.edit<Model>(id)
Loads one record as { project, projectView } or { project, projectEdit }.
fetch.instance
The client itself, with setTimeout(ms) and connect().
sig.<model>
The model's slices and endpoints. store(sig.project, …) is built from it.
A store built from sig.project that archives a project and updates its list:
apps/myapp/lib/project/project.store.ts
  • Typed only in your app. The akanjs/client exports forward to the runtime your app client registered, without its types.
  • Load in the route. A client component reads st.use.* and writes st.do.*; the route loads data and passes it down as init or view.
  • Every call carries the token. After setAuth, each call sends the JWT.

getCookie / setCookie / getAccount / getAuthToken

Read cookies and the signed-in account from any component, server or browser. The auth token sits in a cookie named per app.
getCookie(key)
Reads a cookie, on the server and in the browser.
setCookie(key, value, options?)
Writes a cookie in the browser (path=/, SameSite=Lax, Secure; options overrides only the keys it names). Does nothing on the server.
removeCookie(key)
Deletes a cookie in the browser.
getHeader(key)
Reads a request header on the server. Empty in the browser.
getAuthToken()
The app's JWT from the cookie jar.
getStoredAuthToken()
The JWT from client storage: localStorage on the web, the native runtime's secure storage (iOS Keychain, Android Keystore) in the app.
authTokenKey()
The cookie name: jwt:<appName>.
getAccount<T>()
Decodes the JWT into the account. Another app's or environment's token reads as signed out.
libs/shared builds getSelf() on top of getAccount(). Trimmed, it reads:
libs/shared/webkit/cookie.ts
  • Why the key is per app. Cookies carry no port, so two apps on one host would share a single jwt cookie. Read it with getAuthToken(), never by name.
  • getAccount reads what the server honors. It decodes the app's cookie or an Authorization: Bearer header, the same two credentials the server accepts.

setAuth / initAuth / resetAuth

These three keep fetch, the cookie and client storage holding the same token. Call setAuth after sign-in; the framework already calls initAuth at startup.
setAuth({ jwt })
Gives fetch the token and saves it to the cookie and client storage.
initAuth({ jwt? })
Restores the token from ?jwt= or the cookie at startup. Ignores another app's token.
resetAuth()
Clears the token from fetch, the cookie and client storage, for a session you drop entirely.
Refreshing the token in libs/shared is one call to the server and one setAuth:
libs/shared/ui/Auth/tokenRefresh.util.ts
  • Sign-out swaps the token too. The libs/shared stores call setAuth after sign-in, and after sign-out with the token fetch.signoutUser() returns.
  • A foreign token is dropped. initAuth ignores a token minted for another app or environment, and clears it when it came from the cookie.

Device

Device wraps what the native runtime exposes on a phone: platform, safe area, keyboard, haptics and scroll. The framework loads it once in the browser; read it with Device.getDevice().
Device.getDevice()
Returns the loaded device. Throws before the framework has loaded it.
info.platform
"ios", "android" or "web".
lang
The locale the app opened with.
topSafeAreabottomSafeArea
Notch and home-bar insets in px. 0 on the web.
isMobile
True on a touch device or a mobile browser. isMobileDevice() is the same check.
vibrate(type?)
Haptic feedback: "light", "medium" (default), "heavy", or a duration in ms.
showKeyboard()hideKeyboard()
Opens or closes the native keyboard.
listenKeyboardChanged(fn)unlistenKeyboardChanged()
Reports the keyboard height as it opens and closes.
getScrollTop()setScrollTop(y)
Reads or sets the page's scroll position.
A button that vibrates lightly before it acts:
apps/myapp/ui/HapticButton.tsx
  • Browser only. On the server and before boot, Device.getDevice() throws. Call it from an event handler or an effect.
  • The web is a safe no-op. On the web, keyboard and haptics do nothing and the insets are 0, so no platform check is needed.

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