Tags the prompt hint line and the working spinner with the current model, in that model's colour

An open, modular social platform. You own your identity and social graph, and choose which apps see them.
OSN splits the social graph from the apps that use it. You own your identity and relationships, opt into apps one at a time, and your access rules follow you across all of them. Block someone on OSN and the block reaches every app you connect — unless you say otherwise.
Two names, and they mean different things.
OSN is the system: a specification and the headless core that implements it — identity, the social graph, authorisation, and the OpenID Connect issuer. It ships no user interface, and anyone can run it for their own private social graph.
Musubi is our implementation and the product built on it: the social app, its marketing site, the brand, and musubi.social, the instance we host. Musubi is to OSN as Mastodon is to the fediverse.
The rule that decides which name anything takes: if an independent implementation must use the same string to interoperate, it is OSN; otherwise it is Musubi. So the token audiences, well-known paths and claim names stay OSN, while the app, the RP ID and the hosted instance are Musubi. Full detail in wiki/shared/osn-and-musubi.md.
OSN's graph is general — edges carry their own semantics, so follows and reports-to are one primitive. A corporate directory is a profile of OSN rather than a different system.
There is no separate specification repository yet. OSN today is the reference implementation plus the wire contract it defines; a written specification waits until a second implementer exists.
Phase 1. Three surfaces run in production on Cloudflare; the rest run locally.
| Surface | Packages | Where it runs |
|---|---|---|
| Identity / auth API | @osn/api | Live — Worker on id.musubi.social |
| Wedding invites (Cire) | @cire/api, @cire/invites, @cire/host, @cire/vendor, @cire/db, @cire/theme | Live — cireweddings.com zone |
| Wedding marketing site | @cire/landing | Live — apex cireweddings.com |
| Identity & graph UI | @musubi/social | Live — Pages on the apex musubi.social (also serves the OIDC consent screen) |
| Events (Pulse) | @pulse/web, @pulse/api, @pulse/db | Local only |
| Messaging (Zap) | @zap/api, @zap/db | Worker on zap.cireweddings.com — M1 in flight, client app not started |
| Musubi / Pulse marketing sites | @musubi/landing, @pulse/landing | Dev tier only — no production deploy |
CI deploys the osn-api, cire-api and zap-api Workers, the guest site (Worker SSR) and the identity app, organiser, vendor and cire landing sites (Pages) to production in releases from .github/workflows/production-release.yml, once a day or by hand, one approval each. Each production deploy applies its D1 migrations before the Worker. A merge to main deploys the dev tier at once, through the Alchemy stacks into its own Cloudflare account, every surface as a Worker. See wiki/shared/dev-environment.md and wiki/shared/production-deploy.md.
The identity and social-graph layer, and the OIDC issuer other apps authenticate against.
aud: "osn-access"), public keys at /.well-known/jwks.json. Downstream services verify through @shared/osn-auth-clientA bespoke digital wedding invite: a tactile, animated guest site plus a portal where the couple runs the guest list. Once its own repo, now a sibling workspace. The schema is already multi-tenant (a weddings root table), so it can become a product without a rewrite.
Detail lives in wiki/cire/cire.md and wiki/cire/cire-auth.md.
A unified events platform: the social ease of Facebook Events, the fun of Partiful and Luma, the tooling of Eventbrite.
Secure, playful messaging that doubles as the ecosystem's support and announcements channel. Aim: between Messenger, Instagram and iMessage — playful but not loud, modern, secure, open.
Two top-level views:
Core features:
@shared/crypto, planned)Key differentiator — organisation chats:
support@acme.com with @acme. Phishing gets harder because the verified handle is the only source of truth.@zap/api and @zap/db are scaffolded (M0 done, M1 in flight). @zap/app, the SolidJS client, has not started. See the open issues (gh issue list --repo englishstreetventures/osn --label product:zap) and wiki/zap/zap.md.
Multi-format social content: text posts, image posts, long-form writing, short-form video. Switch off any format you don't want — short video, say — and it disappears.
Organised by domain. Six top-level directories, one workspace prefix each. A seventh, tools/, holds the local-only developer tooling under @tools/*.
osn/ # @osn/* — identity stack, the headless system
api/ # Elysia identity server (:4000; prod Worker id.musubi.social)
auth-ui/ # SolidJS views for OSN's ceremonies (<SignIn>, <Register>, <SessionsView>…)
client/ # Client SDK (Effect-based + SolidJS bindings)
db/ # Drizzle schema — accounts, profiles, passkeys, sessions, graph, orgs
musubi/ # @musubi/* — our implementation of OSN
social/ # SolidJS app for identity + graph management (:1422; prod musubi.social)
landing/ # Marketing site, Astro + Solid (:4324)
pulse/ # @pulse/* — events stack
web/ # SolidJS client (browser SPA)
api/ # Elysia + Eden events server (:3001)
db/ # Drizzle schema — events, RSVPs
landing/ # Marketing site (:4325)
zap/ # @zap/* — messaging stack (M0 scaffolded; client planned)
api/ # Elysia messaging server (:3002)
db/ # Drizzle schema — chats, messages, group state
cire/ # @cire/* — wedding-invite stack
api/ # Elysia on Cloudflare Workers (:8787; prod api.cireweddings.com)
invites/ # Guest invite site, Astro + SolidJS (:4321; prod invite.cireweddings.com)
host/ # Organiser portal (:4322; prod host.cireweddings.com)
vendor/ # Vendor portal (:4326; prod vendor.cireweddings.com)
landing/ # Marketing site for the apex (:4323)
db/ # Drizzle schema + D1 migrations
theme/ # Zero-dependency theming validators (CSS-colour allowlist)
ui/ # cire's house component layer, on top of @shared/ui
invite-designs/ # The invite design catalogue
dietary/ # The dietary-requirement vocabulary (API and UI)
build-tools/ # Build-only Astro integration: each build's CSP names its own API
shared/ # @shared/* — cross-cutting utilities
crypto/ # ARC tokens, recovery-code helpers (Signal Protocol planned)
db-utils/ # Driver-agnostic Drizzle handle, makeDbLive, makeD1DbLive
design-tokens/ # The ui-* token contract every shared component reads
email/ # EmailService Tag — Resend / Cloudflare / Log / Noop transports
feature-flags/ # Key-optional, fail-safe flag client (GrowthBook)
observability/ # OpenTelemetry helpers, Elysia plugin, instrumentedFetch
osn-auth-client/ # Downstream access-JWT verification — JWKS cache, Elysia adapter
rate-limit/ # Per-IP / per-user limiter primitives, client-IP trust policy
redis/ # Redis wrapper, rate-limiter Lua, JTI / rotated-session stores
turnstile/ # Key-optional, fail-closed Turnstile verifier
typescript-config/ # base / node / solid tsconfigs
ui/ # The shared SolidJS primitives (Button, Card, Modal, Field, Table…)
tools/ # @tools/* — local-only developer tooling, never shipped
e2e/ # The end-to-end suite (`bun run test:e2e`)
lab/ # Component and three.js scratchpad (`bun run dev:lab`)
metrics/ # Session-metrics dashboard over .claude/metrics
oxlint/house/ # This repo's own lint rules (@tools/oxlint-house)
pr-metrics/ # The session-performance cards
stryker/ # Stryker mutation testing for a cire package (`test:mutation`)
Prefix rule: every workspace sits under exactly one of osn/, musubi/, pulse/, zap/, cire/, shared/ or tools/, and its package.json name uses the matching prefix.
Dependencies flow one way: shared/* depends on nothing internal; osn/* depends on shared/*; musubi/*, pulse/*, zap/* and cire/* may depend on osn/* and shared/* but never on each other. Cross-domain reads go through a bridge module — Pulse reaches the social graph through graphBridge, Cire through ARC-gated internal endpoints.
aot: false — Elysia's ahead-of-time compilation uses new Function, which workerd forbidsManagedRuntime, never per requestkid, scope and audience — for service-to-service authFour environments, two drivers, one Drizzle type:
| Environment | Driver | Where |
|---|---|---|
local | bun:sqlite | Dev machine — bun run dev and in-memory tests |
dev | Cloudflare D1 | deployed in the dev account by the Alchemy stacks; zap and pulse also run wrangler dev --env dev (miniflare) locally |
staging | Cloudflare D1 | Deployed |
production | Cloudflare D1 | Deployed |
@shared/db-utils broadens the Drizzle handle over both the sync (bun:sqlite) and async (D1) result kinds, so the same query code runs everywhere — service code must await. See wiki/shared/database-environments.md.
@shared/ui primitives, @osn/auth-ui ceremony views, @cire/ui for cire's house style@zap/api is the shared messaging backend, in two modes:
So Pulse users can join event chats without installing Zap.
| Layer | Technology |
|---|---|
| Runtime | Bun (local), Cloudflare Workers (production) |
| Language | TypeScript |
| Backend framework | Elysia |
| Functional core | Effect.ts |
| ORM | Drizzle |
| Database | bun:sqlite (local) → Cloudflare D1 (dev/staging/prod) |
| Cache / limiters | Upstash Redis, native Workers rate-limit bindings |
| API communication | Eden (internal), REST (external) |
| Real-time | WebSockets |
| Encryption | Signal Protocol (planned), ES256 JWTs, WebAuthn |
| Resend HTTP API | |
| Frontend framework | SolidJS |
| Web / marketing sites | Astro + Solid, on Cloudflare Pages |
| Monorepo | Turborepo |
| Testing | Vitest + @effect/vitest |
| Linting | oxlint |
| Formatting | bun fmt (Bun's built-in formatter, config in .oxfmtrc.json) |
| Validation | Elysia TypeBox at the HTTP boundary, Effect Schema in services |
| Observability | OpenTelemetry → Grafana Cloud |
| Feature flags | GrowthBook |
| Versioning | Changesets |
You need Bun — the pinned version is in .bun-version.
bun install # install every workspace
bun run dev # start everything (turbo)
Most of the time you want one stack, not all of them:
bun run dev:osn # identity API
bun run dev:social # identity API + social app
bun run dev:pulse # pulse API + app, identity API, zap API
bun run dev:zap # zap API + identity API
bun run dev:cire # cire API + guest + organiser, identity API
bun run dev:apis # backends only
Packages that need configuration ship a .env.example — copy it to .env and fill it in. Missing secrets degrade rather than break: no Resend key logs emails instead of sending them, no Turnstile secret leaves the bot check inert.
Checks:
bun run typecheck # type-check
bun run test # all tests
bun run lint # oxlint
bun run fmt # bun fmt (scope listed in package.json)
Database work runs from the owning package:
bun run --cwd cire/db db:generate --name <snake_case> # write the next migration (wiki/conventions/commands.md)
bun run --cwd pulse/db db:studio # Drizzle Studio
bun run db:reset # reset every local database
AGENTS.md holds the everyday commands and code rules; wiki/conventions/commands.md has the full list.
Every change goes through a pull request. Nobody pushes to main.
Work on a feature branch, and include a changeset describing the change and the packages it touches:
bun run changeset
Pick the packages, pick the bump type, write a summary. The CI "Changeset Check" job fails without one. It enforces two rules. Package names must match the workspace name exactly — "@pulse/web", not "pulse". One changeset must not mix version-less packages (@cire/*) with versioned ones.
On merge, CI runs changeset version to bump versions and update changelogs, lowers the lint-warning ceiling in scripts/lint-warning-ceiling.txt to the measured count, commits both to main in one commit. That push deploys the dev tier; production picks the change up in its next release.
Read next:
AGENTS.md — the entry point for coding agents: workflow, standing rules, commandswiki/index.md — map of the knowledge baseenglishstreetventures/osn-trackerAll source code, database structures, documentation, and asset components within this repository are the exclusive proprietary property of English Street Ventures Pty Ltd (ACN: 693 716 700, ABN: 25 693 716 700), Sydney, Australia.
Unauthorised copying, distribution, or use of this material is strictly prohibited.
hooks/register.tsx 61 lines1import type { EngineInterface, Register } from "claude-code";
2
3// One colour per model family, matched against the model id or alias that
4// `/model` shows.
5export const FAMILIES = [
6 { match: /fable/i, name: "Fable", color: "#3B82F6" },
7 { match: /opus/i, name: "Opus", color: "#D9481C" },
8 { match: /haiku/i, name: "Haiku", color: "#22A35A" },
9 { match: /sonnet/i, name: "Sonnet", color: "#E5B700" },
10] as const;
11
12export const familyOf = (model: string) => FAMILIES.find((f) => f.match.test(model)) ?? null;
13
14const currentFamily = async ($: EngineInterface) => familyOf(await $.session.model());
15
16export const register: Register = (on) => {
17 // A model switch redraws the tags; a new prompt catches any switch made
18 // some other way.
19 on("config.set", { key: "model" }, async ($, e, next) => {
20 const set = await next(e);
21 $.ui.invalidate("ui.render");
22 return set;
23 });
24
25 on("prompt.submit", async ($, e, next) => {
26 $.ui.invalidate("ui.render");
27 return next(e);
28 });
29
30 on("ui.render", { component: "PromptHint" }, async ($, e, next) => {
31 const [family, drawn] = await Promise.all([currentFamily($), next(e)]);
32 if (!family) return drawn;
33
34 const { Box, Text } = $.ui.resolve(e);
35
36 return (
37 <Box flexDirection="row">
38 <Text color={family.color} weight="semibold">
39 ● {family.name}
40 {" "}
41 </Text>
42 {drawn}
43 </Box>
44 );
45 });
46
47 on("ui.render", { component: "Spinner" }, async ($, e, next) => {
48 const [family, drawn] = await Promise.all([currentFamily($), next(e)]);
49 if (!family) return drawn;
50
51 const { Box, Text } = $.ui.resolve(e);
52
53 return (
54 <Box flexDirection="row">
55 <Text color={family.color}>● </Text>
56 {drawn}
57 </Box>
58 );
59 });
60};
61