Shared Packages
The packages/ workspaces hold everything shared between the two apps. db has its own page; this page covers the other five. The theme running through all of them: define a thing once, import it everywhere.
types (packages/types)
Section titled “types (packages/types)”The client/server contract. Every data shape is a Zod schema; the TypeScript type is inferred from it, so validation and types never drift. Both apps import from @musubi/types — there is no duplicated Event interface anywhere.
| File | Exports |
|---|---|
event.ts |
EventSchema, Event — start/end via z.coerce.date(), calendars: string[], originCalendarID nullish, recurrence nullish |
calendar.ts |
CalendarSchema, Calendar, minimal CalendarInvitePreview, providerFlavor() (detects "apple" from iCloud CalDAV) |
federation.ts |
Member-token lifetime, timestamp parsing, proactive-rotation decision |
settings.ts |
SettingsSchema, Settings, CalendarView enum |
invite.ts, user.ts, google.ts |
their schemas + inferred types |
errors.ts |
BadRequestError, UnauthorizedError, ForbiddenError, NotFoundError (each has a kind for HTTP mapping) |
permissions.ts |
CalendarRole, CalendarAction, and can(role, action) |
Permissions are the single source of truth
Section titled “Permissions are the single source of truth”permissions.ts is worth calling out because both sides depend on it:
const PERMISSIONS: Record<CalendarRole, CalendarAction[]> = { owner: ["editCalendar", "deleteCalendar", "manageMembers", "editEvents", "invite"], editor: ["editEvents", "invite"], viewer: [],};export function can(role, action): boolean { … }The server calls can() (via assertCan) to gate handlers; the client calls the same can() to hide or disable UI the user isn’t allowed to use. If you add an action, update both the CalendarAction type and the PERMISSIONS record — nothing else.
calendar (packages/calendar)
Section titled “calendar (packages/calendar)”Recurrence, date and calendar layout helpers — pure logic, no UI. The
React Native and DOM renderers stay in apps/client and apps/web, while both
clients share their range and segmentation rules through this package.
Recurrence is stored on events.recurrence as iCalendar text — a bare RRULE for simple cases, or multi-line RRULE + EXDATE/RDATE when there are exceptions. It is expanded into individual occurrences at read time, never stored as instances.
recurrence.ts exports:
| Function | Purpose |
|---|---|
splitRecurrence(str) |
→ { rrule, extras[] } (separates the rule from EXDATE/RDATE lines) |
joinRecurrence(rrule, extras) |
the inverse |
excludeOccurrence(rec, date) |
add an EXDATE — “delete just this one occurrence” |
endSeriesBefore(rec, date) |
set UNTIL just before a date — “delete this and future” |
expandRecurringEvents(events, start, end) |
expand rules into occurrences within a window; occurrence ids are "<id>_<startMs>" for stable React keys |
Parsed rules are memoised in a Map keyed by recurrence@dtstart so swiping the calendar doesn’t re-parse. datetime.ts holds the all-day handling (eventDay() reinterprets a UTC-midnight all-day date in the local frame); interfaces.ts has the minimal ICalendarEventBase shape the expansion works against.
The @musubi/calendar/layout subpath keeps layout consumers separate from the
RRULE parser:
| Module | Shared responsibility |
|---|---|
month-grid.ts, ranges.ts |
six-week Month grids, visible ranges, week starts and calendar-day navigation |
day-buckets.ts |
local/UTC day keys, day buckets and all-day continuation segments |
all-day-spans.ts |
continuous row spans and stable lane assignment |
day-segments.ts, overlaps.ts |
per-day clipping and transitive overlap columns |
Musubi’s internal all-day end is the inclusive final calendar date.
Google, Microsoft and CalDAV adapters convert their exclusive provider end at
the API boundary; UI layout must not subtract another day.
auth (packages/auth)
Section titled “auth (packages/auth)”The Better Auth configuration (lib/auth.ts): the Drizzle/Postgres adapter, session + bearer + Expo plugins, email/password, and Google OAuth (accessType: "offline" for refresh tokens; account linking across different emails enabled).
The one hook contributors hit most: on user creation, a default personal calendar is auto-created (isDefault: true, undeletable). Every user — email or social — gets one.
config (packages/config)
Section titled “config (packages/config)”Environment loading with a fail-fast helper:
function envOrThrow(key: string): string { const value = process.env[key]; if (!value) throw new Error(`Missing value from ENV on KEY: ${key}`); return value;}Required at boot (server won’t start without them): DATABASE_URL, ENVIRONMENT, BETTER_AUTH_URL, BETTER_AUTH_SECRET.
Optional, capability-gated: SMTP, Google/Microsoft OAuth credentials, CALDAV_ENC_KEY. The pattern is deliberate — a missing optional key disables one feature instead of crashing the server, so you can run a minimal stack locally. SMTP is checked once before listening; restart the API after changing it. See Running Locally for the full variable table.
LOG_LEVEL is optional and defaults to info; invalid values fail fast at boot.
The config package also exports the process-wide structured logger, shared by
the API and auth hooks.
emails (packages/emails)
Section titled “emails (packages/emails)”Transactional email: sendEmail() (a nodemailer SMTP transport built from config.smtp) plus the HTML templates for password reset, deletion, verification, address changes, and email sign-in codes. @musubi/auth uses the send/template surface; apps/api calls initializeEmailCapability() before listening and serves its instant canSendEmail() snapshot through capability discovery. The package depends only on @musubi/config.
How the packages depend on each other
Section titled “How the packages depend on each other”flowchart TD
client[apps/client]
web[apps/web]
api[apps/api]
auth[packages/auth]
db[packages/db]
calendar[packages/calendar]
types[packages/types]
config[packages/config]
emails[packages/emails]
client --> types
client --> calendar
web --> types
web --> calendar
api --> types
api --> calendar
api --> db
api --> auth
api --> emails
auth --> db
db --> types
api --> config
db --> config
auth --> config
auth --> emails
emails --> config
packages/types and packages/config are the leaves everything leans on — change them thoughtfully.