Identity, Auth & Access Control

Part of: Pickatale Master Build Wiki | Version: v1.7 | Last Updated: 2026-04-18


14. Lifecycle States

Source: (D) | Status: Confirmed as spec

14.1 Account State

State Definition Entry Trigger Exit Trigger
invited Invite email sent; user has not registered Admin/teacher sends invite User completes registration β†’ pending_verification
pending_verification Registered but email not verified Registration form submitted Email link clicked β†’ active; 48h no action β†’ stays pending_verification
active Email verified; can log in and use platform Email verified Subscription expires β†’ expired; admin action β†’ suspended
suspended Access blocked by admin; data retained Admin suspends account Admin reinstates β†’ active
expired Subscription lapsed; login allowed but features locked Subscription end date passed (no grace period renewal) Renews β†’ active; 30 days no renewal β†’ archived
archived Account deactivated; PII scheduled for deletion Admin archives or GDPR request Permanently deleted after retention window

14.2 Subscription State

State Definition Entry Exit
none No subscription on record Account creation Subscribes β†’ trialing or active
trialing Free trial; full access for N days Teacher or school_admin completes registration Trial expires β†’ active (if card on file) or expired
active Paid subscription, current Payment confirmed Renewal fails β†’ past_due; cancelled β†’ cancelled
past_due Payment failed; grace period applies Payment failure Paid within grace period β†’ active; grace expires β†’ expired
cancelled User cancelled; access until period end date User cancels Period end date reached β†’ expired
expired Access period ended with no renewal Period end reached Renews β†’ active

14.3 Child Account State

State Definition Entry Exit
created Account exists; no login yet Teacher imports Child logs in β†’ activated
activated Child has completed first login + placement test First login + placement test done Assigned to class β†’ in_class
in_class Active in a class; receives assignments and bot cycles Teacher assigns to class Removed from class β†’ activated; class archived β†’ inactive
inactive No sessions in 30+ days Inactivity trigger New session β†’ in_class
transferred Moving from one teacher/class to another Teacher initiates transfer Transfer confirmed β†’ in_class under new class
archived Account deactivated (school stop-pay, GDPR request) Admin/parent request Data deleted per retention schedule

14.4 Reading Session State

State Definition
not_started Book selected but no page_turned event yet
active page_turned events received within last 5 minutes
paused No events for 5–30 minutes; session not closed
completed session_ended event received (child reached final page)
abandoned book_abandoned event OR session idle >30 minutes without completion
flagged Session completed but avg_time_per_page anomalous (bot flags for review)

14.5 Intervention State

State Definition Entry Exit
none No intervention on record Default Intervention Engine detects issue β†’ suggested
suggested Bot has flagged learner; awaiting teacher action Bot nightly cycle Teacher acknowledges β†’ assigned; teacher dismisses β†’ none
assigned Teacher has assigned specific intervention content Teacher action Child starts session with assigned content β†’ in_progress
in_progress Child is actively engaging with intervention content Reading session started Quiz result received β†’ completed or escalated
completed Mastery achieved on intervention objective Quiz result = raise Next objective cycle begins
escalated 3+ failed attempts; requires human action Bot detects 3+ drop results Teacher manually resolves


15. Identity and Access Flows

β†’ See Section 28 for full 9-part contracts for all identity flows.

This section is superseded by Section 28 which contains the complete contract format (UI entry point, API endpoints, backend actions, DB writes, events emitted, permission check, entitlement check, audit log, success state, failure paths) for:


16. User Types and Account Ownership

Source: (D) | Status: Confirmed as spec

User Type Definitions

Type Auth Method Can Create Owned By Has Email DB Table
platform_admin email + password (admin panel) anyone Pickatale yes users (role=admin)
school_admin email + password teachers, classes school yes users (role=school_admin)
teacher email + password classes, child accounts school or self yes users (role=teacher)
parent email + password read-only observer via teacher/school invite only yes (via invite link) users (role=parent)
child username + 4-digit PIN nothing teacher OR parent no students (separate table)

