private client workMulti-tenant Education SaaS· 2026

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.

TurborepoHonooRPCNext.jsExpo / React NativeDrizzle ORMPostgresBetter-Auth

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.

api.brighttutorhono · oRPCbrighttutor.commarketing · webadmin.brighttutorconsole · webteacher.brighttutorschedule · webexpo appguardian · nativesms-gatewaydelivery · service
fig. — five apps, one schema. subdomain-scoped roles, shared contract, single Postgres

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

5apps in the monorepo
3audience roles · subdomain-scoped
1oRPC contract end-to-end
ts100% typescript

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.