Skip to content

Development

Requirements: Node.js ≥ 22 and npm. Nothing else — SQLite is embedded.

bash
npm install     # installs all four workspaces
npm run dev     # server on :3001 + Vite on :5173 (proxies /api)

Open the Vite URL. Dev runs with AUTH_DEV_AUTOLOGIN=1, which treats session-less requests as a seeded dev user, so there is no login step. Never set that in production — it bypasses authentication entirely.

Commands

CommandWhat it does
npm run devServer + Vite together (via concurrently)
npm run buildBuilds sharedcorewebserver
npm run typecheckType-checks all workspaces
npm run lintESLint
npm run format / format:writePrettier check / apply
npm testVitest across workspaces
npm run db:migrateApply Drizzle migrations
npm run db:seedSeed demo data
npm run db:generateGenerate a migration from schema changes (run in core)
npm run dev:localStandalone build in a browser, no server
npm run build:androidStandalone bundle + cap sync android

The two builds

web builds twice from the same source:

  • Server build — talks to /api over HTTP. What the Docker image serves.
  • Standalone build (dev:local, build:android) — bundles core into a Web Worker and runs the backend on-device, with an option to point at a real instance instead.

This is why core may not import node:fs and friends: host capabilities are injected through core/src/host.ts, and the Android host simply omits the ones it can't provide. Details in docs/building.md and docs/android.md.

Testing

Five layers, in increasing cost:

CommandLayerWhat it proves
npm testVitest unitDate/segment/recurrence logic, merge decisions, security primitives. The correctness-critical paths — these must be tested.
npm run test:guiPlaywrightReal pointer input against the week view with /api/* mocked. Boots Vite itself.
npm run test:e2ePlaywrightFull-stack persistence smoke: builds and runs the real server on port 3100 against a throwaway DB.
npm run test:localPlaywrightThe standalone build with no server anywhere — registration, authoring, durability, and an assertion that no request ever reaches the network.
npm run test:local:distPlaywrightThe same specs against vite build output served by vite preview — the assets that actually ship in the APK — plus checks on what shipped (no speech model, no service worker, size budget). Dev and prod differ enough in the standalone build that only this catches bundling regressions.
npm run test:encPlaywrightEnd-to-end encryption flows.

The Playwright suites share one config and select themselves via HYPERCAL_SUITE.

Tests worth knowing about by name:

  • core/src/caldav/client/decide.test.ts — the pure sync decision table (what to do with one resource given local state and remote ETag). This is the part that can lose data.
  • core/src/caldav/client/roundtrip.test.ts — the CalDAV client driven against hyper-calendar's own server over real PROPFIND/REPORT/PUT/DELETE. It found a genuine server bug (an event's RRULE being read from the VTIMEZONE's daylight rule).
  • core/src/lib/mergeEvent.test.ts — field-by-field offline conflict merging.

CI

.gitlab-ci.yml, four stages — check → test → build → release — with jobs tiered by what triggered them:

Triggertypecheck, lint, format, auditunit-testsgui-tests, e2e-testsversion-check, build-image, release
push to devmanual
merge request✅ (skipped on Draft)
push to main
tag v*

The Playwright jobs run on the mcr.microsoft.com/playwright image; e2e additionally installs python3 make g++ for better-sqlite3. Reports are kept as artifacts for a week.

A workflow: block gives each change one pipeline — without it, having merge request pipelines means every push also gets a branch pipeline.

The cache key is package-lock.json and caches the npm download store, not node_modules — compiled native modules (better-sqlite3) must not cross images or architectures.

Two notes:

  • Tags don't re-run the test suite — the tagged commit already passed on main. They run version-check → build-image → release and nothing else.
  • audit is the one check that can go red without anyone touching the repo, when a new advisory lands upstream — and it now gates merges too. If that happens with no fix published, raise the threshold or pin the advisory rather than leaving the pipeline red.

See Merge and Release Workflow for the branch model these rules serve.

Before pushing

Work goes straight to dev; main only takes merge requests. Either way:

bash
npm run typecheck && npm run lint && npm run format && npm test

Plus test:gui / test:e2e when touching those areas — on dev those two are manual in CI, so nothing else will run them for you.

House rules:

  • TypeScript strict; no any without a comment justifying it.
  • Every gesture feature works with mouse AND touch.
  • Date/segment logic must have Vitest coverage.
  • Formatting is enforced in CI as of the commit that first made it pass — but keep unrelated reformatting out of feature commits.
  • Contributions are AGPL-3.0, same as the project.

Security issues go through SECURITY.md — a confidential issue or direct email, never a public issue.