Skip to main content
All work

PRODUCT BUILD

Orvia

A week of retail shifts is a constraint-satisfaction problem. Most scheduling software gives you a better spreadsheet, Orvia generates the answer.

Next.js TypeScript Supabase
Role
Design + Engineering
Type
Solo build
Year
2025
Links
demo ↗ github ↗

The problem

Retail managers at small and mid-size shops spend hours each week building shift schedules by hand — cross-referencing employee availability, role requirements, contractual hour caps, and fairness across the roster. Every week, from scratch. It is a solved problem that is still being solved manually because the tools available are either a spreadsheet or an enterprise system with a five-figure annual contract.

Orvia is a shift scheduling system built for regional managers overseeing multiple locations. The interface is simple. The scheduling logic runs entirely in the browser.

Built for

A friend managing three retail locations who was spending around three hours a week hand-editing schedules in Google Sheets. Orvia generates a full seven-day schedule across her stores in a couple of seconds, and scales to any roster size and any number of locations she configures in Supabase.

Orvia dashboard showing three retail locations — Avenlo, Cedar Loop, and Solmere — with staffing coverage and a weekly glance panel

Regional manager dashboard — three locations, current-week staffing coverage at a glance

Decisions that mattered

The obvious product is a grid you fill in manually. Click a slot, assign a worker, move to the next cell. It's familiar, learnable, and completely manual.

Orvia does more. "Generate New Schedule" runs the constraint algorithm in the browser and fills the entire week at once. Individual slots stay editable afterward: click a cell, a dialog opens to reassign the worker or adjust the time window. The default path starts from a complete schedule rather than a blank one, and the manager's job becomes reviewing and correcting instead of building from scratch.

For manual edits, the obvious pattern is drag-and-drop. A tray of employee name pills sits on one side, slots on the other, and you drag the right person into the right cell. It's direct and tactile, and it breaks the moment the roster grows past a dozen people. With fifty employees, finding the right name pill becomes its own cognitive load, and the manager ends up scanning a wall of names to swap a single shift.

Orvia uses a modal instead. Click a slot, a dialog opens with an alphabetically sorted employee list. The modal adds a layer of interface complexity, and it's still the right call. Scrolling a sorted list of fifty names is faster than scanning fifty unsorted pills with no structure.

Orvia schedule view for Cedar Loop, week of April 27 — a generated weekly shift grid by role and time slot, with a success toast confirming the schedule was generated

Cedar Loop — generated weekly schedule by role, with the success notification confirming one-click generation

Editing a shift in Orvia — clicking a schedule cell opens a modal with an alphabetically sorted employee list to reassign the slot

Clicking any cell opens the edit modal — an alphabetically sorted list to reassign the shift

How I built this

Started with a half-formed idea: automate the boring half of shift scheduling and leave the human half to the human. Stack came first. I talked through options with Cursor before writing any code, and picked Supabase because one platform handling auth, database, and row-level security meant fewer integration points to manage as a solo dev.

Schema design was where AI helped the most. Multi-location scheduling has a lot of connected tables: employees, locations, role assignments, shift templates, availability windows, generated schedules. Getting the relationships right matters, because a wrong foreign key two weeks in becomes painful to migrate around. I described the requirements and the scenarios I needed to support, and the model returned schema proposals I could iterate on faster than I'd have built from scratch. That's the part of solo full-stack work that scales least with experience, and the place AI compressed it the most.

The scheduling logic was the opposite story. I wrote out every rule in plain text, including hour caps, role coverage, fairness, contractual constraints; and the model translated it into Supabase edge functions and browser scripts. It took weeks to fine-tune, because business rules and people rules don't generalize cleanly. Every manager has a "this employee can't open on Tuesdays because of school pickup" exception, and trying to encode all of it produced brittle logic that broke in new ways each time I added a constraint. The call I made was to stop coding for every corner case and unload some of the work to the manager. More manual editing than a perfect generator would require, and a lot less than untangling a confidently wrong schedule. Better to be obviously partial than confidently wrong.

 1–3  setup + state init
      workers, templates, recurring assignments, hours
      → exclude manager + inactive
      → keep cross-location shifts  (conflict guard)
      → new ScheduleGenerationState

  4   processRecurringAssignments()   ← highest priority
      stable weekly patterns placed first, slots locked

  5   processPairedPrepBaristaShifts()   ← specific location only
      prep + barista paired together

  6   assignLeads()
      open: start < 12:00  ·  close: end ≥ 21:00
      tie-break: prior week → fewest hours → highest job_level

  7   assignDynamicShifts()
      remaining unfilled slots
      tie-break: fewest hours → highest job_level → unassigned ⚠

  8   saveSchedule()
      → scheduled_shifts (write to Supabase)
      → shift_assignments (write to Supabase)

