Architecture Overview
This section is the contributor’s map of the codebase. Read this page first — it gives you the mental models that the rest of the code assumes you already have. Then jump to the part you’re working on. (Brand new to the project? The Codebase Onboarding course walks you through everything in order, hands-on.)
The one idea that explains everything
Section titled “The one idea that explains everything”Every other calendar app models data as calendars that contain events. Musubi inverts that:
An event is not owned by a calendar. It is linked into one or more calendars.
A single event row can appear in your calendar, your partner’s, and your team’s — edited once, visible everywhere. This is the “結び (knot)” the project is named after, and it is the single most important thing to internalise. It shapes the schema (junction tables, not foreign keys), the permission model (a home calendar governs editing), and the sync engine (external events are linked in like any other).
Concretely:
flowchart LR
E[events] <-->|"calendar_events (many-to-many)"| C[calendars]
E -.->|"originCalendarID — the home calendar<br/>governs who may edit the event"| C
- Link = the same event row gains another
calendar_eventsrow. One event, many calendars. - Fork = a brand-new event row with a new owner. A divergent copy.
- Home calendar (
events.originCalendarID) decides edit rights. You can see an event through any calendar it’s linked into, but you can only edit it if you have editor/owner role on its home calendar.
Keep this diagram in your head; every subsystem is a consequence of it.
Monorepo layout
Section titled “Monorepo layout”Musubi is a pnpm workspace driven by Turborepo. Two apps, several shared packages.
Directoryapps/
Directoryapi/ Express API server (the backend)
- …
Directoryclient/ Expo / React Native app (the frontend)
- …
Directorypackages/
Directorydb/ Drizzle schema, query modules, PostgreSQL migrations
- …
Directorytypes/ Zod schemas + inferred types — the client/server contract
- …
Directorycalendar/ Recurrence (RRULE) expansion + date helpers — logic only, no UI
- …
Directoryauth/ Better Auth configuration
- …
Directoryconfig/ Environment loading (
envOrThrow)- …
Directorydocs/ This documentation site (Astro Starlight)
- …
- turbo.json Task graph (dev/build)
- docker-compose*.yml
The dependency direction is strict and worth memorising:
flowchart LR
client[apps/client]
api[apps/api]
types["packages/types<br/>(shared Zod contract)"]
calendar["packages/calendar<br/>(recurrence)"]
db["packages/db<br/>(server only)"]
auth[packages/auth]
config[packages/config]
client --> types
client --> calendar
api --> types
api --> calendar
api --> db
api --> auth
api --> config
The client has its own local SQLite database (an offline mirror) — it never connects to PostgreSQL. All server data reaches the client through the HTTP API.
The request/data flow, end to end
Section titled “The request/data flow, end to end”Here is what happens when a user creates an event on their phone:
sequenceDiagram
participant M as AddEventModal
participant S as useEventsStore
participant API as useApi (HTTPS)
participant H as handlerCreateEvent
participant DB as packages/db
participant P as sync engine
M->>S: addEvent()
S->>API: createEvent()
API->>H: POST /api/v1/events (requireAuth)
H->>H: assertCan(user, cal, editEvents)
H->>DB: createEvent()
H->>P: pushEventToProviders()
Note over H: notifyCalendarMembers() broadcast (SSE)
H-->>API: 201 + event JSON
API-->>S: write store + local SQLite cache
Most writes cover the same five concerns on the server — validate → authorise → persist → sync outward → broadcast. Provider-backed calendar writes may order provider and database work differently; the exact failure semantics are in the API guide and sync guide.
The shared contract
Section titled “The shared contract”The client and server never trust raw JSON. They communicate through Zod schemas in packages/types — Event, Calendar, Settings, Invite, the error classes, and the permission model. When you change a data shape, you change it there first, and both sides get the new type. This is the single source of truth; see Shared Packages.
Core concepts cheat-sheet
Section titled “Core concepts cheat-sheet”| Concept | One-line meaning | Lives in |
|---|---|---|
| Events beyond calendars | One event, many calendars, via calendar_events |
packages/db/src/schema.ts |
| Home calendar | originCalendarID governs edit rights |
packages/db, apps/api/src/permissions.ts |
| Link vs Fork | Share the same row vs create a divergent copy | apps/api/src/handlers/events.ts |
| Roles | owner / editor / viewer → can(role, action) |
packages/types/src/permissions.ts |
| Soft delete | deletedAt tombstone drives delta sync |
packages/db/src/queries/events.ts |
| Delta sync | Client asks “what changed since T?” | apps/client/hooks/useRefreshData.ts |
| Mirror calendars | External (Google/Outlook/CalDAV) calendars imported as normal calendars | apps/api/src/sync/ |
| Federation | Cross-server sharing: invite-as-capability + shadow accounts | apps/api/src/handlers/federation.ts, apps/client/services/federation.ts |
| Recurrence | Stored as iCal RRULE text, expanded at read time | packages/calendar/src/recurrence.ts |
Where to go next
Section titled “Where to go next”- Building a backend feature? → API Server
- Building a screen or UI? → Client App
- Adding a data field? → Data Model then Shared Packages
- Integrating a calendar provider? → Sync Engine
- Ready to open a PR? → Contributing