A real educator can sign in, land in their district and school, and see a shell whose navigation already matches their role. Nothing is mocked below the UI layer.
Features
- Email/password and Google sign-in with district email domain hinting
- Onboarding: accept invitation, confirm school assignment, set display name
- Role-aware navigation and scope switcher (district → school → caseload)
- Command palette (⌘K) and Copilot launcher (⌘J) wired to real routes
- Notification center shell with unread counts
- Dark mode, reduced motion, and WCAG AA focus states as platform defaults
Pages
/authSign in / sign upnew/onboardingFirst-run setupnew/Dashboard (role router)upgrade/settingsProfile & preferencesupgrade/settings/usersUsers & rolesnew/settings/schoolsSchools & screening windowsnew
Components
- AppShell, SidebarNav, ScopeSwitcher, RoleBadge
- CommandPalette, CopilotLauncher, NotificationCenter
- ds/primitives: Text, IconChip, StudentAvatar, Meter, EmptyState, Skeletons
- PermissionGate — hides only what the policy would reject
Database
- districts, schools, profiles
- user_roles (separate table — never a column on profiles)
- permissions, role_permissions, staff_school_assignments
- Security-definer helpers: has_role, is_staff, current_district, can_access_school
- Indexes on (user_id), (district_id), (school_id) for every scope lookup
Backend
- createServerFn: getMyProfile, getMyScopes, listSchools, inviteStaff, assignRole
- Bearer-token middleware registered in src/start.ts
- Audit write helper invoked on every role grant and scope change
Authentication & access
- Managed cloud auth with leaked-password (HIBP) protection enabled
- No anonymous sign-up; staff arrive by district invitation only
- Protected routes under _authenticated/; public marketing and auth routes stay SSR
- Session-scoped RLS — the app never trusts a role sent from the client
AI
- Copilot shell with context awareness (current route, current student)
- Intent router + guardrail: refuse anything outside the user's RLS scope
Testing
- RLS policy suite: for each role × each table, assert allow and deny
- Cross-tenant leak test — user in District A cannot read District B by ID
- Playwright: sign in → land on correct role dashboard → sign out clears cache
- Axe accessibility pass on shell, nav, and auth screens
Dependencies
- None — this is the base layer
Definition of done
- Every table created in this sprint has GRANTs, RLS enabled, and at least one deny test
- A teacher, coordinator, admin, district admin, and parent account each land on a correct, differently-scoped shell
- Zero security-linter warnings
- Keyboard-only traversal of the shell works; contrast verified at AA
Risk & mitigation
Role sprawl: ten roles is a lot. Mitigated by the specialist archetype — four specialist roles share one permission set with a domain filter.