RBAC · Inventory · Advertising · DBA

Role-Based Access Controland the security architecture behind it

A production loyalty and store-operations platform built for a real 12-store gas station chain, examined role by role, security layer by security layer.

Presented by
12
Stores across Texas
5
Distinct access roles
2
Platforms: mobile + admin web
$0
Cost to the customer

The Project

A loyalty platform for a real 12-store chain

Lucky Stop is a Texas gas-station chain. Their loyalty app is free for customers, earns cashback on every purchase, and gives stores a full operations layer underneath it: staff scheduling, inventory requests, daily reporting, and store-to-store messaging, all gated by exactly who is allowed to see and do what.

The platform pays for itself the same way it pays the business: stores cover a monthly subscription plus a small platform fee on the cashback they issue. Customers never pay a cent.


How It's Built

Tech stack

MobileReact Native + Expo (iOS & Android)
Admin webReact + Vite + TypeScript
BackendNode.js + Express + TypeScript
DatabasePostgreSQL + Prisma ORM
AuthFirebase (phone verification) + JWT sessions
Push & storageFirebase Cloud Messaging + Cloudinary
HostingRender (API) · Vercel (admin) · Expo EAS (mobile)

System Design

Two front ends, one backend, one source of truth

Mobile app (React Native)
Store Manager · Employee · Customer
Admin web (React)
DevAdmin · SuperAdmin
Node.js + Express API
Every role and store check happens here, not on the client
PostgreSQL (Prisma)
Source of truth
Firebase Auth + FCM
Identity & push

DevAdmin and SuperAdmin work entirely from the admin web app. Store Manager, Employee, and Customer work entirely from the mobile app. Both talk to the same backend and the same role checks, so access rules can never drift between the two.


Access Control

Five roles. One hierarchy.

Enforced at the API, on every request, not just hidden in the UI.

RANK 4DevAdmin
RANK 3SuperAdmin
RANK 2Store Manager
RANK 1Employee / Cashier
RANK 0Customer

requireRole(minRole): one generic rule per route. Any role ranked at or above the minimum passes automatically, no per-route allow-lists to keep in sync.

// One rank list, every route checks against it const ROLE_HIERARCHY = [ CUSTOMER, EMPLOYEE, STORE_MANAGER, SUPER_ADMIN, DEV_ADMIN, ]; function hasMinRole(userRole, minRole) { return roleRank(userRole) >= roleRank(minRole); } // applied per route, e.g.: router.post('/billing', authenticate, requireRole(DEV_ADMIN), createBillingRecord);
Why this scales

224+ routes gated this way across the API, every one declares only its minimum role. Adding a new role, or promoting a permission, never means hunting down a scattered allow-list. A store-scoped route adds a second check on top: not just "what role," but "which store."


Permission Matrix

What each role can actually reach

Swipe to see every role →

FeatureDevAdminSuperAdminStore MgrEmployeeCustomer

Multi-Store Scoping

One account, several stores, correctly scoped

A Store Manager or Employee can belong to more than one Lucky Stop location. A join table, UserStoreRole, links a user to every store they can access, and a single allStoresAccess flag lets one designated chain-wide manager bypass the check entirely. Access is looked up live from this table on every store-scoped request, so a change takes effect immediately, not at next login.

Store Manager Lucky Stop #4 Lucky Stop #7 Lucky Truck Stop
Real bug, real fix

A multi-store manager's Scheduling and Chat pages once queried only storeIds[0], their first assigned store, so their other stores silently never loaded. Fixed once it was found, and swept for the same pattern across the rest of the codebase.


Five Roles, In Detail

What each person actually sees


What You Don't See

The security layer

The parts a demo never shows, but a real business depends on.

Sessions re-earn trust every request

A JWT signature alone was never enough. A deactivated or deleted account's token could keep working for up to 7 days, most visibly on iOS where Keychain survives a reinstall. Every authenticated request now re-checks the account is still active directly against the database.

Fraud has to work harder than the fix

No receipt photo, no points. Enforced server-side regardless of what the app's UI allows, and the same photo can never be reused across two transactions. A fraud engine hard-rejects duplicate customer/employee pairs within 2 minutes or purchases over $2,000, soft-flags unusual velocity to a manager, and routes anything over $800 to SuperAdmin.

