Repository navigation
Team Environment
How the team's cloud container reaches the state in which the gates in
verification.md run. AGENTS.md and docs/setup/ci.md win
over this file. Written 2026-10-07 against work/redesign-r4 @ bb4d4000;
the MinIO build and the session-start hook were verified on 2026-10-08. Re-date it when a
step changes.
The container is Linux with 4 cores and 15 GB of memory. The Postgres and MinIO services run in Docker. Everything else runs on the host. Outbound traffic goes through the agent proxy, which allows npm, the Go module proxy and GitHub but not every registry (see "What did not work").
.claude/hooks/session-start.sh, registered in .claude/settings.json, does the work of
the steps below when a Claude Code cloud container starts: the daemon, .env, goose, the web
dependencies, the MinIO image (built if missing), Postgres, MinIO and make migrate
(not make seed). It runs
only where CLAUDE_CODE_REMOTE=true and exits 0 silently elsewhere, so a laptop is
never touched. It reports itself asynchronous, so the session starts while it works.
It does not run steps 8 to 10: those create databases and buckets that need cleanup.
-
It exports
GOTOOLCHAIN=go1.27.0and/root/.local/binonPATHthroughCLAUDE_ENV_FILE, before it does anything slow. -
It skips a step whose result is already there, never fails the session, and logs to
/tmp/quizzivy-session-start.log(a line per step:begin, thenokorFAILEDwith the time taken).docker info,docker psandmake migrateshow the same state. A second run takes seconds. -
A cold container (daemon down, services stopped) is ready in about 10 s; building the MinIO image adds about 150 s.
-
Rerun it by hand, from any directory, to see what is missing:
CLAUDE_CODE_REMOTE=true /home/user/Quizzivy/.claude/hooks/session-start.sh tail -n 30 /tmp/quizzivy-session-start.logWhen run by hand it prints one JSON line (the async notice) and waits for the work to finish. A started session gets
GOTOOLCHAINandPATHfrom the env file; a shell you open yourself needs the exports of steps 5 and 6. -
The hook starts Postgres and MinIO and leaves them running. A step it could not finish is in the log; fix that step by hand from the list below.
-
It recovers the failures a container restart leaves behind. A
/var/run/docker.pidwhose process is notdockerd, and a/var/run/docker/containerd/containerd.pidwhose process is notcontainerd(gone, or a number the new boot gave to something else), are removed beforedockerdstarts, or the daemon refuses to run ("containerd is still running"). When neither names a live daemon,/var/run/docker.sock,containerd-debug.sockandcontainerd.sock.ttrpcfrom the previous boot are removed too (QC-H-17). The daemon is started without the hook's lock, so a second run is not blocked for as long asdockerdlives. A service whose first start fails ("container with given ID already exists", stale runc state) is started once more withdocker compose up -d --force-recreate, which keeps the volumes (ledger F-13, INC-3).
Run from /home/user/Quizzivy. Each step is safe to repeat. The hook does steps 1 to 5 (not
psql) and the migration half of step 7.
-
Start the Docker daemon if
docker infofails. It inherits the proxy variables of the shell, which it needs to pull images.nohup dockerd > /tmp/dockerd.log 2>&1 & until docker info >/dev/null 2>&1; do sleep 1; done -
Create
.envif it is missing:cp .env.example .env, then setJOIN_CODE_KEYto the output ofopenssl rand -base64 32..envis git-ignored; never commit it. -
Make the MinIO image exist.
docker-compose.ymlnamesquizzivy-minio:developmentwithpull_policy: never, so compose starts it only when the image is already local. Check withdocker image inspect quizzivy-minio:development. The image survives a daemon restart. If it is missing, build it on the host, from the pinned commits indocker/minio/README.md.docker compose build miniois the documented route but fails in this container (see "What did not work"). The host build needs the localgolang:1.27-alpineandalpine:3.22images andGOTOOLCHAIN=go1.27.0, and took 150 s on 2026-10-08:B=$(mktemp -d); cd "$B" git init minio && git -C minio fetch --depth=1 https://github.com/minio/minio.git 07c3a429bfed433e49018cb0f78a52145d4bedeb && git -C minio checkout --detach FETCH_HEAD git init mc && git -C mc fetch --depth=1 https://github.com/minio/mc.git 7394ce0dd2a80935aded936b09fa12cbb3cb8096 && git -C mc checkout --detach FETCH_HEAD mkdir out licenses-minio licenses-mc export GOTOOLCHAIN=go1.27.0 CGO_ENABLED=0 (cd minio && go build -trimpath -ldflags="$(go run buildscripts/gen-ldflags.go)" -o ../out/minio .) (cd mc && go build -trimpath -ldflags="$(go run buildscripts/gen-ldflags.go)" -o ../out/mc .) cp minio/LICENSE minio/CREDITS licenses-minio/; cp mc/LICENSE mc/CREDITS licenses-mc/ cat > Dockerfile <<'EOF' FROM golang:1.27-alpine AS certs FROM alpine:3.22 COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY out/minio out/mc /usr/bin/ COPY licenses-minio/ /licenses/minio/ COPY licenses-mc/ /licenses/mc/ ENTRYPOINT ["minio"] EOF docker build -t quizzivy-minio:development . cd /home/user/Quizzivy; rm -rf "$B"Verified:
minio --versionandmc --versionreport the pinned commits, the licence files are in/licenses, andwget(the healthcheck) and/bin/sh(minio-init) exist in the image. It lacks the repository Dockerfile's description label. -
Start the services.
make upalso builds the image, which fails here, so start them directly:docker compose up -d --wait db minio docker compose run --rm minio-initdbispostgres:18on 5432. On its first startdocker/initdb/creates the rolesquizzivy_migrateandquizzivy_appand the databasequizzivyby callingscripts/provision-db.sh. The roles live in thedb-datavolume, so they survive a container restart. If the volume is gone, step 4 creates them again. -
Install the tools the repository expects on
PATH.- goose v3.27.3, the version of
.github/actions/db-tools, in/root/.local/bin(goose --versionshould say v3.27.3). Take the release binary from GitHub as that action does, orGOTOOLCHAIN=go1.27.0 GOBIN=/root/.local/bin go install github.com/pressly/goose/v3/cmd/goose@v3.27.3, andexport PATH=/root/.local/bin:$PATH. -
psql,createdbanddropdbcome from the container'spostgresql-client16. They talk to the 18 server without trouble; CI installs client 18. -
cd web && pnpm install --frozen-lockfile.
- goose v3.27.3, the version of
-
Pin the Go toolchain. The container has Go 1.24.7 and
server/go.modsaysgo 1.27. Without a pin,gotries to fetch a toolchain per command andmake lintfails (rc 2, 267 s). Export it in every shell that runs Go:export GOTOOLCHAIN=go1.27.0 -
Migrate and seed the
quizzivydatabase:make migrate(goose up to version 91 atbb4d4000), thenmake seedif a gate needs the seed data. -
Provision what CI's
server-testjob provisions before the Go integration tier. Without it,attempts/repositories/testsfails for want ofQUEUE_*andimports/repositories/testsfor want ofTEST_TEXT_IMPORT_*. Use a scratch directoryTfor the journal.TEST_DESTRUCTIVE=1, as in CI, lets the migration round trip drop and recreate schemaappinquizzivy, so runmake seedagain afterwards if a later step needs the seed data.export PATH=/root/.local/bin:$PATH GOTOOLCHAIN=go1.27.0 export PGHOST=localhost PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres export TEST_DATABASE_URL='postgres://quizzivy_migrate:migrate@localhost:5432/quizzivy?sslmode=disable' export TEST_DESTRUCTIVE=1 S3_ACCESS_KEY_ID=quizzivy S3_SECRET_ACCESS_KEY=quizzivy-dev-secret export APP_DATABASE_URL='postgres://quizzivy_app:app@localhost:5432/quizzivy?sslmode=disable' T=$(mktemp -d); J=$T/journal.json createdb --owner=quizzivy_migrate quizzivy_grading_queue goose -dir migrations postgres 'postgres://quizzivy_migrate:migrate@localhost:5432/quizzivy_grading_queue?sslmode=disable' up TEXT_IMPORT_APP_BASE_URL='postgres://quizzivy_app:app@localhost:5432/quizzivy?sslmode=disable' \ GITHUB_OUTPUT=$T/output.txt python3 scripts/ci/text-import-purpose.py prepare $Jpreparecreates a databaseqvtext_<token>and a bucketqv-text-<token>and writes their names, with the two connection strings, to$GITHUB_OUTPUTasdatabase,bucket,app_urlandmigrate_url. -
Run the tiers from
server/. The integration step's variables belong to that command only, as in CI, where they are the step'senv:env QUEUE_COMMITTED_DATABASE_URL='postgres://quizzivy_app:app@localhost:5432/quizzivy_grading_queue?sslmode=disable' \ TEST_TEXT_IMPORT_DATABASE_URL=<app_url> TEST_TEXT_IMPORT_MIGRATE_DATABASE_URL=<migrate_url> \ TEST_TEXT_IMPORT_DATABASE_NAME=<database> TEST_TEXT_IMPORT_BUCKET=<bucket> \ S3_ENDPOINT=http://localhost:9000 S3_REGION=us-east-1 S3_BUCKET=quizzivy-imports S3_FORCE_PATH_STYLE=true \ python3 ../scripts/ci/text-import-purpose.py run $J -- go test -tags integration ./internal/... -count=1 go test -tags e2e ./tests/... -count=1The end-to-end tier gets no
S3_*at all: its harness defaults to the compose endpoint and to two buckets,quizzivy-mediaandquizzivy-imports(server/tests/isolation_world_test.go). ExportingS3_BUCKET=quizzivy-importsinto its shell makes the two stores one, andfixture_lifecycle_verification_test.gothen fails withregistered2 resources, want3. That is the environment, not the code. -
Dispose of what step 8 created, from the repository root. Skip nothing: a leftover journal makes the next
preparerefuse.python3 scripts/ci/text-import-purpose.py cleanup $J dropdb quizzivy_grading_queuepsql -X -At -d postgres -c 'select datname from pg_database'should then list onlypostgres,quizzivy,template0andtemplate1. The end-to-end tier creates and removes its ownqve2e_*database and bucket.
| Service | How to start | Port | Stop |
|---|---|---|---|
| Docker daemon | nohup dockerd > /tmp/dockerd.log 2>&1 & |
unix socket |
pkill dockerd (stops every container) |
PostgreSQL 18 (quizzivy-db) |
docker compose up -d --wait db |
5432 | docker compose stop db |
MinIO (quizzivy-minio) |
docker compose up -d --wait minio, then docker compose run --rm minio-init
|
9000 S3, 9001 console | docker compose stop minio |
| API |
make dev-api (needs .env and the migrated database) |
8080 | stop the process |
| Vite dev server | cd web && pnpm dev |
5173 | stop the process |
vite preview (live E2E) |
started by Playwright | 4173 |
pkill -f 'vite preview' before each run |
Leave Postgres and MinIO running between pieces of work; stop the API and Vite
when a verification run ends. Four sub-agents share one Postgres and one MinIO,
so give each test run its own database (createdb --owner=quizzivy_migrate).
docker compose down keeps the volumes; add -v only to start from nothing.
Credentials are the compose defaults: Postgres superuser postgres/postgres,
quizzivy_migrate/migrate, quizzivy_app/app; MinIO
quizzivy/quizzivy-dev-secret. Use localhost for both web and API.
Written 2026-10-10 (chore/gates-local-speed); the policy is in the README, "The heavy lock"
and "Local gates (G-2)".
-
Heavy runs go through
scripts/dev/heavy.sh <command>: two slots,/tmp/quizzivy-heavy.1.lockand.2.lock, plus the old/tmp/quizzivy-heavy.lockheld shared so theflock -oform still excludes it.scripts/dev/heavy.sh --statusshows each slot's holder.HEAVY_SLOTS=1makes it a single slot again. -
Caches.
pnpm lintwritesweb/node_modules/.cache/eslint/.eslintcacheandpnpm format:checkweb/node_modules/.cache/prettier/, both keyed on file content, so a fresh worktree starts cold and a worktree's second run takes seconds.pnpm lint:ciandpnpm format:ciare what CI runs: no cache. A worktree has its ownnode_modules(pnpm install --frozen-lockfile --offlinetakes 4 s from the store), so caches are not shared. -
Changed scope.
pnpm lint:changed,pnpm format:changedandpnpm test:changedact on what changed againstorigin/work/redesign-r4since the merge base;BASE=origin/<branch>overrides it.pnpm test:integrationstill runs whole when a page, route or shared component changes. -
A private live stack. With Postgres and MinIO up (steps 4 and 5), from the worktree under test:
export PATH=/root/.local/bin:$PATH GOTOOLCHAIN=go1.27.0 scripts/dev/live-stack.sh up <name> scripts/dev/live-stack.sh status <name> scripts/dev/live-stack.sh down <name>upcreatesqa_<name>asquizzivy_migrate, runs goose andseed/*.sql, builds the API to/tmp/quizzivy-live/<name>/apiand the web app withVITE_API_BASE_URLset to the stack's API, then starts both in their own process groups. Ports are 20000 to 20499 (API) and 20500 to 20999 (web), derived from the name and moved on when taken; pids, logs, the generated JWT and join-code keys and the build live in/tmp/quizzivy-live/<name>/. It uses the compose MinIO and itsquizzivy-mediabucket and leaves Word import off.downstops both, drops the database and removes the directory; objects the API stored in MinIO stay. It never takes the heavy lock, and the build it runs is as heavy aspnpm build, so start stacks one at a time. Cookies are shared across ports onlocalhost: use one browser context per stack.
-
docker compose build miniofails: BuildKit cannot reach the Alpine mirror (apk add gitanswers HTTP 403 through the proxy). Step 3 builds the image on the host instead. -
make upruns the same build, so use the twodocker composecommands in step 4. -
Go without the pin.
make lintexits 2 after 267 s; withGOTOOLCHAIN=go1.27.0it passes in 184 s. -
make test-apistraight aftermake migrate. The integration tier fails in two packages for want of the CI provisioning in step 8. That is the environment, not the code. -
One shell for both Go tiers. Carrying the integration step's
S3_*into the e2e run fails two fixture-lifecycle tests (step 9). -
The Word converter tests (
TestDocker*,TestRealPipeline*, the "Import converter tests" step in CI) are not run. They need thedocker/word-converterimage, which this piece of work did not try to build. WithTEST_WORD_CONVERTER_IMAGEunset they skip and the packages still readok, so a green local run says nothing about them. Only CI covers them. -
Building
docker/word-converterfails (verified 2026-10-12):apt-get updatein the image reachesdeb.debian.orgthrough the proxy and gets 403, solibreoffice-writercannot be installed. The converter tests and the converter half ofimport.live.spec.tsrun only in CI. WithoutIMPORT_DOCKER_BINARYandIMPORT_CONVERTER_IMAGEthe worker extracts a.docxnatively, which is how that spec was run here: buildcmd/apiandcmd/import-worker, setIMPORT_S3_BUCKET=quizzivy-imports,IMPORT_WORK_DIR(absolute, mode 0700),IMPORT_PROCESSING_ENABLED=true,IMPORT_WORKER_WAKE_URL=http://localhost:8091/wakeandIMPORT_WORKER_WAKE_ADDR=localhost:8091on both, run the worker, thenpnpm e2e:livethrough the wrapper config. -
tsc --noEmitchecks no files; usepnpm typecheck. -
pkill -forpgrep -fwith a pattern from your own command line matches the shell running it and kills it. Find a server by its port instead (ss -ltnp 'sport = :8080') and stop that PID. - Signing in again for every browser script. Sign-in is limited to 20 per hour per email, and reusing a stored session's refresh cookie in a second context rotates it and trips reuse detection. Sign in once and keep one browser context per run; restarting your own API clears the limiter.
-
Imports'
Text*integration tests needTEST_TEXT_IMPORT_*set locally (five tests); CI provides them.
| This container | CI | |
|---|---|---|
| Node | 22.22.0 | 24 |
| pnpm | 10.28.0 | 11.25.0 |
| Go | 1.24.7 host, 1.27.0 by GOTOOLCHAIN
|
1.27 |
psql client |
16.15 (server is 18.6) | 18 |
| Postgres |
postgres:18 in compose, shared by every run |
a service container per job, mirror.gcr.io/library/postgres:18 (Docker Hub's anonymous pull limit; docs/setup/ci.md) |
| MinIO image | built on the host from the pinned commits (step 3) | pulled from ghcr.io, or built by compose from bases tagged out of mirror.gcr.io
|
| Browsers | Chromium 1194 only, in /opt/pw-browsers (PLAYWRIGHT_BROWSERS_PATH); Playwright 1.62.1 looks for headless shell 1234, so a run launches with launchOptions.executablePath: '/opt/pw-browsers/chromium' from an untracked wrapper config. Never playwright install, never commit the wrapper |
what the Playwright config installs |
| Parallelism | 4 cores, one shared Postgres | one job per runner |
| Word converter tests | not run | run |
| Retries, shards | none unless asked (CI=1 for CI's Vitest defaults) |
Playwright retries: 2
|
A change to pnpm-lock.yaml was not exercised under pnpm 11.25.0 here; leave
its verification to CI.
Durations are wall-clock in this container, with other gates sometimes running.
| Gate | rc | Duration | Notes |
|---|---|---|---|
node scripts/check-design-deck.mjs |
0 | under 1 s | |
| goose up/down/up | 0 | 1 s | a first attempt failed rc 2 before the database was migrated |
pnpm lint |
0 | 173 s | |
pnpm typecheck |
0 | 28 s | |
pnpm format:check |
0 | 20 s | |
pnpm test:unit |
0 | 474 s | 320 files, 3936 tests |
pnpm test:integration |
0 | 53 s | 19 files, 120 tests |
pnpm build |
0 | 29 s | |
make gen-check |
0 | 75 s | |
make lint |
0 | 184 s | rc 2 in 267 s without the Go pin |
make test-api |
2 | 113 s | unit tier passes; integration fails only for the missing CI provisioning |
| Go integration tier, CI-provisioned | 0 | 68 s | 66 packages ok, none skipped; converter tests excluded |
| Go e2e tier, CI's job env only | 0 | 44 s |
quizzivy/tests ok in 42.7 s, nothing skipped |
No gate fails at bb4d4000 once the environment matches CI. The first e2e run
here failed twice with registered2 resources, want3 because it inherited the
integration step's S3_BUCKET (step 9 explains why); re-run with CI's job env
only, the same tests pass. Not run here: the Word converter tests, pnpm e2e,
pnpm e2e:content and pnpm e2e:live.
Generated from docs/team/environment.md. Edit that file in the repository; changes made on this page are overwritten.
Generated from the repository's documentation on every push to develop.
Start here
Product
Implementation plan
- 00 Architecture overview
- 10 Phase 0: Scaffold and contract
- 11 Phase 1: Auth, join, shells
- 12 Phase 2: Admin authoring
- 13 Phase 3: Assign and take
- 14 Phase 4: Grade and results
- 15 Phase 5: Hardening
- 16 Phase 4.5: The student at a desk
- 17 Word import: milestone plan
- 18 Word import: teacher workflow
- 19 Versioned content contract
- 20 Data model
- 30 Risk register
- 40 Open items
- 50 Non-functional requirements
- 60 Backend architecture
- 91 September 2026 issue remediation
- 92 Student layouts and interaction rules
Redesign (Phase R)
- 70 Redesign overview
- 71 R1: Foundations (v0.7.0)
- 72 R2: Access (v0.8.0)
- 73 R3: Student console (v0.9.0)
- 74 R4: Teacher workspace (v0.10.0)
- 74d D4: Deck update (v0.11.0)
- 74e E1: Moving a question across a group
- 75 R5: Admin console and email (v0.12.0)
- 76 R6: Question types and scoring (v0.13.0)
- 77 R7: Collaboration (v0.14.0)
- 78 R8: Schedule (v0.15.0)
- 79 R9: Insights (v0.16.0)
- 80 R10: Learn (v0.17.0)
- 81 R11: Landing, leads, v1.0 (v1.0.0)
Setup and operations
- CI
- Deploying
- DNS for quizzivy.com
- Google OAuth client
- Production operations
- Cloudflare R2 bucket
- Word import worker
- R4 rehearsal on a Neon branch
Design
Team
- Agent team
- Verification environment
- Team ledger
- R4 readiness
- R4 verification strategy
- T-R4.51 browser verification
Code READMEs