Architecture: Web Dashboard (apps/web)
Status: Active
Last updated: 2026-05-02
Audience: AI agents and engineers building or modifying dashboard
pages, server actions, plugin UIs, or i18n.
1. Purpose
The Ever Works dashboard is a Next.js 16 App Router application backed
by React 19, Tailwind CSS 4, and next-intl. It is the platform's
primary UI surface — every work CRUD, plugin configuration,
generation kick-off, and member invitation flows through it. This spec
covers the route structure, server-component vs client-component
strategy, server actions, API integration, i18n
plumbing, plugin form rendering, and auth integration.
The user-facing docs/web-dashboard/ tree (start with
dashboard-layout)
documents the visible behaviour of each page; this spec documents
the underlying mechanics.
2. Top-Level Structure
apps/web/src/
├── app/ # Next.js App Router root
│ ├── layout.tsx # Root layout (no locale, just global)
│ ├── globals.css
│ ├── global-error.tsx # Top-level error boundary
│ ├── not-found.tsx # Top-level 404
│ ├── favicon.ico
│ ├── [locale]/ # Locale-prefixed routes (next-intl)
│ │ ├── layout.tsx # Locale-aware layout
│ │ ├── (auth)/ # Route group: login / register / forgot-password
│ │ ├── (dashboard)/ # Route group: authed pages
│ │ │ ├── layout.tsx
│ │ │ ├── layout-client.tsx # Client wrapper for the dashboard chrome
│ │ │ ├── (home)/
│ │ │ ├── activity/
│ │ │ ├── works/
│ │ │ ├── plugins/
│ │ │ ├── settings/
│ │ │ ├── error.tsx
│ │ │ └── toasts.tsx
│ │ └── [...rest]/ # Catch-all 404 inside locale
│ ├── actions/ # Server actions (one file per domain)
│ └── api/ # Next.js Route Handlers (BFF for client code)
├── components/ # Cross-feature UI components
├── lib/ # API client, schemas, helpers
├── hooks/
├── i18n/ # next-intl config + locale files
├── messages/ # Per-locale message bundles
└── middleware.ts # next-intl middleware + auth redirect
The two route groups ((auth), (dashboard)) compose into the
locale segment without adding URL segments — /en/login resolves to
(auth)/login/page.tsx. Code organisation reads like the user
journey, URLs stay clean.
3. Locale & i18n Pipeline
next-intl drives every locale-aware piece of the app:
middleware.tsdetects the locale from the URL path (/en/...,/fr/...) or theaccept-languageheader for cold visits, and redirects/login→/<default>/login.[locale]/layout.tsxreceives the locale param, loads the matching message bundle frommessages/<locale>.json, and wraps the tree in<NextIntlClientProvider>.- Pages call
getTranslations(namespace)(server) oruseTranslations(namespace)(client) to read messages. - Server actions access the locale via
getLocale()so success / error messages localise correctly.
Locale set today: en, fr, es, de, zh, ar, ru, pt, it,
nl, pl, bg, he (per apps/docs/docusaurus.config.ts mirroring,
the dashboard subset is in i18n/config.ts).
A scripts/translate-messages.mjs job (run via pnpm --filter ever-works-web translate:messages) feeds new English keys to a
machine translator and emits per-locale messages/<locale>.json
diffs. Human translators clean up before merge.
4. Server vs Client Components
The default is server components — rendering happens on the API worker, RSC payload streams to the browser. The dashboard is mostly data-display + form-submission, both of which fit RSC well.
A page becomes a client component ('use client' at the top)
only when it needs:
- Interactive widgets that React 19 still requires
useState/useEffectfor (drag-drop reordering, charts, autocomplete). - Real-time data via WebSocket / SSE (live generation progress, AI conversation stream).
- localStorage / cookies read at render time (theme toggle, dismissed banner state).
Mixed pages are common: a server component for the data fetch + form shell, with one or two client islands for the interactive bits. Reference: every work detail page composes this way.
5. Server Actions
apps/web/src/app/actions/ holds server actions — async functions
marked 'use server' that the dashboard's forms submit to. Each
action:
- Resolves the current user via
auth()(a thin wrapper over a server-side fetch that exchanges the session cookie for anAuthenticatedUser). - Calls the API via the typed
apiClientinlib/api/. - Translates errors to localised user-facing messages.
- Optionally calls
revalidatePath(...)orrevalidateTag(...)to bust the RSC cache for affected pages. - Returns a
{success, data?, error?}shape forms can render inline.
Actions are thin — they don't hold business logic. The API enforces
permissions; the action just relays. This mirrors the
auth guard model — every business rule lives on the API
side, the dashboard is a presentation layer with auth-aware data
fetching.
6. The API Client
lib/api/client.ts exports a typed client wrapping fetch. Key
properties:
- Type-safe — every endpoint has a generated DTO type from
@ever-works/contracts(zod schemas → TypeScript viazod-to-json-schema). - Cookie-aware — server-side calls forward the session cookie; client-side calls automatically include credentials.
- Error-typed — non-2xx responses throw an
ApiErrorwithstatus,code,message, surfaced cleanly in actions. - Locale-aware — sends
Accept-Languageso the API can localise error messages (the dashboard usually overrides them with its own locale, but consistent fallback matters).
Client-side direct calls (e.g. AI conversation streaming) go through
the same client via 'use client' components that import from
lib/api/client.ts.
7. Plugin Form Rendering
The dashboard renders plugin settings forms dynamically from the plugin's JSON Schema (see Plugin SDK §8 and Settings System). The renderer:
-
Fetches the plugin's
settingsSchemaviaGET /api/plugins/:pluginId/schema. -
Walks the schema, mapping each property to a widget by
x-widgetor JSON Schema type:Schema type / x-widget Widget string/ defaultText input string+x-widget=passwordPassword input string+enumSelect string+x-widget=textareaTextarea numberNumber input booleanToggle objectNested fieldset arrayRepeater -
Applies
x-showIffor conditional rendering. -
Handles
x-secretspecially:- For an existing setting, shows a "set" badge + "rotate" button — never the value.
- For a new value, shows a password input that submits via the
updateendpoint.
-
Validates client-side via Ajv (compiled at runtime from the same schema); the API runs a second validation pass.
The renderer lives in components/plugin-settings/. It's the same
component for admin / user / work plugin pages — the differences
are in which scope endpoint the form posts to.
8. Auth Integration
The dashboard's auth flow:
- Login page → POSTs credentials to the API → receives JWT +
refresh token → server action sets HTTP-only session cookie →
redirects to
(dashboard). (dashboard)/layout.tsxrunsauth()server-side. If no session, redirects to/login.- Every subsequent page call passes the session cookie; the API client unwraps it to a JWT for upstream calls.
- Refresh on 401 — the API client intercepts 401s, calls the refresh endpoint, retries the original request once.
- Logout clears the cookie + invalidates the refresh token server-side.
OAuth login flows (GitHub, Google) are handled by API redirects — the
dashboard sends the user to /api/auth/<provider>/start, the API
handles the round-trip, then redirects back to a dashboard route
with a fresh session cookie.
See auth for the API-side counterpart.
9. Real-Time UI
Two real-time surfaces:
| Surface | Transport | Used for |
|---|---|---|
| Generation progress | WebSocket (/ws/generation) | Live step-by-step status + cost |
| AI chat | NDJSON over fetch stream | Token-by-token AI conversation |
Both are read by client components only. The server-side dashboard boots into RSC; client islands subscribe after hydration.
The WebSocket protocol mirrors the executor's pipeline:state-changed
events from pipeline-executor.
Each event is a JSON message that updates a Zustand store; the UI
re-renders only the affected components via useSyncExternalStore.
10. Forms & Mutation Flow
Most forms follow the server-action submit + revalidate pattern:
'use client';
function CreateWorkForm() {
const [state, formAction] = useActionState(createWorkAction, null);
return (
<form action={formAction}>
<input name="name" />
<button>Create</button>
{state?.error && <p>{state.error}</p>}
</form>
);
}
The useActionState hook gives forms inline error/success state
without managing it manually. After success, the action calls
revalidatePath('/works') and redirect('/works/<slug>').
Complex client-side state (multi-step wizards, drag-drop) uses Zustand stores scoped to the page; we deliberately avoid Redux.
11. Styling
- Tailwind CSS 4 — utility-first via
@tailwindcss/postcss. New in v4:@themeCSS-based configuration (no JS config file needed). - shadcn/ui components — under
components/ui/. We use theradixbase to get accessibility primitives. - CSS variables for theming —
--primary,--background,--foregroundetc. drive light/dark mode. tailwind-mergeto dedupe conflicting class names in dynamic composition.
The design system is documented in
docs/web-dashboard/ui-component-library.md.
12. Testing
| Layer | Tool | Run |
|---|---|---|
| End-to-end | Playwright | cd apps/web && pnpm test:e2e |
| Visual regression | Playwright | pnpm test:e2e -- --grep visual |
There are deliberately no unit tests for components — the
dashboard is mostly composition over the API and next-intl; integration
tests catch real bugs faster than React-Testing-Library mocks would.
The Playwright suite covers:
- Login / register / forgot-password flows (with mocked OAuth providers).
- Work creation in all three methods (AI / Manual / Import).
- Plugin settings save + read-back across all three configuration modes.
- Member invite + role-update + leave flows.
- Generation cancellation.
- Custom domain add + verify (against a mocked Vercel API).
13. Performance Practices
- RSC by default — server-rendering keeps client bundles small.
next/dynamicfor charts / heavy editors (Monaco for prompt overrides) — seedocs/web-dashboard/components.md.use cachedirective for server-side memoisation of expensive RSC fetches (Next.js 16 cache components).- Image optimisation via
next/imagewith width/height always declared. - No barrel imports — components import from explicit paths to keep tree-shaking honest.
14. Constitution Reconciliation
| Principle | How the dashboard respects it |
|---|---|
| I — Plugin-first | Plugin forms render from each plugin's schema — no hardcoded UI per plugin. |
| II — Capability-driven | Provider pickers query "plugins with capability X" via the API. |
| III — Source-of-truth repos | The dashboard never reads / writes user repos directly — always via the API. |
| IV — Trigger.dev | Long-running operations are dispatched via the API; the dashboard polls or streams. |
| V — Forward-only migrations | N/A (no DB). |
| VI — Tests | Playwright e2e covers the user-visible surface. |
| VII — Secret hygiene | Secret values never enter the dashboard's RSC tree; password widgets handle them. |
| VIII — Plugin counts | The plugin grid renders from the live registry, not a hardcoded list. |
| IX — Behaviour-first | This spec describes observable dashboard behaviour. |
| X — Backwards-compat | New plugin schemas render via the same renderer; new server actions are additive. |
15. References
- Source:
apps/web/src/app/apps/web/src/components/apps/web/src/lib/api/apps/web/src/i18n/
- User docs:
- Related specs: