Providers
Some capabilities need a shared service — a storage client, an HTTP client, a router, a logger, the current user, a permission checker. Providers are how Kitsune exposes those without global imports or hidden singletons.
A provider is registered against a token. Anyone can request the value by token.
import { createProviderToken } from '@atheory-ai/kitsune-core'
export type Logger = { info(message: string, detail?: unknown): void error(message: string, detail?: unknown): void}
export const LoggerToken = createProviderToken<Logger>('logger')A token is a { id: symbol, description: string } pair. The symbol is unique per token; the description is for debugging.
Registering a provider
Section titled “Registering a provider”Inside a module’s setup:
import { defineKitModule } from '@atheory-ai/kitsune-core'import { LoggerToken } from './tokens'
export const loggerModule = defineKitModule({ name: 'logger', setup(runtime) { runtime.provide(LoggerToken, { info: (msg, d) => console.info(msg, d), error: (msg, d) => console.error(msg, d), }) },})Reading a provider
Section titled “Reading a provider”Anywhere that has a runtime reference:
const logger = runtime.inject(LoggerToken)logger?.info('boot')inject returns T | undefined. A missing provider is not an error — it’s a signal that the capability isn’t installed. Code that depends on a service should handle the missing case explicitly.
In React:
import { useKitRuntime } from '@atheory-ai/kitsune-react'
function useLogger() { const runtime = useKitRuntime() return runtime.inject(LoggerToken)}Why tokens, not strings
Section titled “Why tokens, not strings”Token symbols are unique by reference. Two tokens both named 'logger' from different modules don’t collide. This means you can publish a token from a package and consumers know they’re requesting that logger, not any logger.
Type-safe too: inject<T>(ProviderToken<T>) returns the right type without a cast.
Providers vs commands vs events
Section titled “Providers vs commands vs events”You’ll occasionally have to choose between exposing a capability as a provider, a command, or via events. Use this guide:
| Use a provider when… | Use a command when… | Use an event when… |
|---|---|---|
| The consumer needs a stable reference to a service. | One-off, imperative request with a result. | Something happened that many things may care about. |
| The consumer calls multiple methods on it (e.g., a storage client). | The behavior fits a single verb (e.g., notification.show). | The fact has independent observers (analytics, audit). |
| The consumer needs synchronous access. | Async-friendly. | Async-friendly. |
A common pattern: a module registers a provider (for services) and handles commands (for one-shot verbs against that service):
export const storageModule = defineKitModule({ name: 'storage', setup(runtime) { const store = createStorageService() runtime.provide(StorageToken, store)
runtime.handleCommand('storage.set', (cmd) => { store.set(String(cmd.payload?.key), cmd.payload?.value) return { ok: true } })
runtime.handleCommand('storage.get', (cmd) => { return { value: store.get(String(cmd.payload?.key)) } }) },})Now a module can either inject StorageToken for direct access, or dispatch storage.set / storage.get for command-based interaction. Both work; pick whichever fits the call site.
Lifetimes
Section titled “Lifetimes”Providers live as long as the runtime. Set during setup, available until stop. There is no “request-scoped” provider in v1 — those would be a future concept (per-route, per-form, per-boundary scopes).
Overriding for tests
Section titled “Overriding for tests”Tests can override providers by installing a test module last:
const fakeLogger = { info: vi.fn(), error: vi.fn() }const fakeLoggerModule = defineKitModule({ name: 'logger.fake', setup(runtime) { runtime.provide(LoggerToken, fakeLogger) },})
await runtime.install(loggerModule) // productionawait runtime.install(fakeLoggerModule) // overridesThe runtime emits a provider.overwritten diagnostic so you can see when this happens.
Anti-patterns
Section titled “Anti-patterns”- A token in the same file as the consumer. If only one place uses the token, you don’t need a token. Just import the value.
- A token for ephemeral state. Tokens are for services and long-lived values. For ephemeral state, use module-local closures or events.
- A provider with side effects in its constructor. Providers should be cheap to access. Do work in commands, not in
injectresults.