Development
Requirements: Node.js ≥ 22 and npm. Nothing else — SQLite is embedded.
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
| Command | What it does |
|---|---|
npm run dev | Server + Vite together (via concurrently) |
npm run build | Builds shared → core → web → server |
npm run typecheck | Type-checks all workspaces |
npm run lint | ESLint |
npm run format / format:write | Prettier check / apply |
npm test | Vitest across workspaces |
npm run db:migrate | Apply Drizzle migrations |
npm run db:seed | Seed demo data |
npm run db:generate | Generate a migration from schema changes (run in core) |
npm run dev:local | Standalone build in a browser, no server |
npm run build:android | Standalone bundle + cap sync android |
The two builds
web builds twice from the same source:
- Server build — talks to
/apiover HTTP. What the Docker image serves. - Standalone build (
dev:local,build:android) — bundlescoreinto 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:
| Command | Layer | What it proves |
|---|---|---|
npm test | Vitest unit | Date/segment/recurrence logic, merge decisions, security primitives. The correctness-critical paths — these must be tested. |
npm run test:gui | Playwright | Real pointer input against the week view with /api/* mocked. Boots Vite itself. |
npm run test:e2e | Playwright | Full-stack persistence smoke: builds and runs the real server on port 3100 against a throwaway DB. |
npm run test:local | Playwright | The standalone build with no server anywhere — registration, authoring, durability, and an assertion that no request ever reaches the network. |
npm run test:local:dist | Playwright | The 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:enc | Playwright | End-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:
| Trigger | typecheck, lint, format, audit | unit-tests | gui-tests, e2e-tests | version-check, build-image, release |
|---|---|---|---|---|
push to dev | ✅ | ✅ | manual | — |
| 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 runversion-check → build-image → releaseand nothing else. auditis 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:
npm run typecheck && npm run lint && npm run format && npm testPlus 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
anywithout 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.