Ownership Rules

Child Login: Direct or Parent-Switched?

Decision (spec): Child logs in directly using username + PIN at app.readingtester.com. Parent does NOT switch into child account. Parent has read-only view via parents.readingtester.com using their own login.

Who Can Deactivate a Child?

Actor Action Condition
Teacher Archive child in their class Child must be in their class
Parent Delete child account Parent must be linked as students.parent_id
School admin Archive any child in school Always
Platform admin Archive or delete any child Always

Parent Claiming a Teacher-Created Child [V1-PRODUCTION]

Flow: Parent registers β†’ in onboarding, enters child's username β†’ system sends verification email to parent β†’ teacher approves claim (or auto-approves if school allows) β†’ students.parent_id = parent.user_id β†’ parent gains read access to child progress.



17. Registration and Onboarding Flows

Source: (D) | Status: Confirmed as spec

17.1 Teacher Onboarding Wizard

Entry: After email verification (flow 15.1) Steps (in order):

  1. School Details β€” school_name, country, year levels taught. Writes to schools or links to existing.
  2. Data Processing Agreement β€” checkbox: "I accept the DPA". Writes users.dpa_accepted_at = now().
  3. Create First Class β€” class_name, year_level. Writes classes row.
  4. Import or Add Students β€” CSV upload (bulk) OR manual add. Creates students rows.
  5. Print Login Cards β€” download PDF. PIN shown here only.
  6. First Book Assignment (optional β€” can skip) β€” pick from library, assign to class.
  7. Done β†’ redirect to Teacher Portal dashboard.

Minimum to proceed: Steps 1–3 required. Steps 4–7 optional but prompted.


17.2 Parent Onboarding Wizard

Entry: After email verification (flow 15.2) Steps (in order):

  1. Create Child Profile β€” child_name, age/birthdate, language, grade_level.
  2. Interest Onboarding β€” select topics child enjoys (animals, space, sport, etc.). Writes students.interest_tags.
  3. Subscribe (optional) β€” show plan options. If skipped β†’ subscription state = none, limited access.
  4. Done β†’ redirect to Parent Portal dashboard. Child account is now in state created.

17.3 Child First Login

Entry: Child visits app.readingtester.com, enters username + PIN Steps (in order):

  1. Login validated β†’ session created
  2. Check students.placement_test_completed:
    • false β†’ Placement Test screen
  3. Placement Test: 3 passages at different FK levels. Child reads + answers comprehension questions. System calculates FK level from responses. Writes to students.reading_level and learner_profiles (FK + CEFR). Sets placement_test_completed = true.
  4. Interest Picker (if not done by parent): select favourite topics. Writes students.interest_tags.
  5. Library β†’ first recommended book displayed in "Picked for You" hero card.

17.4 Class Creation

Entry: Teacher Portal β†’ Classes β†’ New Class

Required fields: class_name (VARCHAR), year_level (INT 1–13) Optional fields: subject, curriculum_territory

Backend actions:

  1. Insert classes row: class_name, year_level, teacher_id, school_id, state=active
  2. Return class_id

Tables written: classes Emits: no event (internal)


17.5 Student Import (Bulk)

Entry: Teacher Portal β†’ Class β†’ Import Students β†’ Upload CSV

CSV format: name (required), year_level (optional β€” defaults to class year_level) Backend actions:

  1. Parse CSV β†’ validate rows
  2. For each row: generate username (firstname + 3-digit number, unique), generate 4-digit PIN, bcrypt PIN, insert students row (state=created, class_id, teacher_id, school_id)
  3. Return full list of students with plaintext PINs (shown once only, not stored)

Tables written: students Emails sent: none Output: login cards PDF download


17.6 Learner Profile Initialization

