- Monorepo: pnpm 11.x + Turborepo, Node.js 24 (Volta-pinned; see
voltain rootpackage.jsonfor exact versions) - Main branch:
v3 - Package names:
@klicker-uzh/<name>(e.g.,@klicker-uzh/graphql)
- GitHub stacked PRs are enabled for this repository. Always use
$stacked-changeand$gh-stackfor larger features: substantial cross-layer or multi-concern work, changes with distinct reviewer audiences or runtime models, and existing large branches that need decomposition. Keep an ordinary single PR for small, cohesive changes only. - This is a KlickerUZH repository capability, not a GitHub-wide assumption. Verify native stack support before using the workflow in another repository.
pnpm install # install all deps
pnpm run build # build everything (turbo)
pnpm run check # typecheck all packages (tsc --noEmit)
pnpm run lint # eslint (Next.js safety net) across all packages
pnpm run format # biome format (code) + prettier (md/yaml, e2e specs)
pnpm run format:check # check formatting (biome + prettier)
pnpm run check:all # check + format:check + lint + syncpack
pnpm run dev # full dev (requires Infisical secrets)
pnpm run dev:raw # dev without secret injection
pnpm run dev:test # dev in test/cypress modepnpm --filter @klicker-uzh/graphql build
pnpm --filter @klicker-uzh/graphql check
pnpm --filter @klicker-uzh/graphql testpnpm run prisma:migrate # create + apply migration (dev)
pnpm run prisma:setup # reset DB + push schema + seed
pnpm run prisma:reset # reset DB (skip seed)
pnpm --filter @klicker-uzh/prisma prisma:seed # seed explicitly
pnpm run prisma:studio # open Prisma Studio
pnpm run prisma:sync # sync schema to apps/analyticsThe commands above are the legacy host/Infisical path. In the self-contained DevPod, the environment is already injected: use pnpm --filter @klicker-uzh/prisma run prisma:reset:raw --force, then pnpm --filter @klicker-uzh/prisma run prisma:push:raw, then pnpm --filter @klicker-uzh/prisma-data run seed:raw for a full destructive reset and reseed.
pnpm --filter @klicker-uzh/graphql generate # one-shot codegen
pnpm --filter @klicker-uzh/graphql dev # watch mode (codegen + rollup)pnpm run test:run # vitest across all packages
pnpm --filter @klicker-uzh/graphql test # single packageapps/
auth/ # Auth UI (port 3010)
backend-docker/ # Main backend + GraphQL API (port 3000)
chat/ # AI chat UI (port 3004)
frontend-control/ # Mobile controller (port 3003)
frontend-manage/ # Lecturer UI (port 3002)
frontend-pwa/ # Student PWA (port 3001)
response-api/ # Response API (port 7078)
hatchet-worker-general/ # General Hatchet worker
hatchet-worker-response-processor/ # Response processing worker
analytics/ # Analytics service
olat-api/ # LMS integration REST API
lti/ # LTI integration
office-addin/ # Office add-in
docs/ # Documentation site
packages/
prisma/ # Prisma schema + migrations
prisma-data/ # Seed data
graphql/ # GraphQL schema (Pothos), resolvers, codegen, ops
grading/ # Scoring + XP logic
types/ # Shared TS types
util/ # Common utilities
i18n/ # Internationalization
shared-components/ # Shared React components
markdown/ # Markdown renderer
hatchet/ # Hatchet task definitions
next-config/ # Shared Next.js config
transactional/ # Transactional email templates
cypress/ # E2E tests
| Layer | Technology |
|---|---|
| Frontend framework | Next.js 16, React, TypeScript |
| Styling | TailwindCSS, @uzh-bf/design-system |
| GraphQL server | GraphQL Yoga + Pothos schema builder |
| GraphQL client | Apollo Client |
| ORM | Prisma 7 (PostgreSQL) |
| Caching | Redis (ioredis) |
| Workflow orchestration | Hatchet (workers for async processing) |
| Auth | Edu-ID (OIDC), magic links, LTI, delegated login |
| Build | Turborepo + Rollup |
| Test | Vitest (unit), Cypress (E2E) |
| Format + lint | Biome (code fmt+lint), Prettier (md/yaml + e2e specs), ESLint (Next.js safety net) |
Code-first with Pothos in packages/graphql/src/. After changing types/resolvers (src/graphql/) or .graphql ops (src/graphql/ops/), regenerate with pnpm --filter @klicker-uzh/graphql generate (codegen is required — ops are stale otherwise). Op-name prefixes: Q query, M mutation, S subscription, F fragment. The public schema definition is generated at packages/graphql/src/public/schema.graphql.
Prisma split-schema under packages/prisma/src/prisma/schema/. After editing a .prisma file: pnpm run prisma:migrate (creates/applies the migration and explicitly regenerates the TypeScript client), then pnpm run prisma:sync (mirrors model files into apps/analytics while preserving its Python generator and datasource), then rebuild dependents. Update GraphQL types/resolvers if the change affects the API. Prisma 7 reset and migration commands do not seed automatically; use the explicit setup or seed command for local fixtures.
- Lecturers: Edu-ID (OIDC) or delegated login via
apps/auth - Participants: magic link, LTI, username/password, temporary (anonymous)
- JWT tokens; GraphQL resolvers enforce three-layer auth: authenticate -> authorize -> execute
Clone-and-run via a self-contained devcontainer — no Infisical/Doppler, no EduID, no /etc/hosts edits. The container owns the whole stack (Node 24 + pnpm toolchain, Postgres, 3× Redis, MailHog, Hatchet) and runs all core apps in ONE container via turbo dev. Run pnpm/prisma/tests inside the container, never on the host.
devrouter ensure .The same command starts and proves primary and linked checkouts. Use devrouter exec . -- <command...> for one-shot commands or the exact DevPod ID printed by ensure for an interactive shell.
The dev servers auto-start in the background (devrouter exec . -- tail -f /tmp/dev.log; first compile takes ~1min). Host-side devrouter ensure owns lifecycle reconciliation and delivers its matching process helper to the exact validated container. The stack runs every routed app plus the two Hatchet workers (no worker route); analytics, Office add-in, and docs remain outside it. See .devcontainer/README.md.
Routing: devrouter ≥ 0.0.35 fronts the stack over the shared devnet network. One-time host setup must happen before the container starts:
devrouter setup --yes # Traefik + devnet + mkcert CAOne command owns DevPod identity, the Git metadata mount, aliases, runtime proof, and route reconciliation for either checkout kind. Do not use bare devpod up, manual WORKSPACE, or per-app --workspace route loops:
devrouter ensure . # existing primary or linked checkout
devrouter workspace up <branch-name> # create and start a new worktreePrimary-checkout apps use https://{app}.klicker.localhost; linked-worktree apps use https://{app}.klicker.<workspace>.localhost. Postgres for host tooling is at db.klicker[.<workspace>].localhost:5432 (sslmode=require sslnegotiation=direct). The primary checkout also keeps the fixed localhost ports in Repo Layout. Login as lecturer/abcd (see test credentials below). Env in .devcontainer/devcontainer.env (committed, dev-only — no real secrets).
Media uploads and Blob CORS: the manage media library uploads directly from the browser to Azure Blob Storage with a SAS URL. The storage account's Blob service CORS must allow the actual local origin (https://manage.klicker.localhost or https://manage.klicker.<workspace>.localhost), not only production origins such as https://manage.klicker.com. For a dedicated dev storage account, use dev-only localhost rules; keep production storage accounts exact.
Traefik reverse proxy serves the apps on *.klicker.com domains (needs /etc/hosts entries + mkcert certs; Docker Compose runs Postgres, Redis, Traefik, Hatchet-lite). Without Traefik, hit http://localhost:<port> directly — per-app ports are in Repo Layout. The *.klicker.com domains better mirror production cookie/domain behavior.
| URL | App | Port |
|---|---|---|
| https://pwa.klicker.com | Student PWA | 3001 |
| https://manage.klicker.com | Lecturer UI | 3002 |
| https://control.klicker.com | Controller | 3003 |
| https://chat.klicker.com | Chat | 3004 |
| https://auth.klicker.com | Auth | 3010 |
| https://api.klicker.com | Backend/GraphQL | 3000 |
| https://assessment.klicker.com | Assessment PWA (same as PWA) | 3001 |
| https://assessment-api.klicker.com | Assessment API (same as API) | 3000 |
| https://response-api.klicker.com | Response API | 7078 |
| https://response-api-assessment.klicker.com | Response API (assessment) | 7078 |
- Lecturer: username
lecturer, passwordabcd(delegated login) - Students:
testuser1-testuser50, passwordabcdabcd(enrolled in "Testkurs") - Additional:
testuser51-testuser52exist but are not enrolled in any course by default
- TypeScript strict mode everywhere
- Functional components with hooks only (no class components)
- Component naming: PascalCase files,
functionkeyword for component declarations - Biome (code): no semicolons, single quotes, trailing comma es5, 2-space indent, line width 80; imports organized via Biome assist (
organizeImports) - Prettier: Markdown/YAML plus the
playwright/+cypress/e2e specs (Biome excludes those dirs) - Tailwind class sorting is not auto-enforced (deferred; previously
prettier-plugin-tailwindcss) - Imports: use
@and~path aliases - GraphQL ops: import from
@klicker-uzh/graphql - State: Apollo Client for server state, React hooks for local state
- Styling: TailwindCSS utilities only,
twMergefor conditional classes
- pre-commit (husky): a staged
gitleakssecret scan (skipped with a notice when the binary isn't installed; CI enforces it), thenpnpm run check:all(typecheck + format:check via lint-staged + lint + syncpack) - pre-push: runs
pnpm run build - lint-staged: Biome on staged code files, Prettier on staged Markdown/YAML and
playwright/+cypress/specs
- Task tracking: ClickUp is the source of truth; GitHub Issues are not actively used.
- Dev scripts use
./util/_run_with_infisical.shfor secret injection. Avoid starting dev servers unless explicitly asked. - If you add or rename an Infisical-managed env var/secret, also update
turbo.jsonglobalEnvso Turborepo sees it during task execution and cache invalidation. - Never commit secrets,
.envfiles, or credentials. This repo is public — anything committed on any branch, once pushed, is permanent public history that deleting the file later does not remove. - Data hygiene before every commit. Review staged content (
git diff --cached, and open any staged data file) for secrets and real personal data — participant/student names, email addresses, matriculation/Studi-IDs, raw response exports, course rosters. Be especially wary of bulk data files (.csv,.json,.sqldumps): these are the highest-risk carriers and are easy to sweep in withgit add .. Real course-data pulls belong outside the repo (add a.gitignorerule); if such data must be versioned, it goes in a private location with direct identifiers removed first. Pseudonymous ids (participant UUIDs) are lower-risk but still get the same scrutiny. When in doubt, do not commit — ask. - Keep changes small, follow existing patterns in the touched app/package.
- Don't add/update dependencies unless required for the task.
- Feature branches from
v3. Conventional commits preferred. - Keep this file high-level. Facts and non-obvious concepts live in the engineering wiki at docs/index.md; architectural decisions are recorded as ADRs in docs/adr/. Update the matching page/ADR as you work (per the
klicker-wiki-maintenanceskill), rather than growing this overview.
Ground truth for working on this codebase is the agent-facing wiki at docs/index.md (not to be confused with apps/docs, the user-facing site). Read the relevant page before working in an unfamiliar area, and keep it current — any PR that changes behavior must update the affected wiki pages in docs/ and relevant skills in .agents/skills/ within the same PR. The former project/CODEBASE_NOTES.md is a retired pointer stub.
Architectural decisions are recorded as ADRs in docs/adr/ — the decision record of why. The wiki explains non-obvious concepts and links the relevant ADR; it does not itself hold the decision. Retrospective fixes and durable lessons live in docs/solutions/; check both before re-deriving a solved problem.
Skills live in .agents/skills/ (the canonical location); .claude/skills and .github/skills symlink to it, so Claude Code and GitHub stay in sync. Task-shaped klicker-* skills cover the feature lifecycle — environment diagnosis (klicker-environment-doctor), design (klicker-feature-design), API (klicker-graphql-api), schema/data (klicker-data-model), UI (klicker-frontend-ui), testing/verification (klicker-testing-verification), e2e (klicker-cypress-e2e, klicker-playwright-e2e), and wiki upkeep (klicker-wiki-maintenance); the routing table lives in docs/index.md.
agent-browser— mandatory verification for any change touching frontend apps, shared components, styling, i18n text, frontend-facing GraphQL ops, or auth/redirect/cookie flows. Open the page and confirm with before/after screenshots; don't rely on "the logic looks correct". Run vianpx agent-browser, and log in with delegated access, not Edu-ID (credentials under Test credentials). Full workflow + Traefik troubleshooting: .agents/skills/agent-browser/SKILL.md.web-design-guidelines— UI/UX/accessibility review (SKILL.md).vercel-react-best-practices— React/Next performance guidance (SKILL.md).
This repository uses devrouter for local dev routing.
All apps and dependencies are declared in .devrouter.yml.
Full reference (config schema, docker requirements, env injection, commands):
.agents/skills/devrouter/SKILL.md
Quick validation sequence:
- Managed devcontainer consumer images contain no devrouter package or helper;
devrouter ensuredelivers the matching helper at runtime. devrouter updevrouter tls install(required when repo defines tcp/postgres apps)devrouter app ls --repo .- Primary or linked devcontainer checkout:
devrouter ensure . --json - Host/docker runtime app only:
devrouter app run <host-app> --repo . --yes devrouter ls