Server Utility Overview

srvkit/ holds server-only code that services, signals and server jobs call. Keeping it here lets the module files stay focused on business behavior.
It is also the one safe door for external libraries: vendor SDKs and low-level server APIs pass through srvkit first.
Which folder?
Code outside lib/ goes in one of five folders. Choose by what the code touches, not what it is for; the common/ and webkit/ pages open with this same table.
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 srvkit/

srvkit/ holds seven kinds of code. Four step into the request path, and three are tools a service calls:
Guard
Decides whether a request may run an endpoint: sign-in, role or ownership checks.
InternalArg
Reads a trusted value, such as the caller's account, and hands it to exec as an argument.
Middleware
Wraps every signal call and attaches server context before the endpoint's guards run.
WebProxy
Runs before a page load is routed, to redirect, rewrite, or add headers.
Server helper
A reusable function for hashing, encryption, file handling, image inspection or tokens.
Adaptor
A singleton adapt() class wrapping storage, queues, email, payment or a vendor API.
Class utility
Legacy: a server-only class, such as an SDK client, injected through option.ts.
Where the request-path four run
A page load and a signal call take different paths, and each piece sits on only one of them:
Piece
Page load
Signal call
Server level: registered once in option.ts
WebProxy
✓
Redirects, rewrites or adds headers before the page is chosen.
Middleware
✓
Attaches server context, such as the account, before guards run.
Signal level: named on each endpoint or slice
Guard
✓
Allows or refuses the call.
InternalArg
✓
Hands a server-made value to exec as an argument.
✓Runs hereNot here

Server Level: WebProxy And Middleware