Generation pipeline. Setup in 1–3, assignment phases in 4–8.

The frontend moved fastest. Color and type were settled before I wrote a screen, because those decisions compound too fast to fix later. From there I sketched a mid-fidelity layout, screenshotted it to the model, and went straight to implementation, with spacing, shadows, and motion consolidating as I built. No handoff, no spec, just self-iteration on the design as I built it. The small visual things — padding, shadow weight, alignment quirks, motion — aren't necessary for the tool to work. They're necessary for it to feel like something a manager would actually want to open on a Thursday night.

The design system

The system is documented in markdown and mirrored in a single :root block. Every color, type role, spacing value, and corner radius is named once and referenced from there. Without that layer, designing in code drifts into AI default aesthetics: Inter on white, a purple gradient hero, and a primary button in safe blue.

Color tokens

Primary

color-deep-rose #B48883 Primary interactive elements that initiate an action — the single action color.
color-deep-rose-active #9E736E Pressed/hover state of primary interactive elements only.
color-rose-blush #CBA6A2 Selected/active state fill and outline — softer presence than deep-rose for persistent state.

Categorical

color-lavender-cream #D7CDEA Hover and focus fill on interactive surfaces — low-contrast, non-committal highlight.
color-lavender-mid #A597CE Active/selected text on secondary navigation elements.
color-pistachio-mist #D3E3D8 Positive status indicators — surfaces and text that communicate success or completion.

Neutral

color-charcoal-cocoa #2E2B29 Primary text and icon strokes — maximum readable contrast.
color-ash-mocha #847A75 Secondary and supporting text — reduced emphasis, never used for primary labels.
color-blush-cream #FEFCFB Full-page background — the base layer everything sits on.
color-card-ivory #FAF7F5 Raised surfaces: cards, table rows, modals — one step above the page.
color-oat-beige #F2EBE8 Column headers, hover targets, default badge fill — visually groups or highlights within a surface.
color-very-light-beige #DCD4CD Structural separators between surfaces — panels, grids, modals.
color-input-border #DFD2C4 Form field borders — lighter than very-light-beige to reduce visual weight inside dense form layouts.

Semantic

color-error-red #C34E46 Destructive states and validation failures — error text, conflict indicators, destructive confirmations.

Typography tokens

Manrope. A geometric sans-serif with slightly rounded strokes and open apertures. Designed to stay readable at small sizes without feeling cold.

Title
type-title 18px / 600 / 1.4 Page headers, modal titles, section names.
Body
type-body 14px / 400 / 1.5 Reading text, table cells, form descriptions.
Label
type-label 12px / 600 / 1.4 / 0.01em Nav items, button text, form labels, column headers.
Caption
type-caption 11px / 500 / 1.4 Sidebar footer, timestamps, secondary metadata.

Spacing tokens

spacing-xs 8px Tight internal padding within a single element.
spacing-sm 12px Compact element padding.
spacing-md 16px Standard element padding.
spacing-lg 24px Gaps between related groups or sections.
spacing-xl 32px Separation between major layout regions.

Layout, used directly

48px Generous section separation.
64px Major layout separations.
96px Sidebar logo top offset, full-page top margin.

Border radius tokens

radius-sm 8px Inline elements: chips, tags, small indicators.
radius-md 10px Interactive controls: buttons, inputs.
radius-lg 12px Container surfaces: cards, panels, modals.

What I built

Designed and engineered end-to-end. Frontend in Next.js and TypeScript, data and auth in Supabase (PostgreSQL + Edge Functions + RLS). The scheduling algorithm itself runs in the browser (JavaScript), no server round-trip required for generation.

  • Multi-location dashboard: regional managers see all their stores at a glance, with current staffing percentages and active worker counts per location
  • Per-location schedule view: a weekly grid by role and shift type, navigable across weeks with prev/next controls
  • Employee and role management: roster management with availability windows, role assignments, and hour constraints that feed directly into the generation algorithm
  • Weekly context panel: staffing coverage percentages by location, plus upcoming employee birthdays — a small detail that matters in small teams where managers track this manually today

Future Directions

The generation logic is intentionally rule-based and stays that way. The next layer worth exploring is entry, not generation: a manager typing "Maria can't open Tuesdays anymore, she started a class" and having that map to an availability update. Unstructured, natural language input into structured fields sounds like a good case for the addition of an LLM to further maximize efficiency.