Trigger: Placement test completed (flow 17.3) OR teacher manually sets reading level

Backend actions:

  1. POST to Learner Profile Service: learner_id, reading_level (FK), cefr_level
  2. POST to Learner Bot: create initial memory records for this learner
  3. Set students.placement_test_completed = true

Tables written: learner_profiles (in Learner Profile DB), memories (in Learner Bot DB)


17.7 First Reading Session

Entry: Child at Library β†’ selects book OR clicks "Picked for You"

Steps:

  1. Book opened: fires book_opened telemetry event
  2. First page_turned event updates reading_sessions.started_at
  3. Reading proceeds (words counted, FK-levelled text served)
  4. Session ends: fires session_ended β†’ telemetry processes β†’ Learner Bot updated

First session note: No quiz expected after first session. Quiz introduced after 2nd completed book (to avoid friction on first contact).


17.8 First Report Generation

Trigger: Learner Bot nightly cycle (04:00 UTC), minimum 1 completed reading session exists

Output:

First report contents (minimal): reading level established, first book completed, N words read, vocabulary words tapped.



21. Permissions Matrix

Source: (D) | Status: Confirmed as spec

Entitlement tiers: full = active/trialing/gifted/cancelled-before-end | free = none/expired/past_due-after-grace

Feature Child (full) Child (free) Teacher (full) Teacher (free) Parent (full) Parent (free) School Admin Platform Admin
Read books (first 50) βœ… βœ… β€” β€” β€” β€” β€” β€”
Read books (all 9,089) βœ… ❌ β€” β€” β€” β€” β€” β€”
Placement test βœ… βœ… β€” β€” β€” β€” β€” β€”
Comprehension quiz βœ… ❌ β€” β€” β€” β€” β€” β€”
Finger-follow TTS βœ… βœ… β€” β€” β€” β€” β€” β€”
Karaoke TTS βœ… ❌ β€” β€” β€” β€” β€” β€”
Speedread RSVP βœ… (8+, FKβ‰₯3) ❌ β€” β€” β€” β€” β€” β€”
Colour overlay / fonts βœ… βœ… β€” β€” β€” β€” β€” β€”
Miles + Tokens βœ… βœ… β€” β€” β€” β€” β€” β€”
View class list β€” β€” βœ… βœ… β€” β€” βœ… βœ…
Create class β€” β€” βœ… βœ… β€” β€” βœ… βœ…
Import students β€” β€” βœ… βœ… β€” β€” βœ… βœ…
Assign books β€” β€” βœ… ❌ β€” β€” βœ… βœ…
View Learner Bot reports β€” β€” βœ… ❌ β€” β€” βœ… βœ…
Intervention tools β€” β€” βœ… ❌ β€” β€” βœ… βœ…
Attention list β€” β€” βœ… ❌ β€” β€” βœ… βœ…
View parent digest β€” β€” β€” β€” βœ… ❌ β€” βœ…
View child reading history β€” β€” βœ… βœ… βœ… βœ… (limited) βœ… βœ…
Create child account β€” β€” βœ… βœ… βœ… βœ… βœ… βœ…
Reset child PIN β€” β€” βœ… βœ… ❌ ❌ βœ… βœ…
Claim child account β€” β€” ❌ ❌ βœ… βœ… β€” βœ…
Manage school teachers β€” β€” ❌ ❌ β€” β€” βœ… βœ…
View subscription status ❌ ❌ βœ… (own) βœ… (own) βœ… (own) βœ… (own) βœ… (school) βœ… (all)
Upgrade subscription ❌ ❌ βœ… βœ… βœ… βœ… βœ… βœ…
Impersonate user ❌ ❌ ❌ ❌ ❌ ❌ ❌ βœ…
View audit logs ❌ ❌ ❌ ❌ ❌ ❌ βœ… (own school) βœ… (all)
Grant entitlement ❌ ❌ ❌ ❌ ❌ ❌ ❌ βœ…


