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.
Daily development
Section titled “Daily development”| 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.
Verification
Section titled “Verification”pnpm checkcheck is the contributor baseline and runs, in order:
- release/toolchain metadata validation;
- workspace peer-dependency validation;
- client TypeScript (
tsc --noEmit); - API TypeScript (
tsc --noEmit --skipLibCheck); - twelve assertion-based self-check files;
- client ESLint with the repository’s locked warning budget; and
- the root build (currently the Starlight package is the only workspace with a
buildscript).
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 |
Continuous integration
Section titled “Continuous integration”.github/workflows/quality.yml runs for every pull request and every push to
main. It has two independent jobs:
- frozen install →
pnpm check→ regenerate Drizzle migrations and fail if the working tree changes; and - 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.
Existing self-checks
Section titled “Existing self-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:
ENVIRONMENT=test DATABASE_URL=postgresql://... BETTER_AUTH_URL=http://localhost:7531 \ pnpm test:federation-dbRun 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.
Provider database integration
Section titled “Provider database integration”ENVIRONMENT=test \DATABASE_URL=postgresql://... \BETTER_AUTH_URL=http://localhost:7531 \BETTER_AUTH_SECRET=<test-only-secret> \pnpm test:provider-dbRun 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.
Database
Section titled “Database”| 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:
ENVIRONMENT=test \DATABASE_URL=postgresql://postgres:postgres@localhost:5432/musubi_test \BETTER_AUTH_URL=http://localhost:7531 \pnpm test:db:oauthAlways review generated SQL:
pnpm db:generategit diff -- packages/db/drizzlepnpm db:migrateSchema 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.
Documentation
Section titled “Documentation”pnpm docs:devpnpm docs:buildThe 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.
Versions and releases
Section titled “Versions and releases”The root package.json is the single source for both the Musubi product version
and pnpm:
versionis the user-visible SemVer release.apps/client/app.config.tsreads it directly for the native Expo version.packageManagerpins 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:
pnpm release:verifypnpm release:verify 0.1.2pnpm checkThe 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.
Docker and self-hosting
Section titled “Docker and self-hosting”docker compose up -ddocker compose psdocker compose logs -f apidocker compose pulldocker compose up -ddocker compose -f docker-compose.dokploy.yml configdocker compose -f docker-compose.dokploy.yml up -dThe Dokploy file expects the external dokploy-network.
In docker-compose.yml, replace the published image: with the commented
build: block, then run:
docker compose up -d --builddocker build -f packages/docs/Dockerfile -t musubi-docs:local .docker run --rm -p 4321:4321 musubi-docs:localOpen http://localhost:4321/docs/.
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.
Backups
Section titled “Backups”./ops/backup-postgres.sh./ops/verify-postgres-backup.sh /path/to/musubi-backup.dumpCommon overrides:
MUSUBI_BACKUP_DIR=/var/backups/musubi \MUSUBI_BACKUP_RETENTION_DAYS=30 \./ops/backup-postgres.shThe 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.
Before opening a pull request
Section titled “Before opening a pull request”git status --shortgit diff --checkpnpm checkThen 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.