Apps templates

Full design · Apps

Admin dashboard

Kernel, the operations back office for Redgum Bakehouse, a wholesale bakery in Melbourne supplying cafes, restaurants and grocers with bread and pastry.

Live preview. Click around, it works.

About this template

Kernel, the operations back office for Redgum Bakehouse, a wholesale bakery in Melbourne supplying cafes, restaurants and grocers with bread and pastry. Overview shows today's bakes, the runs due out and anything overdue. Accounts holds every wholesale customer with search, filters by region, standing order and account status, and bulk actions to tag, pause or export a selection. Opening an account shows its standing order, credit terms and a full activity history: every order, note, credit hold and status change, oldest to newest. Orders tracks each wholesale order through Received, Confirmed, In production, Packed, Dispatched and Delivered, reassigned between delivery runs and drivers. Team lists everyone with an account and a role: Owner, Manager or Staff, each with its own real permissions, checked with a Viewing as switch that shows exactly what that role can open, edit or approve. Every change anywhere, who made it and what it was before, is written to an audit log a role can filter but never edit. Settings holds the business profile, delivery runs, the role permission matrix and where to connect a real backend.

What the agents are asked

The first message sent when you use this template. You can edit the plan before anything is built.

Adapt Kernel to my own operations back office. Keep the seven screens (Overview, Accounts, Account detail, Orders, Team, Audit log, Settings), the Owner, Manager and Staff roles with permissions that really change what each can see, open or edit, the Viewing as switch, and the audit log that records every change anywhere with who made it and its before and after value. Replace the business, its wholesale accounts, orders, team members and sample activity with mine; keep the sample rows marked Sample and removable in one step. Keep every role check enforced in the database as well as the screen, not only hidden in the interface. When I want real accounts and a shared database, wire in the tables and functions schema.sql describes as the README explains, with every secret key kept on the server.

The operations back office for Redgum Bakehouse, a fictional wholesale bakery supplying cafes, restaurants and grocers around inner Melbourne. Kernel gives a small team one place to see today's work, manage wholesale accounts, move orders through production and delivery, and know exactly who changed what and when, with roles that really change what each person can do.

"Kernel" and "Redgum Bakehouse" are invented for this template. It is not modelled on any real wholesale bakery or back office product. Every person, business and email address in the sample data is fictional; email addresses use the reserved .example domain.

Built on the NovaBuild template kit (docs/guardrails/TEMPLATE-KIT.md): one CONFIG block, one design token block, and a shared data layer that runs in demo mode with no backend or against a real Supabase project once one is connected, with no code change either way.

