UI Layer
Three Applications
The monorepo contains three separate Next.js applications, each serving a different audience:
| App | Audience | Path | Port | Theming | Purpose |
|---|---|---|---|---|---|
| Dashboard | End clients / tenants | apps/dashboard | :3000 | [Live] | Tenant-facing: campaigns, deliverables, reports, costs, approvals |
| DM Portal | Digital marketers & HITL reviewers | apps/dm | :3002 | [Live] | Internal team: review content, manage activities, oversee agents, approve/reject |
| Manage | Super admins (platform team) | apps/manage | :3001 | [Live] | Platform administration: tenant management, master data, global settings, billing |
All three are Next.js App Router apps sharing the same packages/ ecosystem. They are deployed as separate services in Coolify.
Technology Stack
| Concern | Technology | Notes |
|---|---|---|
| Framework | Next.js 15 (App Router) | Server Components, API Routes, Streaming RSC |
| Language | TypeScript 5.x strict | Shared types from packages/shared |
| Styling | Tailwind CSS v4 | Design tokens, utility-first |
| Component library | shadcn/ui | Radix UI primitives, copy-owned components |
| Server state | TanStack Query v5 | Polling, SSE, cache invalidation |
| Forms | React Hook Form + Zod | Schema-validated, type-safe |
| Tables | TanStack Table v8 | Sortable, filterable, paginated |
| Charts | Recharts | Cost dashboard, analytics, activity graphs |
| Markdown render | react-markdown + rehype-highlight | Deliverable content display |
| Rich text editing | TipTap | Inline editing of deliverables in approvals |
| Icons | Lucide React + react-icons | Lucide for all UI icons; react-icons/fa6, fa, si for brand/platform icons |
| Real-time | Server-Sent Events | Live agent output, activity status updates |
| Auth | Fastify JWT (HS256) via @leadmetrics/middleware | JWT sessions, tenant resolution, RBAC |
| SendGrid | Notifications, approval alerts, report delivery | |
| Monorepo build | Turborepo | Parallel builds, shared cache |
App Routing & Tenant Resolution
In the SaaS deployment, tenant is resolved from subdomain before any page renders:
// middleware.ts (shared across all three apps)
export async function middleware(request: NextRequest) {
const host = request.headers.get('host') ?? '';
const subdomain = host.split('.')[0];
// Map subdomain → tenantId
const tenant = await getTenantBySlug(subdomain);
if (!tenant) return NextResponse.redirect('/404');
// Inject tenant context into request headers (picked up by Server Components)
const response = NextResponse.next();
response.headers.set('x-tenant-id', tenant.id);
response.headers.set('x-tenant-slug', tenant.slug);
return response;
}In enterprise on-prem (single-tenant), the middleware reads from SINGLE_TENANT_ID env var instead of the subdomain.
Dashboard App (apps/dashboard)
Audience: End client users — the marketing team members of each tenant.
URL structure:
/dashboard Home — activity feed, stat overview [Live]
/strategy Marketing strategy document [Live]
/goals Goals list [Live]
/leads Leads list [Live]
/deliverables Deliverables list [Live]
/activities Activities list [Live]
/calendar Activity calendar [Live]
/blog Blog posts list [Live]
/blog/[id] Blog post detail / review [Live]
/social Social posts list [Live]
/social/[id] Social post detail [Live]
/channels OAuth-connected publishing channels [Live]
/reports Reports list [Live]
/client-info Business info [Live]
/audit-log Activity log (audit trail) [Live]
/settings/agents Agent configuration for this tenant [Live]
/settings/agents/[id] Agent detail + run stats [Live]
/settings/users Tenant users [Live]
/settings/profile User profile [Live]
/settings/notifications Notification preferences [Live]
/settings/context Context approval [Live]
/chat Agent Chat [Live]See screens-dashboard.md for full screen specifications.
Key characteristics:
- Primarily read-heavy with targeted write actions (submit campaign, approve/reject)
- Server Components for campaign lists and deliverable renders — no data fetching on client
- SSE connections for live activity updates and agent output streaming
- Tenant-scoped: users only see their own tenant’s data
DM Portal App (apps/dm)
Audience: Internal digital marketers and human-in-the-loop reviewers.
Port: :3002
Auth: Cookie-based (dm_access_token / dm_refresh_token). Calls /auth/v1/login with app: "dm". JWT verified server-side in the authenticated layout. Roles allowed: reviewer, admin, super_admin.
URL structure:
/overview Mission Control — pending approvals, running agents [Live]
/approvals Cross-tenant approvals queue [Live]
/activities All activities across tenants (filterable) [Live]
/calendar Activity calendar [Live]
/blog Blog posts list (cross-tenant) [Live]
/blog/[id] Blog post detail / review [Live]
/social Social posts list (cross-tenant) [Live]
/social/[id] Social post detail [Live]
/strategy Strategy viewer [Live]
/goals Goals across tenants [Live]
/deliverables Deliverables list [Live]
/activity-log Audit / activity log [Live]
/agents Agent management (DM-scoped) [Live]
/agents/[id] Agent detail + run stats [Live]
/users DM user list [Live]
/reports Performance reports [Live]
/requests/new Ad-hoc request form [Live]
/profile DM user profile [Live]
/ Redirects → /overviewKey characteristics:
- Cross-tenant visibility — a DM reviewer sees pending approvals across all their assigned tenants
- Activity-focused: creating tasks, monitoring execution, completing and reporting
- Highest-density approval workflow — optimised for reviewing many items quickly
- Real-time: SSE streams for live agent output and new approval notifications
Manage App (apps/manage)
Audience: Platform super-admins (the agency ops team / platform engineering).
Port: :3001
Auth: Cookie-based (manage_access_token / manage_refresh_token). Calls /auth/v1/login with app: "manage". JWT verified server-side in the authenticated layout. Role required: super_admin only.
URL structure:
/overview Platform overview — all tenants, system health [Live]
/tenants Tenant list [Live]
/tenants/[id] Tenant detail: billing, config, users, agents [Live]
/invoices Global invoices list [Live]
/plans Subscription plans [Live]
/users Global users list [Live]
/agents Global agent config [Live]
/agents/[id] Agent detail + run stats + skills [Live]
/skills Global skills library [Live]
/templates/email Email templates [Live]
/templates/telegram Telegram templates [Live]
/audit-logs Audit log (cross-tenant) [Live]
/system/rag-config RAG & AI Config [Live]See docs/apps/manage/screens.md for full screen specifications.
Key characteristics:
- Super-admin only — separate auth check (role:
super_admin) - Read-heavy global view with targeted write actions
- No SSE streams needed — polling is sufficient at this level
- Manages the master data that all tenants inherit
Shared Component Patterns
Agent icon vs. User icon
Throughout all three apps, principals are displayed with a distinguishing icon:
// packages/shared/src/components/PrincipalAvatar.tsx
export function PrincipalAvatar({ principal }: { principal: Principal }) {
if (principal.type === 'agent') {
return (
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-indigo-100">
<Bot className="w-4 h-4 text-indigo-600" /> {/* Lucide Bot icon */}
</div>
);
}
return (
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-slate-100">
<User className="w-4 h-4 text-slate-600" /> {/* Lucide User icon */}
</div>
);
}Activity/task status badge
Consistent across all three apps. “Activity” is the user-facing term; “task” is the internal code term.
const STATUS_CONFIG = {
pending: { label: 'Pending', color: 'slate', pulse: false },
running: { label: 'Running', color: 'blue', pulse: true },
completed: { label: 'Completed', color: 'emerald', pulse: false },
failed: { label: 'Failed', color: 'red', pulse: false },
retrying: { label: 'Retrying', color: 'amber', pulse: true },
awaiting_approval: { label: 'Awaiting Review', color: 'purple', pulse: false },
needs_review: { label: 'Needs Review', color: 'orange', pulse: false },
paused: { label: 'Paused', color: 'slate', pulse: false },
};Real-time Architecture
Real-time updates are delivered via Socket.IO (connected to the Fastify API service). All three topbars connect via connectSocket() and listen for events.
Notification events (notification:new): New notifications pushed to the client in real time. The topbar bell badge increments without a page refresh.
// All three topbars subscribe to this on mount
void connectSocket().then((socket) => {
socket.on("notification:new", (notif) => {
setNotifications((prev) => [{ ...notif, read: false }, ...prev]);
});
});Authentication (JWT)
HS256-signed JWTs issued by the Fastify API (/auth/v1/login). Each portal has its own httpOnly cookie pair and validates JWTs locally via createJwtAuthMiddleware from @leadmetrics/middleware. Server components and actions use requireSession() / requireAuth() / requireSuperAdmin() from @/lib/server-auth.
App-level access gates:
apps/dashboard— tenant users; cookie:dashboard_access_tokenapps/dm— roles:reviewer,admin,super_admin; cookie:dm_access_tokenapps/manage— role:super_adminonly; cookie:manage_access_token
Design System
See design-system.md for the full color palette, typography, spacing, component patterns, and CSS variable definitions.