Admin Panel — anatomy of a multi-tab SaaS dashboard
April 21, 20266 min read
The biggest thing I've built so far lives inside Blindando Sueños, an insurance SaaS. The admin panel is what the internal team touches every day, and it's where most of the interesting engineering decisions happened — five tabs, each one a different department's daily tool, all behind a single server-side role gate.
The architecture that holds it together is almost aggressively boring, and that's the point: the Next.js app is stateless. It owns no database, runs no business logic, keeps no session in memory. Every action — publish this post, pause this conversation, merge these two contacts, send this email — is a thin route handler that does exactly two things. It checks auth.role on the server, and if you're allowed, it forwards a { action, ...params } payload to an n8n webhook. The literal comment at the top of the service module is "all database and email operations go through n8n — Next.js is stateless." The browser never sees a credential, never talks to Mongo or Postgres directly, never decides anything it could decide for you. The gate is server-side because a hidden tab in the UI is a suggestion, not a fence.
What you see below is the real UI code, stripped of that backend. Click anything — tabs, uploads, dropdowns, toggles. Nothing explodes, no data changes, nothing posts to n8n. The embedded version runs on static fixtures: it's the shell, interactive, with the wiring cut.
The five tabs
Posts
The most-used surface, and the one with the deepest action surface — a single post can be generated, regenerated, scheduled, approved, published, paused, resumed, or deleted, each one its own forwarded action. Marketing writes a brief and the panel runs it through a factory → train → reconciler pipeline. The factory expands the brief into the full artifact bundle — copy in three tones, carousel slides as generated images, scheduling metadata. The train is the publishing queue: a reviewer approves, picks a date and time, and at the scheduled moment the train ships to Instagram and Facebook. The reconciler catches drift — if Facebook soft-blocks a post, if Instagram rejects an image size, it marks the record and re-queues. Each post carries a per-platform status map, so the chip you click isn't one global state — it's instagram: published, facebook: pending, the two destinations tracked independently. The timeouts in this tab are tuned to physical reality: four minutes for a publish, because that has to cover the reconciler's cron cycle plus the publisher plus a safety margin. The UI waits as long as the real pipeline takes.
Chatbot
The chatbot lives on the website and in Instagram DMs. This tab lists every session with its transcript, the metadata collected along the way (phone, email, insurance interest, company), and a per-message timeline tagged USR or BOT. The one button that matters is pause: an admin can freeze the bot on a single conversation to answer personally, and the pause expires on its own after 24 hours so nobody has to remember to switch it back on. That detail — automatic expiry — is the difference between a feature people trust and a feature that silently strands a customer because someone forgot a toggle.
Merge
A single prospect arrives from three channels: WhatsApp, Instagram DMs, the website form. They leave slightly different info each time — one channel has the phone, another the email, the third the company name. When a shared signal (most often a matching email) links two records from different channels, a merge request surfaces here. It shows the conflicting fields side by side, you pick the winning value per field, and the panel folds them into a single contact with the chosen fields combined; if it's a false positive, you dismiss it and it's gone. Before this tab existed, the team was quoting the same person three times under three different names.
Citas
A calendar where the team marks days, or specific hours within a day, as unavailable. The grid distinguishes a fully blocked day from a partial one, and separates what the server already knows from edits you've staged but not yet committed — block, unblock, and pending changes are visually distinct before you save. The chatbot reads this table before offering slots to prospects, so if a rep is out sick or the office closes for a holiday, you block the day here and the bot stops offering it. No chasing manual schedule edits across chat scripts.
The email auto-responder. When a prospect writes to the main inbox, a RAG pipeline reads the email, pulls context from past conversations plus the product knowledge base, and drafts a reply in the company's voice. The admin sees the suggested draft pre-filled in an editable box and decides: send as-is, edit then send, or dismiss. The detail view even exposes the RAG chunks the model retrieved, so when a draft is wrong you can see why it was wrong instead of guessing. Quality is high enough that most replies go out unedited, which has saved the sales team a lot of repetitive writing.
Stack, briefly
- Next.js App Router — server components for the shell and the auth gate, client components for every interactive tab (they all hold their own state).
- Tailwind with a custom editorial palette (the cream/charcoal in the demo — very different from this portfolio's dark theme, which is exactly why the demo lives in its own
<iframe>escape hatch and styles itself with inline objects, immune to the surrounding tokens). - n8n is the real backend. Every route handler forwards
{ action, ...params }to a service webhook; the workflows behind them own all the database and email work. The factory, train, and reconciler are each n8n workflows. - MongoDB for contacts and chatbot sessions; Postgres for structured post and scheduling data — both reached through n8n, never from the browser.
- Custom Node.js API for the RAG email responder (pgvector + OpenAI embeddings).
The stateless split is what makes this comfortable to operate. The frontend has no secrets to leak and nothing to migrate; redeploying it is free of consequence. Anything stateful lives behind a webhook, where it can be versioned, audited, and timed out on its own terms.
Originally all of this ran on the VPS I talk about in the ICARUS post-mortem. After that melted, the frontend moved to Vercel and the backend services went to a fresh VPS — this time configured properly from day zero.
The demo above is the real UI, not a Figma mock. If you want to feel what it's like end-to-end, open it full screen and poke everything.