28. Identity and Access Flows β€” Full Contracts

Source: (D) Sig direct instruction | Status: Confirmed as spec. Implementation status per flow noted.

Contract format for every flow:

Auth conventions (all flows):


Flow 28.1: Teacher Registration

UI Entry Point: account.readingtester.com/register β†’ role selector β†’ "I'm a Teacher"

API Endpoint: POST /api/auth/register

Input:

{
  "name": "string",
  "email": "string",
  "password": "string (min 8, 1 upper, 1 number)",
  "role": "teacher",
  "school_name": "string (optional)"
}

Backend Actions (in order):

  1. Validate input (Zod schema). Return 422 if invalid.
  2. Query users by email. If exists β†’ 409.
  3. bcrypt hash password (cost 12).
  4. INSERT users (name, email, password_hash, role='teacher', state='pending_verification', created_at=now()).
  5. If school_name provided: INSERT schools (name, country='', admin_user_id=users.id, state='pending'). UPDATE users.school_id.
  6. INSERT subscriptions (user_id, state='trialing', tier='trial', trial_ends_at=now()+14d).
  7. Generate verification token (UUID v4). INSERT email_verification_tokens (user_id, token_hash=sha256(token), expires_at=now()+48h).
  8. POST to SendGrid: template VERIFY_EMAIL (name, verify_url=account.readingtester.com/verify?token=${token}).
  9. INSERT email_log (user_id, template='VERIFY_EMAIL', status='sent'|'failed').

DB Writes: users, schools (optional), subscriptions, email_verification_tokens, email_log

Events Emitted: none (no event bus yet; email is the notification)

Permission Check: none (public endpoint)

Entitlement Check: none (pre-auth)

Audit Log: INSERT audit_log (action='register', actor_id=new users.id, metadata={role, email_domain})

Success State: HTTP 201 {ok: true, state: 'pending_verification'}. User redirected to "Check your email" page.

Failure Paths:

Condition HTTP Response Action
Email already exists (state=active) 409 {error: 'email_taken'} Client shows "Account exists. Log in."
Email exists (state=pending_verification) 409 {error: 'pending_verification'} Client shows "Check your email" + resend link
Weak password 422 {error: 'password_too_weak', rules: [...]} Client shows rules
SendGrid failure 500β†’handled log to email_log, status='failed' Registration succeeds; show "Email may be delayed"
DB write fails 500 {error: 'server_error'} Rollback all inserts

Flow 28.2: Email Verification

UI Entry Point: Link in VERIFY_EMAIL email β†’ account.readingtester.com/verify?token=<UUID>

API Endpoint: POST /api/auth/verify-email

Input: { "token": "string" }

Backend Actions:

  1. sha256(token) β†’ query email_verification_tokens by token_hash.
  2. Check expires_at > now(). If expired β†’ 410.
  3. Check already used: used_at IS NULL. If used β†’ 410.
  4. UPDATE users.email_verified=true, state='active' WHERE id=token.user_id.
  5. UPDATE email_verification_tokens.used_at=now().
  6. INSERT sessions (user_id, token_hash=sha256(session_uuid), expires_at=now()+7d, ip, user_agent).
  7. Set uc_session cookie.
  8. POST to SendGrid: template WELCOME_TEACHER or WELCOME_PARENT (based on role).

DB Writes: users, email_verification_tokens, sessions, email_log

Events Emitted: none

Permission Check: none (public)

Entitlement Check: none

Audit Log: INSERT audit_log (action='email_verified', actor_id=user_id)

Success State: HTTP 200, uc_session set, redirect to /onboarding.

Failure Paths:

Condition HTTP Response
Token not found 404 "Invalid link. Request a new one."
Token expired 410 "Link expired." + resend button
Token already used 410 "Already verified. Log in."

Flow 28.3: Teacher / Parent Login

