Skip to content

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

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_events row. 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.

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.

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 client and server never trust raw JSON. They communicate through Zod schemas in packages/typesEvent, 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.

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