live in productionMulti-Role Education Platform· 2026

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.

TurborepoHonooRPCNext.jsExpo / React NativeDrizzle ORMPostgresBetter-Auth
Bright Tutor — marketing site on brighteducations.com

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.

api.brighttutorhono · oRPCbrighttutor.commarketing · webadmin.brighttutorconsole · webteacher.brighttutorteacher · webexpo appsteacher + guardian · nativeguardian.brighttutorguardian · web
fig. — six surfaces, 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, 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

6+surfaces in the monorepo
3audience roles · subdomain-scoped
1oRPC contract end-to-end
ts100% typescript

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.