ODOnboard

An employee onboarding and people-operations platform, built so that a new starter is genuinely ready on day one rather than nominally started.

www.odonboard.com

Specification

Evidence for

  • Custom business systems
  • Internal tools and operational software
  • Customer portals

Built on

  • React
  • TypeScript
  • Supabase
  • PostgreSQL
  • Deno edge functions
  • Playwright

Measured

Tables, every one with row-level security
78
supabase/migrations
Row-level security policies
289
grep -c 'create policy'
Database functions
170
distinct public. and security. functions
Server-side jobs
32
supabase/functions
SQL policy tests
31
supabase/tests/**/*.sql
Browser tests
6
e2e/*.spec.ts
Screens
  • ODOnboard: a new starter's onboarding journey, showing each step, who owns it, and what is still blocking day one.

    The employee's own journey. Every step carries an owner and a due date, and the ones that block day one are marked as such rather than left to be inferred.

  • ODOnboard: the HR view, showing which new starters are on track, at risk, and predicted to miss their start date.

    The view an HR lead opens first thing. Not a count of tasks completed — a prediction, per person, of whether the date will be met.

  • ODOnboard: the employee's home screen, showing only their own record, documents and outstanding actions.

    The same system from outside the organisation. This person can see their own record and nothing else, and that is enforced below the application.

The problem it was built for#

Onboarding was scattered across spreadsheets, email threads, managers and HR tasks. Nothing was missing, exactly — every piece of it existed somewhere. What nobody could answer was the only question that matters: is this person actually ready?

That question is harder than it sounds, and it is the reason the system exists. "Ready" is not "all tasks ticked". A new starter can have completed nine things and still be unable to work on Monday because the tenth was the laptop. The system has to know which steps block the start date and which are merely outstanding.

Readiness is computed, not ticked#

Steps carry a required_for_readiness flag, and the nightly snapshot divides each journey by it — completed-and-required over total-required — rather than by task count. Every active starter lands in one of three states:

The three readiness states, and what each one means
StateWhat it means
On trackEvery step that blocks the start date will complete before it.
At riskA blocking step is late enough that the date is in doubt.
Will missThe date cannot now be met. Said early, while it can still be changed.

The distinction between the last two is the whole point. A dashboard that only reports what has already gone wrong is a report; one that says will miss while there is still a fortnight to fix it is a system. The same snapshot also carries average readiness across the tenant and average days-to-ready over the previous ninety days, so an HR lead can see whether the process itself is getting better or worse rather than only how this month's intake is doing.

Every organisation runs its own process on one engine#

Onboarding at a forty-person agency and onboarding at a regulated healthcare group are not the same process, and building a product that pretends otherwise means building a product neither will use. Each tenant composes its own journeys from blueprints, phases, steps and templates; the engine underneath is identical.

That is the multi-tenancy problem stated honestly. One database, one deployment, and any bug in a query that forgets a tenant filter is a data breach rather than a display error.

Which is why the isolation is not in the application#

Every person may only ever see their own record, and every organisation may only ever see its own people. That is enforced by 289 row-level security policies across all 78 tables — in the database, below the API, below the application code. A missed filter in one query returns nothing rather than returning somebody else's employee.

This is the part that is genuinely difficult, and the part that is impossible to verify by looking at the screen. So it is verified by tests that try to break it: thirty-one SQL suites named for the specific thing each one protects.

Policy test suites and what each one attacks
SuiteWhat it tries to do
rls_isolation_sanityRead another tenant's rows as a signed-in user of this one.
employee_files_cross_access_reauditReach another employee's files. Re-run after every change to that table.
tasks_review_bypass_sanityComplete a task that requires review, without the review.
onboarding_rpc_security_sanityCall the onboarding procedures directly, going around the application.

Above those sits a Playwright spec, tenant-isolation.spec.ts, which does the same thing through a real browser against a real signed-in session — because a policy that holds in SQL and leaks through an API route has still leaked.

Leaving is a first-class feature#

A people system holds the most sensitive data a small company has, and the question that follows from that is not "how do we keep it safe" but "what happens when someone asks you to delete it".

So erasure is modelled, not improvised. There is a case record, a subject map, a log of what was erased and when, retention policies per tenant, and a job that performs the purge. A person who asks to be forgotten is a supported workflow with an audit trail, rather than a support ticket and somebody writing SQL.

The same instinct runs through the rest of it: joiner-mover-leaver events are recorded as their own audit stream, document acknowledgements are stored against the version of the document that was acknowledged, and administrative actions land in an audit log rather than in nobody's memory.

What we learned#

"Onboarding" turned out to be too narrow a frame. What we had actually built was a system for moving people through structured business processes with owners, dependencies, deadlines and an audit trail. Joining a company is only the first such process. Leaving one is another; so is a promotion, a role change, a compliance renewal, a return from long-term leave.

We would make that generalisation earlier if we built it again. Several of the tables are named for onboarding and hold things that are not onboarding, which is what it looks like when a product outgrows its own vocabulary and the schema keeps the old one.