Architecture
High-level architecture and main technical modules.
Overview
Yayaw uses the Next.js App Router with locale-prefixed routes (/en, /fr).
The product is organized around an authenticated multi-tenant dashboard, a
public CMS runtime, and a server-only control plane for trusted automation.
Key reader paths:
- Dashboard for the authenticated app shell and section roots
- Pages Catalog, Sections Catalog, and CMS Data Models for the CMS runtime
- Billing Overview for subscriptions, one-time purchases, and code access
- Control Plane for MCP and automation
Main Building Blocks
Frontend and Routing
- App routes in
src/app - Locale middleware and routing in
src/i18nandsrc/lib/middleware - Dashboard UI blocks under
src/blocks - Shared route/navigation configuration drives sidebar, command menu, breadcrumbs, and route visibility.
Dashboard Naming Convention
- Page title defines the page topic (for example:
Media,Billing,Authorization). - Block title defines what entity is managed and implies the action (for example:
Assets manager). - Block description explains, in one short sentence, what users can do in that block.
Authentication and Authorization
- Better Auth configuration in
src/config/better-auth.config.ts - Auth server integration in
src/lib/server/services/auth/auth-drizzle.ts - Better Auth API keys are enabled for control-plane access with explicit
control-plane:read,control-plane:write,control-plane:publish, andcontrol-plane:adminpermissions - Better Auth admin capabilities include impersonation and SCIM provisioning (
@better-auth/scim) - Organization invitation onboarding is passkey-first and uses hashed invite onboarding tokens stored outside the Better Auth invitation table
- Organization role synchronization in
src/lib/server/services/authz - Internal authorization model stays in Yayaw (Permix + Drizzle):
- groups and memberships
- explicit scoped bindings in
group_role_bindings - role policies in
role_policies
- Explainable authorization engine in
src/lib/server/authz:authorizeDetailed(...)for allow/deny + decision chaincan(...)as the boolean facade used by routes and actions
- Admin AuthZ custom server actions for list/detail/bulk/simulate/explain/audit in
src/lib/server/actions/authz/authorization-admin-actions.ts
Data Layer
- Drizzle setup in
src/lib/db - Schemas organized by domain:
- Better Auth schema
- Authorization schema
- System schema
- Dashboard sidebar parents are navigable section dashboards.
/dashboard/contentis the CMS health dashboard; it summarizes active organization content inventory, publication status, media storage, CMS data, and AI generation jobs before users drill into dedicated catalog screens./dashboard/admin,/dashboard/organization,/dashboard/settings, and/dashboard/testare also valid section roots, so sidebar parents never point at hidden or missing pages. The dashboard shell uses shadcn breadcrumbs and a shared section navigation bar derived from the filtered sidebar hierarchy; the section bar lists child routes for the active dashboard section only. - Organization media library in system schema:
media_foldersandmedia_assetsare organization-scoped- assets are stored in public object storage (
mediabucket by default) with persistedpublicUrl - storage is provider-backed: Supabase remains supported and S3-compatible storage supports MinIO, S3, and R2 style deployments
- visual assets can store generated WebP thumbnails for library display; missing thumbnails are backfilled during media library listing and do not count toward user upload quota
- server actions always enforce
scope: { orgId }and reject cross-organization mutations - read/list is available to organization members, upload/edit/delete requires manager or admin role
- upload quotas are plan-driven from billing runtime config (per-file and total organization storage)
- page-builder image generation stores OpenAI WebP outputs through the same media upload/persist pipeline
- Reusable sections catalog in system schema:
ui_section_registry_itemsstores stable built-in, global, and organization section identityui_section_revisionsstores validatedSectionDefinitionV1JSON + diagnosticsui_section_publicationsstores lifecycle status and the latest published revision pointer- section definitions store renderer ids, recipes, component references, and bindings only
- pages use
PageSectionreferences so they follow the latest published section - AI section creation uses a reusable plan workbench shell:
src/components/ai/plan-workbench/*- section adapter drawer:
src/blocks/dashboard/content/sections/block-ai-create-drawer.tsx - streaming plan endpoint:
src/app/api/ai/blocks/plan/route.ts - persisted runs/jobs endpoints (resume/poll/cancel):
src/app/api/ai/blocks/plan/jobs/* - persisted storage tables:
ui_block_ai_threadsui_block_ai_runsui_block_ai_run_events
- runner abstraction:
src/lib/server/services/blocks/block-ai-plan-runner.ts - AI prompt contract includes explicit design-policy instructions (component reuse + project-consistent layout patterns)
- finalize action with explicit missing-import approvals:
createBlockFromAiPlanAction
- AI creation entry points are guarded by the
ai-components-enabledsite setting so the UI and server actions disable together.
- Hybrid pages catalog in system schema:
ui_page_registry_itemsstores page identity, scope/channel, path, and metadataui_page_revisionsstores versionedPuckPageDocumentV1+ diagnostics- Page AI generation uses durable Postgres state:
ui_page_ai_runsui_page_ai_run_events- APIs under
src/app/api/ai/pages/runs/* - Vercel Queues can wake the hosted worker route;
bun run worker:page-aipolls the same DB state for self-hosted long-lived worker deployments
ui_page_publicationsstores lifecycle status and published revision pointer- dashboard UX is split into list/table then dedicated editor route per page
- editor is direct Puck + shadcn (no legacy fallback path)
- save model is autosave + publish/archive with optimistic locking (
baseRevisionId) - conflict handling is explicit (
conflictresponse) with no automatic merge/rebase - runtime reads published Puck documents directly
- global pages resolve from
/[locale]/[...slug] - organization member pages resolve from
/[locale]/o/[orgSlug]/[[...slug]] - publish is blocked on diagnostics errors and reserved-route collisions
- Global registry approval policy in system schema:
ui_registry_templatesstores approved shadcn-compatible aliases + URL templates- import pipelines resolve external aliases only from approved templates
- admin management page is available at
/dashboard/admin/registry-templates
- Generated server actions in
src/lib/server/actions/database/generated
Observability and Feature Flags
- PostHog server/client integration in
src/lib/posthogandsrc/providers - Dynamic flags in
src/lib/flags - Managed flag defaults declared in
src/config/flags.config.tsand seeded bysrc/lib/scripts/seed.ts - Admin site settings are backed by code-managed
feature_flagsrows - Custom Flags are generated from non-managed
feature_flagsrows and only affect runtime behavior when code reads their slug explicitly - Runtime auth capabilities resolved in
src/config/authentication-runtime.config.ts - Dashboard analytics are driven by a server-only view-model in
src/lib/server/services/dashboard:/dashboard/adminis the superadmin platform health surface with Stripe revenue, PostHog audience, registered-user growth, billing risk, and admin actions. The admin sidebar parent is a direct link to this route./dashboardis a permission-aware navigation home built from visible sidebar section roots and quick links, without duplicating organization health or provider metrics- organization owners/admins/managers see organization billing, seats, content inventory, usage, and management actions on organization-scoped section dashboards
- organization members see operational organization activity and safe shortcuts only on the sections they can access
- database, PostHog, and Stripe metric aggregates use the shared Next.js Data Cache for 5 minutes, keyed by route, timeframe, organization, and access scope, while session and authorization decisions remain per-request
- CMS page view analytics read
cms_page_viewedevents for active-organization pages, and include global pages only for actors with globalpage:manage, including top-page rankings for/dashboard/content - provider-level fallbacks keep one failed metric source from breaking the page
- deployment status is provider-backed: Vercel deployment reads remain
available, while self-hosted runtimes can provide static deployment
metadata through
DEPLOYMENT_*variables - PostHog events registered from the app include
organization_idwhen an active organization is available, enabling organization-scoped analytics
Deployment Providers
Yayaw's first portable self-host target is Docker standalone Next.js with
Postgres, S3-compatible object storage, a database-backed Page AI worker, and a
reverse proxy that preserves Host and X-Forwarded-* headers. Vercel remains
a supported provider for hosting, queues, deployment metrics, and project-domain
verification, but those capabilities are no longer assumed by the core runtime.
Runtime provider seams:
- storage:
STORAGE_PROVIDER=supabase|s3 - organization public domains:
PUBLIC_DOMAIN_PROVIDER=vercel|manual-dns - Page AI wake-up:
PAGE_AI_QUEUE_DRIVER=direct|vercel-queue|db-worker - deployment metadata: Vercel runtime variables or static
DEPLOYMENT_*variables
Postgres + Drizzle is the source of truth for application data. Supabase is not required for core persistence; it is only one optional object-storage provider and one local reset workflow.
Control Plane
- Production MCP endpoint:
src/app/api/mcp/route.ts - Local stdio launcher:
src/lib/scripts/mcp/yayaw-mcp-server.ts - Key management script:
src/lib/scripts/mcp/control-plane-key.ts - Shared typed operation registry:
src/lib/server/services/control-plane - Audit storage:
control_plane_audit_events - Production access requires the
control-plane-mcp-enabledsite setting, a valid Better Auth API key or OAuth access token, control-plane permissions, and underlying Yayaw authorization - Write tools require
reason; optimistic publish flows requireexpectedRevisionId; destructive archive tools requireconfirm: true
Future feature rule:
- Treat operational, content, configuration, publish, audit, and status workflows as control-plane candidates by default
- Put reusable business logic behind explicit actor-aware service entrypoints
- Keep UI server actions, MCP tools, CLI scripts, and optional HTTP APIs as thin adapters over the same service logic
- Add typed MCP tools/resources, audit coverage, docs, LLM source updates, and focused tests in the same change when a feature becomes control-plane-capable
- Document intentional exclusions when a feature remains UI-only
Feature Flag Contract
For product features, flags must control:
- runtime behavior (plugins/services/actions)
- UI exposure (cards, actions, navigation entry points)
This avoids partial disables where backend behavior is off but UI links remain visible.
Documentation Architecture
- Human docs: Fumadocs pages under
content/docs - LLM docs source of truth:
content/llm/llm-source.md - Generated assistant docs:
AGENTS.mdGEMINI.md.github/copilot-instructions.md
When changing product behavior, update the relevant English feature doc first,
then update content/llm/llm-source.md when assistant behavior or architectural
source-of-truth changes. Regenerate assistant docs with:
bun run docs:llm:generate