place

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: true caps 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()

ts
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.

ts
app({ pages: [...], caps: [ [RouterCap, pathRouter], [NoteStoreCap, { server: () => inMemoryNoteStore(SEED_NOTES), // SSR-friendly seed client: () => localStorageNoteStore(), // real persistence }], ], }).run()

use() at the call site

ts
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

ts
// 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
Action at a distance
Context's silent failure mode bites in two places: SSR (where the provider didn't render) and refactors (where the provider moves). Capabilities make both errors explicit at the type level.

See also