Skip to content

11. Where to Take Quill Next

You shipped a real notes app in eleven chapters using only the platform and a small architecture above it. Here’s how to keep going.

Each of these is roughly the size of auditModule or analyticsModule — small, self-contained, no UI changes required.

Server sync. A syncModule observes note.created, note.updated, note.deleted and POSTs to your API. Add a provideToken('online') for offline detection and queue writes when offline.

Search. A searchModule observes the same events, maintains an in-memory index, and handles a search.query command. The palette can dispatch search.query and render results.

Tags. A tagsModule extends the persistence module with a tag schema (or sits alongside it). Cards can declare meta-event="tag.added" meta-prop-tag="...".

Markdown. A markdownModule provides a MarkdownToken service. The editor injects it and renders previews. Same pattern as the notes service.

Multi-pane. A windowsModule tracks which note is open in which pane. The cards dispatch note.opened with a pane payload; the module decides which pane updates.

Command palette extensions. Add new entries to PALETTE_ACTIONS. They dispatch commands. The palette doesn’t change.

Things that need a Kitsune feature we don’t have yet

Section titled “Things that need a Kitsune feature we don’t have yet”

These would push the framework forward:

Routing context as a boundary. A future <kit-route> boundary carries route params; the runtime emits route.changed events automatically.

Form-associated <kit-input>. A custom element that fully participates in <form> via ElementInternals, with constraint validation.

Devtools panel. A browser extension that visualises the diagnostic stream — events, commands, modules, providers — as a timeline.

Persistence as a provider pattern. A general KeyValueToken so any module can persist without depending on localStorage.

Composed meta:event channel. The boundary already listens for click; future versions will listen for the composed meta:event custom event for fully programmatic dispatch from inside any component.

If any of these are blocking your real app, that’s signal — open an issue or PR.

Kitsune is intentionally not full-stack. For these, plug in what suits you:

  • Data fetching. TanStack Query, SWR, plain fetch. They observe whatever they need; modules can integrate.
  • Routing. Use the platform’s popstate and Navigation API, or a small router library. Wrap route changes in a boundary.
  • Auth. Wrap your auth client in a module that exposes a SessionToken. Components inject the token; logout becomes a command.
  • Database. Sqlite over WASM, IndexedDB, Supabase, your API — the persistence module is the seam.

Kitsune doesn’t replace these, and shouldn’t. It coordinates between them.

How to think about Kitsune in your own apps

Section titled “How to think about Kitsune in your own apps”

A few rules of thumb that scale:

Boundary every meaningful surface. Page roots, form roots, list-item roots. The right number of boundaries is the number that lets your analytics ask “where did this happen?” answer-ably.

One event for every fact, one command for every verb. Resist the urge to have a function that does both. Events are observed by many. Commands are handled by one. Mixing them causes ordering bugs.

Prefer many small modules over few large ones. A module per capability — analytics, audit, sync, notifications. If a module has more than ~150 lines, it’s probably two modules.

Inject services through tokens, not imports. Every import { storage } from './storage' is a coupling. Use providers and you can swap implementations in tests, in environments, in feature flags.

Don’t use commands for every interaction. Local UI behavior — opening a menu, scrolling a list, focusing an input — doesn’t need to go through the runtime. Commands are for application-level requests. Local interactions should stay local.

Lean on the platform. Whenever you find yourself reaching for a JS library, check if the platform has a primitive: <dialog>, popover, <details>, <input type=...>, ElementInternals, :has(), @container, oklch(). The answer is usually yes now, and the platform’s version is more accessible by default.

If you have a React app today, you don’t have to rewrite to start using Kitsune.

The <KitBoundary> from @atheory-ai/kitsune-react works inside any React tree. Drop one around a feature. Add data-meta-event="..." to a button. You have analytics on that button immediately. Repeat per feature.

The Frameworks → React chapter walks through this.

A few directions on the roadmap:

  • Form-associated <kit-input> with first-class validation.
  • A <kit-route> element that emits route.changed events and provides route context.
  • Devtools for inspecting the runtime loop.
  • More UI componentskit-disclosure, kit-select, kit-tabs, kit-table — each kept thin around platform primitives.
  • First-party moduleslocalStorageModule, indexedDBModule, analyticsModule (with adapters for PostHog/Amplitude/Plausible), auditModule, commandPaletteModule.

If you’re building on Kitsune today and want to influence the roadmap, the GitHub repo is the place.

Eleven chapters. One real app. No React. No virtual DOM. No CSS-in-JS runtime. No bundler config beyond Vite’s default.

You built it on top of <button>, <dialog>, popover, aria-live, oklch, and the cascade — primitives that will outlive every framework. The architecture above them is small enough to fit in your head.

That was the bet. Thanks for reading it through.

← Back to Build Quill — Overview