A focused web app for post-meeting notes, shared follow-up items, and per-student supervision history.
- Astro v6 - Modern web framework with server-first rendering
- React v19 - UI library for interactive components
- TypeScript v5 - Type-safe JavaScript
- Tailwind CSS v4 - Utility-first CSS framework
- Supabase - Authentication and backend-as-a-service
- Cloudflare Workers - Edge deployment runtime
- Node.js v22.14.0 (as specified in
.nvmrc) - npm (comes with Node.js)
- Clone the repository:
git clone https://github.com/ounold/10xdev.git
cd 10xdev- Install dependencies:
npm install-
Set up Supabase and configure environment variables — see Supabase Configuration below.
-
Create a
.dev.varsfile for local Cloudflare dev secrets:
cp .env.example .dev.vars- Run the development server:
npm run devIf npm run dev fails inside Codex on Windows with Access is denied or Cannot read directory "../../..", rerun the same command from a normal PowerShell session outside Codex. In this repository, that error pattern points to Codex sandbox filesystem limits rather than broken app dependencies.
npm run dev- Start development server (Cloudflare workerd runtime)npm run build- Build for productionnpm run preview- Preview production buildnpm run test:e2e- Run the critical-path Playwright dashboard role-flow spec against an already-running local servernpm run test:e2e:headed- Run the same e2e spec with a visible browsernpm run test:e2e:install- Install the Chromium browser used by the local e2e specnpm run test:integration- Run the shared read-model continuity integration checksnpm run lint- Run ESLint with type-checked rulesnpm run lint:fix- Auto-fix ESLint issuesnpm run format- Run Prettier
Use the layers below for different kinds of evidence. They are not interchangeable.
npm run lintnpm run build- the risk-appropriate local test layer for the change:
npm run test:e2efor browser/auth/role-flow changesnpm run test:integrationfor read-model/ordering/contract changes
- hosted smoke when the feature depends on real Supabase auth, linkage, RLS, or remote data shape
Current GitHub CI still enforces only npm run lint and npm run build, so stronger product checks must still be run locally before merge or release.
Use Playwright as browser proof for user-visible flows:
- redirects and route guards
- role separation
- authenticated thread behavior
Green E2E means the browser flow works locally. It does not, by itself, prove that hosted Supabase state is correct.
npm run test:integrationis the cheaper gate for read-model composition, chronology, and continuity semanticsnpm run test:mutationis extra hardening for test strength, not a replacement for lint/build/E2E/integration gates- high coverage percentages do not mean the mutation layer is satisfied
The repository now includes a first browser-level regression check for the shared /dashboard route in tests/e2e/dashboard-role-flow.spec.ts.
Run it like this:
- Start the local server first:
npm run dev- In a second shell, run the e2e spec:
npm run test:e2eImportant local note:
- the Playwright setup intentionally does not start Astro for you
- in this Windows/Codex environment, Playwright-managed
npm run devwas not reliable - if no server is already listening on
http://127.0.0.1:4321, the spec will fail withERR_CONNECTION_REFUSED - this spec is browser-level evidence for the shared dashboard seam, not a substitute for hosted smoke when remote Supabase state matters
Without any extra credentials, the spec always proves:
- unauthenticated
/dashboardrequests redirect to/auth/signin
If you also provide role-specific test credentials in .env or .dev.vars, the same spec can additionally verify:
- unlinked student ->
/pending-access - linked student -> read-only
/dashboard - professor -> roster visibility plus thread-entry sentinel
Supported variables:
E2E_PROFESSOR_EMAIL=
E2E_PROFESSOR_PASSWORD=
E2E_LINKED_STUDENT_EMAIL=
E2E_LINKED_STUDENT_PASSWORD=
E2E_UNLINKED_STUDENT_EMAIL=
E2E_UNLINKED_STUDENT_PASSWORD=If one of those account pairs is missing, the corresponding check is skipped intentionally. A partial green run does not replace hosted verification.
Before assuming a browser-level check needs fresh credentials, inspect the repo-local .auth/ directory.
Current repo-specific fixtures or local fixture targets:
.auth/user.json- saved PlaywrightstorageStatefor a professor session onhttp://127.0.0.1:4321.auth/linked-student-olgierd.json- saved PlaywrightstorageStatefor one linked student session.auth/linked-student-olgierd.meta.json- companion metadata for the linked-student fixture, including own and foreign student ids used by cross-student access checks.auth/claim-student.json- local PlaywrightstorageStatetarget for a student account that should begin on/pending-access.auth/claim-student.meta.json- local companion metadata target for the claim-flow fixture, currently storing the claim-student email anchor
Important usage note:
.auth/is gitignored in this repository, soclaim-student.*fixtures are local artifacts that must be generated or refreshed on your machine- these saved states are not wired into every existing spec automatically
tests/e2e/dashboard-role-flow.spec.tsstill usesE2E_*_EMAIL/E2E_*_PASSWORDtests/e2e/linked-student-foreign-thread.spec.tsalready uses a repo-localstorageStatefixture by defaulttests/e2e/linked-student-note-edit.spec.tsandtests/e2e/linked-student-note-append.spec.tscan also reuse that linked-student fixture throughE2E_LINKED_STUDENT_STORAGE_STATEtests/e2e/linked-student-foreign-note-post.spec.tscan derive a real foreign note id from the saved professor fixture in.auth/user.jsontests/e2e/student-claim-flow.spec.tsfirst looks for.auth/claim-student.json, then falls back toE2E_CLAIM_STUDENT_*orE2E_UNLINKED_STUDENT_*only if that saved state is absent
Agent rule for this repository:
- before concluding that professor or student E2E verification is blocked on missing credentials, check whether an appropriate
.auth/*.jsonPlaywright state already exists and whether the target spec can use it directly or with a small test-only adaptation
The claim-flow E2E follow-up uses a dedicated prep helper instead of manual Studio edits:
- helper path:
tests/e2e/support/studentClaimFixture.ts - purpose: reset all
studentsrows for one email, then rebuild either a single claim-ready row or a duplicate-email blocked state - auth model: this prep is server-side and requires
SUPABASE_URLplusSUPABASE_SERVICE_ROLE_KEY - safety scope: this prep also requires
E2E_PROFESSOR_PROFILE_IDso fixture rows are always attached to the intended professor workspace instead of guessing globally
Planned repo-native paths:
prepareClaimReadyFixture(...)- creates exactly one unlinkedstudentsrow for the target emailprepareDuplicateClaimFixture(...)- creates two unlinked rows for the same email so/pending-accessmust stay blockedresetStudentClaimFixture(...)- cleanup helper for the same email anchorreadClaimStudentFixtureMeta()- reads the local email anchor from.auth/claim-student.meta.jsonwhen.envdoes not define a dedicated claim-student account
Required env for safe fixture prep:
E2E_PROFESSOR_PROFILE_ID=Rule for future E2E work on this slice:
- start from repo-local Playwright
storageStatewhen available - use the claim-fixture helper to prepare data states
- ask for fresh credentials only if neither repo-local state nor controlled fixture prep can satisfy the scenario
Recommended local run for the claim-flow slice:
$env:E2E_BASE_URL='http://localhost:4325'
& 'C:\Program Files\nodejs\npx.cmd' playwright test tests/e2e/student-claim-flow.spec.tsWhat that spec now proves:
- one prepared unlinked row lets the student claim access and land on
/dashboard?claimReady=1 - duplicate prepared rows keep the same student blocked on
/pending-access - both scenarios can be rerun without hand-editing
students.student_profile_id
The repo now includes linked-student browser checks around the shared-note continuity slice:
tests/e2e/linked-student-note-edit.spec.ts- linked student can update an existing shared note without seeing professor-only surfacestests/e2e/linked-student-note-append.spec.ts- linked student can append a new item at the tail of a shared note and the spec restores state afterwardtests/e2e/shared-note-cross-role-visibility.spec.ts- professor can see the linked student's shared-note edit in the same threadtests/e2e/linked-student-foreign-thread.spec.ts- linked student cannot open another student's thread by direct URLtests/e2e/linked-student-foreign-note-post.spec.ts- linked student cannot submit direct POST updates to another student's note id
Recommended local run for this slice:
npx playwright test tests/e2e/linked-student-note-edit.spec.ts tests/e2e/linked-student-note-append.spec.ts tests/e2e/linked-student-foreign-thread.spec.ts tests/e2e/linked-student-foreign-note-post.spec.tsThe repository now includes a first integration-level continuity check for the shared supervision read model in tests/integration/supervision-read-model.test.ts.
Run it like this:
npm run test:integrationWhat it protects:
- newest-first note ordering
- same-
meeting_date, different-created_attie-break behavior - item ordering by
position info/tasksemantic preservation throughsrc/lib/supervision.ts
Important local note:
- this suite is intentionally below the browser layer and exercises Supabase-shaped reads with local stubs
- if
npm run test:integrationfails inside Codex on Windows with config-loading or esbuild filesystem errors, rerun it from a normal PowerShell session outside Codex - a green local integration run does not replace hosted smoke for remote Supabase drift
- use this layer as the cheaper required gate when the risk is chronology, ordering, or continuity composition rather than browser navigation
.
├── src/
│ ├── layouts/ # Astro layouts
│ ├── pages/ # Astro pages
│ │ └── api/ # API endpoints
│ ├── components/ # UI components (Astro & React)
│ └── assets/ # Static assets
├── public/ # Public assets
├── wrangler.jsonc # Cloudflare Workers configThis project uses Supabase for authentication. Environment variables are declared via Astro's astro:env schema and are treated as server-only secrets — they are never exposed to the client.
Requires Docker and ~7 GB RAM.
- Create your
.envfile:
cp .env.example .env- Initialize the local Supabase project (creates a
supabase/config folder):
npx supabase init- Start the local stack (downloads Docker images on first run):
npx supabase start- Copy the credentials printed by the CLI into your
.envand.dev.vars:
SUPABASE_URL=http://127.0.0.1:54321
SUPABASE_KEY=<anon key from CLI output>
- To stop the stack when done:
npx supabase stopThe local Studio UI is available at http://localhost:54323.
This repository now includes supervision-domain migrations under supabase/migrations/. After the local stack starts, apply the repository schema with the usual Supabase migration workflow so local development includes the app's profiles, students, notes, and note_items tables in addition to auth.users.
Recommended local reset/apply flow after schema changes:
npx supabase db resetThat applies the checked-in migrations and the minimal development seed from supabase/seed.sql.
If you prefer to use a hosted Supabase project, add these variables to your .env and .dev.vars files:
| Variable | Description |
|---|---|
SUPABASE_URL |
Project URL from Supabase dashboard → Settings → API |
SUPABASE_KEY |
anon public key from Supabase dashboard → Settings → API |
SUPABASE_URL=https://<project-ref>.supabase.co
SUPABASE_KEY=<anon-key>
If your change adds or modifies database schema, RLS, or SQL helpers, deploy the hosted Supabase changes explicitly before treating the Cloudflare release as complete:
# one-time auth if needed
& '.\node_modules\supabase\bin\supabase.exe' login
# one-time link to the hosted project
& '.\node_modules\supabase\bin\supabase.exe' link --project-ref <project-ref>
# apply checked-in migrations to the linked remote project
& '.\node_modules\supabase\bin\supabase.exe' db push --linkedOnly after the remote database is updated should you proceed with the Cloudflare release flow.
If you run the professor-bootstrap change against a hosted Supabase project, also configure:
SUPABASE_SERVICE_ROLE_KEY=<service-role-key>
BOOTSTRAP_PROFESSOR_EMAIL=<professor-email>
SUPABASE_SERVICE_ROLE_KEY is required because the first-professor claim is performed by a dedicated server-side bootstrap endpoint with an admin client. This is intentional: current RLS blocks self-service role escalation for normal authenticated clients.
BOOTSTRAP_PROFESSOR_EMAIL is the single allowlisted account that may claim the first professor role when no professor exists yet.
By default Supabase requires email confirmation before a user can sign in. To skip this during local development:
- Open the Supabase dashboard for your project
- Go to Authentication → Email → Confirm email
- Toggle it off
Users can then sign in immediately after sign-up without clicking a confirmation link.
| Route | Description |
|---|---|
/auth/signin |
Email/password sign-in form |
/auth/signup |
Email/password sign-up form |
/auth/confirm-email |
Post-signup "check your inbox" page |
/dashboard |
Example protected page (redirects to /auth/signin if unauthenticated) |
Route protection is handled in src/middleware.ts. Add paths to the PROTECTED_ROUTES array there to require authentication.
To verify the MVP professor bootstrap locally with a hosted Supabase project:
- Set
BOOTSTRAP_PROFESSOR_EMAILto the account that should become the first professor. - Sign in with that allowlisted account and open
/dashboard. - Confirm the app lands on the professor shell and shows
Current app role: professor. - Sign out, then sign in with a different non-allowlisted account.
- Confirm that second account lands on
/pending-accessrather than seeing professor content.
This two-account flow is the expected manual verification path for professor-bootstrap.
To verify the current professor-note-history slice against a hosted Supabase project:
- Sign in as the bootstrap professor and open
/dashboard. - Confirm at least one student thread is visible. If the hosted project has no students yet, create one manually in Supabase Studio under
public.studentswithprofessor_profile_idset to the professor profile id. - Open a student thread and confirm existing notes render in chronological order when present.
- Create a fresh dated note with multiple
info/taskrows and confirm it appears on the same thread after submit.
Current hosted-project note:
- the shipped write path verifies professor access with the session client, then persists note writes with the server-side admin client because the hosted Supabase project currently rejects session-client note inserts under RLS. Treat that as a temporary hardening adaptation until the hosted RLS/session-write path is reconciled.
To verify the current professor-student-roster slice against a hosted Supabase project:
- Sign in as the professor and open
/dashboard. - Create one student using only
full_name. - Create another student using both
full_nameandemail. - Confirm each new student appears immediately in the dashboard roster.
- Open the new roster entries and confirm they still link into
/dashboard/students/[studentId].
Current hosted-project note:
- the shipped roster write path first attempts the intended session-client insert and falls back to the server-side admin client only after the route verifies the authenticated professor session. This is a temporary adaptation for hosted Supabase environments where remote RLS currently rejects session-client student inserts.
To verify the current student-read-history slice against a hosted Supabase project:
- As the professor, make sure exactly one
public.studentsrow exists for the student's email. - Set that row's
emailto the real student account email and leavestudent_profile_id = null. - Sign in with that student account and open
/pending-access. - Confirm the page shows the claim action and lets the student link access without leaving the in-app flow.
- Confirm the app redirects the same user into
/dashboardafter claim. - Confirm the student sees only their own chronological supervision history and no professor roster surfaces.
- Sign out and sign in with an account that has no matching roster email.
- Confirm the unlinked account stays on
/pending-accesswith a blocked-state explanation. - Create a duplicate-email conflict in
public.studentsand confirm the student still stays blocked on/pending-access.
Current hosted-project note:
- the primary happy path no longer requires manual
student_profile_idediting - duplicate matching emails intentionally block claim until the professor resolves the roster conflict
Use this when local npm run test:e2e is green and you still need to confirm the hosted auth/linking reality behind the same shared /dashboard seam.
-
Student claim check
- sign in with a hosted student account whose email matches exactly one unlinked
public.students.email - open
/pending-access - confirm the claim action is visible
- submit the claim and confirm the account lands on
/dashboard
- sign in with a hosted student account whose email matches exactly one unlinked
-
Linked student check
- after claim, reopen
/dashboard - confirm the account sees only its own read-only supervision history
- confirm no professor roster or note-creation controls are visible
- after claim, reopen
-
Unlinked student check
- sign in with a hosted student account that has no matching
public.students.student_profile_id - open
/pending-access - confirm the account stays blocked with no claim action
- sign in with a hosted student account that has no matching
-
Duplicate-email blocked check
- prepare two unlinked
public.studentsrows with the same email as the student account - sign in with that student account
- open
/pending-access - confirm the account stays blocked and the page explains that the app will not choose automatically
- prepare two unlinked
-
Professor sentinel check
- sign in as the hosted professor
- open
/dashboard - confirm the roster is visible
- confirm the roster surfaces linked vs email-ready vs missing-email status hints
- open one student thread
- confirm the thread still renders its chronological history
For local browser-level verification, the repo also carries a saved professor Playwright state in .auth/user.json. Prefer checking that fixture before asking for fresh professor credentials.
Treat these hosted checks as required smoke, not optional confidence polish. The local e2e spec protects the route contract, but it cannot prove remote Supabase link state by itself.
The MVP now includes a first-pass supervision data model in Supabase:
profiles— app-level identity and role overauth.usersstudents— professor-owned student records, optionally linked to a student profilenotes— dated post-meeting notes for a single studentnote_items— ordered bullet items for a note, with explicit item type
Row-level security for these tables is planned as a follow-up foundation step; this repository change introduces the schema first so later slices can build on a stable model.
The app-facing TypeScript contract for the supervision domain lives in src/lib/database.ts. Follow-up slices should import these types through the @/* alias instead of recreating the row shapes ad hoc.
This project deploys to Cloudflare Workers.
- Build the project:
npm run buildIf Wrangler or astro build fails on Windows with EPERM while writing under %APPDATA%, run the build with workspace-local XDG directories instead:
$env:XDG_CONFIG_HOME="$PWD\\.tmp-xdg"
$env:XDG_CACHE_HOME="$PWD\\.tmp-xdg-cache"
& 'C:\Program Files\nodejs\node.exe' '.\node_modules\astro\bin\astro.mjs' build- Deploy with Wrangler:
npx wrangler deployRecommended release order for production:
- Push remote Supabase migrations with
supabase db push --linkedif the change touches schema or RLS. - Verify the hosted Supabase project is at the expected schema level.
- Deploy the Worker with
npx wrangler deploy.
Set SUPABASE_URL and SUPABASE_KEY as secrets in your Cloudflare dashboard or via npx wrangler secret put. If you deploy professor bootstrap, also set SUPABASE_SERVICE_ROLE_KEY and BOOTSTRAP_PROFESSOR_EMAIL in the target environment.
GitHub Actions runs lint + build on every push and PR to master. Configure SUPABASE_URL and SUPABASE_KEY as repository secrets in GitHub for the build step.
MIT
