A Next.js app for rubric-based grading:
- Import rubrics, students, and grades.
- Grade students and groups against a grid's rubrics.
- Export grades as CSV.
- Track grading completion across a grid.
Tardigrade's name is a play on the near-indestructible micro-animal + "grade".
- Next.js 16 (App Router) + React 19
- Mantine v9
- PostgreSQL
- Kysely (query + migrations)
- Vitest (unit, integration, and Storybook tests) + Playwright (end-to-end)
- Biome (format/lint)
- Node.js 26+
- pnpm 11+
- Docker (for local PostgreSQL and containerized tests)
Local development environment variables are committed in .env.development and loaded automatically through dotenvx, so no environment setup is needed. To override a value, create a gitignored .env.local (or .env.development.local) at the repository root — dotenvx's flow convention gives those files precedence.
- Install dependencies.
pnpm install- Start PostgreSQL.
pnpm db:up- Run database migrations.
pnpm db:migrate:up- Start the app.
pnpm dev- Open http://localhost:3000.
When finished:
pnpm db:downpnpm dev # Start the Next.js development server.
pnpm build # Build the production app.
pnpm start # Start the built production app.
pnpm storybook # Start Storybook locally.
pnpm storybook:build # Build the static Storybook site.
pnpm storybook:browser:install # Install the Chromium browser used by Storybook tests.pnpm check # Check formatting and linting with Biome.
pnpm check --fix # Format and lint with Biome, applying safe fixes.
pnpm check-types # Run TypeScript type checks.
pnpm test # Run the default test suite.
pnpm test:unit # Run unit tests only.
pnpm test:integration # Run integration tests only.
pnpm test:storybook # Run Storybook component tests only.
pnpm test:watch # Run tests in watch mode.
pnpm test:e2e # Run the Playwright end-to-end smoke test.
pnpm lint:boundaries # Check layer boundaries with dependency-cruiser.See Running integration tests for local and CI database behavior. pnpm test:e2e needs a production build first (pnpm build) and Docker for its ephemeral PostgreSQL; see Testing conventions for the full test-tier map.
pnpm db:up # Start the local PostgreSQL container.
pnpm db:down # Stop the local PostgreSQL container.
pnpm db:logs # Show local PostgreSQL logs.
pnpm db:migrate:status # Show migration status.
pnpm db:migrate:up # Apply pending migrations.
pnpm db:migrate:down # Roll back the latest migration.
pnpm db:reset # Reinitialize the local DB without applying migrations.
pnpm db:types:generate # Regenerate database types.- Open the home page at
/— it redirects to your first grid's Overview, or to/gridsif none exist yet. - Create or open a Grid.
- Use Import to load Rubrics, Students, and optionally Grades.
- Grade students and groups by student or group.
- Open Results for the Grades table and the Analytics breakdown.
- Use Export to download grades as CSV.
- Domain terminology (internal/dev-facing): CONTEXT.md is the canonical glossary — read it before changing domain terms, identifiers, or contracts.
- User-facing vocabulary (UI labels, headings, messages): docs/reference/lexicon.md is the canonical word list — check it before writing new UI copy.
- URL structure: docs/reference/url-conventions.md documents the route tree and its ID/slug segments.
- Issue, PR, and label conventions: docs/guides/issue-and-pr-conventions.md owns the detailed templates, label guidance, and planning notes.
- Full documentation map: docs/index.md indexes every ADR, investigation, design doc, and reference doc in the repo.
rubrics:
- id: r1
label: Rubric 1
criteria:
- id: c1
kind: check
label: Correctness
marks: 2
- id: c2
kind: options
label: Quality
marks:
poor: 0
good: 1
excellent: 2
- id: c3
kind: number
label: Value
minValue: 0
maxValue: 10
minMarks: 0
maxMarks: 5Rules:
rubricsis required and must contain unique rubric ids.- Each rubric requires
idandcriteria. - Criterion ids must be unique across the whole grid, not just within one rubric — persistence conflicts on
(gridId, id), so reusing an id across two rubrics passes this parser but fails on save. - Check criterion:
marksis the number of marks awarded for a Yes answer.falseMarks(optional) sets marks for a No answer (defaults to0). - Options criterion:
marksmust contain at least 2 label/mark entries (label to numeric marks). - Number criterion:
- At least one of
minMarksormaxMarksmust be provided. - When only
maxMarksis given,minMarksdefaults to0andmaxMarksmust be> 0. - When only
minMarksis given,maxMarksdefaults to0andminMarksmust be< 0. minValuedefaults to0,maxValuedefaults to1. IfminValueis provided,maxValuemust also be provided.reversed(optional boolean): reverses the value-to-marks mapping direction.- Final values must satisfy
minMarks <= maxMarksandminValue < maxValue.
- At least one of
- Unrecognized top-level keys and unrecognized criterion fields are rejected, so a stale file (e.g. an old
questions:export) fails loudly instead of importing silently with fields dropped.
last_name,first_name,id,group
Doe,John,john_doe,
Smith,Jane,jane_smith,
Johnson,Bob,bob_johnson,Group ARules:
- Required columns:
last_name,first_name,id. - Optional column:
group. - Empty
groupmeans an individual grade target. - Same
groupgroups students into one grade target.
kind,name,r1:c1,r2:c2
individual,jane_smith,,
individual,john_doe,true,good
group,Group A,false,excellentRules:
- Required columns:
kind,name. kindmust beindividualorgroup.namematches a row to its grade target: for agrouprow, the group's name; for anindividualrow, the student's rosterid(not the student's display name — student names aren't guaranteed unique, but roster ids are).- Grade columns use
rubricId:criterionId. - For export/import round-trip, export must include grade columns (
rubricId:criterionId). Marks-only exports (rubricId:criterionId:marks) do not contain importable grades. - Values by criterion kind:
- Check:
trueorfalse - Options: one of the criterion's labels
- Number: a numeric value
- Check:
- Empty grade cells are ignored.
- Unknown columns are rejected.
- A row with no matching grade target blocks the entire import — nothing is written until every row matches.
- Export-only columns like rubric totals (
rubricId:total),:markscolumns, andfinal_totalare allowed and ignored on import.
- Environment variables are loaded through dotenvx in package scripts. Local development defaults live in
.env.development; override them in a gitignored.env.localor.env.development.local. - Database migrations are handled by Kysely in
src/db/migrate.ts; see Database migrations for migration conventions. - Storybook component tests run with the normal Vitest suite via
pnpm test; usepnpm test:storybookfor the Storybook project alone.