Skip to content

Contributing to GuestCraft

Thank you for helping. This guide explains how to set up the repository, how changes are made and reviewed, and the rules that keep the code base reliable. Security problems are never reported in public issues: see SECURITY.md.

  • Read docs/architecture/repository.md: it says where each kind of change goes and which dependencies are allowed.
  • Decisions are recorded in docs/adr. A change that contradicts an accepted ADR must follow it or propose a new ADR in the same pull request.
  • For anything larger than a bug fix, open an issue first, so that the design can be agreed before you write the code.

Open an issue with the bug report template: what happened and what you expected, the steps to reproduce, the version or commit, and the runtime. Remove guest data and secrets from logs first. Feature proposals use the feature template. Vulnerabilities are never reported in public: follow SECURITY.md.

You need Node.js 24 (the exact version is in .node-version), pnpm 12 through Corepack, and Docker for the integration tests.

Terminal window
corepack enable
pnpm install
pnpm exec lefthook install # optional: fast checks before each commit and push

No test needs a Channex key, a Vercel account or any other external service. Without Docker, the integration tests can use an existing PostgreSQL 17 and PgBouncer: see packages/testing.

Open the repository in GitHub Codespaces or choose Reopen in Container in VS Code with the Dev Containers extension. The dev container uses Node.js 24, installs the pinned pnpm version through Corepack, and starts a separate Docker daemon for Testcontainers and Compose. It runs pnpm install --frozen-lockfile after creation and forwards the web port 3000 plus development ports 4280–4289. Run pnpm quality inside the container to exercise the same checks as CI; no customer credentials are needed.

Command What it does
pnpm test Fast tests (unit and property) of every package, without Docker
pnpm test:coverage Every test, integration included, with coverage and thresholds; needs Docker
pnpm typecheck TypeScript in every package
pnpm lint Biome: formatting and lint rules
pnpm actions:lint actionlint: the GitHub Actions workflows
pnpm boundaries dependency-cruiser: package boundaries and dependency rules
pnpm deadcode Knip: unused files, exports and dependencies
pnpm licenses:check MIT packages never depend on AGPL packages
pnpm secrets:scan gitleaks over the whole Git history
pnpm openapi:lint Spectral: the API rules of .spectral.yaml on openapi/v1.json
pnpm --filter @guestcraft/mobile test:coverage The mobile app’s Jest tests (jest-expo, React Native Testing Library) with its own coverage thresholds
pnpm coverage:ratchet No package’s coverage below main’s (after pnpm test:coverage)
pnpm deps:scan osv-scanner: known vulnerabilities in pnpm-lock.yaml (needs the network)
pnpm mutation:core StrykerJS mutation testing of packages/core (long; nightly in CI)
pnpm quality Every check above except deps:scan and mutation:core, as CI runs them

Each file-based Vitest project sets maxWorkers: 1 to leave CPU and memory headroom on CI. These projects do not inherit the root config’s worker limit. Keep the cap in new project configs; deadlines, retries and coverage thresholds stay unchanged.

