Case study — Shiftova architecture

Shiftova's architecture: how I built and run a scheduling app on my own

Shiftova is a staff scheduling and time-clock app for restaurants and other hourly teams. I designed, built and run all of it: the mobile app, the database, the server code, payments and the app-store releases. This is how it's put together, and the decisions I'd defend in an interview.

Short on time? Read the 1-minute version →

The short version, in plain English

  • One app, three modes: managers build schedules and run payroll reports, employees see their shifts and clock in, and a shared tablet can work as a time clock.
  • The phone app talks to a hosted database (Supabase, built on PostgreSQL). The security rules live in the database itself, so every request only ever returns what that person is allowed to see, whichever screen asks.
  • Anything that needs a secret key or an outside service (payments, push notifications, email) runs in small server functions, started by the app, by changes in the database, or on a timer.
  • The business rules (overtime law for 13 provinces and territories and 50 states and DC, holiday pay, payroll periods, the sales forecast) are plain functions covered by 476 automated tests.

32 / 32

database tables protected by row-level security: every request is filtered by who's asking

13

server functions for payments, push notifications, email, invites and the time-clock kiosk

476

automated tests on the business rules: overtime, holiday pay, payroll and forecasting

iOS + Android

from one TypeScript codebase, live on the App Store, Android in Google Play review

At a glance

My role
Sole engineer: product design, the mobile app, the database, server code, payments, releases and support.
Stack
React Native and Expo (TypeScript); Supabase (PostgreSQL, Auth, Edge Functions, scheduled jobs); Stripe; push notifications through Expo; Resend email.
Size
~31,600 lines of app code across 34 screens and 87 logic modules; 70 database migrations; 13 server functions.
Status
Live and in daily use at a real restaurant, sold as a monthly subscription. Android is in Google Play review.

Skills used

React NativeExpoTypeScriptPostgreSQLSupabaseRow-level securityServerless functionsStripe paymentsPush notificationsSystem designAutomated testingApp Store & Google Play releases

What it does

One app serves three kinds of user. Which screens someone sees depends on their role at the company they're signed in to.

The architecture at a glance

There's no traditional server that I run myself. The phone app talks directly to a hosted database platform, and the database decides what each request is allowed to do. Small server functions handle anything that needs a secret or an outside service.

  1. Phone app (iOS & Android)

    React Native + Expo, one TypeScript codebase. Manager, employee and kiosk modes. Overtime, payroll and forecast logic run here as tested functions.

  2. Supabase (hosted PostgreSQL)

    Sign-in and invites; 32 tables with row-level security on every one; 48 database functions and 41 triggers; 70 versioned migrations.

  3. 13 server functions + 5 scheduled jobs

    Payments (checkout, billing portal, Stripe webhook, nightly reconcile), people (invites, account changes, kiosk), messaging (push, chat, incident emails), and scheduled reminders, alerts and data retention.

  4. Third-party services

    Stripe, Apple and Google push notifications (through Expo), Resend email.

Design decisions I'd point to

These are the choices that shaped the app most, each with the cost that came with it.

1.Security lives in the database, not the app

The problem
The app talks to the database directly. If a screen had a bug, it could ask for someone else's data, like a coworker's pay rate.
What I did
Every one of the 32 tables has row-level security: rules inside the database that filter every request by who's asking and which company they're signed in to. Where people legitimately need a little information about each other (names on the team schedule, say), a narrow database function returns just names instead of opening up the whole profile. When I added a team schedule view for employees, it needed no server change at all: the rules already allowed it, and pay rates stayed private.
The trade-off
Database rules are harder to debug than app code. I check changes against live data inside transactions that always roll back, so nothing is saved.

2.Many businesses in one system, kept apart

The problem
Each business must only ever see its own data, and some people work for more than one business.
What I did
Every piece of business data belongs to a company. A person can belong to several companies and switch between them, and the security rules all key off the company they're currently in, through one shared helper.
The trade-off
Every table, rule and query has to carry that company scope. Letting one person belong to several companies came later, and retrofitting it into every rule took real care.

3.Business rules are plain, tested functions

The problem
Overtime law differs by place (13 provinces and territories and 50 states and DC, with special daily and double-time rules), and holiday pay has its own eligibility tests. A mistake costs a customer real money.
What I did
The rules for overtime, holiday pay, payroll periods and the sales forecast are written as plain functions with no database or screen code in them. 476 automated tests run them in seconds, and the same functions drive the screens and the PDF reports, so the numbers always agree.
The trade-off
Some of that logic runs on the phone, so a fix needs an app release and a store review. There's no over-the-air update channel yet.

4.Events, not polling

The problem
People need to hear about new schedules, shift swaps and upcoming shifts promptly, without the app checking constantly.
What I did
When something changes in the database (a schedule is published, a shift is offered), a trigger calls a server function that sends the push notification. Scheduled jobs send shift reminders and overtime alerts every minute. Who gets notified is decided on the server, mostly inside the database itself, and every send's outcome is recorded so I can check afterwards what actually reached a phone. Checking that record against live data is how I found that deactivated employees could still be notified, and that a schedule update could ping people whose shifts hadn't changed. Both are fixed.
The trade-off
Logic is spread between triggers, server functions and the app, so I document why each piece lives where it does.

5.Payments are enforced by the database too

The problem
A lapsed subscription has to stop changes reliably, but a customer should never lose their records.
What I did
Stripe handles checkout and the billing portal. A webhook updates the company's subscription, and a nightly job re-checks every subscription with Stripe in case a webhook was missed. If a subscription lapses, a database check on 21 tables blocks changes while existing records stay readable.
The trade-off
Two sources of truth (Stripe and the database) have to be kept in sync, which is exactly why the nightly reconcile exists.

6.Correct before fast

The problem
The manager dashboard felt slow, and caching was the obvious fix. But a labour-cost number that's out of date is worse than a slow one. Separately, the database API quietly returns at most 1,000 rows per request, so growing history could have been cut off with no error.
What I did
I sped the dashboard up by cutting the number of round trips instead of caching, so every number is still fresh. Long lists are now read in pages with a fixed sort order, so nothing can be skipped or repeated.
The trade-off
It's not as fast as a cache would be. That was a deliberate choice.

7.A shared tablet is its own kind of user

The problem
A store tablet used as a time clock is signed in all day, in a public spot.
What I did
The tablet signs in as a special kiosk identity with its own narrow permissions. It can show the staff list and clock people in and out by PIN, but it can't read schedules, chat, pay rates or contact details. It's set up through a server function, not by sharing a manager's account.
The trade-off
Every new feature has to consider the kiosk explicitly, so it's never included by accident.

Shipping and running it

How I keep it working

What I'd improve next

Being honest about the gaps matters as much as the design:

Lessons learned

  1. 1. Put the rules where they can't be skipped. Security in the database protects data even from my own bugs in the app.
  2. 2. Keep the important logic boring. Plain functions with no database or screen code are easy to test, easy to reason about, and shared by every screen and report.
  3. 3. Correct beats clever. No caching where stale numbers would mislead, and paging where a silent limit could drop data.
  4. 4. Check the system against reality. Querying live data, safely, found real bugs that tests couldn't, like notifications reaching people they shouldn't.
  5. 5. The product is more than the code. Store reviews, billing, privacy and support are part of shipping software people pay for.

Hiring, or building something similar?

I'm a full-stack developer with a computer science degree and over a decade of building software and analytics for a restaurant franchise business, and I'm open to full-stack, backend and data roles as well as project work. For the data side of the same app, see the sales-forecasting case study.