Skip to content

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.

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),
})
},
})

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)
}

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.

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.

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).

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) // production
await runtime.install(fakeLoggerModule) // overrides

The runtime emits a provider.overwritten diagnostic so you can see when this happens.

  • 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 inject results.