DuTaTo
Active learning app that prevents cognitive debt by using Claude as a Socratic AI tutor. Instead of giving direct answers, the AI enforces reflection, asks probing questions, and gates advanced features by mastery level.
Stack
| Layer | Technology |
|---|---|
| Frontend | Flutter (iOS, Android, Web) |
| State Management | Riverpod |
| Navigation | GoRouter |
| Localization | flutter_localizations + gen_l10n (ARB) — English + French, user-selectable in Settings |
| Backend | Supabase (PostgreSQL + Auth + Edge Functions) |
| AI | Claude (API key) / OpenAI (ChatGPT sign-in) / Gemini (Google sign-in), BYOK — the provider + model chosen in Settings is the only one used. Native calls the provider directly; web goes through the ai-proxy Edge Function |
| Content retrieval | Supabase full-text search → document order |
| Push notifications | Firebase Cloud Messaging + flutter_local_notifications (native only) |
| Spaced Repetition | SM-2 algorithm |
| Hosting | Cloudflare Pages (web) |
Core Concepts
- Cognitive debt prevention — AI never gives direct answers; learners must think first
- Mastery levels (0-5) — gate features by demonstrated knowledge
- Kolb's learning cycle — track CE, RO, AC, AE phases per session
- Mandatory reflection — sessions can't complete without it
- 8 Claude interaction modes — Socratic dialogue, quiz, gated explanation, code review, practice problems, confused student (role reversal), review card generation, assessment
Architecture
Clean Architecture with 3 layers:
lib/
domain/entities/ # Pure Dart classes, no dependencies
data/
models/ # JSON-serializable models (freezed)
datasources/ # Supabase & Claude API clients
presentation/
providers/ # Riverpod providers
screens/ # 14 screen categories
widgets/ # Reusable UI components
config/ # Routes, theme, API configs
core/ # Utils (SM-2), extensions, error handling
Screen categories: admin_onboarding, auth, backoffice, bookmarks, domains, goals, home, lesson, notes, org, progress, review, settings, tutor.
Notes and bookmarks are under Library: the Notes list (/notes) shows the latest notes, pinned first, with a server-side search over every note's title, content and tags (up to 50 matches, with a caption when there are more), and /notes?topicId=… (the lesson's notes action) narrows it to one topic; /bookmarks lists bookmarked topics, lesson parts and tutor sessions. The Terms and the Privacy Policy are linked from the sign-up form, the sign-in screen, the terms acceptance screen and Settings; those links open the hosted pages (web/terms.html, web/privacy.html, or their French copies web/fr/terms.html, web/fr/privacy.html when the app's language is French) on the site of the build's environment (dutato-staging.pages.dev for staging builds, dutato.pages.dev for prod), whose Markdown mirrors live in docs/ (*.fr.md for French).
New users land straight on Home after sign-up and the terms gate — there is no learner onboarding tour. OnboardingGate only shows the pending-invitations modal and sends org admins through the 4-step admin onboarding wizard.
Writes (chat sessions and messages, notes, review logs) go straight to Supabase; there is no offline queue. Client-generated UUIDs keep retries idempotent, and failures surface through the normal error states (a failed review-log write is only logged, since the card's new schedule is already saved).
Content
The app is a platform for structured learning — it does not ship with pre-loaded content for all users. Domain owners create curricula (domains, books, topics, lessons) and control access via visibility settings (private, shared, public) and curriculum shares.
Getting content in
Three paths, in ascending order of effort:
| Path | Screen | Best for |
|---|---|---|
| Starter catalog | /domains/starters |
Trying the app immediately. Curated is_starter curricula, flagged by platform admins only. Adding one is an enrolment, not a copy — no rows are duplicated, so a starter costs one copy of the content however many learners take it, and fixes reach all of them. Progress is still per-user. |
| In-app import | /domains/import |
Your own notes, an article, a chapter. Paste up to 100k characters; the ai-proxy structure mode organizes it into topics and passages using the AI provider you chose in Settings. |
| Curriculum Creator CLI | /domains/curriculum-creator → /domains/upload-curriculum |
A whole book or a code repo. Run an AI agent locally, then upload the resulting manifest.json / structure.json / chunks.json. |
The import and CLI paths share one insert path (createDomain → insertTopics → insertChunks), so a curriculum behaves identically whichever produced it.
Content can also be populated using the tooling in tools/:
pdf_pipeline/— extracts and structures lessons from PDF textbookscurriculum_creator/— AI agent pipeline supporting PDFs, web pages, and code repos
Platforms
| Platform | Status | Notes |
|---|---|---|
| iOS | Primary | Fully functional, all features |
| Android | Supported | Firebase requires real google-services.json (see below) |
| Web (Chrome) | Supported | Responsive desktop layout; AI calls go through the ai-proxy Edge Function; no push or local notifications |
Web-specific behavior
Native-only dependencies are isolated behind conditional-import barrels so the JS bundle never includes dart:io or the Firebase SDKs:
flutter_local_notifications→ no-op stub on web (notification_service_barrel.dart)firebase_core/firebase_messaging→ no-op stubs on web (fcm_setup_barrel.dart,fcm_service_barrel.dart), so FCM push is native-only- AI providers → browsers block direct calls to the provider APIs (CORS), so web routes every AI call through the
ai-proxyEdge Function; native calls the provider directly
On wide screens (≥800px), the bottom NavigationBar is replaced by a side NavigationRail.
Android setup
The Android build requires a real google-services.json from Firebase Console placed at android/app/google-services.json (gitignored). Without it, Firebase initialization fails at runtime (caught gracefully — the app runs but push notifications are disabled).
Getting Started
# Install dependencies
flutter pub get
# Set up environment variables
cp .env.example .env # Add your Supabase URL, anon key, and Claude API key
# Run the app (iOS)
flutter run
# Run the app (Web)
flutter run -d chrome
# Run the app (Android)
flutter run -d android
# Run code generation (after modifying models)
dart run build_runner build --delete-conflicting-outputs
Staging vs production
The app has two environments that ship side-by-side with separate bundle IDs (com.alienard.dutato for prod, com.alienard.dutato.staging for staging) and separate Supabase projects:
- Staging auto-deploys on every merge to
mainvia.github/workflows/ci.yml: schema migrations, every Edge Function folder insupabase/functions/, and web (→dutato-staging.pages.dev) against the staging Supabase project. No local command needed. - Production deploys via
./dto releaseon your machine — web (→dutato.pages.dev), iOS (TestFlight), Android (Firebase App Distribution), all tied to a git tag.
Both apps coexist on the same device so you can compare behavior side-by-side. Internal testers get the staging build first; once you're happy, ./dto release promotes the same commit to production.
Local release workflow
Releases run entirely from your machine — no GitHub Actions minutes consumed:
./dto release # Prod: codegen → clean-tree check → pre-flight → sync prod Supabase → bump → push → web → TestFlight → Firebase
./dto release --staging # Staging apps only: staging iOS TestFlight + staging Android Firebase
The prod pipeline regenerates Dart code, refuses to run on a dirty working tree, pre-flights every downstream track (iOS/Android/web file + env-var checks), syncs the prod Supabase schema + Edge Functions, bumps pubspec.yaml, tags, pushes, deploys prod web to Cloudflare Pages, builds iOS for TestFlight, and builds Android for Firebase App Distribution. Each step aborts on failure; the pre-flight step catches missing Firebase configs / signing keys / env vars before any state mutation so a fixable misconfiguration never publishes an orphan tag; Supabase sync happens before the version bump so a broken sync never tags a release the prod instance can't serve; web/iOS/Android uploads happen after the tag is pushed so partial failures are recoverable with the individual ./dto deploy … commands.
The staging pipeline is lighter: it only builds and uploads the mobile apps because staging web + Supabase were already deployed by CI on merge to main.
Flags: --bump {patch,minor,major}, --skip-supabase, --skip-web, --skip-ios, --skip-android, --staging.
The underlying sub-commands (./dto deploy supabase [--env staging], ./dto version --push, ./dto deploy ios --testflight [--env staging], ./dto deploy android --distribute [--env staging], ./dto deploy web [--env staging]) are still available for debugging or one-off operations. The fallback .github/workflows/release.yml accepts an env input (prod/staging) and is manual-only (workflow_dispatch) so pushing tags never triggers an unused CI run.
The app always talks to the Supabase project baked in at build time (--dart-define=SUPABASE_URL / SUPABASE_ANON_KEY). A build without credentials, or one whose Supabase init fails, shows a "Service unavailable" screen at /unavailable. Hosted projects are migrated with supabase db push (CI for staging, ./dto release / ./dto deploy supabase for prod).
Documentation
The generated Dart API reference is published at dutato-docs.pages.dev on every merge to main (Cloudflare Pages, auto-deployed from CI). It covers the Flutter client (lib/) and is grouped into five categories: Overview, Architecture, Data Sources, Providers, Screens & Widgets. Category intros live in doc/categories/; the site is built with dart doc and configured via dartdoc_options.yaml.
./dto docs # Build HTML into doc/api/ (gitignored)
./dto docs serve # Serve doc/api/ on http://localhost:8081
./dto docs manifest # Regenerate committed doc/api-manifest.json (CI drift gate)
# On macOS: defaults to running dartdoc in a Linux Docker
# image so the manifest is byte-identical to CI's
# ubuntu-latest output. Pass --no-docker to fall back
# to host dartdoc.
./dto ci # Full CI; auto-enables --docs when lib/ changes
The full HTML is never committed — only the lightweight doc/api-manifest.json, a deterministically-sorted list of every public symbol. When a PR changes the public surface, CI's docs-check job fails until the author regenerates and commits the manifest. The deploy job rebuilds HTML from scratch for every push to main.
Developer CLI (dto)
All project tooling is unified under the ./dto CLI (Python/Click, managed by uv). No manual install needed — uv auto-creates the venv on first run.
./dto --help # List all commands
./dto <command> --help # Command-specific help
Commands
| Command | Purpose |
|---|---|
dto serve |
Build web release against local Supabase + Acme demo (default) and serve on localhost:8080. --prod to build against .env credentials instead. |
dto screen |
Automated screenshot pipeline (audit, B2B pitch, App Store) |
dto audit |
Responsiveness, accessibility, and performance audits |
dto visual |
Pixel-level visual-regression baseline + diff (capture / diff / bless) |
dto test |
Run tests (unit, integration, RLS, coverage) |
dto ci |
Full pre-push CI checklist; auto-detects which gates apply from the diff vs origin/main (pass --no-auto to opt out) |
dto deploy |
Build release artifacts (iOS, Android, web). All subcommands accept --env {prod,staging} |
dto version |
Bump version in pubspec.yaml, commit, and tag |
dto seed |
Seed Acme demo org into local Supabase |
dto db |
Database backup and restore |
dto user |
User management (delete) |
dto pipeline |
PDF-to-curriculum content pipeline |
dto curriculum |
Curriculum creator (upload, condense, status) |
dto deploy supabase |
Sync prod (default) or staging (--env staging) Supabase: push migrations, deploy every Edge Function folder in supabase/functions/ |
dto release |
Full prod release pipeline (codegen → prod Supabase → bump → web → TestFlight → Firebase) |
dto release --staging |
Staging release: build & upload staging iOS TestFlight + Android Firebase only |
Quick Reference
# Build & serve web locally
./dto serve # local Supabase + demo seed + serve :8080 (default)
./dto serve --prod # use prod credentials from .env instead
./dto serve --skip-build # Just serve existing build
# Screenshots (default: local Supabase + Acme demo seed for populated screens)
./dto screen --all # Full pipeline (build + all screenshots)
./dto screen --audit # 40-screen audit (desktop + mobile)
./dto screen --b2b # 12 B2B pitch screenshots
./dto screen --audit --multi-role # + role-gated variants as manager/member
./dto screen --audit --demo-user=manager # Screenshot as a specific role
# Opt out of demo seed (build against remote Supabase from .env — gives empty/loader screens)
./dto screen --all --no-demo
# Audits (responsiveness, accessibility, performance) — demo mode default
./dto audit responsive # 9 viewports × 30 routes, overflow + touch targets
./dto audit responsive --strict # Same, but exits 1 on overflow / sub-44px tap targets (local-only gate; bundled into ./dto ci --responsive)
./dto audit a11y # Flutter semantics + axe + tab order + focus rings
./dto audit perf # Core Web Vitals + Flutter first-frame + Lighthouse
./dto audit all # Run all 3 with single build + seed
./dto audit perf --cold-only --skip-lighthouse # Fastest perf run (no warm, no Lighthouse)
# Visual regression (baseline at test/visual/baseline/{desktop,mobile}/)
./dto visual diff # Re-capture + pixelmatch vs baseline (local check, exits 1 on regression)
./dto visual capture # First-time baseline capture (refuses if baseline exists)
./dto visual bless # Overwrite baseline after a deliberate UI change (alias for capture --force)
./dto visual diff --tolerance=0.01 # Loosen the 0.5% default fail threshold
# Testing
./dto test # Unit/widget tests
./dto test --integration # Integration tests (starts local Supabase)
./dto test --all # Unit + integration
./dto test --rls # RLS Docker smoke tests
./dto test --coverage # Unit tests + 70% coverage gate
./dto ci # Full CI; auto-detects gates from the diff vs origin/main
./dto ci --no-auto # Base steps only (no auto-detect)
./dto ci --rls # Force --rls on top of auto-detection
./dto ci --responsive --visual # Force the local-only UI gates (auto-enabled when lib/, assets/, or web/ change)
./dto ci --goldens --web-build # Force the closed-gap remote-mirror gates
# (also auto-enabled by lib/, assets/, web/, pubspec.* changes)
# Build & deploy (prod is default; append `--env staging` for staging builds)
./dto deploy ios # Build prod IPA + install on device
./dto deploy ios --testflight # Build + upload to prod TestFlight
./dto deploy ios --testflight --env staging # Build + upload to staging TestFlight
./dto deploy android # Build prod APK + install via ADB
./dto deploy android --env staging # Build staging APK + install via ADB
./dto deploy android --distribute # Build prod APK + upload to Firebase App Distribution
./dto deploy web # Build prod web release (local)
./dto deploy web --env staging # Build staging web release (local)
./dto deploy supabase # Sync prod Supabase (migrations + every Edge Function folder)
./dto deploy supabase --env staging # Sync staging Supabase (rarely needed — CI handles this)
# Version management
./dto version # Patch bump (0.1.3 → 0.1.4)
./dto version --minor # Minor bump
./dto version --push # Bump + push to origin
# Demo data
./dto seed # Seed Acme Learning Corp (17 users, 3 teams)
# Database
./dto db backup # Dump public schema + auth.users
./dto db restore # List available backups
./dto db restore backups/<timestamp> # Restore specific backup
# User management
./dto user delete --email user@example.com
./dto user delete --email user@example.com --dry-run
# Content pipeline
./dto pipeline # Process all PDF books
./dto pipeline --file book.pdf --domain code --title "Clean Code" --author "R. Martin"
./dto pipeline extract book.pdf # Extract text only
./dto pipeline upload --domain code # Upload to Supabase
./dto pipeline embeddings # Generate vector embeddings
# Curriculum creator
./dto curriculum status output/postgresql/
./dto curriculum upload --input output/postgresql/ --owner org --org-id <uuid>
./dto curriculum condense --input output/postgresql/ --plan <plan.json>
Testing
CI runs 8 parallel jobs on PRs to main:
- Lint & Format —
dart format+flutter analyze+ thedtoCLI pytest suite (~1min) - Edge Function Check —
deno checkacross every Edge Function +deno testacrosssupabase/functions/(the ai-proxy suites: prompt-injection corpus, prompts, sanitize, validate, model-id guard, hourly rate-limit cap; send-push's opt-in rule; export-user-data's payload; the_sharedDB helpers) (~30s) - Unit Tests — unit/widget tests with 70% coverage gate (~3min)
- Integration Tests — datasource tests against local Supabase with 80% coverage gate (~3min)
- RLS & RPC Tests — Dockerized PostgreSQL running RLS assertions and RPC smoke tests (~2min)
- Widget Goldens — Alchemist visual-regression snapshots for shared widgets (~1min)
- Docs Manifest Drift — fails if
doc/api-manifest.jsonis stale (~2min) - Web Build (staging) — Flutter web release build with staging Supabase secrets, uploaded as the deploy artifact (~3min)
The responsive audit (./dto audit responsive --strict, 9-viewport Playwright run) and the pixelmatch visual-diff (./dto visual diff) are intentionally not in PR CI: they hang unpredictably on GHA's headless Chromium and would burn ~22 runner minutes per PR. They run locally only — auto-detected by ./dto ci when lib/, assets/, or web/ changes.
The goldens and web-build PR jobs are mirrored locally (auto-fired on the same triggers): --goldens runs Alchemist's golden suite inside the Linux Flutter Docker image (so macOS skia drift doesn't break the gate), and --web-build runs flutter build web --release natively with placeholder --dart-define values for the staging secrets — the build embeds them as compile-time constants but doesn't actually call out to Supabase, so placeholders catch every build-time error CI catches without needing real credentials.
Native-config regressions (Android manifest intent-filter, iOS Info.plist OAuth scheme, project.pbxproj bundle-ID drift, xcconfig flavor wiring) are caught at the unit-test layer by test/platform/oauth_deep_link_config_test.dart, which parses the native files and cross-checks them against EnvConfig.deepLinkScheme. The full end-to-end Android APK + emulator smoke and iOS xcodebuild flavor build would add ~54min per PR (macOS minutes bill at 10× Linux) so they're not run in PR CI — ./dto ci auto-detects native-config changes and runs the local gates:
./dto ci # auto-runs --android-deeplink when android/ changes (needs an emulator running)
./dto ci # auto-runs --ios when ios/ changes (needs Xcode)
If a gate's preconditions are missing (no emulator, no Xcode, no Docker), ./dto ci aborts with an actionable message — silent skipping is exactly the bug auto-detection exists to prevent. The path → gate trigger table lives in tools/dto/dto_cli/auto_gates.py.
On push to main, Deploy to Cloudflare Pages downloads the web-build artifact (no rebuild). Stale CI runs are auto-cancelled when new commits are pushed (concurrency.cancel-in-progress).
Demo Data (Sales Demos)
Seeds a realistic org ("Acme Learning Corp") with 17 users, 3 teams, manager hierarchy, assignments, learning paths, skills and role profiles, certifications and compliance requirements, XP/badges, and notifications against local Supabase.
./dto seed # One command — starts Supabase if needed
Data persists across supabase stop/start. After supabase db reset, re-run ./dto seed. Login: alice.morgan@acme-demo.test / DemoPass-Acme-123!. See tools/demo_seed/README.md for all accounts.
Project Structure
dutato/
lib/ # Flutter app source
docs/ # Methodology documentation
methodology.md # Learning theory, cognitive debt, mastery gating
content-structure.md # Content inventory and topic tree design
claude-prompts.md # 8 Claude system prompts
spaced-repetition.md # SM-2 algorithm and review card types
RELEASE.md # Release process
PRIVACY_POLICY.md # Mirror of web/privacy.html (kept identical by test/legal/)
PRIVACY_POLICY.fr.md # French mirror of web/fr/privacy.html
TERMS_OF_USE.md # Mirror of the in-app terms and web/terms.html
TERMS_OF_USE.fr.md # French mirror of the in-app terms and web/fr/terms.html
plans/ # Development milestones (plan-1 through plan-52)
supabase/
migrations/ # Database schema (98 migrations)
functions/ # Edge Functions: ai-proxy, send-push, export-user-data
assets/splash/ # Splash-screen images
dto # CLI wrapper (run ./dto --help)
tools/
dto/ # CLI package (Python/Click)
pdf_pipeline/ # Content extraction and processing
curriculum_creator/ # AI agent-operated curriculum pipeline
db_backup/ # Database backup and restore utilities
demo_seed/ # Sales demo org (Acme Learning Corp, 17 users)
visual/ # pixelmatch + diff.js (./dto visual harness)
web/ # Flutter web platform (index.html, manifest, icons) + the hosted privacy.html and terms.html (French copies in web/fr/)
test/ # 277 test files
helpers/ # Shared mocks & Supabase test init
visual/baseline/ # Committed PNG baselines for ./dto visual diff (local check)
integration/ # 39 integration test files (tagged, require Supabase)
rls/ # RLS policy + RPC smoke tests (Docker + PostgreSQL)
platform/ # Native config guardrails (OAuth deep-link, bundle IDs)
legal/ # In-app terms, web pages and Markdown mirrors stay identical, in English and French; translation_sources.json pins each French section to the English it translates
Database
Supabase PostgreSQL with 98 migrations. Key table groups:
- Content: domains (with owner_id, visibility, is_starter), books, topics (hierarchical), content_chunks, curriculum_levels, curriculum_shares, domain_variants, reading_questions, reading_responses, user_starter_enrollments (opt-in reference to a curated starter)
- User progress: profiles (with target_retention, fsrs_parameters, admin_onboarding_step / admin_onboarding_completed_at, seen_tooltips; the learner
onboarding_step/onboarding_completed_atcolumns are no longer used), user_topic_progress, learning_sessions, conversation_messages, learning_objectives - Review system: review_cards (FSRS + SM-2 dual-algorithm), review_log (immutable per-event history), quizzes, assessments, mastery_overrides
- Calibration: confidence_events, topic_retention_stats
- Prerequisites: topic_prerequisites (required/recommended strength)
- Goals & annotations: learning_goals, user_annotations, user_notes, user_bookmarks
- Organizations: organizations, org_members, org_invitations, org_verified_domains (an org admin's domain stays pending until a platform admin verifies it from the back office; public mailbox domains are refused, migration 083), teams, team_members, curriculum_assignments (with status tracking), manager_assignments, platform_admins. Members read each other's profiles only while both are active members (migration 085). The Settings → Privacy switches for organization invitations and curriculum sharing are enforced by the database (migration 088), and a refusal names the switch (096). People who leave an organization lose access to it, and the organization stops seeing their progress, XP and badges; the certificates it issued them stay on its records (migrations 093, 097). People join only by invitation or a verified domain; admins manage roles up to admin, only owners grant or revoke ownership, and an organization always keeps one owner (migration 098)
- Enterprise: certifications, user_certificates, compliance_requirements, skills, role_profiles, role_skill_expectations, member_role_assignments, learning_paths, learning_path_steps, learning_path_enrollments. Migration 074 removed the plan gates from certifications, compliance_requirements and learning_paths, so their writes are role-gated only (org owner/admin/manager).
- Gamification: user_xp, xp_events, badges, user_badges (XP is recorded by the server only:
record_xp_eventis service-role only, migration 082) - Infrastructure: device_tokens, rate_limits (ai-proxy hourly cap), sanitization_logs, audit_log (append-only visibility/share events for legal compliance), user_ai_settings (per-user AI provider/key/model, RLS-scoped to owner; writes require
profiles.ai_sync_consent_at, so "Stop syncing" is enforced by the database, migration 081; clearing the consent deletes the synced rows in the same transaction and a racing upload waits for it, migrations 084, 087 and 095) - Dropped in migration 075 (plan 52): the billing tables and entitlement functions, the SSO / API-key / webhook / Slack-Notion-Lucca integration tables, and
_schema_meta(bring-your-own Supabase).plans/plan-52-poc-cut.mdhas the re-add map.ai_limits_for_useris back as a service-role-only stub with no caps (migration 089), so the pre-cut ai-proxy keeps answering until the new one is deployed.
Schema defined in supabase/migrations/. Row-Level Security (RLS) policies enforce access control — see test/rls/ for smoke tests, including rls_enterprise_test.sql (which asserts that an org admin with no plan can write learning paths, certifications and compliance requirements), rls_starter_test.sql (which pins the platform-admin-only is_starter flag and the starter-enrolment policies), rls_bookmarks_test.sql (per-user bookmark isolation, one bookmark per topic / part / session since migration 076, and resolve_bookmark_targets since migration 092, which resolves the Bookmarks screen in one request: only the caller's bookmarks, parts numbered in the lesson's reading order, and a target the caller can no longer read resolved to NULLs) and rls_account_deletion_test.sql (no foreign key on a delete path from auth.users may be NO ACTION/RESTRICT, and no ON DELETE SET NULL may land on a NOT NULL or CHECK-constrained column; deleting an account or a curriculum succeeds and keeps organization and starter curricula, migration 078; an organization curriculum then goes to the organization's most senior member (highest role, then earliest to join), who also becomes owner if the deleted user was the only one, migration 086; deleting a learning path or curriculum a certification targets keeps the certification and the certificates issued for it, migration 080; and every foreign key a curriculum deletion reaches is pinned, because the privacy policy and the account-deletion dialog describe exactly what it removes). rpc_smoke_test.sql also pins that only an org owner/admin/manager can run team_progress_report and compliance_status (migration 077), and fails on any SECURITY DEFINER function a client role can execute without its body checking the caller (migration 079: XP, badges, certificates, org notifications, email lookup, domain auto-join; the ai-proxy rate-limit functions and, since migration 082, record_xp_event are service-role only). It also pins search_user_notes (migration 091: every one of the caller's notes, by plain-text substring over title, content and tags) and the ai_limits_for_user stub. rls_smoke_test.sql checks that only a curriculum's owner can share it (migration 094) and that sessions written before the app sent UTC are marked as such (migration 090). The container's auth stub grants functions to anon, authenticated and service_role the way hosted Supabase does, so a function revoked only FROM PUBLIC is still caught (RPC-GRANTS).
AI and Edge Functions
The learner brings their own AI access: a Claude API key, a ChatGPT sign-in (OpenAI OAuth), or a Google sign-in (Gemini). The provider and model chosen in Settings → AI are persisted in secure storage (per-provider credentials and model also sync to user_ai_settings once the user consents to cloud sync) and are the only ones any AI call uses until the learner changes them there. Native builds call the provider directly; web builds go through ai-proxy, because browsers block direct calls to the provider APIs (CORS). In-app curriculum import is the exception: it always uses ai-proxy's structure mode, on every platform, because its prompt and input cap live server-side.
Edge Functions in supabase/functions/:
ai-proxy— AI relay for web builds and for curriculum import: JWT check, mode whitelist, server-side prompt assembly (get_tutor_context), prompt-injection input scoring and response validation, a model-id shape guard, one JSON answer per request (a request asking to stream gets a 400), and a 500 requests/hour per-user abuse cap onrate_limits(calls run on the learner's own credentials, so this is not a usage quota)send-push— sends the overdue-cards and streak FCM reminders, only to users who turned that reminder on in Settings → Notifications (opt-in: a switch never saved is off, as the app shows it); service-role only; on prod it is driven by thesend-push-morning/send-push-eveningpg_cron jobsexport-user-data— GDPR data export (Article 20) of the signed-in user's data as a JSON download.test/data/export_user_data_columns_test.dartreplays the migrations and fails when a column of an exported table is neither exported nor excluded with a reason
Libraries
- config/ai_provider_config
- config/env_config
- config/mastery_ring
- config/retrieval_config
- config/review_stamp
- config/routes
- config/supabase_config
- core/errors/error_mapper
- core/errors/error_snack
- core/errors/failures
- core/extensions/datetime_ext
- core/l10n/ai_provider_labels
- core/l10n/error_labels
- core/l10n/mastery_labels
- core/learning/prereq_gap_parser
- Extract
[PREREQUISITE_GAP: concept_name]tags emitted by the tutor. - core/learning/rubric_steps
- core/utils/calibration
- core/utils/common_words
- core/utils/deliberate_practice
- core/utils/file_saver
- core/utils/file_saver_barrel
- core/utils/file_saver_web
- core/utils/fsrs
- core/utils/interleaving
- core/utils/local_day
- core/utils/password_validator
- core/utils/retention_tracker
- core/utils/spaced_repetition
- core/utils/streak_calculator
- data/datasources/ai_datasource
- data/datasources/claude_datasource
- data/datasources/gemini_datasource
- data/datasources/google_auth_service
- data/datasources/list_models_via_proxy
- data/datasources/openai_auth_service
- data/datasources/openai_datasource
- data/datasources/org_datasource
- data/datasources/proxy_ai_datasource
- data/datasources/supabase_datasource
- data/datasources/user_ai_settings_datasource
- data/datasources/user_prefs_datasource
- data/models/bookmark_model
- data/models/certification_models
- data/models/content_chunk_model
- data/models/curriculum_assignment_model
- data/models/curriculum_level_model
- data/models/domain_model
- data/models/gamification_models
- data/models/json_converters
- data/models/learning_goal_model
- data/models/learning_objective_model
- data/models/learning_path_model
- data/models/learning_session_model
- data/models/manager_assignment_model
- data/models/mastery_override_model
- data/models/org_analytics_models
- data/models/org_invitation_model
- data/models/org_member_model
- data/models/org_notification_model
- data/models/org_verified_domain_model
- data/models/organization_model
- data/models/profile_model
- data/models/reading_question_model
- data/models/review_card_model
- data/models/review_log_model
- data/models/skill_framework_models
- data/models/team_model
- data/models/team_progress_entry_model
- data/models/topic_model
- data/models/user_ai_settings_model
- data/models/user_note_model
- data/models/user_topic_progress_model
- data/services/content_retriever
- data/services/fcm_service
- data/services/fcm_service_barrel
- Conditional-import barrel for FcmService. Native builds export the
real implementation; web gets a no-op stub so
firebase_messagingnever reaches the JS bundle. Always import this barrel — never the underlying files. - data/services/fcm_service_stub
- data/services/fcm_setup
- data/services/fcm_setup_barrel
- Conditional-import barrel for initFirebaseAndFcm. Native (iOS /
Android) builds export the real implementation from
fcm_setup.dart; web builds get the no-op stub fromfcm_setup_stub.dart. Always import this barrel — never the underlying files — so platform branching stays invisible to callers and the firebase_* packages never reach the web JS bundle. - data/services/fcm_setup_stub
- data/services/keyword_extractor
- data/services/notification_service
- data/services/notification_service_barrel
- data/services/notification_service_stub
- data/services/prerequisite_service
- domain/entities/bookmark
- domain/entities/certification
- domain/entities/content_chunk
- domain/entities/curriculum_assignment
- domain/entities/curriculum_level
- domain/entities/domain_entity
- domain/entities/gamification
- domain/entities/learning_goal
- domain/entities/learning_objective
- domain/entities/learning_path
- domain/entities/learning_session
- domain/entities/manager_assignment
- domain/entities/mastery
- domain/entities/mastery_override
- domain/entities/notification_preferences
- domain/entities/org_analytics
- domain/entities/org_invitation
- domain/entities/org_member
- domain/entities/org_notification
- domain/entities/org_verified_domain
- domain/entities/organization
- domain/entities/reading_question
- domain/entities/review_card
- domain/entities/review_log
- domain/entities/skill_framework
- domain/entities/team
- domain/entities/team_progress_entry
- domain/entities/topic
- domain/entities/user_note
- domain/services/domain_grouping
- domain/services/mastery_aggregate
- l10n/app_localizations
- l10n/app_localizations_en
- l10n/app_localizations_fr
- main
- presentation/providers/auth_provider
- presentation/providers/bookmark_providers
- presentation/providers/card_generation_provider
- presentation/providers/mfa_provider
- presentation/providers/notification_providers
- presentation/providers/onboarding_provider
- presentation/providers/org_providers
- presentation/providers/providers
- presentation/providers/sign_out_provider
- presentation/providers/user_prefs_provider
- presentation/screens/admin_onboarding/admin_onboarding_curriculum_screen
- presentation/screens/admin_onboarding/admin_onboarding_members_screen
- presentation/screens/admin_onboarding/admin_onboarding_org_basics_screen
- presentation/screens/admin_onboarding/admin_onboarding_org_picker_screen
- presentation/screens/admin_onboarding/admin_onboarding_scaffold
- presentation/screens/admin_onboarding/admin_onboarding_screens
- Deferred-load barrel for the admin org-creation flow. Only shown to org admins on first sign-in after they accept an admin invite — regular learners never load this chunk.
- presentation/screens/admin_onboarding/admin_onboarding_tour_screen
- presentation/screens/auth/auth_screens
- Deferred-load barrel for the auth flow (login, forgot, reset). Pulled
in when an unauthenticated visitor lands on one of those routes —
already-authenticated users on
/never load this chunk. - presentation/screens/auth/forgot_password_screen
- presentation/screens/auth/login_screen
- presentation/screens/auth/mfa_enroll_screen
- presentation/screens/auth/mfa_screens
- Deferred-load barrel for the 2FA flow. Splits
qr_flutter(used by the enroll screen for the TOTP QR code) out of the entry chunk — users without 2FA never download it. - presentation/screens/auth/mfa_verify_screen
- presentation/screens/auth/reset_password_screen
- presentation/screens/auth/terms_acceptance_screen
- presentation/screens/backoffice/backoffice_org_detail_screen
- presentation/screens/backoffice/backoffice_screen
- presentation/screens/backoffice/backoffice_screens
- Deferred-load barrel for the platform-admin backoffice. Gated by
PlatformAdminGuard; non-admin users never reach these routes, so
keeping them out of the entry chunk has zero UX cost for everyone
else. Re-exports PlatformAdminGuard so it lives entirely in this
chunk —
routes.dartreaches it through the deferred prefix. - presentation/screens/backoffice/backoffice_users_screen
- presentation/screens/bookmarks/bookmarks_screen
- presentation/screens/domains/curriculum_creator_screen
- presentation/screens/domains/curriculum_creator_screens
- Deferred-load barrel for the content-acquisition flows: the starter
catalog (
/domains/starters), the in-app import (/domains/import), and the CLI-oriented pair (/domains/curriculum-creatorand/domains/upload-curriculum). - presentation/screens/domains/domain_list_screen
- presentation/screens/domains/domain_screens
- Deferred-load barrel for the domains and topic-tree flow. Includes
the bottom-nav target (
DomainListScreen) and the topic tree shown when drilling into a domain. Defers anything domain-specific out of the entry chunk. - presentation/screens/domains/import_curriculum_screen
- presentation/screens/domains/starter_catalog_screen
- presentation/screens/domains/topic_tree_screen
- presentation/screens/domains/upload_curriculum_screen
- presentation/screens/goals/goals_list_screen
- presentation/screens/home/home_screen
- presentation/screens/lesson/lesson_screens
- Deferred-load barrel for the lesson viewer. Reached from
/domains/:slug/topics/:id/lesson— splitsflutter_animateand the markdown-rendering stack out of the entry chunk. - presentation/screens/lesson/lesson_viewer_screen
- presentation/screens/notes/note_detail_screen
- presentation/screens/notes/notes_list_screen
- presentation/screens/notes/notes_screens
- Deferred-load barrel for the notes flow: the Library → Notes list
(
/notes, also opened filtered to a topic from the lesson's notes action) and the note editor (/notes/new,/notes/:noteId), which the list, the tutor's quick-note "View" action and the lesson link to. - presentation/screens/org/assignment_detail_screen
- presentation/screens/org/badges_screen
- presentation/screens/org/certificate_view_screen
- presentation/screens/org/certification_labels
- presentation/screens/org/certifications_screen
- presentation/screens/org/compliance_dashboard_screen
- presentation/screens/org/curriculum_assignment_screen
- presentation/screens/org/gamification_labels
- presentation/screens/org/gamification_settings_screen
- presentation/screens/org/leaderboard_screen
- presentation/screens/org/learning_paths_screen
- presentation/screens/org/member_detail_screen
- presentation/screens/org/member_management_screen
- presentation/screens/org/notification_center_screen
- presentation/screens/org/org_analytics_screen
- presentation/screens/org/org_dashboard_screen
- presentation/screens/org/org_progress_report_screen
- presentation/screens/org/org_role_labels
- presentation/screens/org/org_screens
- Deferred-load barrel for the org admin shell (Plan 50+). Holds 25+
screens that are only reachable from
/org/:orgId/...routes — so learners on personal accounts never download this chunk. Includes the only consumers offl_chartin the app (analytics, member detail, compliance dashboard) and the OrgShellScaffold wrapper itself, so the scaffold also stays out of the entry chunk. - presentation/screens/org/org_settings_screen
- presentation/screens/org/path_detail_screen
- presentation/screens/org/path_progress_screen
- presentation/screens/org/role_profiles_screen
- presentation/screens/org/skill_framework_screen
- presentation/screens/org/skill_gap_screen
- presentation/screens/org/team_management_screen
- presentation/screens/progress/learning_guide_screen
- presentation/screens/progress/override_history_screen
- presentation/screens/progress/progress_dashboard_screen
- presentation/screens/progress/progress_extras
- Deferred-load barrel for the progress / goals / bookmarks flow. Holds every screen reachable from the Progress and Library bottom-nav destinations — including the dashboards themselves, so the entry chunk only ships the home view + shell scaffold.
- presentation/screens/review/card_proposals_screen
- presentation/screens/review/review_extras
- Deferred-load barrel for the review flow. Includes the main
ReviewSessionScreen(bottom-nav target for/review) so theflutter_markdown_plusstack used by the answer renderer never reaches the entry chunk. - presentation/screens/review/review_session_screen
- presentation/screens/settings/account_settings_screen
- presentation/screens/settings/ai_settings_screen
- presentation/screens/settings/change_password_screen
- presentation/screens/settings/learning_settings_screen
- presentation/screens/settings/notifications_settings_screen
- presentation/screens/settings/privacy_settings_screen
- presentation/screens/settings/settings_extras
- Deferred-load barrel for the entire settings flow. Includes the
landing page (
SettingsScreen) plus the seven sub-screens, so the settings UI loads only when the user opens the Settings tab. - presentation/screens/settings/settings_screen
- presentation/screens/settings/widgets/oauth_section
- presentation/screens/settings/widgets/section_header
- presentation/screens/tutor/chat_screen
- presentation/screens/tutor/chat_screens
- Deferred-load barrel for the tutor flow. Includes the main
TutorModeScreen(bottom-nav target for/tutor) and the deeperChatScreen, so theflutter_code_editor+flutter_highlightstack used by the in-chat code editor never reaches the entry chunk. - presentation/screens/tutor/tutor_mode_screen
- presentation/widgets/active_goal_card
- presentation/widgets/ai_sync_consent_dialog
- presentation/widgets/badge_popup
- presentation/widgets/bar_chart_category_label
- presentation/widgets/brand_wordmark
- presentation/widgets/chapter_mark
- presentation/widgets/chat_bubble
- presentation/widgets/chunk_image_builder
- presentation/widgets/code_block_builder
- presentation/widgets/code_editor_input
- presentation/widgets/components/kpi_card
- presentation/widgets/components/section_tabs
- presentation/widgets/components/status_badge
- presentation/widgets/deferred_screen
- presentation/widgets/domain_manage_sheet
- presentation/widgets/domain_stat_tile
- presentation/widgets/due_today_card
- presentation/widgets/dutato_logo
- presentation/widgets/dutato_markdown_style
- presentation/widgets/editorial_row
- presentation/widgets/empty_state
- presentation/widgets/forced_password_update_dialog
- presentation/widgets/legal_links
- presentation/widgets/level_manage_sheet
- presentation/widgets/mastery_progress_stepper
- presentation/widgets/mode_tabs
- presentation/widgets/notification_bell
- presentation/widgets/onboarding_gate
- presentation/widgets/onboarding_skip_link
- presentation/widgets/org_detail_scaffold
- presentation/widgets/org_shell_scaffold
- presentation/widgets/password_strength_indicator
- presentation/widgets/path_stepper
- presentation/widgets/pending_invitations_modal
- presentation/widgets/platform_admin_guard
- presentation/widgets/progress_ring
- presentation/widgets/quick_note_sheet
- presentation/widgets/reflection_form
- presentation/widgets/role_gate
- presentation/widgets/session_rubric
- presentation/widgets/skeletons
- presentation/widgets/streak_indicator
- presentation/widgets/streak_sparkline
- presentation/widgets/suggested_actions
- presentation/widgets/terms_gate
- presentation/widgets/topic_tree_widget
- presentation/widgets/tutor_bubble
- presentation/widgets/visibility_change_modal
- presentation/widgets/web_content_frame
- presentation/widgets/xp_bar
- theme