Skip to main content

Local Development Setup

There are two ways to run Pawtograder on your machine. Point the frontend at the shared staging backend when you are working on the UI, or run the whole Supabase stack in Docker when you need to change migrations, RLS policies, or edge functions, or to run the Playwright suite. Start with the staging path: it needs no Docker and no database.

Prerequisites

Do not use a globally installed Supabase CLI. On older versions, supabase db reset fails partway through the storage-policy migrations with ERROR: must be owner of table objects (AGENTS.md:25). npx supabase resolves the pinned 2.105.0 from node_modules, and the CLI’s own “a new version is available” notice is safe to ignore.

Frontend against the staging backend

1

Clone the repository

2

Install dependencies

The warnings from amazon-chime-sdk-component-library-react about Node versions, along with the wall of deprecation notices, are expected (README.md:35).
3

Create .env.local

.env.local.staging is the only env template tracked in git, and it holds five browser-facing values: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, NEXT_PUBLIC_PAWTOGRADER_WEB_URL, NEXT_PUBLIC_ENABLE_SIGNUPS, and NEXT_PUBLIC_GITHUB_OAUTH_CLIENT_ID. Change the web URL so auth redirects come back to your machine instead of the deployed site (AGENTS.md:30):
4

Start the dev server

5

Open the app

Open http://localhost:3000. npm run dev runs next dev over plain HTTP (package.json:13); there is no TLS on this path and no certificate to accept.
Staging ships with the signup UI turned off: .env.local.staging:27 sets NEXT_PUBLIC_ENABLE_SIGNUPS=false, so you cannot create your own account on this path and should ask a maintainer for one (AGENTS.md:127). Sign in with email and password rather than GitHub, Discord, or Entra, because staging’s redirect allow-list only accepts https://staging.pawtograder.net and an OAuth round trip lands on the deployed site instead of your dev server (.env.local.staging:16-18). To create accounts yourself, use the full local stack below.

Full local Supabase stack

Use this path for schema changes, RLS policy work, edge functions, and the Playwright end-to-end tests.
1

Install dependencies

2

Start Supabase from a fresh database

npx supabase start restores the project’s previous Docker volume by default, which can leave you on an old schema and produce errors like column ... does not exist (AGENTS.md:20). Wipe the volume first, then start:
supabase start runs every migration in supabase/migrations/ against the empty database (AGENTS.md:24). The first run downloads several container images and takes a few minutes. If you want the seed data as well, follow it with a reset, which reapplies the migrations and then runs supabase/seed.sql (supabase/config.toml:45-50; pass --no-seed to skip the seed step):
--no-backup deletes the local database volume and everything in it. Only run it when you are willing to lose your local data.
To confirm the schema is current, compare the newest row in supabase_migrations.schema_migrations against the newest filename under supabase/migrations/ (AGENTS.md:27).
3

Fill in .env.local

Read the local URLs and keys out of the CLI rather than copying them by hand (AGENTS.md:28):
That prints API_URL, ANON_KEY, SERVICE_ROLE_KEY, DB_URL, and the rest. Set these in .env.local:
Never expose SUPABASE_SERVICE_ROLE_KEY to the browser or commit it to version control. Keep it in server-only code and CI secrets.
4

Serve the edge functions

Without --env-file, the functions start with none of your secrets and fail in ways that look like auth bugs (AGENTS.md:32, :129). They are served at http://127.0.0.1:54321/functions/v1/<name>.
5

Start the dev server

6

Seed test data

See Seeding test data for templates and login credentials.
Local auth does not require email confirmation (supabase/config.toml:135 sets enable_confirmations = false). With ENABLE_SIGNUPS=true, registering any address adds you to the seeded “Demo Class” (supabase/seed.sql:1) as a student. Include the word instructor in the address to get the instructor role instead (supabase/migrations/20250414005348_emailer.sql:117-121).

Local services

Supabase Studio

Open http://127.0.0.1:54323 to browse and edit tables, manage auth users, test RLS policies, and inspect storage buckets.

Email testing