UI Entry Point: account.readingtester.com/login OR teacher.readingtester.com (redirects to account center)

API Endpoint: POST /api/auth/login

Input: { "email": "string", "password": "string" }

Backend Actions:

  1. Query users by email. If not found β†’ 401 (identical response β€” no enumeration).
  2. Check users.locked_until. If > now() β†’ 423 with {retry_after: locked_until}.
  3. bcrypt.compare(password, password_hash). If false: increment users.failed_attempts. If failed_attempts β‰₯ 5: set locked_until=now()+15m, send ACCOUNT_LOCKED_ALERT email. Return 401.
  4. If valid: reset users.failed_attempts=0, locked_until=null.
  5. Check users.state:
    • pending_verification β†’ 403 {error: 'email_not_verified'}
    • suspended β†’ 403 {error: 'account_suspended'}
    • archived β†’ 403 {error: 'account_archived'}
  6. INSERT sessions (user_id, token_hash, expires_at=now()+7d, ip, user_agent).
  7. Set uc_session cookie.
  8. Query subscriptions.state β€” include in session payload for client-side feature gating hints (server always re-checks).

DB Writes: users (failed_attempts, locked_until), sessions, email_log (if lock email sent)

Events Emitted: none

Permission Check: none (public)

Entitlement Check: none (checked per-request downstream)

Audit Log: INSERT audit_log (action='login', actor_id=user_id, metadata={ip, user_agent, success: true/false})

Success State: HTTP 200, uc_session set, return {ok: true, role, redirect: '/dashboard'}.

Failure Paths:

Condition HTTP Response
Wrong password 401 {error: 'invalid_credentials'}
Account locked 423 {error: 'account_locked', retry_after: ISO}
Email not verified 403 {error: 'email_not_verified'} + resend link
Account suspended 403 {error: 'account_suspended', message: 'Contact support'}
Account archived 403 {error: 'account_archived'}

Flow 28.4: Child Login (Username + PIN)

UI Entry Point: app.readingtester.com β†’ username field + 4-digit PIN

API Endpoint: POST /api/auth/child-login

Input: { "username": "string", "pin": "string (4 digits)" }

Backend Actions:

  1. Query students by username (case-insensitive). If not found β†’ 401.
  2. Check students.locked=true. If true β†’ 423 {error: 'account_locked', message: 'Ask your teacher to reset your PIN'}.
  3. Check students.state. If archived β†’ 403.
  4. bcrypt.compare(pin, pin_hash). If false: increment students.failed_attempts. If β‰₯ 5: students.locked=true. INSERT teacher_notifications (teacher_id, type='child_locked_pin', child_name, student_id). Return 401.
  5. If valid: reset students.failed_attempts=0.
  6. Call checkEntitlement(students.school_id OR students.parent_id) β†’ determine tier.
  7. INSERT sessions (student_id, role='child', learner_id=students.uuid, class_id, expires_at=now()+24h).
  8. Set uc_session cookie (role=child, sub=students.uuid, class_id, entitlement_tier).

DB Writes: students (failed_attempts, locked), sessions, teacher_notifications (on lock)

Events Emitted: none

Permission Check: none (public)

Entitlement Check: called to embed tier in session

Audit Log: INSERT audit_log (action='child_login', actor_id=students.uuid, metadata={ip, success})

Success State: HTTP 200, uc_session set. Client checks students.placement_test_completed:

Failure Paths:

Condition HTTP Response
Username not found 401 {error: 'invalid_credentials'}
Wrong PIN 401 {error: 'invalid_credentials', attempts_remaining: N}
PIN locked (β‰₯5 fails) 423 {error: 'account_locked'}
Account archived 403 {error: 'account_inactive', message: 'Contact your teacher'}

Flow 28.5: Logout

UI Entry Point: Any page β†’ header "Sign Out" button

API Endpoint: POST /api/auth/logout

Input: none (reads uc_session cookie)

