Bright Tutor
Multi-tenant tutoring platform with role-aware subdomains and a shared schema across five apps: a marketing site, an admin console, a teacher dashboard, a React Native app for guardians, and a Hono+oRPC API. The architectural piece I'm most proud of from 2026 — every app talks the same type-safe contract end to end.
The brief
A tutoring business wanted to stop running on spreadsheets, WhatsApp, and three disconnected admin panels. They had teachers, guardians, and an internal ops team — three audiences, three workflows, and three opinions on what “the dashboard” meant.
The brief: one platform, three first-class experiences, one source of truth. The constraint: it had to feel native to each role — not the same screen with different fields hidden.
The problem
Three jobs to be done:
- Teachers need to manage their schedule, see who's enrolled, log attendance, and get paid. They use it on phones.
- Guardians need to enrol their kid, see attendance + progress, and pay tuition. They use it on phones too.
- Admins need to onboard teachers, set tuition, run payroll, and answer support — at a desk, on a real screen.
A single dashboard with role-conditional fields would have been faster to ship and miserable to use. So I built five apps.
The approach
The system is a Turborepo monorepo with five apps and shared packages. Every app talks to the same Hono server through an oRPC contract — so types flow end-to-end from the Drizzle schema to the React Native screen.
The hard parts
Subdomain-routed roles
Teachers log in at teacher.brighttutor.*, guardians at the native app or guardian.brighttutor.*, admins at admin.brighttutor.*. Routing is at the subdomain — not at a /teacher path — so every screen, every cookie, every error page belongs to the role. It's also a pleasant local DNS exercise; the README ships with the /etc/hosts lines for the dev loop.
End-to-end types across five apps
Every endpoint, every error code, every payload flows through one oRPC router. The React Native app, the admin console, and the teacher web app all import the same typed client. When the schema changes in packages/db, every app that uses the affected route fails its type check in CI. There's no “hopefully the mobile build picks it up”.
- Turborepoinfra
- Pipelines: shared db gens block app builds.
- Hono + oRPCapi
- One router, typed clients in every app.
- Drizzle + Postgresdata
- Single schema, generated migrations checked in.
- Better-Authauth
- Role-scoped sessions across subdomains.
- Expo / React Nativemobile
- Guardian app — same oRPC client, native UI.
- SMS gatewaychannels
- A small worker so OTP + reminders don't block requests.
Notifications across channels
Teachers want push. Guardians want SMS. Admins want email. I split delivery into a small sms-gateway app so the API server never blocks on a third-party SMS provider, and so retries and rate limiting live in one place. Push goes through Expo's service; email through a small Brevo wrapper.
What's running today
The platform is in active client deployment. Specifics intentionally private — happy to walk through the architecture in detail under NDA.
What I'd do differently
I'd start with the oRPC contract before any UI. Half of the early friction came from web and native diverging on payload shapes before the server told them “no, this is the shape”. With the contract in place first, every app gets the same answer the same way.