Frontend
The dashboard is a Next.js App Router application. Server components render the shell; client components own interactivity and all server state.
Rendering model
| Layer | Role |
|---|---|
Server components |
Layouts, page shells, static structure. The default — no |
Client components |
Anything with state, effects or event handlers. Marked |
React Query |
All server state. Client components fetch through hooks in |
Pages never query Prisma directly. Everything goes through the application’s own HTTP API, so the dashboard and any external consumer see the same contract — and a schema change breaks loudly in one place rather than silently in two.
WorkspaceGate
The dashboard layout wraps its children in WorkspaceGate: nothing renders until GET /api/me has
resolved and the user has at least one workspace with an active selection. A loading screen is shown until
then.
Without the gate, components would render against an unresolved workspace and issue queries that either fail or return another tenant’s default. The gate makes "the workspace is known" an invariant for every component below it.
The sidebar workspace switcher posts to POST /api/subscriptions/active and then invalidates the React
Query cache, so a switch re-fetches everything rather than mixing two workspaces' data.
Component organisation
| Directory | Contents |
|---|---|
|
shadcn/ui primitives — generated, never edited directly |
|
Activity feed |
|
Agent editor, version history, analysis views |
|
Login and signup forms |
|
Call tables and filters |
|
Campaign detail tabs, targets, calls, the batch re-analyse dialog |
|
Contact list and detail |
|
Email campaign detail, recipients, variations |
|
Domains and sending accounts |
|
Inbox |
|
Task board |
|
Charts |
|
Rep management |
|
Seller list |
|
Payability supplier list and status badges |
|
Sequence builder and enrollment views |
|
Target registries |
|
Test case management |
|
MDX renderers for the in-app docs site |
components/ui/ is off-limits because it is regenerated by the shadcn CLI. Extend by composition — wrap
the primitive in a feature component rather than editing it.
Standard client component
"use client"
interface Props { campaignId: string }
export function CampaignTargetsTable({ campaignId }: Props) {
// 1. Local state
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(25)
const [filters, setFilters] = useState<FilterState>({})
// 2. Server state
const { data, isLoading } = useCampaignTargets(campaignId, { page, pageSize, ...filters })
// 3. Handlers
// 4. JSX
}
Order is consistent across the codebase: local state, server state, handlers, JSX.
Naming
| Kind | Convention |
|---|---|
Files |
|
Components |
|
Hooks |
|
Types |
|
Import order
-
React / Next.js
-
Third-party libraries
-
Internal components (
@/components/…) -
Internal utilities (
@/lib/…) -
Types
-
Relative imports
Forms
React Hook Form with zodResolver, always. A dialog form resets and closes on mutation success; errors
surface through form state or a sonner toast.
The Zod schema that validates the form is the same one that types the API payload, so a client-side validation rule cannot drift from the server’s.
Key libraries
| Library | Use |
|---|---|
|
All server state |
|
Forms |
|
Report charts |
|
Rich-text email editor |
|
In-app diagrams |
|
Spreadsheet import and export |
|
Toasts |
|
Date formatting |
|
Icons |
|
Light/dark switching |
|
Prompt version diffs |
|
JSON payload editing |
|
Chart export |
|
Layout and interaction primitives |