-
Notifications
You must be signed in to change notification settings - Fork 1
Supabase Sync
Rubric Maker works entirely offline via localStorage. The Supabase sync layer is an opt-in feature that adds:
- Cross-device sync — your rubrics, students, and grades are available on every device you sign in to.
- Colleague sharing — share a rubric or class with another teacher account (read-only or editable).
- File storage offload — attachments and export templates are stored in Supabase Storage instead of base64 strings in localStorage, removing the 5–10 MB browser storage limit for files.
localStorage remains the synchronous primary store. Supabase syncs in the background and the app loads instantly from localStorage even when offline or disconnected.
Browser
└── AppContext (localStorage — primary, synchronous)
└── StorageSync singleton (background, async)
├── SupabaseAdapter — Supabase client wrapper
└── AttachmentSync — file upload/download via Supabase Storage
The AppContext reducer is unchanged — components never talk to Supabase directly. StorageSync subscribes to state changes, mirrors them to Supabase, and hydrates localStorage from the DB on reconnect.
- Supabase CLI installed
- Docker Desktop running
# Start a local Supabase stack
supabase startThe CLI prints something like:
API URL: http://127.0.0.1:54321
anon key: eyJhbGci...
Studio: http://127.0.0.1:54323
In the app, go to Settings → Database → Connect & Sync and paste the URL and anon key.
Alternatively, add them to .env.local to pre-fill the form automatically:
VITE_SUPABASE_URL=http://127.0.0.1:54321
VITE_SUPABASE_ANON_KEY=eyJhbGci...
.env.localis gitignored. Never commit real credentials to the repository.
supabase stopGo to supabase.com and create a new project.
Region: Choose eu-central-1 (Frankfurt) for AVG/GDPR compliance when serving EU users.
The 40+ migration files in supabase/migrations/ set up the full schema, applied sequentially. Run them in order using the Supabase dashboard SQL editor, or push them via the CLI:
supabase db push --db-url "postgresql://postgres:<password>@<host>:5432/postgres"Key migrations (not exhaustive — see supabase/migrations/ for the full sequential history):
| Migration | What it does |
|---|---|
001_initial_schema.sql |
Creates the core tables (rubrics, students, classes, student_rubrics, attachments, etc.) |
002_rls_policies.sql |
Enables Row Level Security — each user can only read/write their own rows |
003_storage_buckets.sql |
Creates the attachments and templates storage buckets with RLS |
004_profile_trigger.sql |
Auto-creates a profiles row when a new auth user signs up |
033_tests_tables.sql |
Tests, student test attempts, and proctoring tables |
034_recordings_storage.sql |
Speaking-session recording storage bucket |
037_rename_user_role_to_teacher.sql |
Renames the user role to teacher; adds admin/observer roles |
038_audit_logs.sql |
Adds the audit_logs table backing Admin → Audit |
In the app, go to Settings → Database → Connect & Sync and enter your project URL and anon key (found in Project Settings → API in the Supabase dashboard).
All tables follow the same pattern: id (text, primary key), owner_id (uuid, FK to profiles), and a data JSONB column holding the full entity. This avoids complex camelCase ↔ snake_case column mapping while keeping RLS policies simple.
| Table | Description |
|---|---|
profiles |
One row per auth user; auto-created by trigger |
rubrics |
Rubric definitions |
classes |
Class definitions |
class_members |
Many-to-many: classes shared with other teachers (viewer or editor role) |
students |
Student records, linked to a class |
student_rubrics |
Graded rubrics + peer reviews (distinguished by is_peer_review) |
attachments |
Attachment metadata; the file lives in Supabase Storage |
comment_bank |
Reusable feedback snippets |
export_templates |
Word mail-merge template metadata; file in Storage |
grade_scales |
Custom grade scale definitions |
comment_snippets |
Individual comment snippet entries |
favorite_standards |
Saved CSP standard codes |
tests / student_tests
|
Test definitions and student attempts/results |
essay_assignments |
Standalone essay-workspace assignments and templates |
audit_logs |
Admin/grade/auth event log (Admin → Audit) |
Recordings (speaking sessions) are stored as blobs in Supabase Storage, not in a jsonb table — see mediaStore.ts / RecordingSync.ts in Architecture.
RubricMaker is offline-first: localStorage is always the source of truth, and Supabase is an optional sync layer. Conflicts can arise whenever the same record is edited on two devices (or one device edits offline while another edits online). The merge logic lives in src/utils/syncMerge.ts and runs at three points: startup sync, network-reconnect hydration, and after an in-page OTP sign-in.
Every collection with an edit/update action carries an updatedAt timestamp, set automatically whenever a record is created or saved:
- Rubrics, graded rubrics (
studentRubrics), peer reviews, classes, students, grade scales, comment snippets, comment bank items, self-assessments, speaking sessions, and document analysis results.
When the same record exists both locally and remotely, the merge keeps whichever copy has the later updatedAt — the entire record is replaced, not merged field-by-field. There is no field-level merge: if two devices edit different fields of the same rubric while both offline, the device that reconnects last with the newer timestamp wins for the whole record, and the other device's concurrent edit to that record is discarded.
attachments, exportTemplates, and favoriteStandards are add/delete-only (no edit action), so they have no updatedAt and rely on pending-queue protection only (below).
Local edits made while offline are queued in rm_pending_sync (localStorage) and flushed automatically when the app comes back online. While a record has a queued (not-yet-flushed) write, the merge always keeps the local copy, regardless of timestamps — this prevents reconnect-hydration from clobbering an edit that hasn't reached the server yet.
Remote state is the baseline for deletions: if a record exists locally but was deleted remotely (and has no pending queue entry protecting it), it is dropped during the merge. A pending local delete is not "resurrected" by a stale remote copy.
- Record-level only — concurrent edits to different fields of the same record are not merged; one edit wins entirely, and the other device's concurrent change is silently lost.
- Clock-dependent — LWW compares ISO timestamps from each device's local clock. Significant clock skew between devices could cause an older edit to win.
- No conflict UI — there is currently no notification when a concurrent edit was discarded by LWW. Teachers working on the same rubric, class, or grade from multiple devices at the same time should be aware that the most recently saved version wins silently.
| Stage | Mechanism |
|---|---|
| First connect | Anonymous session — no email required |
| Upgrade to named account | Email magic-link (OTP) sign-in via Settings |
| Sharing rubrics/classes | Both parties must have a named (non-anonymous) account |
Anonymous sessions persist across browser restarts via the Supabase auth cookie. Upgrading to a named account merges the anonymous data into the new user's account.
Once signed in with a named account:
- Open a rubric or class in the app.
- Go to Share and enter a colleague's email address.
- Choose Viewer (read-only) or Editor access.
- The colleague sees the shared item appear in their list on next sync.
Sharing is managed via the class_members table and RLS policies that grant read access to shared rows.
If you have existing data in localStorage and want to move it to Supabase:
- Connect to Supabase (Settings → Database → Connect & Sync).
- Click Push local → database to bulk-upload all localStorage data to Supabase.
| Variable | Description | Required |
|---|---|---|
VITE_SUPABASE_URL |
Your Supabase project URL (e.g. https://xyz.supabase.co) |
No — can be entered in UI |
VITE_SUPABASE_ANON_KEY |
Supabase anon (public) key | No — can be entered in UI |
Copy .env.example to .env.local and fill in the values. .env.local is never committed to version control.
- Check that the URL starts with
https://(cloud) orhttp://127.0.0.1:54321(local). - Check that the anon key is the full JWT string.
- For local: confirm
supabase startis running and Docker is up.
Click Pull Now in Settings → Database. The initial hydration runs automatically on connect, but can be triggered manually.
After a long offline period, Supabase may have newer data than localStorage. Use Pull Now to overwrite localStorage with the DB state, or Push local → database to do the reverse.