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/appLocale middleware and routing in
src/i18nandsrc/lib/middlewareDashboard UI blocks under
src/blocksShared 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.tsAuth server integration in
src/lib/server/services/auth/auth-drizzle.tsBetter Auth API keys are enabled for control-plane access with explicit
control-plane:read,control-plane:write,control-plane:publish, andcontrol-plane:adminpermissionsBetter Auth admin capabilities include impersonation and organization-scoped managed SCIM provisioning (
@better-auth/scim). SCIM credentials are issued one time from organization settings; provisioned groups never map to Yayaw roles or policies automatically.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/authzInternal authorization model stays in Yayaw (Permix + Drizzle):
groups and memberships
explicit scoped bindings in
group_role_bindingsrole 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/dbSchemas organized by domain:
Better Auth schema
Authorization schema
System schema
Dashboard sidebar parents are navigable section dashboards.
/dashboard/contentis the content screen; it summarizes the active organization's publication status, page views, media storage, and what needs attention 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-scopedassets are stored in public object storage (
mediabucket by default) with persistedpublicUrlS3-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 mutationsread/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 pointersection 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 approvedAI 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.tsxstreaming plan endpoint:
src/app/api/ai/blocks/plan/route.tspersisted 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.tsAI 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+ diagnosticsPage AI generation uses durable Postgres state:
ui_page_ai_runsui_page_ai_run_eventsAPIs 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 pointerdashboard 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/rebaseruntime 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 templatesimport pipelines resolve external aliases only from approved templates
the admin management page,
/dashboard/admin/registry-templates, is a global screen (see Dashboard): a full-page table of the approved registries for whoever holds a globalregistry-template:manage, with their create, edit and delete forms; the page syncs the known registries (the shadcn defaults and those ofcomponents.json) each time it loads
Generated server actions in
src/lib/server/actions/database/generated
Observability and Feature Flags
PostHog server/client integration in
src/lib/posthogandsrc/providersDynamic flags in
src/lib/flagsManaged flag defaults declared in
src/config/flags.config.tsand seeded bysrc/lib/scripts/seed.tsAdmin site settings are backed by code-managed
feature_flagsrowsCustom Flags are generated from non-managed
feature_flagsrows and only affect runtime behavior when code reads their slug explicitlyRuntime auth capabilities resolved in
src/config/authentication-runtime.config.tsDashboard overviews are screens (
src/lib/server/services/screens, see Dashboard): YaYaw Table dashboard documents whose widgets read sources that checkcan()on every read, and host blocks that decide for themselves:/dashboard/adminis the superadmin platform health surface, a global screen: Stripe revenue, the PostHog or Umami audience, users, organizations, subscriptions, billing risks and the deployment, each behind a globalcan(). The admin sidebar parent is a direct link to this route./dashboardis a permission-aware navigation home with an exhaustive, locally searchable directory of authorized static destinations. A section root is linked only when independently authorized; an authorized child does not grant access to its parent overview.favorites and recent destinations use a versioned, user/organization-scoped
localStoragepayload. Every stored href is re-filtered against the current server-authorized destination set before display or persistence.dynamic-data applications live in a separate searchable browser so an unbounded model catalog does not overwhelm the static directory. The home does not duplicate organization health or provider metrics.
the organization overview shows billing, seats and members to the members whose groups allow it (
billing:read,member:list), never from an organization role string, and each kind of activity behind its own permission; members see only the destinations they can access on the homethe screens' database, PostHog and Stripe aggregates use the shared Next.js Data Cache for 5 minutes, keyed by source, organization, access and request, 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/contenta provider that is not configured, fails or times out shows a notice in its widgets instead of breaking the page
the admin overview shows the deployment's metadata, from the
DEPLOYMENT_*variables of the self-hosted runtime; Vercel's API is never readPostHog 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=s3organization public domains:
PUBLIC_DOMAIN_PROVIDER=vercel|manual-dnsPage AI wake-up:
PAGE_AI_QUEUE_DRIVER=direct|vercel-queue|db-workerdeployment 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.tsLocal stdio launcher:
src/lib/scripts/mcp/yayaw-mcp-server.tsKey management script:
src/lib/scripts/mcp/control-plane-key.tsShared typed operation registry:
src/lib/server/services/control-planeAudit storage:
control_plane_audit_eventsProduction access requires the
control-plane-mcp-enabledsite setting, a valid Better Auth API key or OAuth access token, control-plane permissions, and underlying Yayaw authorizationWrite 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/docsLLM docs source of truth:
content/llm/llm-source.mdGenerated 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