SLOPSHOPPER

model-colour

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

newspinnerprompt
★ 1v0.1.0NOASSERTIONupdated 2026-10-26englishstreetventures/osn/.claude/mods/model-colour
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · model-colour
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ● ✻ Thinking… ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ● Opus ⟨Claude Code's own drawing⟩

Draws

Spinner
● ✻ Thinking…
Prompt hint
● Opus ⟨Claude Code's own drawing⟩
README

Open Social Network (OSN)

An open, modular social platform. You own your identity and social graph, and choose which apps see them.

Vision

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.

OSN and Musubi

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.

Core Principles

  • Modular: each capability (events, messaging, weddings, social media) is a standalone app. Use what you want, ignore the rest.
  • Transparent data: read every piece of personalisation data we hold on you, and wipe it any time.
  • Private by design: E2E encryption for messaging (Signal Protocol), fine-grained visibility rules, hidden-attendance options.
  • Open standards: OSN is an OIDC issuer, so third parties can build on it. Self-hosting is planned for enterprise use.

Status

Phase 1. Three surfaces run in production on Cloudflare; the rest run locally.

SurfacePackagesWhere it runs
Identity / auth API@osn/apiLive — Worker on id.musubi.social
Wedding invites (Cire)@cire/api, @cire/invites, @cire/host, @cire/vendor, @cire/db, @cire/themeLive — cireweddings.com zone
Wedding marketing site@cire/landingLive — apex cireweddings.com
Identity & graph UI@musubi/socialLive — Pages on the apex musubi.social (also serves the OIDC consent screen)
Events (Pulse)@pulse/web, @pulse/api, @pulse/dbLocal only
Messaging (Zap)@zap/api, @zap/dbWorker on zap.cireweddings.com — M1 in flight, client app not started
Musubi / Pulse marketing sites@musubi/landing, @pulse/landingDev 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.

Applications

OSN Core

The identity and social-graph layer, and the OIDC issuer other apps authenticate against.

  • Passkeys are the only primary login (WebAuthn / FIDO2). OTP survives as a step-up factor; magic links and PKCE primary flows are gone
  • Server-side session store with rotation and reuse detection (Copenhagen Book C1/C2/C3)
  • Short-lived ES256 access tokens (5-minute TTL, aud: "osn-access"), public keys at /.well-known/jwks.json. Downstream services verify through @shared/osn-auth-client
  • Per-device session list, "sign out everywhere else", per-passkey rename and delete
  • Recovery codes for a lost device (Copenhagen Book M2)
  • Step-up "sudo" tokens for sensitive actions — recovery-code generation, email change, passkey delete
  • Cross-device login by QR code
  • Social graph: connections, close friends, blocked users
  • Many profiles per account (one login, several public handles)
  • Friends-of-friends recommendations
  • Organisations, with ARC service-to-service tokens for internal calls

Cire (Wedding invites)

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

  • Guest site — claim code unlocks the invite, then events, details and RSVP
  • Organiser portal — guest and event tables, spreadsheet import, per-section invite theming, co-hosts added by OSN handle
  • Vendor portal — vendor profiles and couple enquiries
  • Two auth models that never overlap. Guests trade a family claim code for a hashed session cookie, and never need an OSN account. Organisers sign in with their OSN passkey, checked against wedding ownership
  • Optional account linking lets a guest attach their seat to an OSN account

Detail lives in wiki/cire/cire.md and wiki/cire/cire-auth.md.

Pulse (Events)

A unified events platform: the social ease of Facebook Events, the fun of Partiful and Luma, the tooling of Eventbrite.

  • Discovery by location, category, date, friends attending, interests
  • Default view: what's happening today near you
  • Event lifecycle: Not Started → Started (configurable ticket cutoff) → Ongoing → Finished
  • Recurring events as a parent series with child instances
  • Organiser tools: moderation, cross-event blacklists, ticket setup
  • Venue pages — a nightclub with its DJ schedule, genres and nightly events
  • Event group chats, served by the messaging backend
  • Calendar view with iCal export (one-way sync to Google and Apple Calendar)
  • Hidden attendance for private events
  • Share-source attribution — which channel brought each RSVP in

Zap (Messaging)

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:

  1. Socials — DMs, group chats, organisation chats; filterable.
  2. AI — conversations with AI models, kept out of the normal inbox.

Core features:

  • DMs and group chats
  • Disappearing messages, per chat
  • Themes, stickers, GIFs, polls, easter-egg mini-games
  • E2E encryption via Signal Protocol (@shared/crypto, planned)
  • Event group chats reachable from both Pulse and Zap, with the event overview in chat settings
  • Backups: encrypted cloud, self-hosted cloud, or local only
  • Device transfer