A misconfigured rate can't bleed the business

Tiered cashback, category bonuses, and promotions can stack, so every grant's effective rate is hard-capped at 10% server-side, even if a rate gets misconfigured upstream.

PINs can't be guessed or quietly reused

A blocklist of common PINs, bcrypt hashing, account lockout after 5 failed attempts, and a 3-entry PIN history that blocks immediate reuse, enforced even when an admin resets a PIN on someone's behalf.

High-risk actions leave a trail

Deactivating a user, resetting a PIN, removing store access, deleting an offer: all written to an audit log with who, what, and when, surfaced to SuperAdmin as a live high-risk action feed.

Store access is checked live

A manager's assigned stores are looked up fresh from the database on every store-scoped request, so adding or removing a store from someone's access takes effect immediately, not after their next login.

Self-identified, fixed same day

An early build of the forgot-PIN flow returned the one-time verification code in the API response, and the admin login screen auto-filled it in dev mode. A focused security pass found and closed all three issues in one commit: the OTP no longer round-trips to the client at all, and recovery codes are delivered by email instead.

Layered Defenses
Login attempts rate-limited: 10 per 15 minutes
Helmet + a strict CORS origin allowlist
Every request body validated with Zod, 78+ endpoints
Request bodies capped at 5MB
PINs hashed with bcrypt at cost factor 12
Constant-time compare defeats login timing attacks
In-store hardware keys expire after 15 minutes
Max 5 concurrent reward redemptions per customer

Not a Mockup

The rates engine and the audit log, live

Real screenshots from the actual admin console, not wireframes.

Live tiered cashback and category bonus rate configuration in the admin console
Tiered cashback + category bonus configuration
SuperAdmin audit log and high-risk action feed in the admin console
SuperAdmin audit log and high-risk action feed

Business Model

How the platform actually makes money

📍

Scale

12 stores, Texas.

💳

Revenue model

Free for customers. Stores pay a monthly subscription plus a small platform fee on cashback issued.

🏆

Cashback

Tiered cashback (5 tiers) plus per-category bonus rates, with a flat cents-per-gallon mode available for gas and diesel.

🎁

Redemption

Points redeem for real product credits in-store, not a third-party gift card.


Cashback Engine

Five tiers, plus category and fuel bonuses on top

🥉
Bronze
1%
From 0 · All 12 locations
🥈
Silver
2%
From 5,000 pts · 7 free fountain refills
🥇
Gold
3%
From 15,000 pts · +5¢/gal on gas
💎
Diamond
4%
From 30,000 pts · +7¢/gal, early access
👑
Platinum
5%
From 45,000 pts · +10¢/gal, top-tier vault

Rates, category bonuses, and a flat cents-per-gallon mode for gas and diesel are all configured live by SuperAdmin/DevAdmin, never hardcoded. The screenshot above is this exact table.


Engineering Highlights

Built for real hardware and real releases

🖨️

Hardware integration

Serial (RS-232C, DTR/DSR handshake) link to Verifone and Topaz in-store receipt printers, so a customer can self-grant points straight from a printed receipt.

⚙️

Full CI/CD, both platforms

Android: signed release builds with R8/ProGuard minification. iOS: automated build, archive, export, and upload to App Store Connect.

📍

Location-aware pricing

GPS-based store detection automatically surfaces the correct gas/diesel prices for the customer's actual physical location, reusing the app's existing age-verification location gate.

🧮

Real fraud economics

Every grant carries a computed dev-cut / store-cost split, so the platform fee model is enforced at the transaction level, not just on a monthly invoice.


Where It Stands

Live, in production, still shipping

All 12 stores on the platform today, running real daily transaction volume

Shelf and price label printing, chain-wide catalog, store-scoped inventory requests

Location-aware fuel pricing and a hardware integration with in-store receipt printers

Customer dispute handling, staff scheduling, and manager-to-manager chat

Android release builds ship with R8/ProGuard minification; iOS ships through an automated build-and-upload pipeline


My Role

What I built

Thank You

Lucky Stop: RBAC and security architecture

Cliff Industries

← Back to Lucky Stop Rewards Get in touch →