ERP B2B Premium is divided into two bounded contexts: the public web layer (Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ency07/B2B/llms.txt
Use this file to discover all available pages before exploring further.
src/web/) handles marketing, lead generation, and the wizard funnel — all of which are accessible without authentication. The authenticated ERP layer (src/erp/) contains the dashboard, CRM, inventory, billing, and operations modules, accessible only to verified tenant users. Both contexts share a common platform layer (src/platform/) that owns auth client factories, tenant resolution, and the middleware dispatcher.
Bounded Context Diagram
Every browser request flows throughmiddleware.ts, which calls middlewareDispatcher(). The dispatcher inspects the pathname and hands off the request to the appropriate guard. Unrecognized public paths are passed through without modification.
Platform Layer (src/platform/)
The platform layer is the only place that holds cross-cutting infrastructure. Nothing in src/web/ or src/erp/ re-implements auth or tenant logic — they always import from src/platform/.
| Module | File | Responsibility |
|---|---|---|
| Auth clients | auth/clients.ts | Four Supabase client factories (see below) |
| Middleware dispatcher | middleware/dispatcher.ts | Routes requests to the correct guard |
| Tenant resolver | tenant/tenant-resolver.ts | Resolves tenant code → UUID via static map + dynamic DB cache |
| Server guards | auth/server-guards.ts | requireAction(), getAuthContext(), validateTenantAccess() |
Supabase Client Factories (auth/clients.ts)
Four distinct clients are exported, each with a well-defined scope and credential set:
| Factory | Key used | Session | Purpose |
|---|---|---|---|
getErpBrowserClient() | Anon key | Persisted (sb-erp-local) | ERP dashboard browser interactions |
getPortalBrowserClient() | Anon key | Persisted (sb-portal-local) | Customer portal browser interactions |
getPublicServerClient() | Anon key | None (stateless) | Public Server Actions — wizard, catalog |
getSupabaseAdmin() | Service role key | None (stateless) | RLS bypass — migrations, seeds, admin ops |
getErpBrowserClient() and getPortalBrowserClient() use separate storage keys so ERP staff and portal customers never share a session cookie, even on the same origin. getSupabaseAdmin() is lazily initialized and throws immediately if SUPABASE_SERVICE_ROLE_KEY is absent, preventing silent permission escalation.
Public Web Data Flow
The public site’s primary conversion path is the pre-engineering wizard. When a visitor completes the wizard and submits,submitWizardData() in src/web/actions/wizard.ts orchestrates the following sequence:
getPublicServerClient() (anon key + RLS), never the admin client. The RLS INSERT policies for anon act as the final data boundary for each table.
ERP Layer (src/erp/)
Server Actions in src/erp/actions/ follow a strict two-gate pattern before touching Supabase:
requireAction(permission)— verifies the caller holds the required RBAC permission (e.g.clients.create,invoices.manage). Throws if the session is absent or the role is insufficient.validateTenantAccess(userId, role, tenantId)— confirms the authenticated user belongs to the requested tenant, preventing cross-tenant data access even for valid sessions.
getAuthContext() first to obtain userId and role without throwing, allowing richer error messaging before delegating to the guards.
Fail-Closed Middleware
The dispatcher wraps its entire routing logic in atry/catch. If any unhandled error occurs — a guard throwing, a missing import, or a transient runtime error — the dispatcher redirects the request to /error?code=middleware_failure rather than calling NextResponse.next(). This fail-closed design guarantees that a broken guard can never silently allow an unauthenticated request through to a protected route.