Skip to content

Commands & checks

Run workspace commands from the repository root unless a section says otherwise. Prefer pnpm --filter <workspace> ... over changing directories: copy-pasted commands then work from the same location in local shells and CI.

Goal Command
Activate the repository’s exact pnpm version corepack enable
Install exactly the workspace declared by the lockfile pnpm install --frozen-lockfile
Start API, client Metro, and docs in the Turbo TUI pnpm dev
Start all dev tasks with copyable logs pnpm exec turbo run dev --ui=stream
Start only the API pnpm --filter @musubi/api dev
Start only Expo/Metro pnpm --filter @musubi/client start
Build/install Android development client pnpm --filter @musubi/client android
Build/install iOS development client (macOS) pnpm --filter @musubi/client ios
Start only Starlight pnpm docs:dev

The client package’s dev script uses Metro’s --localhost mode. Use start when a physical device needs LAN discovery.

Terminal window
pnpm check

check is the contributor baseline. It validates release metadata and peer dependencies, typechecks the design system, native client, API, and web client, runs package self-checks and Vitest suites across config, types, email, auth, calendar, client, API, database, and web, lints both clients, then builds the web client and documentation site.

check does not run the browser E2E suite. It is a separate CI job because it needs a dev server and a Chromium download, and leaving it out keeps the baseline fast. The cost is real: a change can pass check and still break E2E, which is a thing that has happened. Before opening a pull request that touches the web client, run:

Terminal window
pnpm verify # check, then the E2E suite

Run an individual layer while iterating:

Check Command
All TypeScript pnpm typecheck
Client TypeScript pnpm --filter @musubi/client exec tsc --noEmit
API TypeScript pnpm exec tsc -p apps/api/tsconfig.json --noEmit --skipLibCheck
Web TypeScript pnpm --filter @musubi/web typecheck
All existing self-checks pnpm test
Browser E2E (Chromium, needs a dev server) pnpm test:e2e
Everything, E2E included pnpm verify
Docs production build + Pagefind index pnpm docs:build
Root Turbo build pnpm build
Client ESLint pnpm lint
Workspace peer dependencies pnpm peers:check
Release/toolchain metadata pnpm release:verify
Secrets in the browser bundle pnpm scan:bundle

pnpm scan:bundle builds the web client with a canary in every server-only variable (BETTER_AUTH_SECRET, DATABASE_URL, SMTP_PASS, …) and fails if one of them turns up in a file the browser can fetch. It also matches private-key blocks, credentialled database URLs and provider key prefixes, and — on a machine that has a real .env — the actual values in it. CI runs it as its own job.

.github/workflows/quality.yml runs for every pull request and every push to main. Independent jobs run the clean-install verification and migration-drift check, the Chromium web E2E suite, the browser-bundle secret scan, and local builds of both production images. The manual release workflow reuses this gate before publishing.

These are plain Node assertions executed with tsx, not a test-runner suite:

File Covers
apps/client/lib/eventForm.test.ts Event-form validation
apps/client/lib/rrule.test.ts RRULE building and parsing
apps/client/lib/ics.test.ts Lightweight client .ics import
apps/api/src/sync/orchestrator.test.ts Provider/account failure isolation, retry, strict/scoped modes
apps/api/src/sync/adapters/provider_http.test.ts Fake Google/Graph pagination, 410 reset, partial-page discard, series cache reset/reuse
apps/api/src/federation.test.ts Member-token timing, origin validation, invite-preview minimization
apps/api/src/request_validation.test.ts UUID/timestamp guards and HTTP error classification
apps/api/src/scheduling.test.ts Non-overlapping scheduled execution and guard recovery
apps/api/src/handlers/calendars.test.ts Calendar-detail membership authorization and data-read ordering
apps/api/src/sync/oauth.test.ts OAuth refresh/revoke helpers and token crypto
apps/api/src/sync/adapters/microsoft.test.ts Graph date/cursor/event conversion
packages/db/src/schema.test.ts One-row settings and event–calendar uniqueness constraints

Federation also has an opt-in database lifecycle check:

Terminal window
ENVIRONMENT=test DATABASE_URL=postgresql://... BETTER_AUTH_URL=http://localhost:7531 \
pnpm test:federation-db

Run it only against a disposable, fully migrated PostgreSQL database. It creates and removes scoped fixtures, and verifies expiry cleanup, compare-and-swap rotation, unproved-profile isolation, proved shadow reuse and final-membership revocation.

