Skip to content

Data model ​

Tables ​

sql
-- One JSON document per user
create table public.user_data (
  user_id    uuid primary key references auth.users (id) on delete cascade,
  data       jsonb not null default '{}'::jsonb,
  updated_at timestamptz not null default now()
);

-- Daily counter of AI requests (only the server can access it)
create table public.ai_usage (
  user_id uuid not null references auth.users (id) on delete cascade,
  day     date not null,
  count   int  not null default 0,
  primary key (user_id, day)
);

Access rules (RLS) ​

Tableselectinsertupdatedelete
user_dataauth.uid() = user_idauth.uid() = user_idauth.uid() = user_idauth.uid() = user_id
ai_usage————

ai_usage has RLS enabled with no policies at all: no client can read or write it; only the server can, with the secret key. user_data is in the supabase_realtime publication so devices stay in sync.

The full SQL is in supabase/migrations/20260927000000_init.sql.

The document ​

Field values such as "load": "alta" or "epoca": "normal" are internal identifiers and stay in Portuguese whatever the interface language.

jsonc
{
  "owner": "uuid",               // who the local copy belongs to
  "plan": {                      // null until the user creates one
    "title": "", "startDate": "2026-09-25", "examDate": "2027-01-04",
    "hoursPerDay": [3, 3.5, 3.5, 3.5, 4, 3.5, 3],       // Sunday … Saturday
    "subjects": [{ "id": "ed", "name": "Estruturas de Dados", "short": "ED",
                   "area": "uni", "load": "alta", "ects": 7, "examDate": "", "ucId": "ed", "color": "#c39cff" }],
    "weeklyPlan": [{ "day": 2, "subject": "ed", "session": "3 blocos 40+10", "minutes": 150, "focus": "…" }],
    "phases": [{ "name": "Aprender", "label": "60 / 40", "start": "…", "end": "…", "ratio": "…" }],
    "tips": [{ "title": "…", "text": "…" }]
  },
  "attempts": [                  // append-only
    { "id": "…", "at": 1790000000000, "subject": "ed",
      "kind": "practice",        // learn | practice | probe
      "done": 5, "correct": 3,
      "confidence": 3,           // 1–4, given before marking
      "assisted": false,         // AI, notes or worked examples
      "minutes": 40, "explanation": "…" }
  ],
  "sessions":  { "2026-09-27": { "ed": { "done": true, "timestamp": 0 } } },
  "focus":     { "2026-09-27": { "ed": 80 } },            // minutes per day and subject
  "checklist": { "W04": [true, false, true, true, false] },
  "curriculum": {
    "degree": "…", "targetAverage": 17,
    "ucs": [{ "id": "…", "name": "…", "short": "…", "ects": 6, "year": 2, "semester": 1,
              "optional": false, "grade": null, "gradeType": "", "gradeDate": "",
              "passGrade": 9.5, "inPlan": true, "prereqs": ["…"],
              "assessments": [{ "id": "…", "name": "Teste 1", "kind": "teste", "epoca": "normal",
                                "date": "…", "weight": 40, "minGrade": 7.5, "grade": null }] }]
  },
  "examResults": { "c2": { "grade": 16, "at": 0 } },      // subjects outside the degree record
  "timerConfig": { "work": 40, "break": 10, "longBreak": 15, "sessionsBeforeLong": 4 },
  "settings": { "sound": true },
  "updatedAt": 1790000000000
}

ucs are the modules in the degree record; grades use the Portuguese 0–20 scale, where 9.5 is a pass (passGrade).

Derived fields (never saved) ​

  • derivedExamDate and prereqWeak on subjects linked to a module — calculated from the degree record every time the plan loads. They are kept out of the document so that saving the plan never freezes a resit date as if it had been entered by hand.
  • Streaks, hours, the week's tests, mastery, calibration, averages — always calculated from the history.

Sync ​

WhenWhat happens
Every changeSaved locally; an upsert to user_data is sent ~0.8 s later
Change on another deviceArrives via Realtime; if newer, it replaces the local copy — but attempts are always merged by id
Signing in on a deviceMerges local + cloud field by field (below) before saving
Local copy from another accountDiscarded, never merged
Signing outSends anything pending and deletes the local copy

Merge on sign-in ​

FieldRule
attemptsUnion by id (append-only)
sessionsUnion by day
focusMaximum per day and subject
checklistLogical OR per box
examResultsMost recent per subject
plan, curriculum, timerConfig, settingsMost recent document

Local copy ​

Key (localStorage)Contents
estudar_dataThe document
estudar_timerTimer state (phase, end time, block, subject, closed-book test in progress)
estudar_server_configLast /api/config response, for starting offline
estudar_last_uidSo whoever has already signed in on this device can open the app offline
estudar_intro_seenWelcome page already seen
estudar_langInterface language (pt or en)

Released under the MIT licence.