The VOZI schema is intentionally minimal. It stores only what is necessary to sync child profiles and phoneme progress across devices, and nothing more. No audio, no speech transcripts, and no recognized text ever leave the device — only aggregated metrics and profile data are persisted remotely. This philosophy is baked into the schema itself: there is no column in any table that could hold an audio payload or a raw transcription string.Documentation Index
Fetch the complete documentation index at: https://mintlify.com/AlonsoSam/vozi-android/llms.txt
Use this file to discover all available pages before exploring further.
Entity Relationships
The five tables form two ownership chains rooted at the authenticated adult:adultsis a 1:1 mirror ofauth.users(sameid)childrenbelongs to oneadultsrow viaadult_idsound_progressandpractice_attemptsboth belong to onechildrenrow viachild_idpremiumbelongs to oneadultsrow viaadult_id(primary key and foreign key share the same column)
Table Reference
adults
The adults table is a 1:1 mirror of Supabase’s built-in auth.users table. It is created automatically when a new user registers via the on_auth_user_created trigger — no manual insert is needed.
| Column | Type | Nullable | Description |
|---|---|---|---|
id | uuid | NOT NULL | Primary key. References auth.users(id) — cascades on delete. |
email | text | NULL | The adult’s email address, copied from auth.users at registration. |
created_at | timestamptz | NOT NULL | Row creation timestamp. Defaults to now(). |
children
Each row represents one child profile linked to an adult. The name field stores an alias or nickname — it is explicitly not a real name. This table has no updated_at or deleted_at columns; the sync merge rule accounts for this (see Sync & Auth).
| Column | Type | Nullable | Description |
|---|---|---|---|
id | uuid | NOT NULL | Primary key. Generated with gen_random_uuid(). |
adult_id | uuid | NOT NULL | Foreign key → adults(id). Cascades on delete. |
name | text | NOT NULL | Child’s alias (nickname only — not a real name). |
age | text | NOT NULL | Age band: '4-5' or '6-7'. |
avatar | text | NOT NULL | Avatar key (e.g. 'fox'). Defaults to 'fox'. |
total_points | integer | NOT NULL | Accumulated reward points. Defaults to 0. |
created_at | timestamptz | NOT NULL | Row creation timestamp. Defaults to now(). |
idx_children_adulton(adult_id)— speeds up the RLS ownership check and the pull query that fetches all children for the authenticated adult.
sound_progress
Each row aggregates a child’s history with a single phoneme. The combination of (child_id, sound_code) is unique — there is exactly one row per child per sound. The updated_at column is maintained automatically by the trg_sound_progress_updated trigger.
| Column | Type | Nullable | Description |
|---|---|---|---|
id | uuid | NOT NULL | Primary key. Generated with gen_random_uuid(). |
child_id | uuid | NOT NULL | Foreign key → children(id). Cascades on delete. |
sound_code | text | NOT NULL | Phoneme identifier: R, RR, S, L, TR, PR, PL, BR, or BL. |
attempts_count | integer | NOT NULL | Total number of practice attempts for this sound. Defaults to 0. |
correct_count | integer | NOT NULL | Number of attempts scored as correct. Defaults to 0. |
best_score | numeric | NOT NULL | Highest single-attempt score, from 0 to 1 (e.g. 0.95 = 95%). Defaults to 0. |
is_completed | boolean | NOT NULL | Whether the child has completed this sound. Defaults to false. |
updated_at | timestamptz | NOT NULL | Last update timestamp. Auto-refreshed by trigger on every UPDATE. |
UNIQUE (child_id, sound_code)— enforces one row per child per phoneme. Upserts from the app use this as the conflict target.
idx_sound_progress_childon(child_id).
practice_attempts
Each row records the outcome of one individual practice attempt. This table is append-only — there are no update policies in the RLS configuration, only SELECT and INSERT.
| Column | Type | Nullable | Description |
|---|---|---|---|
id | uuid | NOT NULL | Primary key. Assigned by the app (stable across devices). |
child_id | uuid | NOT NULL | Foreign key → children(id). Cascades on delete. |
sound_code | text | NOT NULL | Phoneme practiced (e.g. R, TR). |
target_word | text | NOT NULL | The word the child was asked to pronounce. |
score | numeric | NOT NULL | Recognition confidence score, 0 to 1. Defaults to 0. |
was_correct | boolean | NOT NULL | Whether the attempt was scored as a pass. Defaults to false. |
created_at | timestamptz | NOT NULL | When the attempt occurred. |
idx_practice_attempts_childon(child_id).
premium
One row per adult, tracking whether the Premium tier is active. Because adult_id is both the primary key and the foreign key, there is no separate id column. The updated_at column is maintained automatically by the trg_premium_updated trigger.
| Column | Type | Nullable | Description |
|---|---|---|---|
adult_id | uuid | NOT NULL | Primary key. Foreign key → adults(id). Cascades on delete. |
is_premium | boolean | NOT NULL | Whether Premium is currently active for this adult. Defaults to false. |
updated_at | timestamptz | NOT NULL | Last update timestamp. Auto-refreshed by trigger on every UPDATE. |
Row Level Security
RLS is enabled on all five tables. The core rule is simple: each authenticated adult can only see and modify their own rows, and rows that belong to their children. No cross-account access is possible, even with a valid JWT. The policies use a helper function to avoid duplicating the ownership check across multiple tables:adults — RLS policies
adults — RLS policies
id matches their own auth.uid().children — RLS policies
children — RLS policies
adult_id = auth.uid(), so adults can only access their own children.sound_progress — RLS policies
sound_progress — RLS policies
vozi_owns_child() — progress is accessible only if the child belongs to the signed-in adult.practice_attempts — RLS policies
practice_attempts — RLS policies
practice_attempts is append-only in RLS: there is a SELECT and INSERT policy but no UPDATE or DELETE. Once an attempt is written, it is immutable from the client.premium — RLS policies
premium — RLS policies
Auto-Registration Trigger
When a new adult creates an account, Supabase inserts a row intoauth.users. The on_auth_user_created trigger catches that event and automatically creates the corresponding row in public.adults:
security definer, which means it executes with the privileges of the function owner (not the calling user), allowing it to insert into adults even though RLS is active. The on conflict (id) do nothing clause makes repeated calls safe — the trigger is fully idempotent.
vozi_touch_updated_at Helper
The vozi_touch_updated_at() function keeps updated_at accurate on any table that has an updated_at column, without requiring the app to send a timestamp on every write:
BEFORE UPDATE trigger on both sound_progress and premium: