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▾
Common Utility Overview
common/ holds code that behaves the same on the server and in the browser: small, pure helpers that need no runtime of their own. A service, a signal, a store, a page and a *.constant.ts file can all import the same helper.Reach for it when both sides need the same formatting, validation, metadata or transform, so the two never disagree.
Which folder?
Code outside
lib/ goes in one of five folders. Choose by what the code touches, not what it is for; the srvkit/ and webkit/ pages open with this same table.FolderDescription
common/
Pure, isomorphic, zero-dependency; imports only sibling common/* and akanjs/base, not Err.
webkit/
Touches window, navigator or the native bridge (akanjs/client/native), or is a React hook.
srvkit/
Touches node:*, Bun, process.env, a secret, or a server SDK.
ui/
Renders JSX or defines a recipe, bound to no model; a model-bound component goes in its module.
plugin/
A build-time or CLI-time AkanPlugin, registered in akan.config.ts.
What Belongs In common/
Five kinds of helper usually live here. Each needs nothing but its arguments, so it gives the same answer on either side:
KindDescription
Formatter
Formatting that a service's output and the UI share, such as bytes, money or short labels.
Validator
A validation or predicate that must give the same answer on the server and in the browser.
Random and string utility
A small generic helper: random codes, padding, shuffling or a short string transform.
Metadata builder
A small object or builder that describes a query, filter or display without running it.
Content transform
A pure transform of stored content, such as rich-editor JSON into plain text.
- Two are hypothetical, three are real.
formatBytesandisWebUrlsit in a sample app; the other three are files in this workspace. - A constant file may import
common/, neverui/,webkit/orsrvkit/.summary.constant.tsis loaded on both sides, so thegetQueryMetabuilder it calls has to live incommon/.
Barrel And File Shape
Like
ui/, webkit/ and srvkit/, common/ is a barrel folder: its index.ts re-exports every helper in it. Its one-line entries look like this:libs/util/common/index.ts
So a caller imports the folder, never a single file —
@libs/<lib>/common or @apps/<app>/common:libs/shared/lib/user/user.service.ts
- One file, one export.
randomCode.tsexportsrandomCode, so a helper is found by its name. - Only camelCase file names reach the barrel. A dotted name such as
queryMeta.helper.ts, and every test file, stays private to the folder. - Siblings import each other by relative path.
randomCode.tsimports./pad, not its own barrel. index.tsis generated. Add, rename or delete a helper file; never edit the index by hand.
Using It On Both Sides
A common helper runs wherever it is imported: in Bun for a service, in the browser for a store or a client component. So it may use only what both sides have:
What the helper uses
common/
webkit/
srvkit/
Both sides have it
./<sibling> · akanjs/base
✓
A sibling file and
akanjs/base are the only value imports a common file makes.URL · Intl · Math · JSON
✓
Standard JavaScript built-ins exist in Bun and in every browser.
import type
✓
Erased before bundling, so the type may come from any package.
Only the browser has it
window · document · navigator
✓
Browser globals do not exist on the server.
akanjs/client/native · React hook
✓
The native-app bridge is browser-only, and a React hook needs a client component.
Only the server has it
node:* · fs · Bun
✓
Server runtime APIs that a browser bundle cannot load.
process.env · secret
✓
Server settings and secrets must never reach the browser bundle.
server SDK
✓
A vendor client for payment, mail or storage.
✓Put it hereNot here
One helper, both sides
withRedirectQuery adds query params to a redirect URL that may already carry some. It needs only URLSearchParams and string methods:libs/shared/common/redirectQuery.ts
The user module in
libs/shared/lib/user/ calls it on both sides, along with two @libs/util/common helpers:| File | Runs on |
|---|---|
| ↳ Call | |
| user.service.ts | Server |
withRedirectQuery(signupRedirect, { userId: user.id }) | |
| user.service.ts | Server |
randomCode(6) | |
| user.store.ts | Browser |
router.push(withRedirectQuery(redirect, { userId })) | |
| User.Util.tsx | Browser |
pad(phoneCodeRemain.minute, 2) | |
- One import for both. The service and the store import the same name from
@libs/shared/common; nothing changes per side. - The two sides cannot drift. The service builds the signup redirect and the store builds the next step's URL with one function, so they agree on the query format.
Practical Rules
Where a helper goes
- Both sides need it →
common/. Service or signal code and page or component code run the same logic. - Needs a server-only API →
srvkit/. - Needs a browser-only API →
webkit/. - Never beside a model, never in
base/. A helper file does not go insidelib/<model>/, and there is nobase/folder; shared utilities go in that app's or lib's owncommon/.
Inside a common file
- Small, pure, imported from the barrel. One job per helper, no side effects, and callers use the folder path.
- Return, do not throw.
Errcannot be imported intocommon/, so return a sentinel such asnullorfalseand let the service or store decide. - Write
// FIXME:, not//!.common/ships to the browser, and a//!comment survives minification.



A common file imports neither side. Lint rejects a value import from the client side (a store, a module component,
ui/, webkit/, akanjs/client) and from the server side (a service, document, signal, dictionary, srvkit/, akanjs/server). import type is erased before bundling, so a type from either side stays legal.