Back to portfolio
Enterprise · PRJ-003

Bully Barrel

The operational source of truth for a security-rental and field-service business.

Visit live site
18
Operational routes — one source of truth
8
Core domain entities
~840
Invoice rows rendered without lag
The Problem

Bully Barrel Security rents portable, self-contained job-site security systems — cameras, alarms, access control, and 24/7 dispatch — to construction and development companies, billed by the month. This application is the internal operations backend that actually runs that business, used by office and admin staff, dispatchers, and field technicians.

The hard part is the operational complexity of a rental-plus-field-service company at scale: knowing which of hundreds of units are deployed versus sitting in inventory, catching a low battery before the customer does, tracking who owes money and how much, tying equipment and orders to physical job sites, and telling each technician exactly what to do today — drop off a unit, pick one up, run a repair, or perform a diagnosis.

Before this, that coordination lived in spreadsheets and ad-hoc messaging. The app replaces both with a single operational source of truth.

The Architecture

Bully Barrel is a client-rendered React single-page application talking to a hosted, serverless backend through a generated entity client SDK rather than a hand-written API. That is the central architectural bet: model the business as entities, lean on the platform for data, auth, and server functions, and spend the engineering effort on the operational UI itself.

01

Entity-oriented data model

Instead of bespoke REST routes, data is accessed through an entity client with list/filter/create/update on the core domain — Customer, Order, Invoice, JobSite, AlarmUnit, ServiceVisit, CustomerInteraction, and User. A new screen is mostly a query plus a form, which is what makes a surface of 18 routes feasible to build and maintain.

02

The operational surface

Eighteen routes span the full workflow: an Operations Dashboard of daily KPIs (deployed units, today’s jobs, low-battery alerts, monthly revenue, active customers) with the day’s schedule and a six-month revenue trend; Orders & Rentals for recurring monthly subscriptions and one-off jobs; a Customers CRM with each account’s orders, invoices, and interaction history; and Reports spanning financial, equipment, customer, operations, and performance analytics.

03

Asset & inventory tracking

Every unit is an asset record with serial, model, lifecycle status (available, deployed, maintenance, repair, retired), battery level, and WiFi/camera attributes. Job Sites tie units and orders to physical locations with full street address and GPS coordinates, so the system always knows where each unit is and what state it is in.

04

Billing & invoicing

A complete billing module handles itemized line items, subtotal/total/balance-due, an invoice lifecycle (draft, sent, paid, overdue, cancelled), and a payment-received flow. Live card processing is deliberately stubbed for now (test-mode placeholders, payments recorded manually as paid), keeping the operational and billing surfaces complete while deferring payment-gateway integration and its compliance scope.

05

Field operations & roles

Identity and authorization are delegated to the platform’s auth service, and the UI branches on role (admin, technician, dispatcher). The technician-facing Tech Schedule and Tech Active Jobs views filter orders to the signed-in tech by assigned_tech_id and drive field work across four job types — drop-off, pickup, repair, and diagnosis.

06

State, navigation & UI system

Server state runs through TanStack React Query — queries keyed per entity, mutations that invalidate those keys to keep the UI fresh — while view state stays local. Detail selection is encoded in URL query parameters so deep links and the Back button behave. The UI is standardized on Tailwind plus a shadcn/ui component layer over Radix headless primitives, with Recharts for analytics.

Key Decisions & Tradeoffs

The reasoning behind the build — and what each choice cost.

A generated entity client SDK over a hand-written API

Why

Modeling the domain as entities (list/filter/create/update) rather than bespoke endpoints means almost no backend boilerplate and very fast feature delivery — a new screen is mostly a query plus a form.

Tradeoff

Business logic that would normally live server-side (pricing, invoice generation, validation) tends to drift into the client, and the app is coupled to the SDK’s data conventions, which makes complex server-side queries and transactions harder.

Deliberately thin state management

Why

Server state is handled by TanStack React Query with per-entity query keys and invalidating mutations; view state stays in local component state — no Redux ceremony, automatic caching and refetching, less code.

Tradeoff

No single global store, so cross-screen state and optimistic flows are managed ad hoc.

Delegate auth to the platform and branch the UI on role

Why

Reading the current user and role (admin, technician, dispatcher) and rendering accordingly means no custom auth code and clean role-based views, like filtering the technician schedule by assigned_tech_id.

Tradeoff

Authorization is partly enforced in the client, so it is a UX convenience rather than a hard security boundary — real enforcement has to move server-side.

Standardize on Tailwind + headless accessible components

Why

A utility-first system plus a shadcn/ui layer over Radix primitives (dialogs, selects, tabs, popovers) and a dedicated charting library gives consistent, accessible components and fast dashboards without building primitives from scratch.

Tradeoff

A heavier dependency surface and a larger bundle.

Route detail views through the URL

Why

Encoding invoice and customer detail selection in query parameters makes the Back button and deep links behave correctly and keeps navigation shareable and predictable.

Tradeoff

Slightly more wiring than purely local state.

Stub live payments, ship the operational surface first

Why

The billing and payment workflow is fully built in the UI with payments recorded manually as paid; deferring live card processing keeps the operational and billing surfaces complete while postponing the payment-gateway integration and its compliance scope.

Tradeoff

No real charges yet — payment-conversion and churn cannot be measured until Stripe is wired in.

Outcome & Performance

Demonstrated end to end with a realistic dataset, the system runs the operational dimensions it was built for: 118 active monthly subscribers across ~125 customer accounts; 185 active monthly rentals (priced per day — most at $8, with $4 and premium $15 tiers) producing about $48,900 billed per month and ~$42,700 collected in-month at roughly a 90% collection rate; a six-month revenue trend ramping from ~$25K to ~$44K/month; an inventory of 209 units (185 deployed); and a technician workload of 64 scheduled appointments across drop-off, pickup, repair, and diagnosis.

It holds up under that load: the dashboard renders live KPIs over hundreds of records without noticeable lag, including invoice lists of roughly 840 rows. On the engineering side, the app is about 50 source files and ~10,100 lines across 18 pages and 12 shared components, every file validated to compile, building cleanly (~2,900 modules, zero errors) and deploying as a static SPA.

The honest limitation: server-side functions and live payment processing are not yet active, so true production usage, churn, and payment-conversion metrics cannot be derived from the client alone — those come once the serverless functions and Stripe integration go live.

Capabilities
React 19React Router v6TanStack Query v5Serverless backend + SDKEntity-oriented data modelTailwind + shadcn/ui (Radix)RechartsRole-based accessVite · esbuildStripe (planned)