Extend

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 flags

First 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 add step: 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.

Next steps