Screens

  • Overview (#/): today's orders, runs due out, anything overdue, accounts needing attention, and a rolling feed of recent activity across every account.
  • Accounts (#/accounts): every wholesale account, searchable by business, contact or email, filterable by region and status, with bulk tag, pause, reactivate and CSV export for an owner or manager (CONFIG.features.bulkActions).
  • Account detail (#/accounts/<id>): standing order, credit terms, recent orders and a full activity timeline: every note, credit hold, order placed and status change, oldest last.
  • Orders (#/orders): every order grouped by status from Received to Delivered, searchable and filterable by status and delivery run, with a one tap "move to the next status" and, for an owner or manager, a direct status picker and run and driver assignment.
  • Team (#/team): everyone with access and their role, Owner, Manager or Staff, with a plain description of what each role can do. An owner can invite, suspend, remove and change a role; the business always keeps at least one owner.
  • Audit log (#/audit): owner and manager only. Every insert, update and delete to accounts, orders and the team, who made it, and exactly what changed, filterable by record type.
  • Settings (#/settings): owner and manager only. The business profile and locale (read from CONFIG), delivery runs in use, the full role and permission matrix, the backend connection, and, for an owner, removing the sample data.

Roles, and where each one is enforced

CapabilityOwnerManagerStaff
View accounts and ordersYesYesYes
Create or edit an accountYesYesNo
Create or edit an order (items, price, account)YesYesNo
Move an order to its next statusYesYesYes, one step at a time only
Set an order to any statusYesYesNo
Add a note to an accountYesYesYes
Place a credit holdYesYesNo
View the teamYesYesYes
Invite, suspend or remove a team memberYesNoNo
Change a team member's roleYesNoNo
View the audit logYesYesNo
Remove sample dataYesNoNo

The demo's "Viewing as" switch (top bar) simulates signing in as each role: it decides what the interface shows, and the same rule decides what the interface refuses. On a connected backend the same table is enforced twice more, independently of the interface:

  1. Row level security in schema.sql grants each table only to authenticated, then narrows it by the caller's row in team_members (current_team_role()). A role's column level grants matter as much as its row policies: nobody, at any role, holds a grant to update orders.status or team_members.role directly, so even an owner's own screen can only change either through the functions below.
  2. SECURITY DEFINER functions (change_order_status, change_team_role, record_activity) check the caller's role themselves and refuse exactly what the table above says they should. The demo mode rpc handlers in script.js mirror each function's checks and messages so the two never drift apart (docs/ops/LESSONS.md L35).

Signature: role based access with a real audit trail

The three things this template is built to prove:

  1. The audit trail is never written by a screen. On a connected backend, one trigger (nova_write_audit) fires after every insert, update and delete on team_members, accounts and orders and writes audit_log itself, with the acting person's name and role looked up from their own session, not sent by the client. Nobody, at any role, holds an insert grant on audit_log. Demo mode has no database to run a trigger in, so the small set of table write helpers in script.js (insertAccountRow, updateOrderRow, and so on) do by hand exactly what the trigger would have, and skip it the moment store.mode is "supabase", so nothing is ever logged twice.
  2. "Viewing as" shows exactly what a role can do, not only its menu. Switching role hides and locks the Audit log and Settings entry points for Staff with a settle and a latch (the "handover"), and typing #/audit or #/settings directly while Viewing as Staff is refused by the router itself (render's route guard), the same way the real backend would refuse the request.
  3. A change a customer can point to. Opening the Audit log shows who changed what and its exact before and after values, not just that "something changed"; calling change_order_status twice in a row as Staff (say, from Received to Confirmed, then Confirmed to Confirmed again) is refused the second time with the same message schema.sql's function would give, proving the demo and the real function agree.

Art direction

Two portraits, one shoot: owner-dominic (Team, Settings) and manager-priti (Team), the only two sample people with a photograph, everyone else gets a typeset initials tile. Both are lit with real directional daylight at camera left or right, cool 5400K, an 85mm lens at f/2.8, fine film grain and a muted true to life grade, natural skin texture, no airbrushing, shot in the bakery's own spaces (a stainless steel bench and proving baskets, a loading dock roller door) rather than a studio, so the two faces the interface actually shows read as this business, not stock photography. No text, logos or readable signage in either frame.

Data model (schema.sql)

team_members, accounts, orders, account_activity (the human readable timeline), audit_log (the immutable compliance trail). Every table has row level security; anon has no access to anything, because Kernel is an internal tool, not a public site. See the file's own comments for the full grant and policy reasoning, and the VERIFY block at the end for how to check it after applying.

claim_invite() links a freshly signed in person to the team_members row an owner already created for their email, matched against their own confirmed email (nova_confirmed_email(), read from auth.users, never a value they type in or the unverified JWT claim). start_as_owner(p_name) is the other half (docs/ops/LESSONS.md L60): on a fresh project nobody has invited anyone yet, so the first person to sign in with a confirmed email and call it becomes the owner, once only, under an advisory lock; it refuses once a real owner already exists. In Supabase mode script.js calls claim_invite() after sign in and, when that finds no invite and no owner exists yet, shows "Start as owner"; otherwise it shows "Ask your team owner to invite" the signed in email.

remove_sample_data() is the only way the sample rows go away: nobody, at any role, holds a delete grant on accounts, orders or account_activity (accounts and orders are never deleted otherwise; account_activity is a timeline), so Settings' "Remove sample data" calls this function rather than deleting rows itself. It skips a sample account a real order or note still points to, and skips the business's last active owner, rather than letting one row stop the rest. A trigger, nova_guard_last_owner, backs the same rule everywhere else too: it refuses to delete, suspend or demote the business's only active owner no matter which column or row level policy would otherwise have allowed it, so a stray direct update can never leave Kernel with nobody able to sign in as owner.

Going live

Set CONFIG.backend in script.js to { mode: 'supabase', url: 'https://<project>.supabase.co', anonKey: '<publishable key>' } once schema.sql is applied to a real Supabase project. Everything else, every screen, every role check, every write, works unchanged: the kit's data layer switches from localStorage to PostgREST with no other code change. Never put a service_role key or a sb_secret_... key here; the kit refuses to start with one and falls back to demo mode.

Turn Confirm email on in Supabase Authentication settings before anyone signs up: claim_invite() and start_as_owner() both trust a signed in email only once auth.users.email_confirmed_at is set, and with confirmation off anyone could sign up as an address they do not own and claim it first. The first person to sign up (with a confirmed email) sees "Start as owner" and becomes the owner; everyone after that needs an owner to add their name and email from Team first, then sign up with the same email to claim it.

Sample data

Every sample row is marked is_sample. An owner can remove it in one step from Settings, through remove_sample_data() (above), which keeps anything a real visitor has added or changed themselves; a sampleVersion bump (script.js) replaces stale sample rows in a returning visitor's browser without ever touching their own data (docs/ops/LESSONS.md L21). The Undo the toast offers after removing is a demo mode convenience only: it re-inserts from a snapshot taken in the browser before the call, and on a live backend that insert would need grants (a role, a status) nobody holds on purpose, so Undo only appears while store.mode is "demo". On a connected backend the rows the function deleted are genuinely gone, the same as they would be from a direct SQL delete.