Mailpit captures everything the stack tries to send, so you can read magic links, password resets, and notification emails at http://127.0.0.1:54324. The config key is still the legacy [inbucket] (supabase/config.toml:70), but the CLI runs Mailpit and labels it that way.

MCP endpoints

Two different servers carry the “MCP” label locally:
  • http://127.0.0.1:54321/mcp belongs to the Supabase CLI itself and appears in the npx supabase status output. It exposes your local Postgres to an AI assistant.
  • http://127.0.0.1:54321/functions/v1/mcp-server is Pawtograder’s own MCP server, implemented as an edge function (supabase/config.toml:775-779). It answers only while npx supabase functions serve is running. See the MCP server guide.

Seeding test data

npm run seed runs scripts/SeedDB.ts (package.json:30), which creates a class with students, graders, assignments, rubrics, discussions, help requests, and surveys. Pick a size with --template; the choices are micro (the default), small, large, tcrs, marketing, and custom (scripts/SeedDB.ts:583).
The run prints a lot of output. Search back for “Login Credentials” to find the seeded accounts (scripts/DatabaseSeedingUtils.ts:2188); they all use the password change-it unless you set TEST_PASSWORD (scripts/DatabaseSeedingUtils.ts:1475). Seeding is for manual browser testing only. The E2E tests build their own fixtures and do not need it (AGENTS.md:62).

Environment variables

.env.local is untracked and is the only file the app, the scripts, and the test helpers read.

Frontend against staging

Copying .env.local.staging gives you everything this path needs: The server-side keys in the next table are not needed here. utils/supabase/client.ts:8 falls back from SUPABASE_URL to NEXT_PUBLIC_SUPABASE_URL, and SUPABASE_SERVICE_ROLE_KEY is only read by createAdminClient, which the seed scripts and test helpers use.

Full local stack

End-to-end tests

AGENTS.md:55-63 lists what the edge functions need before the E2E suite will pass:

Integration-specific variables

These stay unset for ordinary local work. Set one only when you are working on that integration:

Two names for the signup flag

lib/features.ts:12-25 checks ENABLE_SIGNUPS first and falls back to NEXT_PUBLIC_ENABLE_SIGNUPS. Only the NEXT_PUBLIC_ form reaches the browser bundle, which is why the env templates and the image builds use it (.env.local.staging:27, .github/workflows/release-images.yml:227, preview.yml:343) while the E2E lane sets the bare form (deploy.yml:189). Either works locally; set one, not both.
Next.js inlines every NEXT_PUBLIC_* value at build time. Editing NEXT_PUBLIC_PAWTOGRADER_WEB_URL in .env.local does not change an existing .next build, and a mismatch between the build, .env.local, and the test runner causes silent magic-link and auth-redirect failures (AGENTS.md:29-31, :50). After changing it, rebuild from a clean output directory:

Development commands

Daily development

npm run typecheck:functions only covers supabase/functions/, and it ends in an || echo fallback, so it prints problems without failing (package.json:37). The Next.js app has no typecheck script; run npx tsc --noEmit if you want one.

Database operations

npm run client-local rewrites utils/supabase/SupabaseTypes.d.ts and copies it into supabase/functions/_shared/ (package.json:28). It is generated, so never hand-edit it.

Testing

For a full Playwright run, use a production build on port 3001 rather than next dev. The per-route compile cost of next dev causes widespread timeouts across the suite (AGENTS.md:48). Build with NEXT_PUBLIC_PAWTOGRADER_WEB_URL=http://localhost:3001, serve with PORT=3001 npm run start, and run the tests with a matching BASE_URL (AGENTS.md:50-52). npm run test:e2e:local is for iterating on a single test in dev mode.

Build and run a production bundle

npm run start:https runs scripts/next-start-https.cjs, which terminates TLS in Node using a certificate pair you provide. It looks for localhost-key.pem and localhost.pem under .certs/, then certificates/, and exits with instructions if it finds neither; it does not generate a certificate for you (scripts/next-start-https.cjs:6-19, :128-159). Override the port with PORT and the paths with SSL_KEY_PATH and SSL_CERT_PATH (scripts/next-start-https.cjs:99, :106-124).

