Build the First Loop
The Quick Start used built-in modules. Here you’ll write one yourself in ten lines so the runtime stops being magic.
A module is a value
Section titled “A module is a value”import { defineKitModule } from '@atheory-ai/kitsune-core'
export const auditModule = defineKitModule({ name: 'audit', events: { 'note.saved': (event) => { console.info('[audit]', { when: new Date(event.timestamp).toISOString(), what: event.type, where: event.context?.surface, what_entity: event.entity ?? event.context?.entity, }) }, },})That’s the whole module. A name and a map of event handlers. Listening to '*' matches every event. Listening to a specific type matches that type.
Modules can also handle commands:
export const clipboardModule = defineKitModule({ name: 'clipboard', commands: { 'clipboard.copy': async (command) => { const text = String(command.payload?.text ?? '') await navigator.clipboard.writeText(text) return { copied: text.length } }, },})Commands return a result. Events don’t.
Install the module on the shell
Section titled “Install the module on the shell”import '@atheory-ai/kitsune-app'import '@atheory-ai/kitsune-ui'import { notificationModule } from '@atheory-ai/kitsune-ui'import { auditModule, clipboardModule } from './modules.js'
customElements.whenDefined('kit-shell').then(() => { const shell = document.querySelector('kit-shell')! shell.modules = [auditModule, clipboardModule, notificationModule()]})The shell installs its modules on connectedCallback. Order doesn’t matter for events (all matching handlers fire); for commands, the last registered handler wins.
Make a button talk to your module
Section titled “Make a button talk to your module”<kit-shell name="my-app"> <kit-boundary surface="quick-actions" feature="utilities"> <kit-button meta-event="note.saved" meta-command="clipboard.copy" meta-prop-text="A modular frontend just copied this." > Save and copy </kit-button> </kit-boundary></kit-shell>Click. Check the console: an audit line appears. Check the clipboard: the text is there. Add notificationModule() and a <kit-toast-region> and you can also tell the user what happened — without changing the button.
What the runtime did under the hood
Section titled “What the runtime did under the hood”click reaches <kit-boundary> → boundary calls parseMetadata() on the clicked element → boundary asks the shell's runtime to: runtime.emit({ type: 'note.saved', context: { surface: 'quick-actions', feature: 'utilities' } }) runtime.command({ type: 'clipboard.copy', payload: { text: '...' }, context: ... }) → emit fans out to every event handler subscribed to 'note.saved' (and '*') → command finds the handler registered for 'clipboard.copy' and awaits its result → diagnostics fire for each step (the debug module logs them)The button never imported the audit module. The audit module never imported the button. They speak through the runtime.
What’s next
Section titled “What’s next”- The Runtime Loop — the longer version of this story
- Modules — patterns for production modules (services, lifecycle, provider tokens)
- Build Quill — chapter 5 turns the loop above into a localStorage persistence module