Capabilities
Capabilities are typed slots: a named contract that components consume and the app provides. Same job as React context, four differences that matter:
- Typed end-to-end. The cap's type flows to every
.use()site without a generic ceremony. - Scoped, not global. Provisions sit in lexical scope; no "above the provider" rules to remember.
- SSR-aware.
clientOnly: truecaps that get touched during SSR auto-emit a placeholder span and run their body on hydration. - Per-runtime install. Same cap, different impls on server vs client.
defineCapability()
import { defineCapability } from '@place/capability'
interface NoteStore {
all(): readonly Note[]
create(input: NoteInput): string
}
export const NoteStoreCap = defineCapability<NoteStore>('NoteStore', {
clientOnly: true, // touching this cap during SSR auto-emits a
// placeholder; the body runs on the client only.
})
A capability is a key + a type. It doesn't carry a default value — provision sites are responsible for that, and unprovisioned use throws with a clear message.
Per-runtime install
The framework dispatches the right factory based on the runtime. Same cap; the server renders against the seed store, the client hydrates against localStorage. No typeof window checks anywhere in your code.
app({
pages: [...],
caps: [
[RouterCap, pathRouter],
[NoteStoreCap, {
server: () => inMemoryNoteStore(SEED_NOTES), // SSR-friendly seed
client: () => localStorageNoteStore(), // real persistence
}],
],
}).run()
use() at the call site
const NotesList = component(() => {
const store = NoteStoreCap.use() // fully typed; throws if unwired
return (
<ul>
{() => store.all().map((n) => <li>{n.title}</li>)}
</ul>
)
})
NoteStoreCap.use() returns the typed instance. If the cap is clientOnly and not installed (SSR), .use() throws a special ClientOnlyAbort that the component machinery catches — the component renders as an empty placeholder, then mounts the real body on hydration. You don't write a guard.
Why not React context
// React: silently undefined on SSR if the provider isn't above. No
// type-system way to know whether the consumer is "safe" to render.
const v = useContext(MyContext)
if (!v) throw new Error('MyContext not provided') // every consumer ceremony