Adopting Incrementally
If you already have a frontend and you don’t want to rewrite it, Kitsune can be adopted gradually. Here’s a practical sequence.
Step 1 — Add the provider at the root
Section titled “Step 1 — Add the provider at the root”The lowest-risk first move. One file changes.
// Reactimport { KitShellProvider } from '@atheory-ai/kitsune-react'
<KitShellProvider modules={[]}> <ExistingApp /></KitShellProvider>Or for a non-React app:
<kit-shell name="my-app"> <!-- existing markup --></kit-shell>import '@atheory-ai/kitsune-app'const shell = document.querySelector('kit-shell')!shell.modules = []You now have a runtime. It does nothing yet. That’s fine.
Step 2 — Pick one feature; wrap it in a boundary
Section titled “Step 2 — Pick one feature; wrap it in a boundary”Don’t try to boundary the whole app at once. Pick the feature most in need of cross-cutting capability — analytics, audit, observability — and wrap that.
<KitBoundary surface="checkout" feature="commerce"> <ExistingCheckoutPage /></KitBoundary>Existing components don’t have to know they’re inside a boundary. The boundary is a context provider; descendants opt in.
Step 3 — Add metadata to one component
Section titled “Step 3 — Add metadata to one component”Find a button, a form, a card — anything that fires a meaningful interaction. Add data-meta-event (and data-meta-prop-* for context):
<button onClick={existingHandler} data-meta-event="checkout.started" data-meta-prop-cart-id={cart.id}> Pay</button>The existing handler still runs. Kitsune just also observes the click and emits an event.
Step 4 — Install one module
Section titled “Step 4 — Install one module”Pick the cross-cutting concern your team most wants to standardize. Often this is analytics. Write a tiny module:
import { defineKitModule } from '@atheory-ai/kitsune-core'import { posthog } from './existing-posthog-client'
export const analytics = defineKitModule({ name: 'analytics', events: { '*': (event) => { posthog.capture(event.type, { ...event.payload, surface: event.context?.surface, feature: event.context?.feature, }) }, },})Install it on the shell. The button you tagged in step 3 is now tracked. No other change.
Step 5 — Migrate one capability out of components
Section titled “Step 5 — Migrate one capability out of components”Find code that’s repeated across components — the analytics calls, the audit calls, the observability breadcrumbs. Pick one and move it to a module.
This isn’t a rewrite. It’s a migration:
- The module subscribes to the same event types your existing components were tracking.
- You delete the duplicated tracking call from each component.
- Run the app. The module observes; the analytics platform sees the same events.
- If something’s wrong, restore the inline call temporarily.
Iterate per feature. The module replaces N inline calls with one observer.
Step 6 — Replace one component
Section titled “Step 6 — Replace one component”If the tracking-only migration goes well, try replacing one component with its native equivalent.
A typical first candidate: your <Modal> component. Replace it with <kit-dialog>. The trigger button’s onClick becomes meta-command="dialog.open" meta-prop-target="...". The Cancel button becomes meta-command="dialog.close". Your custom focus-trap library deletes itself.
Start small. The bigger the framework component, the riskier the swap. Modals, toasts, tooltips, popovers — these are the easy wins because the platform now does them better.
Step 7 — Boundary the rest of the app
Section titled “Step 7 — Boundary the rest of the app”Once one feature is comfortable, repeat. Add boundaries around routes. Add metadata to interactions. Migrate cross-cutting code into modules. Each step is small. Each step is reversible.
You don’t have to finish.
When this works well
Section titled “When this works well”Incremental adoption works best when:
- Your codebase has clear cross-cutting concerns (analytics, audit, observability) that are duplicated across components.
- You have long-lived screens that won’t be rewritten in the next quarter.
- The team values long-term maintainability over short-term shipping speed.
It works less well when:
- The app is months from a full rewrite anyway. (Use Kitsune in the rewrite, don’t migrate.)
- The cross-cutting concerns are already well-isolated (e.g., a single analytics provider that sits at the route level). The architectural payoff is smaller.
When to write new code in Kitsune-style from day one
Section titled “When to write new code in Kitsune-style from day one”For new features in an existing app:
- Wrap the feature in a boundary.
- Use
data-meta-*for declarative event/command emission. - Put cross-cutting logic in modules from the start.
The new feature is a clean Kitsune surface. The rest of the app continues unchanged. Over time, more features look like the new feature.
Migration anti-patterns
Section titled “Migration anti-patterns”- Don’t rewrite the whole router first. Routing is the hardest piece and the lowest-leverage to migrate. Leave it.
- Don’t replace your form library wholesale. Wrap forms in boundaries; emit on submit; keep the rest.
- Don’t try to remove React. You can ship a Kitsune-shaped React app forever. The architectural payoff doesn’t require dropping React.
What’s next
Section titled “What’s next”- Other Frameworks
- Reference: kit-boundary
- Tutorial Chapter 10 — see what “analytics with zero UI changes” looks like in a clean Kitsune app