A full-featured realtime social network on a single Convex Realtime backend —
profiles, friend graph, audience-scoped feed, six-reaction posts, comments,
notifications, and Messenger-style DMs, all live-synced through reactive
queries (no sockets, no polling). Scaffolded by ndev stack scaffold
(lane: convex-realtime), then grown into the real social domain.
This capture comes from the fully local self-hosted Convex stack after a real
signup and post. It is the reviewed evidence declared in
portfolio/manifest.yaml.
- Identity — Convex Auth (email+password), auto-provisioned profile with deterministic-hue avatar/cover (zero file storage), editable bio/work/location.
- Friend graph — request → accept lifecycle (decline / cancel / unfriend), cross-request auto-accept, and People You May Know ranked by mutual friends.
- Feed — paginated, newest-first, server-side visibility:
friends-audience posts reach only the author's friends;publicposts reach everyone. - Reactions — the classic six (👍❤️😆😮😢😡), one per user per post, hover picker, denormalized tallies kept exact in the same transaction.
- Comments — inline threads with exact counts and owner/author delete rights.
- Notifications — bell with unread badge for friend requests, accepts, reactions, and comments; mark-read / mark-all-read.
- Messages — per-pair conversations, realtime delivery, unread accounting, read receipts, conversation previews.
- People search — full-text search index over display names, live from the nav bar.
- SaaS spine (from the scaffold) — Stripe-mirrored subscriptions; the free
tier caps lifetime posts at 100, Pro lifts it (
convex/lib/plans.ts).
openbook/
├── convex/ # the backend: schema + social functions + tests
│ ├── schema.ts # profiles · posts · comments · reactions · friendships
│ │ # · notifications · conversations · messages · billing
│ ├── lib/social.ts # pairKey · visibility rule · enrichment · notify fan-out
│ └── social.test.ts # convex-test suite for every social rule
├── packages/shared/ # zod inputs · reaction registry · auth hooks · api re-export
└── apps/web/ # Vite + React 19 + react-router social UI
└── src/ui/openbook.css # social shell (ob-*) on the garrid OKLCH token spine
pnpm install
CONVEX_PORT=3310 CONVEX_SITE_PORT=3311 pnpm selfhost # Docker OSS Convex + keys + push
(cd apps/web && VITE_CONVEX_URL=http://127.0.0.1:3310 pnpm dev)Or against Convex cloud: npx convex dev --once && pnpm auth:setup && pnpm dev.
| Gate | Command | Result |
|---|---|---|
| Types | pnpm typecheck |
2/2 packages clean |
| Unit (simulated backend) | pnpm test / make test |
28/28 (social rules + billing gate + stripe + suggestion pending exclusion) |
| Live E2E (real backend) | CONVEX_SELF_HOSTED_URL=http://127.0.0.1:3310 node scripts/verify-live.mjs |
19/19 — 3 users over the wire: search → friend → feed visibility → react → comment → notify → DM unread → delete cascade |
| Browser console | ndev browser exec <s> console --kind js --fail-on-error |
0 errors |
| Production build | pnpm build |
✓ (Vite production bundle) |
| Publication boundary | pnpm verify:publication / make publish-ready |
Go publication-tool tests, types, 28 application tests, build, production dependency licenses, and both secret-scan modes pass |
The live E2E is the same script that verifies a cloud deployment — point
VITE_CONVEX_URL at it and re-run.
- Visibility lives in one function —
lib/social.ts::postVisibleTo; bothposts.feedandposts.forProfilefilter server-side, so a hidden post never reaches a client. - Tallies are denormalized but exact —
reactionCounts/commentCountmove in the same mutation transaction as the child row; tests pin add / switch / remove arithmetic. - Notifications never self-ping —
notify()drops actor==recipient, and switching a reaction kind doesn't re-notify. - One row per pair — friendships and conversations are keyed by a sorted
pairKey, making request/accept and DM-open idempotent by construction.
- Add a table in
convex/schema.ts. - Add
query/mutationfunctions inconvex/and rules inconvex/lib/social.ts. - Pin behavior in
convex/social.test.ts(convex-test, runs in ms). - Consume from the web app via
apifrom@openbook/shared.
MIT. See LICENSE and
THIRD_PARTY_NOTICES.md.