Both are registered once in the option chain and apply to every request of their kind. A WebProxy acts on page loads before routing; a Middleware wraps every signal call.
WebProxy
Page load
WebProxyredirect · rewrite · headers
Page render
A WebProxy is a class with one use(request) method. This one sends a retired URL to its new page:
apps/koyo/srvkit/legacyPageRedirect.ts
What use() returns decides what happens next:
Return value
↳ What happens
Response
Sent as is; later proxies and the page do not run.
AkanResponse.redirect(url, status?)
A redirect Response; the status defaults to 307.
AkanResponse.next({ request })
Continues with the request headers you changed.
AkanResponse.rewrite(url)
Serves another path while the address bar stays the same.
undefined
Passes the request on unchanged.
  • Page loads only. API routes, the websocket and /_akan/* paths never pass through a WebProxy. Static files (a path with an extension) skip it too, unless a matcher names them.
  • A client-side navigation skips it. A <Link> to /ko/old-docs or a router.push there is not redirected and renders /ko/old-docs itself, usually a 404; the built-in locale and basePath handling still applies. Link to the new page directly, and keep access control out of proxies: guards on the endpoints, and getSelf({ unauthorize }) in a _layout.tsx.
  • The locale redirect runs first. The built-in proxies run before yours and turn /old-docs into /ko/old-docs, so match the path with its locale segment.
  • Narrow it with a matcher. In applyWebProxy({ proxy, matcher }), the matcher is a path prefix, a RegExp, or a function of the request.
Middleware
Signal call
Middlewareattach context
Signal endpointGuard → exec
This Middleware resolves the caller and stores it where guards and InternalArgs read it:
apps/koyo/srvkit/requestUserMiddleware.ts
  • use(env) runs once per process. It receives the server options; only the function it returns runs on every call.
  • Branch on transport here, and only here. Middleware writes account onto the HTTP request or the socket data, and every guard and InternalArg reads it back with context.get("account").
  • Mounting libs/shared already does this. Its AccountMiddleware turns the JWT into account, so most apps never write their own.
  • refName is the registration key. Two middlewares with the same refName replace each other. For a single endpoint, use the middlewares signal option instead.
Register them in option.ts
Once both are declared in srvkit, register them in the app's or library's option chain:
apps/koyo/lib/option.ts
  • Several at once, in order. applyMiddleware and applyWebProxy each take several classes, and proxies run in the order you list them.
  • Libraries bring their own. An app runs the middleware and proxies of every library it mounts, and its own option.ts is applied last.

Signal Level: Guard And InternalArg

Guards and internal args are named per endpoint or slice. A Guard decides whether the call may run; an InternalArg turns trusted server context into an exec argument.
After Middleware prepares the context
Middleware
Guardallow or refuse
InternalArgbuild exec args
Signal exec
Service logic
Guard
A Guard is a class with canPass(context). SignedIn reads only the caller; CanCancelOrder also needs the call's arguments:
apps/koyo/srvkit/guards.ts
That difference is what static scope declares. It has no default, so every guard states it:
scopeReads
↳ In an MCP listing
"account"The caller only
An MCP listing runs it with no arguments and hides what this caller certainly cannot use.
"resource"The call's arguments too
An MCP listing skips it, so the entry stays visible and is stopped at call time.
  • Keep static name. fetch serializes guard names and the API explorer filters on them.
  • Read the caller with context.get("account"). Guards also run on websocket calls, and a pubsub room re-runs them whenever the socket's credential changes, so never branch on getHttpContext().
  • Side-effect free, and fail closed. A guard must be safe to re-run. No resource named means false; a load that throws means logger.warn, then false.
  • Every guard must pass. The guards array is checked in order, and the first refusal answers 403.
InternalArg
An InternalArg reads the request context and hands a server-made value to exec, so business logic gets it without asking the client:
apps/koyo/srvkit/internalArgs.ts
Guards and internal args meet in the signal file. Guards go in the option, and .with() appends an InternalArg after the declared params:
apps/koyo/lib/order/order.signal.ts
  • Arguments arrive in order. exec receives the .param() values first, then each .with() value.
  • null refuses the call. An InternalArg that returns null answers 401, unless you write .with(CurrentUserId, { nullable: true }) and let exec receive null.
  • Never trust a client-supplied id. Take the acting user from an InternalArg, not from a .param().
Before writing your own, check the ones that already ship:
ReqResIpWs
From akanjs/signal: raw request, response, caller IP, and the socket with its socketId.
AccountSelfMeAgentCall
From @libs/shared/srvkit: account, signed-in user, admin, and whether a model is calling.

Service Logic And External Libraries

When a service needs crypto, an AI SDK, an HTTP client or another server-only package, wrap it in srvkit first. How the service then reaches it depends on what you wrapped:
Function helper
Imported straight from the srvkit barrel.
Singleton adaptor
plug(Class) in the service, with nothing in option.ts.
Class instance (legacy)
Built in option.ts .use(), then injected with use<T>().
Function helper
A function helper needs no wiring. This one uses node:crypto, which a service may not import itself:
apps/koyo/srvkit/createOrderHash.ts
Class instance: the legacy shape
It takes three files. First, a plain class in srvkit:
apps/koyo/srvkit/emailClient.ts
Next, build one instance in option.ts under a key:
apps/koyo/lib/option.ts
Last, a use<T>() field with the same name in the service:
apps/koyo/lib/order/order.service.ts
  • The field name is the key. use<T>() looks the value up by the service field name, so emailClient must match the key in .use().
  • A function helper skips all of this. The same service imports createOrderHash straight from @apps/koyo/srvkit.

Adaptor And plug

An adaptor is a singleton adapt() class that turns an outside system into a service dependency. Declare it in srvkit and plug() it where it is needed; it registers itself, so option.ts needs no entry.
apps/koyo/srvkit/paymentApi.ts
The service plugs it by class:
apps/koyo/lib/order/order.service.ts
The adapt() builder hands you four injectors; destructure only the ones you use:
use<T>()
A value option.ts provides under the same key: the legacy path.
env(fn)
A value read from the server options once, when the server starts.
plug(Class)
Another adaptor, or a built-in role such as StorageAdaptorRole.
memory(Type, { of })
A value this adaptor keeps in the cache adaptor; local: true keeps it in-process instead.
  • One per process. adapt() is for singletons; a value object you create per use stays a plain class you new.
  • Adaptors can plug adaptors, as long as the plugs never form a cycle.
  • Logger and lifecycle come built in. Use this.logger instead of a new Logger, and put startup work in override async onInit().
  • Route remote calls through one #api() with signal: AbortSignal.timeout(20_000), so no request hangs forever.

Practical Rules

Where the code goes
  • Move noisy server code out. Put server-only helper code in srvkit when a service or signal would otherwise become noisy.
  • External libraries enter through srvkit. Wrap them here before any convention file uses them.
  • Guard to protect, InternalArg to supply. Guards protect requests; internal args provide context-derived signal arguments.
  • adapt and plug for shared systems. Use them when a service needs a reusable external system dependency.
  • App or library. App-specific integrations go in the app's srvkit; reusable ones go in a library's srvkit.
Inside a srvkit file
  • camelCase file, PascalCase class. paymentApi.ts exports PaymentApi.
  • Server only, in both directions. Client files (ui/, webkit/, *.store.ts, every .tsx) cannot import srvkit, and srvkit cannot import a store, ui/, webkit/ or the st barrel.
  • Throw Err, never Error. Import Err from ../lib/dict; an adaptor that catches logs with logger.error and returns null.
  • Resolve secrets inside a function. Write process.env.X ?? options.x in the function that needs it, never at module scope.
  • #private is the house style here. Its lint ban covers only constant, document, service and store files.

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