Overview
The portal is where every DUBTEL AI user works: platform staff, resellers, their business clients, and read-only users. I built it in Next.js with TanStack Query for server state and Zustand for client state, on top of the multi-tenant FastAPI backend I also built.
One portal, many brands
Resellers sell DUBTEL AI as their own. The portal resolves the tenant from the domain, so the login screen already shows the reseller's logo and name before anyone signs in. After login, the sidebar, favicon, and invoice headers all use the tenant's logo, which is uploaded through the portal and stored in object storage.
Role-aware everything
Four roles see four different products:
- System Admin gets a console for the whole platform: overview, tenant management, users across tenants, billing, pricing, and named settings. A global tenant switcher narrows every list to one reseller, and lists show visible tenant group headers when viewing all tenants.
- Tenant Admins manage their business clients and pricing.
- Account Admins manage their own client's agents, numbers, and users.
- Account Users can view but not change anything.
Navigation and write actions are gated by role in the UI to match the API's permissions. The API still enforces them.
Billing and invoices
Billing lists support view and download on every row. The invoice PDF took three attempts:
html2canvasfailed on the Tailwind v4 styles, because it can't parse modernlab()colours.- Drawing the invoice with jsPDF's API avoided that, but was fiddly to keep in sync with the on-screen design.
- The final version asks the server for the PDF, so the browser just downloads a file and there's no print dialog.
The browser is a poor place to generate documents. The server has the data and a real rendering engine.
Deploys
The portal ships through GitLab CI, with qa deploying to QA and master to production. When the target servers moved to plain Apache web roots, I switched the build to Next.js static export. The one dynamic route (billing/[id]) became a static page that reads the ID from the query string, and static export is only enabled in CI, so local development keeps the full dev server.
Also shipped
- A public Help Center with role-specific guides
- Activity Log pages for System, Tenant Admin, and Account Admin
- A redesigned login page and light-mode default based on feedback
- Consistent date formatting, centralized in one helper after a round of "Jul 31, 2026" vs MM/DD/YYYY fixes