Backend Actions:

  1. Parse session_token from uc_session cookie.
  2. UPDATE sessions SET invalidated_at=now() WHERE token_hash=sha256(token).
  3. Set uc_session cookie with expired date (clear cookie).

DB Writes: sessions

Events Emitted: none

Permission Check: valid uc_session (if none β†’ still clear cookie and redirect)

Entitlement Check: none

Audit Log: INSERT audit_log (action='logout', actor_id=user_id)

Success State: HTTP 200, cookie cleared, redirect to /login.

Failure Paths: Session already invalidated β†’ same outcome (idempotent).


Flow 28.6: Forgot Password

UI Entry Point: /login β†’ "Forgot password?" link β†’ account.readingtester.com/forgot-password

Child exception: Children have no email. Must use teacher PIN reset (Flow 29.7).

API Endpoint: POST /api/auth/forgot-password

Input: { "email": "string" }

Backend Actions:

  1. Query users by email. If not found: respond identically (no enumeration).
  2. If found: generate token (UUID v4). INSERT password_reset_tokens (user_id, token_hash=sha256(token), expires_at=now()+1h).
  3. POST to SendGrid: template PASSWORD_RESET (name, reset_url=account.readingtester.com/reset-password?token=...).
  4. INSERT email_log.

DB Writes: password_reset_tokens, email_log

Events Emitted: none

Permission Check: none (public)

Entitlement Check: none

Audit Log: INSERT audit_log (action='forgot_password', metadata={email_domain}) β€” no user_id (not authenticated)

Success State: HTTP 200 {ok: true}. Client shows "If that email exists, you'll receive a link."

Failure Paths: None user-facing (all paths return 200).


Flow 28.7: Reset Password

UI Entry Point: Link in PASSWORD_RESET email β†’ account.readingtester.com/reset-password?token=<UUID>

API Endpoint: POST /api/auth/reset-password

Input: { "token": "string", "password": "string" }

Backend Actions:

  1. sha256(token) β†’ query password_reset_tokens. If not found β†’ 404.
  2. Check expires_at > now(). If expired β†’ 410.
  3. Check used_at IS NULL. If used β†’ 410.
  4. Validate new password strength. If weak β†’ 422.
  5. bcrypt hash new password (cost 12).
  6. UPDATE users.password_hash, failed_attempts=0, locked_until=null.
  7. UPDATE password_reset_tokens.used_at=now().
  8. UPDATE sessions SET invalidated_at=now() WHERE user_id=token.user_id (invalidate ALL sessions).
  9. POST to SendGrid: template PASSWORD_CHANGED.

DB Writes: users, password_reset_tokens, sessions, email_log

Events Emitted: none

Permission Check: none (token-based)

Entitlement Check: none

Audit Log: INSERT audit_log (action='password_reset', actor_id=user_id)

Success State: HTTP 200. Redirect to /login?message=password_changed.

Failure Paths:

Condition HTTP Response
Token not found 404 "Invalid link."
Token expired 410 "Link expired." + new request link
Token already used 410 "Link already used."
Weak password 422 Rules list

Flow 28.8: Invite Acceptance

UI Entry Point: INVITE_EMAIL link β†’ account.readingtester.com/accept-invite?token=<UUID>

API Endpoint: GET /api/auth/invite?token=<UUID> (prefill form), then POST /api/auth/accept-invite

GET Input: ?token=UUID GET Output: {email: "prefilled@...", role: "teacher", school_name: "...", valid: true}

POST Input:

{
  "token": "string",
  "name": "string",
  "password": "string"
}

