Kumbatio
Guides

Production Patterns

What survived contact with real use - integration patterns from Anasa, Meltemi, Entromail, and kumbat.io, four apps built on the SDK.

Four real applications run on energy-system today: Anasa (a local-first writing workspace, in public alpha), Meltemi (an email client in private beta from entro314 labs, the studio behind Kumbatio - built outside the Kumbatio product line), Entromail (a webmail platform from the same studio, pre-release), and kumbat.io itself. This page collects the patterns that survived contact with real use - including the ones the SDK's own docs didn't anticipate.

Every pattern here is shipped code, not a proposal.

Two ways to hold the engine

The SDK doesn't care whether you use its React layer. Both of these are in production:

Provider tree (Anasa). The documented path: create the engine, hand it to EnergyProvider, let applyToDOM stamp the document, read state through useEnergyState and useStrategy.

Module singleton (Meltemi). No provider at all. The engine lives in a plain module, and React binds to it with useSyncExternalStore over engine.subscribe:

// energy.ts - module scope, imported by anything
export const energyEngine = createEnergyEngine({
  initialLevel: 100,
  persistence: localStoragePersistence('myapp:energy'),
  originId: stableOriginId(),
})
// React binding - the whole adapter
export function useEnergyState() {
  return useSyncExternalStore(
    (cb) => energyEngine.subscribe(cb),
    () => energyEngine.getState(),
  )
}

The singleton pattern matters when your state layer (a Zustand store, a service worker, non-React code) needs the engine as much as your components do. Meltemi's store reads energy at six mutation sites and never touches React context to do it.

Custom persistence and write identity

localStoragePersistence is a starting point, not a ceiling. Anasa implements the EnergyPersistence interface - load, save, observe - against its own settings service, so energy state lives with the rest of the app's preferences and syncs the same way.

The part that bites: preserve revision and origin on the round trip. The engine uses them to tell its own echoes apart from genuinely external writes. If your persistence layer strips or re-mints them, every save observed back looks like a newer external write, and the engine persists forever in a loop. Pass the state through untouched.

Two related disciplines from Anasa's integration:

  • Mint one stable originId per install (e.g. myapp:app:<uuid>, cached in storage) and pass it to createEnergyEngine. Synthesized states - defaults you construct outside the engine - get their own deterministic origin so they're distinguishable.
  • Call engine.flush() on exit paths, with a bound. Anasa races flush() against a 3-second timeout on shutdown so a stuck retry can't wedge the quit. Durability is worth waiting for; hanging is not.

Extend strategies by layering, not forking

Both apps needed behavior the built-in strategies don't model, and both solved it the same way: resolve the built-in, then layer app-specific config on top.

Anasa's UI-visibility resolver starts from uiVisibilityStrategy.resolve(level) and adds its own surfaces - a right panel, and an AI-availability tier ('none' | 'minimal' | 'full'). Meltemi maps levels onto its own MailScope (all | priority | committed | readonly) that drives inbox filtering. Neither app forked a strategy; the package supplies the level semantics, the product supplies the domain.

The same shape works for the lookup strategies: Meltemi's snooze dialog preselects a preset by reading deferralStrategy.resolve(level).defaultPresetId and mapping it onto its own preset list, and its undo-send window comes straight from interactionForgivenessStrategy. See Authoring Strategies for building your own from scratch.

Read level copy from the package, once

Entromail wraps getEnergyLevel in a single describeEnergy(level) helper returning { label, description }, and every surface that names a level goes through it - the battery control, the hint bar, the settings summary, the keyboard-shortcut toast. Nothing hardcodes "Steady" or writes its own gloss.

That matters because level prose is product copy, not semver-covered API (the changelog says so explicitly, and the Rest description has already been rewritten once). An app that inlines the wording gets a silent drift between its five surfaces on the next upgrade; an app that reads the table gets the correction for free. Where the copy has to be app-specific, derive it - Entromail's hint bar composes the package label with its own scope description rather than replacing it.

Defer, don't drop - end to end

Meltemi is the first full production test of the notification gate. Native mail-arrival events are published into a gate from createNotificationGate; at low energy or during a focus session (via createFocusSessionController) they're held, and when energy climbs they're released as one summarized catch-up banner - with sound only for immediate deliveries, never for catch-ups. Nothing is lost; nothing interrupts.

If you adopt one runtime piece of the SDK, make it this one. It's the difference between "quiet mode" and an inbox you can trust while resting.

Gate the AI you didn't ask for, not the AI you did

Both apps converged on the same rule independently: energy level gates passive AI - panels that appear on their own - but never invoked AI.

  • Meltemi wraps its reading-pane AI panel in a presence check built from presenceAtOrAbove(75); the command palette and agent console stay available at every level.
  • Anasa's AI surfaces scale full at 50 and above, minimal at 25, none at 0 - but explicitly-triggered assistance still answers.

Low energy means less unsolicited stimulus, not fewer capabilities. That's the decision filter applied to AI.

The escape-hatch invariant

At low levels, dim and de-emphasize - never remove the way out. In practice:

  • Meltemi dims peripheral chrome to an opacity floor of 0.4 (restored on hover), and the energy battery control is excluded from dimming - the control that raises the level must never fade with it.
  • At level 0, Meltemi deliberately shows the full thread list read-only instead of filtering it: hiding rows would strand threads someone already had open. Rest reads what's already there.

Whatever your app dims at 25 and 0, the level control itself is exempt. This invariant started as an Anasa lesson and transferred to Meltemi unchanged.

Upgrades should be boring

Meltemi integrated at 0.4.0 and rode every minor release to 1.0 with zero changes to its integration code: additive surface, stable semantics for the five levels.

2.0 was the exception, and it is the more useful story. No type signature moved in it - it is a major because for this package a shipped strategy table's values are API, and two of its four fixes changed runtime behavior. An upgrade that the compiler waves through can still change what your app does, which is why the semver contract here covers behavior and why conformance.json ships the tables: you can diff what your users will actually experience, not just what still typechecks.

Concretely, upgrading from 1.x breaks you only if you pinned react below 19.2 (which never worked with the React entry point), persisted state carrying properties outside the published schema, or depended on a batched notification being delivered after suppression started. See the upgrade guide.

What apps build on top

The SDK deliberately stops at state and strategy resolution. Anasa shows what the layer above can look like: it records energy readings into its own durable store, derives hour-of-day and day-of-week patterns on-device, and offers a suggested level with a stated confidence and reason. The engine doesn't predict - apps that know their users can.

Shipped something on energy-system? Tell us at hello@kumbat.io - adopters shipping real features get the loudest voice in the roadmap.