Bright Tutor
Tuition media platform for a tutoring business: public marketplace, admin console, and role-aware web + native surfaces for teachers and guardians, all on one Hono+oRPC API with a shared Drizzle schema.

The brief
A tutoring business was running on Excel. Enrolments, teacher schedules, guardian follow-ups, and fee status all lived in spreadsheets that drifted apart weekly. Teachers, guardians, and admins each had their own workaround. Nothing shared a source of truth.
The brief: replace the spreadsheet with one platform and give each role a surface that fits how they actually work, on web, native, or both.
The problem
The status quo:
-
Everything in sheets: teacher assignments, guardian contacts, payment status, and attendance lived in separate workbooks.
-
Manual follow-ups: reminders and updates depended on someone remembering to check a row and act on it.
-
Three audiences, zero shared workflow: admins, teachers, and guardians each needed different tools, but the business only had spreadsheets and a group chat.
A single dashboard with role-conditional fields would have been faster to ship and miserable to use. So I built six surfaces around one API.
The approach
The system is a Turborepo monorepo with six client surfaces 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, cookie, and 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 six surfaces
Every endpoint, error code, and 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
- Teacher + guardian apps, same oRPC client, native UI.
One schema, three roles
Each audience gets a surface shaped for their job, not a shared dashboard with fields hidden:
-
Teachers: schedule, attendance, and payouts on the web portal and the native app.
-
Guardians: enrolment, progress, and tuition on the web portal and the native app.
-
Admins: onboarding, payroll, and support from the console.
Enrolment, scheduling, and attendance write to the same Postgres tables regardless of which surface initiated the change. That replaces the spreadsheet: one source of truth, visible everywhere it needs to be.
What's running today
The marketing site is live at brighteducations.com. Role surfaces are in active deployment across the monorepo.
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.