A lightweight SvelteKit app that demonstrates the core orchestration patterns from CommunionLink: auth, organizations, invitations, and a registry-driven plugin hub.
The app now uses a restrained shadcn-style UI with shared light/dark theme tokens, while still keeping the product intentionally simple.
npm install
# Create a .env file (see below)
npm run devPUBLIC_SUPABASE_URL=https://your-project.supabase.co
PUBLIC_SUPABASE_ANON_KEY=your-anon-key
Apply the Supabase migrations in order:
supabase db push # if using Supabase CLI
# or paste each file in supabase/migrations/ into the SQL editorIf you pull newer hub, organization, profile, or messaging code into an existing database, run migrations before restarting the app. Current app work depends on migrations 017 through 043. Missing earlier hub migrations can still surface runtime errors such as column hub_events.ends_at does not exist, relation "public.hub_event_reminders" does not exist, column hub_broadcasts.delivery_state does not exist, column hub_events.delivery_state does not exist, relation "public.hub_notification_preferences" does not exist, relation "public.hub_execution_ledger" does not exist, column hub_notification_reads.notification_key does not exist, relation "public.hub_event_attendances" does not exist, column reference "profile_id" is ambiguous, relation "public.hub_operator_workflow_state" does not exist, or column hub_notification_preferences.message_enabled does not exist. Missing migration 026_expand_member_event_visibility_for_recent_history.sql will also make member-facing recent event history disappear as soon as an event starts, missing 028_fix_get_organization_members_ambiguity.sql can break member-directory and admin roster views, and missing 029_create_hub_operator_workflow_state.sql can break shared queue and follow-up review persistence. The newer release batches matter too: missing 030_add_broadcast_acknowledgments.sql breaks broadcast acknowledgments and follow-up views, missing 031_add_contact_last_read_at.sql breaks message seen-state hydration through get_contact_last_read_at(uuid[]), missing 032_add_push_subscriptions.sql breaks device push registration against public.push_subscriptions, missing 034_allow_org_name_update.sql breaks organization name edits through update_organization_name(uuid, text), missing 035_add_profile_bio.sql or 037_include_profile_bio_in_member_roster.sql breaks profile bio saves and directory/member-roster bio rendering, missing 036_add_account_deletion_request.sql or 038_add_account_deletion_review_fields.sql breaks deletion request submission and admin review queues, and missing 039_soft_delete_messages.sql breaks direct-message deletion plus thread hydration that reads messages.deleted_at. The current 0.1.38 batch matters too: missing 040_add_message_thread_archive.sql breaks archived-thread hydration and archive or restore actions that read message_threads.archived_at or call archive_message_thread(uuid) and unarchive_message_thread(uuid), missing 041_add_message_thread_mute.sql breaks muted-thread hydration and mute or unmute actions that read message_threads.muted_at or call mute_message_thread(uuid) and unmute_message_thread(uuid), missing 042_add_invitation_expiry.sql breaks invitation review and resend recovery that read organization_invitations.expires_at, and missing 043_add_hub_plugin_visibility.sql breaks role-targeted hub section settings that read hub_plugins.visibility_mode.
If you need an operational recovery checklist instead of a feature roadmap, use docs/hub-schema-recovery.md. If you are working on the current release batch, start with docs/roadmap-0.1.38.md and docs/roadmap-0.1.38-checklist.md. If you need the latest shipped release handoff, use docs/release-notes-0.1.37.md. If you need the prior rollout runbook, use docs/rollout-0.1.32-checklist.md. If you need older pre-0.1.30 handoff context, use docs/agent-handoff-0.1.29.md.
These are the best starting points for contributors:
- Start with the newest roadmap, then use the latest shipped release notes if you need the last completed handoff.
- docs/roadmap-0.1.38.md
- docs/roadmap-0.1.38-checklist.md
- docs/release-notes-0.1.37.md
- docs/roadmap-0.1.37.md
- docs/roadmap-0.1.37-checklist.md
- docs/release-notes-0.1.36.md
- docs/roadmap-0.1.36.md
- docs/roadmap-0.1.36-checklist.md
- docs/roadmap-0.1.35.md
- docs/roadmap-0.1.35-checklist.md
- docs/roadmap-0.1.32.md
- docs/roadmap-0.1.32-checklist.md
- docs/ui-guardrails.md
The app has six layers. Every file belongs to exactly one layer, and layers only depend downward.
┌─────────────────────────────┐
│ Routes (pages/composition) │ src/routes/
├─────────────────────────────┤
│ Components (app UI) │ src/lib/components/
├─────────────────────────────┤
│ Stores (reactive state) │ src/lib/stores/
├─────────────────────────────┤
│ Repositories (Supabase) │ src/lib/repositories/
├─────────────────────────────┤
│ Models (types + helpers) │ src/lib/models/
├─────────────────────────────┤
│ Migrations (DB schema) │ supabase/migrations/
└─────────────────────────────┘
The rule: components compose UI → stores coordinate state → repositories talk to Supabase → models define shapes. No layer reaches across another.
src/
lib/
supabaseClient.ts ← Supabase client singleton
models/
userModel.ts ← UserDetails type + defaults
organizationModel.ts ← Organization types + normalizers
authHelpers.ts ← Validation, error mapping
repositories/
profileRepository.ts ← Auth + profile CRUD
organizationRepository.ts ← Org CRUD, join, invite
hubRepository.ts ← Barrel exports for hub repository domains
hubRepository/
broadcasts.ts ← Broadcast lifecycle queries and mutations
events.ts ← Event, RSVP, attendance, and reminder queries
executionLedger.ts ← Scheduled delivery and reminder execution data
notifications.ts ← Alert preferences and read-state queries
resources.ts ← Hub resource CRUD and ordering
plugins.ts ← Plugin activation queries and mutations
stores/
currentUser.svelte.ts ← Reactive auth + profile state
currentOrganization.svelte.ts ← Reactive org membership state
authBoundary.svelte.ts ← Login/onboarding gate logic
pluginRegistry.ts ← Plugin definitions + helpers
currentHub.svelte.ts ← Hub coordinator store public API
currentHub/
broadcasts.ts ← Broadcast-focused store mutations
events.ts ← Event, RSVP, attendance, and reminder store mutations
notifications.ts ← Alert preference and read-state store mutations
resources.ts ← Resource store mutations and reordering
load.ts ← Hub load pipeline and hydration helpers
derived.ts ← Read-only selectors and derived hub summaries
sync.ts ← Delivery metadata and execution-ledger synchronization
state.ts ← Shared reset/default/hydration state helpers
components/
auth/
AuthGate.svelte ← Top-level access controller
LoginForm.svelte ← Dual-channel login form
NameOnboarding.svelte ← Name collection modal
OrganizationOnboarding.svelte ← Create/join/invite org
profile/
ProfileSection.svelte ← Profile summary card
ProfileDetailsCard.svelte ← Profile details editor
ProfileSecurityCard.svelte ← Security editor
organization/
OrganizationOverviewCard.svelte ← Org overview content
OrganizationAccessCard.svelte ← Join code + invitations
hub/
member/
HubActivityFeed.svelte ← Mixed recent activity feed
MemberCommitmentsCard.svelte ← Member commitments and recent events
BroadcastsSection.svelte ← Member broadcast list
EventsSection.svelte ← Member event list
ResourcesSection.svelte ← Shared links and documents
admin/
HubManageSummaryCard.svelte ← Admin hub summary
PluginActivationCard.svelte ← Toggle plugins on/off
BroadcastEditor.svelte ← Create/delete broadcasts
EventEditor.svelte ← Create/delete events
ui/
Header.svelte ← Shared header shell
BottomNav.svelte ← Shared bottom navigation
Toaster.svelte ← Shared toast outlet
UnsavedChangesGuard.svelte ← Global unsaved-work navigation guard
routes/
+layout.svelte ← Wraps all pages with AuthGate
+page.svelte ← Home/account overview
hub/
+page.svelte ← Hub coordinator (registry-driven)
manage/
+layout.svelte ← Hub manage shell + tabs
sections/+page.svelte ← Plugin setup
content/+page.svelte ← Broadcast/event editors
organization/
+layout.svelte ← Organization shell + tabs
overview/+page.svelte ← Organization overview
access/+page.svelte ← Join code + invites
members/+page.svelte ← Member visibility for admins
profile/
+layout.svelte ← Profile shell + tabs
details/+page.svelte ← Profile details
security/+page.svelte ← Security settings
supabase/
migrations/
001_create_profiles.sql
002_create_organizations.sql
003_create_invitations.sql
004_create_hub_tables.sql
006_add_profile_avatar_support.sql
007_add_get_organization_members.sql
currentUserrestores the Supabase session and subscribes to auth changes.currentOrganizationresolves whether the user belongs to an organization.authBoundaryderives three gates from those stores:- Not logged in → show
LoginForm - Logged in but no name → show
NameOnboarding - Logged in but no organization → show
OrganizationOnboarding
- Not logged in → show
AuthGaterenders the correct surface. Routes never solve auth independently.
LoginForm → currentUser.login → Supabase Auth
↓ (auth state change)
currentUser.isLoggedIn = true
↓
authBoundary.needsNameOnboarding → NameOnboarding
↓ (name saved)
authBoundary.needsOrganizationOnboarding → OrganizationOnboarding
↓ (org created/joined)
App renders route content
The hub uses a registry-driven pattern. Plugins are not hardcoded into pages — they are defined in a central registry, and the hub page loops over the active ones.
export const PLUGIN_REGISTRY: Record<PluginKey, PluginDefinition> = {
broadcasts: { key: 'broadcasts', title: 'Broadcasts', ... },
events: { key: 'events', title: 'Events', ... },
};<!-- src/routes/hub/+page.svelte -->
{#each activePlugins as plugin (plugin.key)}
{#if plugin.key === 'broadcasts'}
<BroadcastsSection />
{:else if plugin.key === 'events'}
<EventsSection />
{/if}
{/each}hub_pluginstable stores(organization_id, plugin_key, is_enabled).- Missing row = disabled.
- Admin toggles go through
currentHub.toggle()→hubRepository.togglePlugin().
The hub now follows a small coordinator pattern instead of keeping all behavior in one giant store file.
- Routes and components call the public API on
currentHub.svelte.ts. currentHub/load.tsfetches and hydrates hub state for the active organization.currentHub/derived.tscomputes read-only views like activity feeds, delivery status, and engagement summaries.currentHub/broadcasts.ts,events.ts,notifications.ts, andresources.tsown mutation logic by domain.currentHub/sync.tskeeps scheduled delivery metadata and the execution ledger aligned with the latest content state.hubRepository/files stay focused on Supabase reads/writes by table domain.
This keeps the dependency direction the same as the rest of the app:
components/routes -> currentHub store -> hubRepository -> Supabase
When you are debugging a hub issue, use this shortcut:
- Load or hydration bug: start in
currentHub/load.ts - Wrong UI summary or label: start in
currentHub/derived.ts - Mutation bug: start in the matching
currentHub/<domain>.ts - Missing/stale delivery or queue state: start in
currentHub/sync.ts - SQL or query bug: start in
src/lib/repositories/hubRepository/
- Add a key to the
PluginKeyunion inpluginRegistry.ts. - Add a
PluginDefinitionentry inPLUGIN_REGISTRY. - Update the
PluginStateMaptype defaults. - Create a member section component in
components/hub/member/. - Create an admin editor component in
components/hub/admin/. - Add an
{#if}branch in the hub coordinator page. - Add a Supabase migration for the plugin's data table.
- Add repository functions in
hubRepository.ts. - Wire the store in
currentHub.svelte.ts.
Organizations are minimal: id, name, join_code. No types, tiers, or branding.
Three ways to join:
| Method | Who | How |
|---|---|---|
| Create | Anyone | Becomes the first admin |
| Join code | Anyone | Admin shares a 6-character code |
| Invitation | Admin sends | Email or phone token |
Admin capabilities:
- Regenerate join code
- Send invitations (email or phone)
- View pending invitations
- View member count
npm test # Run all unit tests
npm run test:watch # Watch mode
npm run test:smoke:install # Install Chromium for Playwright once per machine
npm run test:smoke # Run the browser smoke harness against local routesThe browser smoke harness runs the real app routes in a fixture-backed smoke mode via ?smoke=1. It covers home, Alerts, manage content, manage sections, and profile alert preferences without requiring a live signed-in Supabase session, but it is still a release-safety harness rather than a replacement for normal Supabase-backed development.
The smoke flows intentionally stay narrow. They currently cover one happy-path plugin toggle, one alert filter/read interaction, one attendance closeout mutation, one workflow write plus stale re-surface path in the operations queue, and one workflow-schema failure path. They do not try to exhaustively cover every editor path, reminder edge case, or multi-user synchronization scenario.
Smoke mode is also intentionally local-only. Plugin toggles, alert read state, attendance closeout, queue triage, broadcast acknowledgments, and messaging all mutate in-memory or browser-local state during smoke runs so the harness remains safe under blank PUBLIC_SUPABASE_* env vars. Real-time messaging falls back to fixture-backed polling in smoke mode, and push subscription registration is skipped entirely.
For manual failure-path checks, ?smoke=1&smokeScenario=stale-workflow-schema simulates a missing shared workflow-state schema and should surface the 029_create_hub_operator_workflow_state.sql recovery guidance in both home and manage load states. stale-hub-schema is still available if you need to force the older delivery-column drift path.
Tests cover:
- Auth validation and error mapping (
authHelpers.test.ts) - Plugin registry logic (
pluginRegistry.test.ts) - Organization model normalizers (
organizationModel.test.ts)
Apply in order. Each migration is additive — never drops existing tables.
| File | What it creates |
|---|---|
001_create_profiles.sql |
profiles table + RLS |
002_create_organizations.sql |
organizations, memberships, RPCs |
003_create_invitations.sql |
organization_invitations + accept RPC |
004_create_hub_tables.sql |
hub_plugins, hub_broadcasts, hub_events + RLS |
- No styles. This is a structural wireframe. Add your own CSS/Tailwind/etc.
- Normal app flows stay Supabase-backed. A narrow fixture-backed smoke mode exists only for browser rollout checks; it does not replace local Supabase setup.
- Stores are singletons. Each store is a class instance exported as a module-level
const. Svelte 5$stateand$derivedmake them reactive. - Repositories are the Supabase boundary. Only
src/lib/repositories/files know table names, column shapes, or RPC names. - Models are pure. No side effects, no imports from Supabase or Svelte.
- The plugin registry is the source of truth. Titles, descriptions, and ordering come from the registry, not from components.