Add your own code
Add your own state, services and drivers in three steps: a provider in your project, a swapped driver, a package of your own.
You add your own code in three levels, and you start at the first.
| Level | You want | You write |
|---|---|---|
| 1. Your project | A store, a client, a feature flag service | A provider and a config file |
| 2. A driver | Different storage, vault or notifications on one shell | A driver and one register call |
| 3. A package | The same thing in many projects | A package with definePackage |
The code in levels 1 and 2 is from examples/everywhere.
Your own code in one project
A provider puts a service in the app. Pages read it with useService().
Generate the provider
kitr make:provider flagsFirst time? Run npm i -g kitr once. Inside a project, the global kitr runs your project's own version.
This writes app/providers/flags-provider.ts and lists it in kitrrc.ts.
Fill it in
// app/providers/flags-provider.ts
import { defineProvider } from 'kitr/app'
export interface FlagsConfig {
flags: Record<string, boolean>
}
export class Flags {
constructor(private readonly values: Record<string, boolean>) {}
isOn(name: string): boolean {
return this.values[name] ?? false
}
}
declare module '@kitr/core' {
interface ConfigRegistry {
flags: FlagsConfig
}
interface ContainerBindings {
flags: Flags
}
}
export default defineProvider({
name: 'flags',
register(app) {
const config = app.config.get('flags', { flags: {} })
app.container.singleton('flags', () => new Flags(config.flags))
},
})
What the declare module block does
The declare module block types app.config.get('flags') and useService('flags'). Without it both still work, but return untyped values.
Add its options
// config/flags.ts
import type { FlagsConfig } from '#app/providers/flags-provider'
export default {
flags: { beta: true },
} satisfies FlagsConfig
config/index.ts lists every file in config/. kitr add rewrites it. Add the line by hand when you create a file yourself.
Read it in a page
import { useService } from '@kitr/react'
export default function HomePage() {
const flags = useService('flags')
return flags.isOn('beta') ? <p>Beta features are on.</p> : null
}
Use a state library
Same steps. Create the store in register() and bind it:
import { createStore } from 'zustand/vanilla'
app.container.singleton('cart', () => createStore<CartState>(() => ({ items: [] })))
Pages read it with useService('cart'). Tests create their own app, so state never leaks between them.
Swap a driver
Device packages (@kitr/secure-storage, @kitr/notifications, @kitr/files, @kitr/native, @kitr/updater) ask the app for a driver. The best match wins, and priority decides.
Driver priority table
| Source | Priority |
|---|---|
| Web fallback | 0 |
| Shell drivers (Electron, Tauri, Capacitor) | 10 |
| Yours | any number above |
Register your own
A secure-storage driver is four functions.
// app/vault.ts
import type { SecureStorageDriver } from '@kitr/secure-storage'
import { defineProvider } from 'kitr/app'
export function createMapVault(): SecureStorageDriver {
const values = new Map<string, string>()
return {
name: 'map-vault',
secure: false,
get: async (key) => values.get(key) ?? null,
set: async (key, value) => {
values.set(key, value)
},
remove: async (key) => {
values.delete(key)
},
keys: async () => [...values.keys()],
}
}
export default defineProvider({
name: 'map-vault',
register(app) {
app.drivers.register('secure-storage', createMapVault(), { platform: 'test', priority: 20 })
},
})
List the provider in kitrrc.ts. The package picks it up. Pass platform: 'desktop' or 'mobile' to label it for devtools and kitr doctor.
Test it by booting an app with only that provider:
const app = createApp({ providers: [vaultProvider] })
await app.boot()
const storage = createSecureStorage({}, app)
expect(storage.driver.name).toBe('map-vault')
Skip the registry
Pass the driver in the package's config:
// config/secure-storage.ts
import { defineConfig } from '@kitr/secure-storage'
import { createMapVault } from '#app/vault'
export default defineConfig({ driver: createMapVault() })
Fall back to the web driver
In a native shell with no driver for a package, the service throws E_DRIVER_UNAVAILABLE. Opt in to the web driver per package:
// config/notifications.ts
import { defineConfig } from '@kitr/notifications'
export default defineConfig({ fallbackToWeb: true })
The web driver then runs inside the WebView.
Write a Tauri, Electron or Capacitor driver
Each device package exports its driver interface and the built-in drivers as examples. Copy the closest one.
| Shell | Built-in driver to copy |
|---|---|
| Electron | createElectronSecureStorageDriver (talks to the main process over the kitrIpc bridge) |
| Tauri | createTauriNotificationsDriver (calls Tauri plugins) |
| Capacitor | createCapacitorSecureStorageDriver (calls Capacitor plugins) |
Register the new driver with platform set to the shell. Add native plugins with ctx.deps.add() in your package's kitr.ts when you ship it (level 3).
Your own package
When the same provider belongs in many projects, make it a package. A package adds:
- a
kitr addstep: questions, config file, provider, dependencies - docs and an example for this site
- generators (
kitr make:<kind>)
// src/kitr.ts
import { definePackage } from '@kitr/core/package'
export default definePackage({
name: 'flags',
add(ctx) {
ctx.provider.add('@acme/flags/provider')
ctx.config('flags', "export default { flags: {} }\n")
},
remove(ctx) {
ctx.provider.remove('@acme/flags/provider')
},
})
The full contract is in Building a package and Package contract.