Every change comes with tests at the right level (ADR 0005):

  • *.unit.test.ts next to the code, for rules without I/O.
  • *.property.test.ts next to the code, for invariants checked with fast-check against a simple, independent model.
  • test/integration/*.integration.test.ts, for anything that touches PostgreSQL or another process. They run against a real PostgreSQL 17 started by Testcontainers.

Rules:

  • Every bug fix adds a test that fails without the fix.
  • No .only and no .skip: Biome rejects both. A flaky test is quarantined through an issue, as ADR 0005 describes, not skipped in place.
  • The core’s rules against ambient I/O apply to its tests too: they use the injected clock and random source of @guestcraft/testing, never Date.now() or Math.random().
  • Tests that depend on the current date take it from an injected clock. Never hard-code today’s date or use the runner’s wall clock for an expected value. Run the shifted-clock command in @guestcraft/testing at calendar boundaries.
  • Inject time, randomness and identifiers; never read the system clock in the domain.
  • Fixtures are synthetic. Never commit real guest data, card data, tokens or API keys.

Coverage thresholds are per package and are checked in CI; they are not lowered to make a pull request pass.

The enforced thresholds in vitest.config.ts, ordered as statements / branches / functions / lines, are:

Scope Minimum
Global and web application 85 / 80 / 85 / 85
Core, auth, API, server, jobs, countries, i18n, tokens and UI components 95 / 90 / 95 / 95
Channex client and GuestCraft SDK 95 / 90 / 95 / 95
Database and Channex simulator 90 / 85 / 90 / 90
Mobile app, under Jest (apps/mobile/jest.config.js) 85 / 80 / 85 / 85
Core money and identifiers, each file 99 / 95 / 100 / 99

All handwritten source is included, even when no test imports it. React Native cannot run under Vitest, so Vitest’s coverage leaves apps/mobile out and the app’s own Jest run counts every file of its src/ instead; pnpm quality and CI run both. The reusable helpers in @guestcraft/testing count too; generated code, fixtures and tests have explicit exclusions. A new package must receive an appropriate explicit threshold rather than relying only on the global average.

Before product work, write observable acceptance criteria: the objects a person manages, the operations they perform, their permissions and what persists after a reload. A feature is complete only when its supported workflow works through the relevant domain, database, API, OpenAPI, SDK and UI layers. A deliberately partial change must name the remaining layers and behavior.

Include denied access, invalid input and recovery in acceptance evidence. A table, type, mock, reusable UI primitive or external API client is not evidence that a PMS workflow exists. Likewise, connecting Channex and listing its properties does not implement local import, mapping, reservation processing or inventory publication.

Coverage measures the exercised implementation, not product scope. Keep planned, partial and implemented capabilities separate in documentation and PR reports. The same rules apply to human contributors and coding agents; see AGENTS.md.

The application currently ships one UI language: English. Put every user-visible message in packages/i18n/catalogs/en.json and regenerate the runtime catalog with pnpm i18n:catalogs. Keep locale-aware formatting and guest language data independent of that UI catalog.

  • TypeScript, strict, ESM. Only erasable syntax: no enum, namespace or constructor parameter properties.
  • Relative imports include the .ts extension.
  • No process.env outside the configuration module of server, the app entry points and the test harness (packages/testing).
  • Errors follow ADR 0008: expected outcomes are returned as Result, infrastructure failures are thrown with a retry flag, bugs throw and stop the operation. No empty or silent catch.
  • Money is an integer number of minor units with its currency; dates of stays are Temporal.PlainDate (ADR 0007).

Every new source file starts with an SPDX header. In AGPL packages:

// SPDX-FileCopyrightText: 2026 GuestCraft contributors
// SPDX-License-Identifier: AGPL-3.0-only

In MIT packages (see the license field of the package’s package.json), use MIT instead. Files that cannot carry a comment are covered by REUSE.toml. reuse lint must pass.

Contributions are accepted under the Developer Certificate of Origin: sign off each commit with git commit -s, certifying that you have the right to submit the work under the project’s licenses. The sign-off is required, and checked, from the first outside contribution (ADR 0004); the maintainers’ commits before then carry none.

  • main requires a pull request, an up-to-date branch, passing GitHub Actions quality and Docker smoke checks, and resolved review conversations. These protections apply to administrators too; force pushes and branch deletion are disabled. The solo-maintainer configuration does not impose a second GitHub approval; owner review below remains a process rule, not an automated approval.
  • Branch from main; keep pull requests small and focused on one change.
  • Run pnpm quality locally before pushing. CI runs on pull requests, including drafts.
  • Write commit messages in the Conventional Commits style, in English: feat(channex-client): parse revision feed pages. The types decide the next version and the changelog: feat is a new minor version, fix and perf a patch. Mark a breaking change with ! (feat(api)!: …) or a BREAKING CHANGE: footer. Breaking means removing or changing an API operation or field, adding a required setting, renaming an environment variable or changing behaviour operators rely on. See releasing.
  • Releases are pull requests too: release-please keeps one release pull request on main, and the owner merges it after the release checklist. Do not edit CHANGELOG.md or the version fields by hand.
  • Fill in the pull request template. A pull request is merged when CI is green and a maintainer has approved it. Changes to the core, migrations, security code and public contracts need the approval of a code owner, and the independent reviews of the review protocol.
  • Update the documentation in the same pull request: READMEs, ADRs, the architecture notes.
  • For every new packages/db/migrations/NNNN_*.sql, add an expand or contract entry to packages/db/migrations/kinds.json; when unsure, use contract. Run pnpm --filter @guestcraft/db migrations:revisions and pnpm --filter @guestcraft/db migrations:lint. Run the revisions command again after any merge or rebase that touches migrations, even if Git reports no conflict; the generated checksums and kinds must reflect the combined history. expand promises that the previous runtime still reads and writes after SQL and reapplied grants; inspect the grant floor and review old-runtime behavior. The current tests simulate a previous revision prefix; they do not run a released previous binary. Use contract for changes that require new code and document the roll-forward boundary. Do not edit an applied migration or its declaration. See ADR 0038.

The workflows and scripts are the evidence of what is enforced today; the quality program lists every gate, its status, the review protocol and the prioritized gaps. The OpenSSF Best Practices self-assessment answers each passing-level criterion with its evidence, and the security design records the secure-design principles and every use of cryptography.

  • Required on every pull request (quality): the commands above, including gitleaks over the history, the Spectral API rules with their self-test, and the coverage ratchet against tooling/quality/coverage-baseline.json, and actionlint on the workflows. smoke builds the checkout’s images and starts them with Compose.
  • On every pull request, not yet required: browser (Playwright on the production build) and dependencies (osv-scanner; a pull request fails only on the vulnerabilities it adds). image builds the release images natively on amd64 and arm64 with their SBOMs, without pushing; it runs on every push to main and on the pull requests that change the build inputs.
  • On every pull request: dependency review blocks newly introduced high-severity vulnerabilities without commenting. License direction stays in pnpm licenses:check.
  • On main: successful quality and mutation reports become line-coverage and core-mutation endpoint files on the orphan badges branch. Only source runs from this repository’s main branch can publish.
  • On every push to main: release updates the release pull request; after a release pull request is merged, it publishes the attested images and the GitHub release (releasing).
  • Nightly: property tests at 10,000 cases in two non-UTC time zones, mutation testing of the core (overall threshold in stryker.config.mjs), the full dependency scan and the Channex contract-drift check; weekly, the shifted-clock matrix.
  • Weekly and on main pushes and branch-protection changes: OpenSSF Scorecard publishes results and uploads SARIF to code scanning.
  • By GitHub, free on a public repository: CodeQL (default setup), secret scanning with push protection, Dependabot alerts and private vulnerability reporting.

A coverage drop below the baseline fails the ratchet. Add tests; lowering a baseline value is a hand edit whose reason goes in the pull request description. The tool binaries are pinned by SHA-256 in tooling/requirements/tools.json.

ADR 0005 also targets changed-line coverage, API fuzzing and breaking-change checks; those gates are not yet implemented and must not be reported as passing. The dependency review action is enabled on pull requests. GitHub’s secret scanning and push protection settings were verified through the repository API on 2026-10-28; see quality-program row 23.

Browser acceptance covers front desk, team/custom roles, policies, Channex import and resident-worker ARI publication on the production build with disposable PostgreSQL. The request-pump ARI harness is available separately. Screenshot/axe matrices currently share workflow deadlines and have known timeout limitations; a listed CI capability does not claim a particular run passed. Manual assistive-technology certification and a browser INP budget remain separate work.

Everyone taking part in the project follows the Code of Conduct.