A multi-tenant business management platform unifying petroleum marketing, market operations, and rentals — with multi-branch support, role-based access control, audit logging, and expense tracking, built end-to-end on Next.js 15 Server Actions and Supabase.
KPL is a single platform that runs three distinct businesses for one organization: petroleum marketing (pumps, inventory, purchases, and daily sales), market operations (a product catalog, inventory, and sales), and rentals (renters, rent locations, payments, and notes). Each lives behind a shared organizational backbone.
That backbone is what makes it more than three CRUD apps: multi-branch tenancy, role-based access control, audit logging, and expense tracking span every domain, so a single deployment serves multiple branches with the right people seeing the right data.
Petroleum, market, and rentals each have their own data model and daily workflows, but share one auth layer, one tenancy model, and one design system — instead of three separate tools the organization would have to stitch together.
The data model is branch-scoped from the ground up, so one instance cleanly separates each location’s pumps, inventory, sales, and rentals while rolling up to an organization-wide view.
Row Level Security on every table means access rules live in Postgres itself, not just application code. A bug in a handler can’t leak another branch’s data — the database refuses the row.
Mutations run as Next.js Server Actions — typed, co-located with the UI, and validated with Zod on the way in. No hand-written API surface to keep in sync, and far less client/server contract drift.
Sensitive operations are recorded, so the organization keeps a trail of who changed what — essential when money, inventory, and rent payments move through the same system.
A single global admin, a configurable set of account admins, and branch-scoped roles give a clear chain of authority without hard-coding people into the codebase.
A single Next.js 15 App Router application backed by Supabase, with mutations expressed as Server Actions rather than a separate API tier:
app/ Next.js 15 App Router — routes, layouts, and Server Actions. Server Actions are the write path; RSC reads run on the server. petroleum/ Pumps, inventory, purchases, daily sales tracking. market/ Product catalog, inventory, sales. rentals/ Renters, rent locations, payments, notes. org/ Branches, roles & access control, audit log, expenses. components/ shadcn/ui primitives + TanStack tables and Recharts views. lib/ Supabase clients (anon + service role), Zod schemas, helpers. supabase/ Postgres schema with Row Level Security on every table.
Request / data flow: a component invokes a Server Action → the input is validated with a Zod schema → the action writes through Supabase → Row Level Security enforces branch and role boundaries in Postgres → the audit log captures the change → the affected route re-renders from server state.
| Area | Choice | Notes |
|---|---|---|
| Framework | Next.js 15 · App Router | Server Actions for all mutations; React Server Components for reads. |
| Database | Supabase (Postgres) | Row Level Security enabled on every table; multi-tenant, branch-scoped. |
| Auth | Supabase Auth | Global admin + account-admin allowlist; branch-scoped roles. |
| UI | Tailwind CSS · shadcn/ui | Composable, accessible component primitives. |
| Forms | Zod · React Hook Form | One schema validates the form and the Server Action. |
| Tables | TanStack Table | Headless, sortable, filterable data grids. |
| Charts | Recharts | Sales and inventory dashboards. |
| Runtime | Node 22.14.0 | Pinned via .nvmrc. |
| Deploy | Cloudflare Pages | Built with @cloudflare/next-on-pages. |
Access control is enforced in Postgres with Row Level Security on all tables, not bolted on in the app layer. The application can be wrong and the data still stays isolated — the database is the last line of defense, not the first.
Writes are Server Actions co-located with the components that call them. That removes an entire API surface, keeps types end-to-end, and lets validation, mutation, and revalidation live in one place.
Each Zod schema validates both the React Hook Form on the client and the Server Action on the server, so the form and the mutation can never disagree about what a valid record looks like.
An anon-key client handles user-scoped reads under RLS; the service-role client is server-only for privileged operations and is never exposed to the browser or committed to source.
Admin identity is environment-driven — a single GLOBAL_ADMIN_EMAIL and a comma-separated ACCOUNT_ADMIN_EMAILS allowlist — so authority can change without a code change, and a HASH_SALT keeps sensitive values from being stored in the clear.
HASH_SALT; all configuration is environment-driven.All three domains shipped — petroleum marketing, market operations, and rentals — on top of multi-branch tenancy, role-based access control, audit logging, and expense tracking, with RLS enforced across the schema.
Deeper cross-domain reporting and dashboards — rolling pumps, market, and rent activity into branch-level and organization-wide views on the existing Recharts and TanStack Table foundation.