Set up a development environment
Use this guide to run Quaso from source, find the code for a change, and prepare a pull request. For translation work on an existing instance, use the website guide.
Getting started
Install Git and Node.js at the version recorded in .nvmrc. Docker and
Cloudflare tools are needed only for the checks described in testing.
Node runs the TypeScript sources directly by stripping their types, so the code uses only
erasable syntax: no enums, namespaces or constructor parameter properties, and type-only imports
use import type. npm run typecheck checks the types with tsc.
The Node version in .nvmrc is also the node and gcr.io/distroless/nodejs26 image tags in
deploy/Dockerfile and scripts/docker_workflow.ts, where images are pinned by digest
(git grep -e node:26 -e nodejs26). When you change the version, update each tag together with
the digest that docker buildx imagetools inspect <image>:<tag> prints first (the multi-platform
index).
git clone https://github.com/explodi/quaso.git
cd quaso
npm ci
npm run dev
npm run dev creates .quaso/ (ignored by Git), fills a SQLite database with sample data, and
starts the app on port 5173 and API server on port 8000. Open the printed address ending in
/auth/dev-login to sign in as the development administrator. The app uses a fake translator until
you save a Gemini key in Settings → AI translation.
Keep that terminal open. In a second terminal at the repository root, check the running project and run the normal tests:
npm run cli -- status
npm test
The status command lists the sample project's language progress. The CLI uses .quaso/dev-api-key
unless you set QUASO_API_KEY. Once dependencies are cached, the normal tests use local data and
need no provider key.
To start again, stop the server and run npm run dev:reset. This deletes .quaso/, including its
accounts and translations, then creates new sample data.
If ports 5173 or 8000 are in use, choose other ports before starting development:
QUASO_WEB_PORT=5390 QUASO_SERVER_PORT=8123 npm run dev
The development CLI still defaults to port 8000. For this example, point it at the changed API port:
QUASO_HOSTNAME=http://localhost:8123 npm run cli -- status
The development seed and root CLI use a stable, partly translated fixture. For a playable game that starts with untranslated English and German targets, follow Quaso Quest's setup, which runs its own Docker instance.
Front-end toolchain
The app and project website use Vite 8 and React Compiler through the React plugin's Babel preset.
Their package.json files record the versions.
Keep the application and project website on the same Vite toolchain. npm run lint:react runs
ESLint's react-hooks/set-state-in-effect and react-hooks/set-state-in-render as errors.
The full npm run lint, included in npm run check, enforces the same rules. Derive values during rendering. Change state in event handlers or subscriptions to external data.
Do not suppress these rules to copy props into local state.
Tasks
| Task | Does |
|---|---|
npm run dev |
Starts the development server and reloads changed code |
npm test |
Tests in the packages, scripts, and site; excludes browser, acceptance, and Cloudflare suites |
npm run check |
Formatting, lint, types, headers, portability, licenses, and the normal tests |
npm run build |
Builds the translation app into packages/web/dist |
npm run docker |
Builds the Docker image |
npm run e2e |
Runs Puppeteer tests against the real server and built website |
npm run site |
The project landing page, with updates as you edit |
npm run site:preview |
Builds and serves the project website, including documentation |
npm run cli -- … |
Runs the quaso CLI from source |
npm run cf:dev, cf:test, cf:deploy |
Cloudflare development, tests, and deployment; requirements vary by task |
Where things live
| Folder | Contents |
|---|---|
packages/core |
i18next JSON and whole-file Markdown, plural rules, placeholders, file output, quality checks, schema definitions, and API types. TypeScript without file or network access. |
packages/service |
Uploads, downloads, review, accounts, permissions, and translation, meaning, terminology, and style-guide jobs. Runs in the server on local SQLite or on Cloudflare D1 and R2. Its main logic uses standard web APIs and its ports (Sql, Store, Scheduler). |
packages/server |
The Node application: HTTP routes, sign-in, local storage, the website's files |
packages/design-system |
Shared React controls, themes, fonts, icons, and brand assets for the app and public site |
packages/web |
The React application and design-system catalog, built with Vite |
packages/cli |
The quaso command, for Node ≥ 22 |
packages/cloudflare |
The Worker, the container controller, wrangler.jsonc and the cf:* scripts |
deploy/ |
The Dockerfile, compose.yaml, Caddyfile and .env.example |
examples/ |
A small i18next app with quaso.config.json and CI workflows |
docs/ |
Documentation for users, operators and contributors |
site/ |
The project's public website |
Architecture explains how the parts fit together. The design-system guide covers shared UI conventions and package boundaries.
Conventions
Follow these rules when changing the code:
- Tests sit next to the code (
foo.test.ts) and run with Vitest:npm testfor the normal suite, ornpx vitest run path/to/file.test.tsfor one file. Most service tests use an in-memory SQLite database and the fake translator. See testing for suites that need a browser, container, or provider. - License headers. Start code files with an SPDX license header, using the file type's comment syntax, such as
// SPDX-License-Identifier: MITfor TypeScript. CI checks it. - Portability.
coreandservicemust not importnode:,npm:or other runtime-specific modules, or use a runtime's global namespace, outside their tests (CI checks it), because they also run in browsers and inworkerd. - SQL in the service goes through the
Sqlport. Use revision-checkedread()andcommit()batches; do not putBEGIN,COMMIT, or PRAGMAs in application statements. Bind at most 100 parameters per statement. Older synchronous helpers use the separateSyncSqlinterface and itstransaction()method. - Migrations only go forward. Add one file in
packages/service/src/migrations/, register it inMIGRATIONS, and never edit a released migration. Follow the steps below. - Dependencies. A new dependency needs a reason in the pull request that adds it: what it saves, and why Node's built-ins or a small module of our own won't do. Only permissive licenses (MIT, BSD, ISC, Apache-2.0); CI checks them.
- Commits and pull requests. Small and focused. Reference the issue, and describe what changed
and how you tested it.
npm run checkmust pass.
Adding a migration
When the database schema changes, follow these steps:
- Add
packages/service/src/migrations/NNN_description.ts, with its SPDX header and an exportedMigrationvalue containing the next version, a short name and SQL. Use the previous migration as the format example. Do not edit a released migration or renumber existing ones. - Import it in
packages/service/src/migrations.tsand append it toMIGRATIONSin version order.DATABASE_VERSIONfollows that array. Keep SQL portable: the adapter supplies transactions; D1 does not acceptBEGIN,COMMIT,SAVEPOINTor arbitrary PRAGMAs. - Write a migration test with representative data from the previous schema. Assert preservation of translations, history, roles and settings, and verify the new behavior. Tests must also cover upgrading older supported schemas, rather than only creating an empty latest database.
- Generate the Cloudflare scenario again with
npm run cf:fixtures, review its diff, then runnpm run cf:fixtures -- --checkandnpm run cf:testto exercise the same SQL in workerd. - Run
npm run check. Document operator-visible changes inCHANGELOG.mdand any necessary backup, upgrade or rollback steps. Before release, test with a copy of a real database.
Writing tests and pull requests
See testing for the test layers and optional external tests. Prefer assertions about behavior readers can observe, especially permissions and preservation of source files and proofread translations. Keep tests independent and use the fake translator instead of network calls. Add a regression test when fixing a bug that could silently lose or expose data.
Commits should describe one focused change. A pull request explains the problem, resulting
behavior, validation, and any migration or deployment effect. Include screenshots for visible UI
changes and explain each new dependency. Run npm run check before requesting review. Also run browser or Cloudflare checks when those paths change, and
npm run site:build when documentation changes. The release guide covers
versioning.
Branch protection
CI runs npm run check for pull requests and pushes to main or master. To make a failing check block merging,
turn on branch protection for main in the repository settings and require the npm run check
status check.
Code of conduct
Everyone taking part follows our code of conduct.