When adding pure logic, follow the current assertion style or introduce a project-wide runner as one focused change; do not create a third test convention.

Terminal window
ENVIRONMENT=test \
DATABASE_URL=postgresql://... \
BETTER_AUTH_URL=http://localhost:7531 \
BETTER_AUTH_SECRET=<test-only-secret> \
pnpm test:provider-db

Run this only against a disposable, fully migrated PostgreSQL database. The test starts its own local fake OAuth token endpoint, scopes fixtures to random IDs, and cleans them up. It verifies encrypted token rotation, account-specific permanent revocation and transient retry behavior without live Google/Microsoft credentials.

Goal Command
Generate SQL from schema.ts pnpm db:generate
Apply pending PostgreSQL migrations pnpm db:migrate
Start only the bundled database docker compose up -d db
Inspect database service docker compose ps db
Tail database logs docker compose logs -f db

The account-scoped OAuth fallback has a PostgreSQL integration self-check:

Terminal window
ENVIRONMENT=test \
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/musubi_test \
BETTER_AUTH_URL=http://localhost:7531 \
pnpm test:db:oauth

Always review generated SQL:

Terminal window
pnpm db:generate
git diff -- packages/db/drizzle
pnpm db:migrate

Schema changes and their migration must ship together. Drizzle cannot infer intent for every rename; verify that a rename did not become destructive drop-and-create SQL.

Terminal window
pnpm docs:dev
pnpm docs:build

The dev URL is http://localhost:4321/docs/. The build validates Starlight frontmatter, MDX/component rendering and Mermaid transformation, then creates the Pagefind search index. Browser QA/link crawling is a separate verification step; a successful build alone does not prove every handwritten URL resolves.

The docs are served under /docs, so test that prefix explicitly. The generated files live under packages/docs/dist/ and are ignored.

The root package.json is the single source for both the Musubi product version and pnpm:

  • version is the user-visible SemVer release. apps/client/app.config.ts reads it directly for the native Expo version.
  • packageManager pins the pnpm version Corepack uses locally, in CI, and in both Docker builds.
  • workspace manifests are private and intentionally have neither field.
  • EAS keeps platform build numbers remotely; those monotonically increasing numbers are separate from the user-visible product version.

For a release, change only the root version, then validate it:

Terminal window
pnpm release:verify
pnpm release:verify 0.1.2
pnpm check

The optional argument represents the version requested by the release job and must match the manifest. The validator also rejects nested toolchain/version metadata and distribution placeholders. The manual Publish Docker image workflow runs the same command before logging in to Docker Hub, publishing an image, or creating a tag.

Production iOS builds additionally require EXPO_PUBLIC_IOS_APP_STORE_URL=https://apps.apple.com/.../id<digits> in the EAS build environment. app.config.ts rejects a missing or malformed URL for the production profile so a forced-update screen cannot ship with a dead link.

Terminal window
docker compose up -d # full stack: gateway + web + api + db
docker compose ps
docker compose logs -f api
docker compose pull && docker compose up -d
docker compose -f docker-compose.api.yml up -d # API + db only

Container startup applies PostgreSQL migrations through packages/db/src/migrate.ts before starting the API. All runtime images use digest-pinned Node, frozen installs, health checks, and a non-root user. The API and docs use production pnpm deploy closures; the web builder installs its full build graph, but only Nitro output reaches runtime. Global npm/Corepack binaries are removed from runtime stages.

Terminal window
./ops/backup-postgres.sh
./ops/verify-postgres-backup.sh /path/to/musubi-backup.dump

Common overrides:

Terminal window
MUSUBI_BACKUP_DIR=/var/backups/musubi \
MUSUBI_BACKUP_RETENTION_DAYS=30 \
./ops/backup-postgres.sh

The backup script uses a private umask, writes to a partial file, validates the dump with pg_restore --list, and only then publishes it. The verification script restores into an isolated temporary PostgreSQL container.

Terminal window
git status --short
git diff --check
pnpm check

Then run the platform-specific flow you changed. Examples:

  • API handler: happy path, unauthorized request, forbidden role, invalid body.
  • Client interaction: iOS and Android where practical, plus cold cache and refreshed data.
  • Provider adapter: create/update/delete, all-day, recurrence, cursor reset, read-only calendar, token expiry.
  • Documentation: desktop + mobile navigation, search, changed links, and a production build.