Key differentiator — organisation chats:

  • Verified organisations — blue-tick verification for businesses, public bodies and NGOs.
  • Support over a Zap handle — a business replaces support@acme.com with @acme. Phishing gets harder because the verified handle is the only source of truth.
  • Embeddable web widget — a third-party site swaps its email-capture support form for "Enter your OSN handle". The thread lands in Zap under the verified organisation with full history. Checkouts can capture a handle next to (or instead of) an email to smooth post-purchase support.
  • Organisation tooling — dashboards for triage, agent assignment, analytics, SLA monitoring and audit.
  • Locality and government channels — users opt into a locality (home city, plus a temporary one while travelling) and get official notices — floods, evacuations, public safety. AI-assisted queries route people to authoritative answers ("where is the nearest relief centre?") in real time.

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

Social media (spec only, deferred)

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.

Architecture

Monorepo structure

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.

Backend

  • Elysia on Bun locally, on Cloudflare Workers in production. Workers builds run with aot: false — Elysia's ahead-of-time compilation uses new Function, which workerd forbids
  • Effect.ts for services. The layer graph is built once at boot as a shared ManagedRuntime, never per request
  • Eden treaty for type-safe internal calls; plain REST for third-party consumers
  • ARC tokens — self-issued ES256 JWTs with kid, scope and audience — for service-to-service auth
  • WebSockets for live messaging and event updates (planned)
  • Rate limiting on auth, graph and org writes. Fail-closed. Native Workers rate-limit bindings for the 60-second windows, Upstash Redis for the rest
  • Turnstile bot protection on register, login, claim and RSVP. Key-optional: no secret means the check is inert

Databases

Four environments, two drivers, one Drizzle type:

EnvironmentDriverWhere
localbun:sqliteDev machine — bun run dev and in-memory tests
devCloudflare D1deployed in the dev account by the Alchemy stacks; zap and pulse also run wrangler dev --env dev (miniflare) locally
stagingCloudflare D1Deployed
productionCloudflare D1Deployed

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

Frontend

  • Standalone apps first, working toward a hybrid super-app
  • iOS first, then web, then Android (Android deferred)
  • SolidJS everywhere; Astro + Solid for the web surfaces and marketing sites
  • Components in three layers, all built on Kobalte in the Zaidan (shadcn-for-Solid) style: @shared/ui primitives, @osn/auth-ui ceremony views, @cire/ui for cire's house style

Messaging architecture

@zap/api is the shared messaging backend, in two modes:

  • Direct — the user has opted into Zap
  • Indirect — the user only touches messaging through another app, e.g. a Pulse event chat

So Pulse users can join event chats without installing Zap.

Tech Stack

LayerTechnology
RuntimeBun (local), Cloudflare Workers (production)
LanguageTypeScript
Backend frameworkElysia
Functional coreEffect.ts
ORMDrizzle
Databasebun:sqlite (local) → Cloudflare D1 (dev/staging/prod)
Cache / limitersUpstash Redis, native Workers rate-limit bindings
API communicationEden (internal), REST (external)
Real-timeWebSockets
EncryptionSignal Protocol (planned), ES256 JWTs, WebAuthn
EmailResend HTTP API
Frontend frameworkSolidJS
Web / marketing sitesAstro + Solid, on Cloudflare Pages
MonorepoTurborepo
TestingVitest + @effect/vitest
Lintingoxlint
Formattingbun fmt (Bun's built-in formatter, config in .oxfmtrc.json)
ValidationElysia TypeBox at the HTTP boundary, Effect Schema in services
ObservabilityOpenTelemetry → Grafana Cloud
Feature flagsGrowthBook
VersioningChangesets

Getting Started

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.

Data Models (conceptual)

Account and profile

  • Identity (OIDC subject), passkeys, sessions, recovery codes
  • One account, many profiles — each with its own public handle
  • Interests and personalisation data, readable and resettable by the user

Social graph

  • Connections (mutual follow / friend)
  • Close friends
  • Blocks, OSN-wide or per app
  • App authorisations

Event

  • Basics: title, description, location, date and time
  • Lifecycle state, moved automatically by the clock
  • Configuration: late joins, ticket limits
  • Parent series reference, for recurring events
  • Associated chat
  • Organiser, attendees, category, tags

Event series

  • Recurrence pattern
  • Child event instances

Message / chat

  • E2E encrypted content
  • Type: DM, group, or event-linked
  • Participants, and an event reference for event chats

Wedding

  • Wedding root, owner profile, co-hosts
  • Families with claim codes, and the guests inside them
  • Events, sections and per-section theming
  • RSVPs, dietary notes, vendor enquiries

Moderation

  • User reporting across every app
  • Community notes — collaborative context, Twitter-style
  • Organiser controls for event spaces
  • Organisation blacklists spanning many events

Contributing

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, commands
  • wiki/index.md — map of the knowledge base
  • GitHub Issues — planned work, open decisions; findings live in the private englishstreetventures/osn-tracker

License

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

Source 1 files
hooks/register.tsx 61 lines
1import 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