Backend Actions (POST):

  1. sha256(token) β†’ query invites. If not found β†’ 404.
  2. Check expires_at > now(). If expired β†’ 410.
  3. Check used_at IS NULL. If used β†’ 410.
  4. Check email not already in users. If exists β†’ 409.
  5. bcrypt hash password. INSERT users (name, email=invite.target_email, password_hash, role=invite.target_role, school_id=invite.school_id, state='pending_verification').
  6. INSERT subscriptions (state='trialing', trial_ends_at=now()+14d) OR inherit school subscription if school_id is set.
  7. UPDATE invites.used_at=now().
  8. Standard email verification flow (Flow 28.2) triggered automatically.

DB Writes: invites, users, subscriptions, email_verification_tokens, email_log

Events Emitted: none

Permission Check: none (token-based)

Entitlement Check: none

Audit Log: INSERT audit_log (action='invite_accepted', metadata={inviter_id, target_email, role})

Success State: User created, verification email sent, redirect to "Check your email".

Failure Paths:

Condition HTTP Response
Token expired 410 "Invite expired. Ask sender to resend."
Email already registered 409 "Account exists. Log in."
Token already used 410 "Already accepted."

Flow 28.9: Session Validation (Middleware β€” Every Protected Request)

Every API endpoint behind auth runs this middleware:

GET /api/auth/session

Steps:

  1. Read uc_session cookie.
  2. Decrypt/verify JWT. If invalid signature β†’ 401.
  3. Query sessions by token_hash. If not found OR invalidated_at IS NOT NULL OR expires_at < now() β†’ 401.
  4. Return {user_id, role, school_id, class_id, entitlement_tier}.

On 401: Client clears cookie, redirects to /login?next=<current_path> (child sessions β†’ /login only, no next param).


Flow 28.10: Session Expiry (Automatic)

Trigger: uc_session cookie TTL expires (7 days inactivity) OR server-side invalidation

Client behaviour: Next API call returns 401 β†’ client redirects to login.

Session extension: Every authenticated request: UPDATE sessions.expires_at=now()+7d (sliding window). Children: 24h sliding window.

DB Writes: sessions.expires_at


Flow 28.11: Suspended / Disabled Account

Trigger: Admin sets users.state='suspended'

Immediate effects:

  1. UPDATE all sessions.invalidated_at=now() for user (all devices logged out instantly).
  2. Send ACCOUNT_SUSPENDED email.

Login attempt while suspended: Flow 28.3 Step 5 β†’ 403 {error: 'account_suspended'}.

Reactivation: Admin sets users.state='active' β†’ INSERT audit_log.



32. Permissions Matrix β€” Complete [V1-PRODUCTION]

V1 Roles: child, teacher, parent, school_admin, platform_admin

Feature child teacher parent school_admin platform_admin
Read library (free tier, up to 50 books)βœ… (free)β€”β€”β€”βœ…
Read library (all books)βœ… (teacher_paid/enterprise/gifted)β€”β€”β€”βœ…
Take reading quizzesβœ… (teacher_paid/enterprise/gifted)β€”β€”β€”βœ…
View own reading progressβœ…β€”βœ… (own child only)βœ…βœ…
Manage classesβ€”βœ… (own classes)β€”βœ…βœ…
Add/remove studentsβ€”βœ… (own classes)β€”βœ…βœ…
View student progressβ€”βœ… (own students)βœ… (own child, limited)βœ… (school)βœ…
Invite parentsβ€”βœ…β€”βœ…βœ…
Reset student PINβ€”βœ… (own students)❌ (V1-DEFERRED)βœ…βœ…
Manage school settingsβ€”β€”β€”βœ…βœ…
Manage billing / licenseβ€”βœ… (own license)β€”β€”βœ…
Impersonate usersβ€”β€”β€”β€”βœ…
View audit logβ€”β€”β€”βœ… (own school)βœ…
Manage all schoolsβ€”β€”β€”β€”βœ…
Ingest external entitlementsβ€”β€”β€”β€”βœ…

* = also requires ageβ‰₯8 AND FKβ‰₯3.0 † = v1: parent cannot create child accounts β€” child accounts created by school/teacher flow only ‑ = within same school only