Skip to main content

Contributing

Guidelines for contributing to Lumio.

Getting Started​

  1. Clone the repository (access required - Lumio is a closed-source project)
  2. Set up the dev stack: just stack-up
  3. Run the API: just dev-api
  4. Run the web app: just dev-web

Code Style​

  • Rust - Follow rustfmt.toml conventions (just fmt), pass clippy with -D warnings
  • TypeScript - ESLint + double quotes
  • Commits - Conventional commits format

Before you push​

CommandWhat it covers
just verify-allverify-rust + lint-ts + typecheck-ts - the fast inner loop
just lint-allclippy (-D warnings) + ESLint
just test-alltest-rust + test-v8 + test-ts
just verify-releaseverify-all plus OpenAPI / GraphQL schema and Bazel vendor drift - run before a version-bump or crate-update change
just bazel-build / just bazel-testthe Bazel gates CI runs for Rust - required for any Rust change
pnpm --filter @lumio/<app> buildthe production next build CI runs for web, admin, id, stats and shopify - required for any change under apps/{web,admin,id,stats,shopify}; a change under shared/, packages/protocol or to pnpm-lock.yaml builds all five (build @zaflun/lumio-protocol first for web)
pnpm -r --filter "./packages/**" buildthe tsup build CI runs for the public @zaflun/* packages under packages/ - required for any change under packages/

just verify-all does not detect schema or vendor drift; only just verify-release does.

Run these one at a time. Each recipe already uses every core it is given, so a build, a test run, a clippy pass and a format check started in parallel do not finish sooner - they contend for the same CPU and slow each other down (badly so on a shared machine). Run the checks in sequence, cheapest first - fmt-check, then lint, then check / typecheck, then tests, then the Bazel gates - and let each finish before starting the next. Do not background one or start a second alongside it, and leave the job counts the recipes set (test-rust pins CARGO_BUILD_JOBS=2) alone.

A recipe is sequential in its dependencies, but not necessarily in what it spawns: lint-ts and typecheck-ts expand to pnpm -r lint / pnpm -r typecheck, and pnpm -r runs package scripts about four at a time. On a machine you share, pass --workspace-concurrency=1 or scope the run with --filter to the packages you touched.

AI coding agents do not use just at all. The recipes are a human convenience layer; an agent runs the underlying command for each step so the steps stay individually visible, reportable and sequential. The recipe-to-command mapping and the reasoning live in AGENTS.md, section Verification - Agents run the tools directly.

Cargo green is not Bazel green. CI builds and tests the Rust workspace with Bazel, which fails on things Cargo never sees - a dependency missing from a hand-written BUILD.bazel, a test that reads a fixture off the filesystem instead of embedding it, or vendor/ drift after a lock change. For any change touching Rust, run just bazel-build and just bazel-test before you push.

Typecheck green is not next build green. tsc never resolves Next.js file conventions, route manifests, server/client boundaries, the React Compiler or prerendering. The Next Build job runs the real production build per affected app - the same build the release image performs - so run pnpm --filter @lumio/<app> build locally for any frontend change. It is path-filtered: an app builds when its own directory changed, and a change under shared/, packages/protocol/, the root manifests or ci.yml builds all four.

Always-dynamic route groups declare themselves. serverGqlSSR() reads the auth cookie, so during the production build's static-generation pass Next throws a DynamicServerError (digest DYNAMIC_SERVER_USAGE) purely as a signal to bail the route out of prerendering. Our SSR pages wrap their data loads in try/catch so a failed query renders a visible error instead of an empty success state - and that catch swallows the bail-out signal, turning it into a build-time Failed to load ...: Error: Dynamic server usage or, silently, into a notFound() / redirect() fired at build time. The fix lives at the segment root rather than in every catch: the always-dynamic route groups in apps/web and apps/admin export const dynamic = "force-dynamic" from their layout.tsx, which Next inherits down to every page in the group. It changes no output - those routes were already server-rendered on demand - it only skips a prerender attempt that could never succeed. Add the same line when you introduce a route group whose layout or pages call serverGqlSSR(); if a segment genuinely must stay prerenderable, start its catch blocks with unstable_rethrow(err) from next/navigation so Next's own errors pass through.

Typecheck green is not tsup build green either. The public @zaflun/* packages under packages/ are bundled by tsup (entry resolution, export map, .d.ts rollup) - none of which tsc --noEmit or ESLint exercise, so a broken entry or export only surfaced at npm publish time. The Packages Build job runs pnpm -r --filter "./packages/**" build in dependency order (so cli and sdk get their built dependencies first); run it locally for any change under packages/. It is path-filtered: any packages/ file, a root manifest, or ci.yml arms it.

Pull Requests​

  • Create a feature branch off next and open the pull request into next, the staging integration branch
  • Open every pull request as a draft (gh pr create --draft) and mark it ready for review (gh pr ready) only once the work is finished
  • Include tests for new functionality
  • Update apps/docs/ in the same change - a code change without matching doc updates is incomplete
  • Ensure CI passes and the change is reviewed before it merges

Why draft first​

CI is draft-gated: every job in .github/workflows/ci.yml is skipped while a pull request is a draft, so iterating on a draft costs no GitHub Actions minutes. The full pipeline runs when the pull request is marked ready for review, and on every push after that.

Two consequences worth knowing:

  • Verify locally while drafted - just verify-all and just test-all, plus just bazel-build / just bazel-test for a Rust change and just verify-release for a version bump or crate update. That is what replaces the per-push CI run, so run all of it before marking the pull request ready, and say in the pull request which checks you ran.
  • A draft's green checks are not evidence. The CI Done aggregate treats skipped jobs as passing, so a draft shows green with nothing run. Only a run triggered by marking the pull request ready - or by a push while it is not a draft - proves anything.

See Reporting an Issue for where each kind of report goes and how a fix travels from next to production.

Architecture​

See the Developer Guide section for detailed system design documentation.