Skip to content

Client

The client is a React Native app built with Expo SDK 56. It runs on iOS and Android.

ConcernLibrary
FrameworkExpo SDK 56 / React Native 0.85
RoutingExpo Router — file-based, tab + stack navigation
State managementZustand
Calendar component@musubi/calendar — forked in-repo, extended with recurrence
AnimationsReanimated v4 + Gesture Handler
Auth clientBetter Auth
FontsInter Tight, Noto Serif, Shippori Mincho B1
apps/client/
├── app/
│ ├── _layout.tsx # Root layout — font loading, auth guard, splash
│ ├── (auth)/
│ │ ├── welcome.tsx # Landing screen — server URL + sign in / sign up
│ │ ├── sign-in.tsx # Email + password sign-in
│ │ └── sign-up.tsx # Registration (step 1 of 3)
│ ├── (tabs)/
│ │ ├── _layout.tsx # Tab navigator + initial data fetch
│ │ ├── index.tsx # Main calendar view
│ │ ├── agenda.tsx # Upcoming events list
│ │ ├── calendars.tsx # Manage calendars
│ │ └── settings.tsx # User settings
│ └── invite/
│ └── [token].tsx # Deep-link invite acceptance
├── components/
│ ├── calendar/ # Calendar-specific UI (modals, filter bar, skeletons…)
│ └── ... # General components
├── constants/
│ ├── colors.ts # Named colour swatches (appColors array)
│ ├── theme.ts # StyleSheet, fonts, calendarTheme
│ └── const.ts # Japanese month/day name arrays
├── hooks/
│ ├── useVisibleEvents.ts # Filters + sorts events by active calendars
│ ├── useEventsStream.ts # SSE connection for real-time event updates
│ └── useModalAnimation.ts # Shared bottom-sheet animation hook
├── services/
│ └── api.ts # Typed HTTP client for all API endpoints
├── store/
│ ├── useEventsStore.ts # Events state + CRUD
│ ├── useCalendarsStore.ts # Calendars state, active filter, solo mode
│ └── useSettingsStore.ts # User settings (view mode, week start, kanji)
└── contexts/
└── ServerContext.tsx # Server URL + auth client instance

The first screen new users see. Lets them set a custom server URL (for self-hosters), then navigate to sign-in or sign-up.

Three-step account creation. Step 1 (currently implemented): name, email, and passphrase. Step 2 and 3 are planned — see What’s Coming.

Email and passphrase. Both fields are wired for platform autofill (textContentType on iOS, autoComplete on Android) so password managers including Bitwarden work out of the box.

The main screen. Displays a calendar in day, week, or month view. Recurring events are expanded from their RRULE strings for the visible date range. Tap an event to see details; use the FAB to create a new one. Long-press a calendar filter pill to isolate (solo) that calendar.

A scrollable list of upcoming events across all active calendars, grouped by date. Only shows events from today onwards.

Create calendars, view members, send invite links, and manage calendar settings.

Set the default calendar view, week start day, and toggle the kanji header decoration.

Handles deep links (https://musubi.frgtn.dev/invite/<token>). Shows the calendar you’re being invited to, with member count and upcoming events. If you’re already a member of that calendar, the screen skips straight to the app.

Musubi uses a forked version of react-native-big-calendar, maintained in packages/calendar/. The fork adds:

  • Recurrence expansionexpandRecurringEvents() turns RRULE strings into individual occurrences for the current view window
  • Enriched events optimisation — pre-computes a date-keyed lookup map so per-cell rendering does O(1) lookups instead of scanning the full event array

Use Calendar from @musubi/calendar, not the npm package directly:

import { Calendar } from '@musubi/calendar';

Three Zustand stores cover all app state:

useEventsStore — the list of events fetched from the server, and the CRUD actions that call the API and update the store.

useCalendarsStore — the list of calendars, which ones are currently shown (activeCals), and solo mode (long-pressing a filter pill to isolate one calendar).

useSettingsStore — user preferences persisted via the API.

useEventsStream opens a persistent SSE connection to GET /api/stream. The server pushes event payloads whenever something changes in a calendar you’re a member of. The hook calls the appropriate store actions to keep local state current.

The app uses a fixed dark palette. Named tokens live in constants/theme.ts:

TokenHexUsage
fg#e8e4d9Primary text
fg272% of fgSecondary text
fg348% of fgMuted text, labels
fg428% of fgPlaceholders
accent#c8553dButtons, errors, highlights
bg#0c0c0eMain background
bg1#131316Tab bar, cards
bg2#1a1a1eElevated surfaces
bg3#222226Pills, input backgrounds

Never hardcode hex values in components. Always use colors from constants/theme.ts.

  1. Create a file in the appropriate app/ directory
  2. Export a default React component as the screen
  3. If it’s a new tab, add it to app/(tabs)/_layout.tsx
  4. Use styles.screen as the root container style — it handles background colour and flex