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 - 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
- ordinary pages use unpinned
PageSectionreferences so they follow the latest published section; strict contextual design drafts use server-issuedsectionRevisionIdpins so review and publication resolve the exact immutable section revisions that were visually approved - 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
- Dynamic project data models are controlled through MCP and stored as generated
native Postgres tables for operational read/write performance. Their
declarations, publications, and deployment history live in system registry
tables, while generated table DDL is additive and server-generated. Record
CRUD/query runs through the same control plane against deployed tables with
model-validated payloads, bounded filters, and parameterized SQL values.
Admin > Dynamic Data is the static control-plane surface for inspecting
models, querying deployed table rows, and managing record payloads when the
actor has
dynamic-data:manage. Product-facing dashboard sections are declared by MCP throughui.nav.sectionand render at/dashboard/{section}/dynamic-datawithout code changes once visible deployed models are assigned to that section. All dashboard reads and writes still resolve the model resource and run through scopeddynamic-dataauthorization server-side. The dashboard model list includes a bounded no-code draft form for creating native-table model drafts from names, slugs, scope, navigation section, and scalar fields; publication and deployment still go through the same validated dynamic-data service/MCP pipeline before any generated DDL is applied. Model managers can also publish the latest ready draft from the dashboard model list, open the generated deployment plan for the published revision, and apply that reviewed plan from the model detail; those actions call the same plan, publish, and deploy services as MCP and stay gated bydynamic-data:manage. That list includes models visible through broad global/organization grants and through resource-scoped model grants, and per-model manage access controls the generated record actions. Extension metadata is visible in the model list and model detail so product bundles can be inspected by extension key, capabilities, tags, and entity bindings without opening raw JSON. For deployed models, model detail and generated runtime actions are rendered from the same normalized runtime manifest returned by MCP, so operators inspect installed routes andui.runtimeActionsinstead of unpublished draft UI. Model definitions can include validatedui.tablemetadata so MCP callers can tune Yayaw Table column renderers, order, visibility, sorting, calculations, and table/kanban/gallery presentation without changing repository code. They can also include validatedui.formmetadata that compiles into the existing Yayaw Table form builder for record create/edit drawers or modals, including field order, visibility, labels, helper copy, read-only state, and safe widget hints.ui.table.rowActionscan declare row-level dashboard actions that call deployed dynamic runtime routes through server-side record reloading, bounded literal/record/system bindings, Yayawdynamic-dataauthorization, and the existing runtime executor, so product control actions can be added by MCP without custom React code.ui.detail.summarycan declare a selected-record header, badges, and key/value fields from safe literal, record, or system bindings, whileui.detail.relatedRecordscan declare read-only related-record panels that reuse Yayaw Table against target dynamic models. Related panels use server-side source record reloads, target model authorization, and parameterized native-table filters, so product admin detail pages can grow by MCP configuration instead of repository-specific components. - Session, page-view, and high-volume event data remain analytics-provider traffic rather than MCP-routed writes; MCP can configure and query analytics surfaces, but it should not be the hot ingestion path.
- Runtime analytics event emission is controlled through managed feature flags
in
/dashboard/admin/flags.cms-page-view-analytics-enabledgates CMS page view events without adding a separate analytics route-configuration surface.
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=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. S3-compatible object storage is the media persistence layer.
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