Source: ddx-documentation/features.md
Functional view of the workspace: pages and capabilities per audience, each row citing the route file it lives in. Technical view: [shell-tree.md](/en/docs/shell-tree), [patterns.md](/en/docs/patterns), `api/openapi.json`. Every route below is a file today; a page that is not listed here has no route. Path prefix for the **File** column: `ddx-web/src/app/[locale]/`. `<P>` = `(app)/portal/[orgSlug]/[role]`. Every page is locale-prefixed (`en`, `de`, `fr`, `es`; strings in `ddx-web/messages/<locale>/`). ## 1. Roles The portal URL is `/{locale}/portal/{slug}/{role}` (ADR 0003). `slug` is the org slug. `role` is a portal role, not an org role: | Portal role | Who lands here | Source | |---|---|---| | `staff` | org members with role `staff` | `packages/ddx-contracts/src/read-models.ts` (`PORTAL_ROLES`), re-exported by `ddx-web/src/lib/auth/roles.ts` | | `member` | org members with role `member` | same | | `client` | org members with role `client` | same | | `partner` | reserved role slug, own nav (home + settings) | same | | `public` | the default: every member whose org role has no portal role — `org-admin` lands here | `normalizePortalRoles()` in `roles.ts`; comment in `portal-registry.ts` | A role that the session does not hold renders `forbidden()`. The role is gated once, in `PortalPage` (`allowedRoles: rolesFor('<nav item>')`); ddx-api still decides data access per request (org roles `super-admin` · `org-admin` · `staff` · `member` · `client`; `super-admin` is a platform role that acts as org-admin of the target org, ADR 0013). ## 2. Public site (no sign-in) | Capability | Route | File | |---|---|---| | Landing page in SiteShell | `/{locale}` | `(marketing)/page.tsx` | | Component gallery: browse every inventory entry, design-system page | `/{locale}/gallery`, `/gallery/[entryId]`, `/gallery/design-system` | `(app)/gallery/page.tsx`, `(app)/gallery/[entryId]/page.tsx`, `(app)/gallery/design-system/page.tsx` | | R&D docs rendered from `ddx-documentation/` | `/{locale}/docs/<path>` | `(app)/docs/[[...slug]]/page.tsx` | | Locale switch, theme (light/dark), density (compact · cosy · large) | header of every page | `ddx-web/src/components/site/SiteHeader.tsx` | | Platform branding: the header, tab title, auth panel and loader show the platform display name (per locale), logo, palette, font and illustration; anonymous read of `GET /platform/branding`. The `/auth` tab title uses the platform name too | every site and auth page | `ddx-web/src/components/site/SiteHeader.tsx`, `SiteAuthFrame.tsx`, `(auth)/auth/layout.tsx`, `ddx-web/src/components/providers/BrandingProvider.tsx` | | Unknown URL | any | `[...rest]/page.tsx`, `ddx-web/src/app/global-not-found.tsx` | ## 3. Sign-in and org selection | Capability | Route | File | |---|---|---| | Sign in (email + password, better-auth via the BFF) | `/{locale}/auth/login` | `(auth)/auth/login/page.tsx` | | Sign out | `/{locale}/auth/logout` | `(auth)/auth/logout/page.tsx` | | Pick an org when the session has none active | `/{locale}/auth/select-org` | `(auth)/auth/select-org/page.tsx` | | Pick an org when the user has several; a user with exactly one org starts in it (the session is created with that org active) | `/{locale}/auth/select-org` | `(auth)/auth/select-org/page.tsx`; `ddx-api/src/auth/auth.instance.ts` | | Create organization: a user with no org sees the create form (no dead end); a user with orgs can create another from a dialog. The creator becomes `org-admin` and lands on the branding wizard (`?onboarding=1`) | `/{locale}/auth/select-org` | `(auth)/auth/select-org/create-org-client.tsx`; `POST /orgs` | | Entry after sign-in: resolves org + member role, redirects to the role home | `/{locale}/portal` | `(app)/portal/page.tsx` | | Register, OTP, magic link, forgot / reset password, verify e-mail: pages exist; their BFF endpoints answer `501 unsupported` until a real backend exists | `/{locale}/auth/<name>` | `(auth)/auth/{register,otp,magic-link,forgot-password,reset-password,verify-email}/page.tsx`; `ddx-web/src/app/api/auth/*/route.ts` | | Unknown `/auth/*` URL | `/{locale}/auth/...` | `(auth)/auth/[...rest]/page.tsx` | ## 4. Portal — pages per role Nav items come from `ddx-web/src/lib/navigation/portal-registry.ts` (13 items). Columns show which portal role sees each item. All routes live under `<P>/`. | Capability | Route segment | staff | member | client | partner / public | File | |---|---|---|---|---|---|---| | Role home (dashboard) | `` | yes | yes | yes | yes | `<P>/page.tsx` | | Projects: list, search, filter, page; view modes table · cards · list (rows) · timeline · kanban (drag moves status along the lifecycle) · calendar (by period) · export via `?view=`; action bar with create, search, facets, group-by and density; bulk bar on selection (export selected, delete); sticky header and pager. A client sees only projects granted to them | `projects` | yes | yes (read) | grants only | yes (org-admin) | `<P>/projects/page.tsx` | | Project detail with tabs: Overview, Tasks, Attachments, Audit (chain verified pill), Access (grants to users and roles). Audit shows to org-admin and the project owner; Access to holders of `share` (org-admin, owner) | `projects/[id]` | yes | yes (read) | grant only | yes (org-admin) | `<P>/projects/[id]/page.tsx` | | Project print | `projects/[id]/print` | yes | yes (read) | grant only | yes | `<P>/projects/[id]/print/page.tsx` | | Own profile: view, edit (display name, pseudonym, motto), avatar and signature upload, print | `profile`, `profile/print` | yes | yes | yes | yes | `<P>/profile/page.tsx`, `<P>/profile/print/page.tsx` | | Organization: view, edit, logo upload, print. org-admin sees the `admin` projection (tax id, brand preset); other roles see `detail`; status is editable by super-admin only | `organization`, `organization/print` | yes (read) | yes (read) | yes (read) | yes (edit for org-admin) | `<P>/organization/page.tsx`, `<P>/organization/print/page.tsx` | | Members: roster (name, image, title, role; e-mail for org-admin only), member print | `members`, `members/[memberId]/print` | yes | yes | yes | yes | `<P>/members/page.tsx`, `<P>/members/[memberId]/print/page.tsx` | | Task detail (tasks live under a project) | `projects/[id]/tasks/[taskId]` | yes | yes (read) | — | — | `<P>/projects/[id]/tasks/[taskId]/page.tsx` | | Create project, wizard, edit | `projects/new`, `projects/new/wizard`, `projects/[id]/edit` | yes (write) | — | — | — | `<P>/projects/new/page.tsx`, `<P>/projects/new/wizard/page.tsx`, `<P>/projects/[id]/edit/page.tsx` | | Settings (section 5) | `settings` | yes | yes | yes | yes | `<P>/settings/page.tsx` | | Branding wizard: 5 steps (name per locale, palette, font, illustration, logo) plus a review, live preview of the draft, per-step skip, finish saves the changed keys at org tier. Opens after create-org; no nav row | `settings/branding` | — | — | — | yes (org-admin only; staff gets access denied) | `<P>/settings/branding/page.tsx` | | Workspace with resizable panels (P-workspace) | `workspace` | yes | yes | — | — | `<P>/workspace/page.tsx` | | Data table demo (fill mode, filters) | `data` | yes | yes | — | — | `<P>/data/page.tsx` | | Forms demo (creates tasks) | `forms` | yes | — | — | — | `<P>/forms/page.tsx` | | Realtime events view (SSE) | `realtime` | yes | — | — | — | `<P>/realtime/page.tsx` | | Streaming AI text | `stream` | yes | — | — | — | `<P>/stream/page.tsx` | | Chat (threads, messages) | `chat` | yes | — | yes | — | `<P>/chat/page.tsx` | | Voice assistant | `voice` | yes | — | — | — | `<P>/voice/page.tsx` | | Media: library, detail, upload | `media`, `media/[id]`, `media/upload` | yes | — | yes | — | `<P>/media/page.tsx`, `<P>/media/[id]/page.tsx`, `<P>/media/upload/page.tsx` | | Call room | `room` (nav item `call`) | yes | yes | yes | — | `<P>/room/page.tsx` | | Schedule (calendar) | `schedule` | yes | yes | — | — | `<P>/schedule/page.tsx` | | Audio recorder | `recorder` (no nav item) | yes | — | — | — | `<P>/recorder/page.tsx` | | Document viewer, print layout | `documents/[id]`, `documents/[id]/print` (no nav item) | yes | — | — | — | `<P>/documents/[id]/page.tsx`, `<P>/documents/[id]/print/page.tsx` | Reading the table: `—` = the nav hides the item and the page renders `forbidden()` for that role. Staff sees every pattern demo; the access sets are `PORTAL_NAV` in `portal-registry.ts` and `PROJECT_WRITE_ROLES` in `<P>/projects/_islands/model.ts`. Portal chrome shared by every page above (the header and side nav show the tenant name and logo, see section 5b): header (org switcher with role badge, location crumb, ⌘K command palette, notification bell with unread badge, language menu, appearance menu for theme and density, help link, user menu), side nav (`expanded` · `collapsed` · `auto`), status line, footer status bar, mobile menu — [shell-tree.md](/en/docs/shell-tree). ## 5. Settings (unified, ADR 0014) One page, `<P>/settings/page.tsx`, one read: `GET /orgs/:orgSlug/settings/effective` through the BFF. The page renders what ddx-api returns; it never merges tiers. - **Tiers**: platform (stored in `PlatformSetting`, edited only by platform `super-admin`) → org (stored, edited by `org-admin`) → user (stored, edited by the user for their own value). Registry: `packages/ddx-contracts/src/settings.ts`. Guide: [settings.md](/en/docs/settings). - **Keys today** (`<scope>.<group>.<key>`): `portal.locale.default` (org + user) · `portal.locale.enabled` (org) · `portal.features.ai` (org; each org chooses; staff roles read) · `portal.notifications.digest` (org + user) · `portal.display.timeZone` (org + user) · `portal.display.themeMode` light · dark · system · `portal.display.density` compact · cosy · large · `portal.display.motion` full · reduced · `portal.display.textSize` sm · md · lg (type only, never row height; all org + user) · `module.demo-entities.viewMode` table · cards (per-module preference, org + user) · the five `portal.branding.*` keys (section 5b; org only, no user tier). - **Platform settings page**: `/{locale}/platform/settings` lists the platform tier and edits one key at a time (`GET /platform/settings`, `PATCH /platform/settings/:key`, `version` required, 409 on a stale version). Only platform `super-admin` may call it. A write fans out `settings.changed` (key + version, never the value) to every org. - **Appearance**: theme, density and motion read through `UnifiedSettingsProvider` and `useUnifiedSetting(key)`; the header menus write the user tier. - **What a member sees**: only the keys their org role may read, each with value, source tier and an `editable` flag. `client` sees 4 keys (no `portal.features.ai`). - **Open item**: `public`-persona access for org-admin is `NEEDS_REVIEW` (ADR 0014). ## 5b. Branding (tenant branding, ADR 0019) Five keys, `portal.branding.{displayName,palette,font,illustration,logo}`: platform default, org override, no user tier. Key table: [settings.md](/en/docs/settings) §Branding keys. | Audience | Capability | Where | |---|---|---| | Platform `super-admin` | Sets the platform branding default: display name per locale (en, fr, de, es), palette (`dudoxx`, `neutral`, `solace-grid`, `verdant-loop`), font pair, illustration, logo default. One key at a time, `version` required | `/{locale}/platform/settings`; `ddx-web/src/components/platform/templates/PlatformPageSettings.tsx` | | `org-admin` | Sets the tenant branding in the org settings page (branding section with live preview), or in the onboarding wizard right after create-org. Logo: platform, generated monogram or wordmark, or an uploaded image (PNG, JPEG, WebP) | `<P>/settings/_islands/BrandingSettingsSection.tsx`, `<P>/settings/branding/_islands/BrandingWizard.tsx` | | Every portal member | Sees the org name, logo, palette, font and illustration in the portal. An org that set no name or logo shows its own org name and uploaded logo, else a mark generated from the org name; never the platform name or logo. Other orgs and `/auth/select-org` keep the platform branding | `ddx-web/src/components/providers/BrandingProvider.tsx` (`PortalBrandingScope`) | | Anyone (no sign-in) | The site and `/auth` show the platform branding, read anonymously | `GET /platform/branding`; `ddx-web/src/lib/branding/platform-branding.server.ts` | Known gaps (ADR 0019): the home hero title (`home.title`) and `site.brand.home` label are fixed text; the first hard load after a palette change can paint the previous palette once; orphaned logo objects are not cleaned up. ## 5a. Debug overlay (dev aid) A Bug button in the header and `Alt+Shift+D` open a debug panel. It exists only when `NEXT_PUBLIC_UI_DEBUG` is on in `ddx-web/.env.local`. Detail: [debug-overlay.md](/en/docs/debug-overlay). ## 6. Cross-cutting capabilities | Capability | Behaviour | Where | |---|---|---| | Realtime | One stream per browser (leader tab relays to the other tabs) through `/api/v1/events`, replayed from a Redis log after a drop; events invalidate TanStack Query keys, so open tabs update live. Presence lists the org members who are online. `job.*` events reach staff roles only. Org admins manage outbound webhooks (signed, retried, dead-lettered); external systems post signed inbound webhooks. Platform super-admin reads `GET /events/stats`; the dev inspector is `/gallery/sse-inspector`. Status: after an API restart the browser makes one extra `/events` reconnect (3 tries in 30 s, rule ≤ 2) — open defect, owner-deferred, replay intact ([019](../plans/sse-bus/progress/019.md)) | `ddx-web/src/app/api/v1/events/route.ts`, `ddx-web/src/components/providers/SseProvider.tsx`, `ddx-web/src/lib/sse/`, `ddx-api/src/sse/`; catalog [sse-events.md](/en/docs/sse-events), [ADR 0016](/en/docs/adr/0016-sse-bus) | | Media | Upload, list, detail, streamed content with Range → 206; browser never sees MinIO or a presigned URL (ADR 0011) | `ddx-api/src/media/media.controller.ts`; BFF `ddx-web/src/app/api/v1/[[...path]]/route.ts` | | Chat and notifications | Threads with messages, per-user notifications, mark read | `ddx-api/src/communication/threads.controller.ts`, `notifications.controller.ts` | | AI demo stream | Server-streamed text for the stream page | `ddx-api/src/ai/ai.controller.ts` | | Projects and tasks | CRUD with org isolation, served by the entity kernel ([entity-kernel.md](/en/docs/entity-kernel)); `staff` holds read, list, create, update, delete, attach; `org-admin` holds every op; `member` reads; a `client` reads only through a grant. Task assignee and project change by org-admin and staff only, and the assignee must be an active member | `packages/ddx-contracts/src/descriptors/{project,task}.ts`, `ddx-api/src/kernel/` | | Access grants (ACL) | A project or task owner or org-admin grants `read`, `update` or `attach` to a user or a role; a task inherits its project's grants. One call replaces the whole list; a user must be an active member and a role one of the org roles. An unreadable record answers 404 | `GET\|PUT /orgs/{orgSlug}/projects/{id}/acl`; `ddx-web/src/components/entity/EntityAccessPanel.tsx` | | Audit chain | Every kernel mutation writes a hash-chained audit row (grants shown before and after on an ACL change). Anyone holding `audit` can verify the chain | `GET /orgs/{orgSlug}/projects/{id}/audit`, `…/audit/verify`; `EntityAuditPanel.tsx` | | Attachments | Files on a record: cover image on a project, documents on a task, avatar, signature, logo. Upload and download go through ddx-api and the BFF | `…/{id}/attachments`; `EntityAttachmentsPanel.tsx` | | Members | Org member roster with projections: `summary` for everyone (no e-mail), `admin` for org-admin; title edit by org-admin | `ddx-api/src/members/members.controller.ts`; `ddx-api/src/identity/` | | API keys | Create, list, delete a user key | `ddx-api` `/api-keys` (`ddx-api/src/auth/api-keys/`) | | Audit (platform) | Settings writes and non-member platform writes are audited | `ddx-api/src/audit/audit.service.ts` | | Multi-tenancy | Every record belongs to one org; the org comes from the signed token claim, never a client header | `ddx-api/src/tenancy/` | | i18n | `en`, `de`, `fr`, `es` in lockstep | `ddx-web/messages/` | | List stack | Every list page (projects, members, project tasks, data, media, workspace) shares one stack: action bar (create, view switch, density, search `?q=`, facets, group-by `?group=`), bulk bar on selection (count, clear, delete with confirm, CSV export of the selection), views table · cards · compact list · timeline · board · calendar · export, sticky header and pager with shimmer while loading. Facets in the toolbar are single-select; ddx-api already serves multi-value (`in`), range, relation-option (`GET …/facets/:field`) and bulk-delete (`POST …/bulk-delete`, ≤ 100 ids) through the one list-query contract, and the web parser is shared (`lib/kernel/entity-service.ts`, `query-keys.ts`) — toolbar UI for them is not built | [patterns.md](/en/docs/patterns) §List stack; `ddx-web/src/components/data/` | | Org list and permission source | The org switcher and `/auth/select-org` read the caller's orgs from `GET /me/orgs`; the portal layout reads org, role and the permission map (`can`) from `GET /orgs/:orgSlug/overview`; `requirePermission` reads `can`. No page reads the better-auth organization plugin (only `set-active` is a write). Role change: `PATCH /orgs/:orgSlug/members/:memberId/role` | `ddx-web/src/lib/dal/organization.ts`; `ddx-api/src/read-models/`; [ADR 0017](/en/docs/adr/0017-bff-layered) | | Portal header controls | Org switcher (searchable), location crumb, ⌘K / Ctrl K palette trigger, notification bell and popover, 4-language menu (endonyms), theme light · dark · system and density, help, user menu (profile, settings, organization, sign-out) | `ddx-web/src/components/portal/shell/PortalHeader*.tsx`, `PortalOrgSwitcher.tsx`, `PortalUserMenu.tsx` | | Profile locale and time zone | Language is a 4-endonym select; time zone is a searchable combobox grouped by IANA region with the UTC offset | `<P>/profile/_islands/ProfileForm.tsx`; `ddx-web/src/lib/format/time-zones.ts` | | Cookie settings | The consent banner reopens from the site footer, the portal status bar and the portal user menu | `ddx-web/src/components/consent/` | | Full-width frames | Portal and site page frames use the full viewport width with the shell gutter; entity forms and detail grids add columns with width. Prose, auth cards, call cards and print previews stay capped | `ddx-web/src/components/portal/page/types.ts`, `ddx-web/src/components/site/types.ts` | | Portal loading | A hard load of any portal route shows a static shell skeleton (header, side nav, status bar) before data | `ddx-web/src/components/portal/shell/` (`PortalShellSkeleton`), `<P>/../loading.tsx` | | Print of a record | Print routes: documents, projects, profile, organization, member. `?view=print` redirects to `/print` | `<P>/{documents/[id],projects/[id],profile,organization,members/[memberId]}/print/page.tsx` | | Operator deploy | Operator role only, never a portal role. `./ddx-manage.sh remote deploy\|rollback\|route\|logs\|status\|pull-logs --target <id>` or `/ddx-deploy`: release deploy with canary check, rollback to the previous release, publish of the public route, remote logs, status with boot check, log pull. Runs on operator request from the main session; never on push or a timer. Production: `https://booter.dudoxx.com` | `deploy/`, `ddx-manage.sh`; runbook [deploy/README.md](/en/docs/deploy/README); targets `context/DEPLOY_TARGETS.md` | --- Last reconciled: 2026-10-25 · plan tenant-branding task 010
Functional view of the workspace: pages and capabilities per audience, each row citing the
route file it lives in. Technical view: ,
, api/openapi.json. Every route below is a file today; a page
that is not listed here has no route.
Path prefix for the File column: ddx-web/src/app/[locale]/. <P> = (app)/portal/[orgSlug]/[role].
Every page is locale-prefixed (en, de, fr, es; strings in ddx-web/messages/<locale>/).
The portal URL is /{locale}/portal/{slug}/{role} (ADR 0003). slug is the org slug.
role is a portal role, not an org role:
A role that the session does not hold renders forbidden(). The role is gated once, in
PortalPage (allowedRoles: rolesFor('<nav item>')); ddx-api still decides data access per
request (org roles super-admin · org-admin · staff · member · client; super-admin is a platform role that acts as org-admin of the target org, ADR 0013).
Nav items come from ddx-web/src/lib/navigation/portal-registry.ts (13 items). Columns show
which portal role sees each item. All routes live under <P>/.
Reading the table: — = the nav hides the item and the page renders forbidden() for that
role. Staff sees every pattern demo; the access sets are PORTAL_NAV in portal-registry.ts
and PROJECT_WRITE_ROLES in <P>/projects/_islands/model.ts.
Portal chrome shared by every page above (the header and side nav show the tenant name and logo, see section 5b): header (org switcher with role badge, location crumb, ⌘K command palette, notification bell with unread badge, language menu, appearance menu for theme and density, help link, user menu), side nav (expanded · collapsed · auto), status line, footer
status bar, mobile menu — .
One page, <P>/settings/page.tsx, one read: GET /orgs/:orgSlug/settings/effective through
the BFF. The page renders what ddx-api returns; it never merges tiers.
PlatformSetting, edited only by platform super-admin) →
org (stored, edited by org-admin) → user (stored, edited by the user for their own value).
Registry: packages/ddx-contracts/src/settings.ts. Guide: .<scope>.<group>.<key>): portal.locale.default (org + user) ·
portal.locale.enabled (org) · portal.features.ai (org; each org chooses; staff roles read) ·
portal.notifications.digest (org + user) · portal.display.timeZone (org + user) ·
portal.display.themeMode light · dark · system · portal.display.density compact · cosy · large ·
portal.display.motion full · reduced · portal.display.textSize sm · md · lg (type only, never row height; all org + user) · module.demo-entities.viewMode
table · cards (per-module preference, org + user) · the five portal.branding.* keys (section 5b; org only, no user tier)./{locale}/platform/settings lists the platform tier and edits one
key at a time (GET /platform/settings, PATCH /platform/settings/:key, version required,
409 on a stale version). Only platform super-admin may call it. A write fans out
settings.changed (key + version, never the value) to every org.UnifiedSettingsProvider and
useUnifiedSetting(key); the header menus write the user tier.editable flag. client sees 4 keys (no portal.features.ai).public-persona access for org-admin is NEEDS_REVIEW (ADR 0014).Five keys, portal.branding.{displayName,palette,font,illustration,logo}: platform default, org
override, no user tier. Key table: §Branding keys.
Known gaps (ADR 0019): the home hero title (home.title) and site.brand.home label are fixed
text; the first hard load after a palette change can paint the previous palette once; orphaned logo
objects are not cleaned up.
A Bug button in the header and Alt+Shift+D open a debug panel. It exists only when
NEXT_PUBLIC_UI_DEBUG is on in ddx-web/.env.local. Detail: .
Last reconciled: 2026-10-25 · plan tenant-branding task 010
adr/
deploy/
research/