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 and runs, in order:

  1. release/toolchain metadata validation;
  2. workspace peer-dependency validation;
  3. client TypeScript (tsc --noEmit);
  4. API TypeScript (tsc --noEmit --skipLibCheck);
  5. twelve assertion-based self-check files;
  6. client ESLint with the repository’s locked warning budget; and
  7. the root build (currently the Starlight package is the only workspace with a build script).

Run an individual layer while iterating:

Check Command
Client TypeScript pnpm --filter @musubi/client exec tsc --noEmit
API TypeScript pnpm exec tsc -p apps/api/tsconfig.json --noEmit --skipLibCheck
All existing self-checks pnpm test
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

.github/workflows/quality.yml runs for every pull request and every push to main. It has two independent jobs:

  1. frozen install → pnpm check → regenerate Drizzle migrations and fail if the working tree changes; and
  2. build the production API image without publishing it.

The manual Docker release workflow calls the same reusable quality workflow and cannot publish an image or GitHub release until both jobs pass. Keep local verification fast with pnpm check; CI owns the clean-install, migration-drift, and container-build checks.

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
docker compose ps
docker compose logs -f api
docker compose pull
docker compose up -d

Container startup applies PostgreSQL migrations through packages/db/src/migrate.ts before starting the API. Both repository Dockerfiles use the root Corepack pin, a frozen lockfile, a production-only dependency closure, and a digest-pinned Node base image. The API uses pnpm deploy so its runtime does not inherit Expo through Drizzle’s shared monorepo peer context.

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.