Operator CLI

npm run cli runs cli/index.ts through tsx (package.json:41-42); the same source ships to npm as @pawtograder/cli (AGENTS.md:71). Every command needs a login token, and the full command surface is documented in the CLI guide.

Continuous integration

Every push and every pull request triggers .github/workflows/lint.yml, which runs three independent jobs:
  1. npm run lint, which is ESLint plus a Prettier check (lint.yml:33)
  2. Helm chart guard-rail render tests, charts/pawtograder/tests/render-guardrails.sh (lint.yml:47)
  3. Deno unit tests for the gradebook recalculation expressions, npm run test:functions (lint.yml:60)
The Playwright suite runs in .github/workflows/deploy.yml, which stands up its own Supabase stack and a production build on port 3001, then runs npx playwright test (deploy.yml:464) and a narrow Jest integration pass over tests/unit/issue (deploy.yml:393). The remaining workflows are event-scoped or dispatch-only: preview.yml builds a per-PR preview environment, release-images.yml and publish-cli.yml publish artifacts on branch and tag pushes, update-docs.yml runs on pushes to staging, a11y-vo-smoke.yml runs only on the a11y/vo-smoke branch, and canvas-e2e.yml, playwright-standalone.yml, a11y-nvda.yml, and a11y-voiceover.yml are workflow_dispatch only. No workflow runs a TypeScript typecheck, and npm test is never run as a whole suite.

Running the same checks locally

Run npm run format before every commit. CI rejects unformatted code (AGENTS.md:42).

Project structure

Troubleshooting

Supabase will not start

Check that Docker is running, then look for conflicts on the ports in Local services: 54321 through 54325, plus 54327. If the stack is wedged, clear it and start over:
docker system prune is sometimes suggested at this point. It removes stopped containers and dangling images across your whole machine, not just this project, so run it only if you know what else is on the daemon.

ERROR: must be owner of table objects

A CLI older than the pinned 2.105.0 ran the migrations — usually a globally installed supabase that your shell resolved ahead of the one in node_modules. Find out which binary you are getting, restore the pinned copy, then invoke it through npx from here on (AGENTS.md:25):
Reinstalling alone is not enough if you keep typing bare supabase: the global binary still wins, and the reset fails the same way.

no partition of relation "audit" found for row

public.audit is partitioned by day. The migration creates partitions for the day it runs plus the next seven, and a pg_cron job extends the window every night (supabase/migrations/20251228143943_partitioned_audit_system.sql:40-61 for the initial window, :83-125 for the function, :128-146 for the cron job). A database that sat stopped past the end of that window has no partition for today. Create the missing ones:

column ... does not exist or a missing table

The stack restored a stale volume instead of migrating a fresh one. Redo the volume-wiping start sequence from Full local Supabase stack (AGENTS.md:20-24).

Database connection errors

Confirm the stack is up with npx supabase status, then check that .env.local carries the URLs and keys that npx supabase status -o env reports.

TypeScript errors after schema changes

Regenerate the types:

Edge functions return WORKER_ERROR

Make sure they are running with your env file, then probe one directly. A healthy function answers with a JSON error about the missing token rather than WORKER_ERROR (AGENTS.md:63):

Circuit breaker active in E2E runs

The GitHub circuit breaker tripped. This means a call reached the real GitHub path instead of the E2E bypass; it does not mean credentials are missing, since dummy values are enough locally. Fix the root cause, then reset the breaker (AGENTS.md:54):

Agent and AI-assistant configuration

The repository root carries the guidance that AI coding assistants read:
  • AGENTS.md is the operational reference for Cursor Cloud and other agents. It covers Docker daemon setup, the fresh-volume Supabase sequence, seeding, ports, the operator CLI, and the known local failure modes.
  • CLAUDE.md points at AGENTS.md for commands and adds the architecture map: Supabase client factories, edge functions, realtime controllers, route layout, and the TableController pattern.
  • WRITING.md holds the prose conventions for everything in the repository.
  • .claude/ holds Claude Code project settings.

Next Steps