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. 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:
pnpm verify # check, then the E2E suiteRun 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.
Continuous integration
Section titled “Continuous integration”.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.
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 -d # full stack: gateway + web + api + dbdocker compose psdocker compose logs -f apidocker compose pull && docker compose up -d
docker compose -f docker-compose.api.yml up -d # API + db onlydocker compose -f docker-compose.dokploy.yml configPUBLIC_HOST=musubi.example.com docker compose -f docker-compose.dokploy.yml up -dThe Dokploy file expects the external dokploy-network and PUBLIC_HOST; in
Dokploy itself both come from the project, so this is only for checking the
file by hand.
In docker-compose.yml, replace the published image: with the commented
build: block (either service — the web image has one too), 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.
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.
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.