# Dynamic Page Builder – Full Architecture

A Strapi-like, schema-driven CMS for Medusa (Node.js backend + Admin UI + PostgreSQL).  
**Everything is dynamic; no hardcoded component types.**

---

## PART 1: DATABASE ARCHITECTURE

### Entity Relationship Overview

```
pages 1───N page_components N───1 component_instances
                                      │
                                      N───1 component_types 1───N component_fields
```

- **Schema layer:** `component_types` + `component_fields` define what components are and what fields they have.
- **Content layer:** `component_instances` hold the actual data (JSONB) for each usage.
- **Layout layer:** `page_components` links a page to instances and defines order (position).

---

### 1. `pages`

| Column       | Type        | Notes |
|-------------|-------------|--------|
| id          | UUID PK     | |
| slug        | TEXT UNIQUE | URL path, e.g. `home`, `about` |
| title       | TEXT        | |
| description | TEXT NULL   | Internal note |
| status      | TEXT        | `draft` \| `published` |
| meta        | JSONB NULL  | SEO: title, description, og_image, etc. |
| created_at  | TIMESTAMPTZ | |
| updated_at  | TIMESTAMPTZ | |

**Indexes:** `slug`, `status`, `updated_at`.

---

### 2. `component_types` (schema definition)

Defines a “kind” of component (e.g. Hero, ProductGrid, PromoCard). No content here, only schema.

| Column        | Type        | Notes |
|---------------|-------------|--------|
| id            | UUID PK     | |
| name          | TEXT UNIQUE | Internal key, e.g. `hero`, `product_grid` |
| display_name  | TEXT        | Admin label, e.g. "Hero" |
| description   | TEXT NULL   | Shown in “Add section” |
| reusable      | BOOLEAN     | If true, one instance can be used on many pages |
| repeatable    | BOOLEAN     | If true, admin can add multiple instances of this type per page |
| created_at    | TIMESTAMPTZ | |
| updated_at    | TIMESTAMPTZ | |

**Indexes:** `name` (unique).

---

### 3. `component_fields` (field definitions per component type)

One row per field of a component type. Defines label, API key, type, validation, order.

| Column          | Type        | Notes |
|-----------------|-------------|--------|
| id              | UUID PK     | |
| component_type_id | UUID FK  | → component_types.id |
| label           | TEXT        | Shown in admin form |
| api_key         | TEXT        | Key in instance `data` JSON, e.g. `headline`, `cta_url` |
| field_type      | TEXT        | See field types below |
| required        | BOOLEAN     | |
| default_value   | JSONB NULL | Default for new instances |
| validation_rules| JSONB NULL | e.g. `{ "maxLength": 100, "pattern": "^https?://" }` |
| position        | INT         | Order in form (drag) |
| created_at      | TIMESTAMPTZ | |
| updated_at      | TIMESTAMPTZ | |

**Unique:** `(component_type_id, api_key)`.

**Suggested `field_type` enum:**  
`text`, `richtext`, `number`, `boolean`, `image`, `multi_image`, `select`, `multiselect`, `product_picker`, `category_picker`, `json`, `url`, `email`, `date`, `datetime`, `repeatable` (nested group), `component` (nested single component), `dynamic_zone` (optional; pick one of N types).

**Indexes:** `component_type_id`, `(component_type_id, position)`.

---

### 4. `component_instances` (actual content)

One row = one filled-in component (one Hero, one ProductGrid, etc.). Data is stored in `data` according to the type’s field definitions.

| Column           | Type        | Notes |
|------------------|-------------|--------|
| id               | UUID PK     | |
| component_type_id| UUID FK     | → component_types.id |
| data             | JSONB       | `{ "headline": "...", "cta_url": "...", "image": "..." }` |
| created_at       | TIMESTAMPTZ | |
| updated_at       | TIMESTAMPTZ | |

**Indexes:** `component_type_id`.

**Relationship:**  
- Schema comes from `component_types` + `component_fields`.  
- This table only stores values; validation and form generation use the schema, not hardcoded logic.

---

### 5. `page_components` (layout mapping)

Links a page to component instances and defines order. One row = one “slot” on the page.

| Column                 | Type        | Notes |
|------------------------|-------------|--------|
| id                     | UUID PK     | |
| page_id                | UUID FK     | → pages.id |
| component_instance_id  | UUID FK     | → component_instances.id |
| position               | INT         | 1, 2, 3… for ordering |
| created_at             | TIMESTAMPTZ | |
| updated_at             | TIMESTAMPTZ | |

**Unique:** `(page_id, position)` if you enforce one instance per position, or allow duplicates and use position only for sort order.

**Relationships:**
- One **page** has many **page_components** (ordered by `position`).
- Each **page_component** points to one **component_instance**.
- One **component_instance** belongs to one **component_type** and can be referenced by many **page_components** when the component is **reusable**.

---

### How Relationships Work

1. **Creating a component type (schema)**  
   Admin creates `component_types` row and several `component_fields` rows. No content yet.

2. **Adding a section to a page**  
   - Admin picks a component type (e.g. Hero).  
   - Backend (or admin) creates a `component_instances` row with `component_type_id` and `data` (default or empty).  
   - Backend creates a `page_components` row: `page_id`, `component_instance_id`, `position`.

3. **Editing section content**  
   Admin form is built from `component_fields` for that `component_type_id`. Values are read/updated in `component_instances.data`.

4. **Reusable components**  
   When `component_types.reusable = true`, one `component_instances` row can be referenced by multiple `page_components` (same or different pages). Editing that instance updates every usage.

5. **Draft vs published**  
   Handled on `pages.status`. Queries for “public” page use `status = 'published'`. Draft is only in admin.

---

## PART 2: ADMIN UI REQUIREMENTS (Admin Panel)

Assume React (Medusa Admin or Next.js) with: **React Hook Form**, **dnd-kit** (or similar), **dynamic field renderer**.

---

### 2.1 Component Builder

**Purpose:** Create and edit component types and their fields (schema only).

- **List:** GET component types → table/cards with name, display_name, reusable, repeatable, “Edit” / “Delete”.
- **Create:** Form for component type (name, display_name, description, reusable, repeatable) → POST component-type → redirect to “Edit fields”.
- **Edit schema:**  
  - Load type + fields.  
  - Reorder fields (drag) → PUT reorder (e.g. update `position` for each field).  
  - Add field: modal/form (label, api_key, field_type, required, default_value, validation_rules) → POST component-field.  
  - Edit field: same form, PUT component-field.  
  - Delete field: DELETE component-field (and optionally clean default in instances).
- **Delete type:** DELETE component-type (and optionally cascade or block if instances exist).

**UI architecture:**
- **ComponentTypeForm:** React Hook Form, fields = name, display_name, description, reusable, repeatable.
- **FieldList:** Sortable list (dnd-kit) of `component_fields`; each row = label, api_key, field_type, required, actions (Edit, Delete).
- **FieldEditorModal:** Dynamic form for one field: label, api_key, field_type (select), required, default_value (by type), validation_rules (key-value or JSON).  
  - `field_type` select drives which widget is used for `default_value` and for validation options (e.g. maxLength for text, options for select).

---

### 2.2 Page Builder

**Purpose:** Create/edit pages and their layout (ordered list of sections).

- **List:** GET pages → table with slug, title, status, updated_at, “Edit”, “Duplicate”, “Delete”, “Preview”.
- **Create page:** Form (slug, title, description, status=draft) → POST page → redirect to page editor.
- **Page editor:**  
  - Left or top: page meta (slug, title, description, status, SEO meta).  
  - Main area: **ordered list of sections** (each section = one `page_components` + its `component_instance`).  
  - “Add section”: open **component picker** (list of `component_types` by display_name); on choose, create `component_instance` + `page_components` with next `position`, then expand new section.  
  - Each section: **dynamic form** (see 2.4) + “Remove section” (delete `page_components` and optionally `component_instances` if not reusable elsewhere) + drag handle.  
  - Reorder: dnd-kit → on drop, PATCH/PUT reorder (send new ordered list of `page_components.id` or positions).  
  - “Save draft” / “Publish”: PUT page with `status`.  
- **Duplicate page:** GET page + its page_components + component_instances; create new page + new instances (copy data) + new page_components; redirect to new page editor.
- **Delete page:** DELETE page (and optionally orphaned instances or cascade page_components).

**UI architecture:**
- **PageList:** Table, filters by status, actions per row.
- **PageMetaForm:** slug, title, description, status (draft/published), meta (SEO).
- **SectionList:** Sortable (dnd-kit) list of section cards; each card = drag handle + **DynamicSectionForm** + remove.
- **ComponentPickerModal:** Fetches component types; on select, callback with `component_type_id` so parent creates instance + page_component.
- **DynamicSectionForm:** Given `component_instance_id`, load instance + its type + type’s fields; render **DynamicFieldRenderer** per field; on submit, PUT instance with new `data`.

---

### 2.3 Component Library (optional screen)

**Purpose:** Manage reusable component instances that can be placed on multiple pages.

- **List:** GET component_instances where type is `reusable` (or GET types where reusable, then for each type list instances). Show type display_name, instance id, “Use on page”, “Edit”, “Duplicate”.
- **Edit instance:** Same **DynamicSectionForm** as in page builder; updating saves to `component_instances`; all pages using this instance see the change.
- **Duplicate instance:** Copy `component_instances` row (new id, same data); then admin can add it to a page.

---

### 2.4 Dynamic field renderer (forms)

**Single source of truth:** `component_fields` (and nested fields for repeatable/component).

- **Resolver:** For each field row, switch on `field_type` and render one control (input, textarea, rich text, number, checkbox, image upload, multi-image, select, multiselect, product picker, url, date, json, etc.).  
- **Binding:** Use React Hook Form: `register` or Controller with `name = api_key`; form values = `component_instances.data`.  
- **Repeatable:** One field with `field_type = repeatable` has nested `component_fields` (or a “sub-schema”). Render a list of “items”; each item is a sub-form (same dynamic renderer); form value = array of objects.  
- **Validation:** Map `validation_rules` + `required` to RHF validation (e.g. maxLength, pattern, required).

So: **no hardcoded component UIs**; only a single **FieldRenderer** that reads `field_type` and optional `validation_rules` and renders the right input and validates.

---

## PART 3: BACKEND API (Medusa / Express)

Assume Medusa-style routes under `/admin` (authenticated) and store under `/store` (public). Controllers resolve CMS service and delegate to it.

---

### Component types

- **POST /admin/component-types**  
  Body: `{ name, display_name, description?, reusable, repeatable }`.  
  Create `component_types` row; return type. Optionally create default fields.

- **GET /admin/component-types**  
  List all; optionally expand `component_fields` for builder UI.

- **GET /admin/component-types/:id**  
  One type with fields (ordered by position).

- **PUT /admin/component-types/:id**  
  Update name, display_name, description, reusable, repeatable.

- **DELETE /admin/component-types/:id**  
  Delete type; either cascade delete fields (and optionally instances) or forbid if instances exist.

---

### Component fields

- **POST /admin/component-types/:typeId/fields**  
  Body: `{ label, api_key, field_type, required?, default_value?, validation_rules?, position? }`.  
  Create `component_fields` row; return field.

- **PUT /admin/component-types/:typeId/fields/:fieldId**  
  Update field; optionally validate that existing instance `data` still fits (e.g. no removal of required keys without migration).

- **PUT /admin/component-types/:typeId/fields/reorder**  
  Body: `{ field_ids: [uuid, ...] }`. Update `position` for each field.

- **DELETE /admin/component-types/:typeId/fields/:fieldId**  
  Remove field; instance `data` can keep the key (ignored) or you run a migration to delete keys.

---

### Component instances

- **POST /admin/component-instances**  
  Body: `{ component_type_id, data? }`.  
  Create instance; `data` default from type’s field default_value; return instance.

- **GET /admin/component-instances/:id**  
  Return instance with `component_type` (and fields) for form rendering.

- **PUT /admin/component-instances/:id**  
  Body: `{ data }`. Validate `data` against type’s fields (type, required); update instance.

- **DELETE /admin/component-instances/:id**  
  Delete instance; remove or null out references in `page_components` (or cascade delete page_components).

---

### Pages

- **POST /admin/pages**  
  Body: `{ slug, title, description?, status?, meta? }`. Create page; return page.

- **GET /admin/pages**  
  List pages (admin); filter by status, search by title/slug.

- **GET /admin/pages/:id**  
  Page by id with full layout: `page_components` ordered by position, each with `component_instance` (and instance’s `component_type` + `component_fields`). Used by page builder and preview.

- **PUT /admin/pages/:id**  
  Update slug, title, description, status, meta.

- **DELETE /admin/pages/:id**  
  Delete page; delete or orphan `page_components`; optionally delete non-reusable instances.

- **GET /store/pages/:slug** (or **GET /admin/pages/preview/:slug**)  
  Public (or preview): only `status = 'published'` (or allow draft for preview with token). Return same shape: page + ordered page_components + component_instances + types + fields. Storefront uses this to render.

---

### Page layout (sections)

- **POST /admin/pages/:id/components**  
  Body: `{ component_type_id }`.  
  Create `component_instances` row; create `page_components` row with `page_id`, new instance id, `position = max(position)+1`. Return new instance + page_component.

- **PUT /admin/pages/:id/components/reorder**  
  Body: `{ component_instance_ids: [uuid, ...] }` or `{ order: [ { page_component_id, position } ] }`.  
  Update `position` of each `page_components` row so layout order matches.

- **DELETE /admin/pages/:id/components/:pageComponentId**  
  Delete `page_components` row; if instance is not referenced elsewhere, delete `component_instances` row.

---

### Controller logic summary

- **Validation:** For instances, validate `data` against the type’s fields (required, type, validation_rules). Use a shared **validateInstanceData(fields, data)**.
- **Transforms:** When returning page with layout, join page → page_components → component_instances → component_types (+ fields) so the client gets one tree (page.sections[] with instance + type + fields).
- **Idempotency:** Slug unique on pages; name unique on component_types; (component_type_id, api_key) unique on component_fields.
- **Auth:** All `/admin/*` require admin auth; `/store/pages/:slug` is public (published only).

---

## PART 4: FRONTEND RENDERER (Storefront)

**Goal:** Fetch one payload (page + sections with instance + type + fields) and render everything dynamically.

---

### 4.1 Data shape from API

Example **GET /store/pages/home**:

```json
{
  "page": {
    "id": "...",
    "slug": "home",
    "title": "Home",
    "status": "published",
    "meta": { "title": "...", "description": "..." }
  },
  "sections": [
    {
      "page_component_id": "...",
      "position": 1,
      "instance": {
        "id": "...",
        "component_type_id": "...",
        "data": { "headline": "Welcome", "cta_url": "/shop", "image": "..." }
      },
      "type": {
        "name": "hero",
        "display_name": "Hero",
        "fields": [
          { "api_key": "headline", "field_type": "text", "label": "Headline" },
          { "api_key": "cta_url", "field_type": "url", "label": "CTA URL" },
          { "api_key": "image", "field_type": "image", "label": "Image" }
        ]
      }
    }
  ]
}
```

Storefront receives **sections** in order; each has **instance.data** and **type.name** + **type.fields**.

---

### 4.2 Component registry (optional optimization)

- **Registry:** A map `type.name → React component`, e.g. `hero → HeroBlock`, `product_grid → ProductGridBlock`.  
- **Purpose:** When you want custom layout/styling per type (e.g. Hero with specific CSS and structure), you register a component that receives `instance.data` and optionally `type.fields` and renders.

---

### 4.3 DynamicComponent (fallback / generic)

- **Input:** One section: `instance.data`, `type.fields`, `type.name`.  
- **Logic:** Loop over `type.fields` (ordered); for each field, switch on `field_type` and render a presentational element (heading, paragraph, image, link, etc.).  
- **No form controls;** only display. Handles: text, richtext (HTML), number, boolean, image (img), multi_image (gallery), url (link), select (single value), json (optional custom block).  
- **Nested:** If a field is repeatable or component, recurse into **DynamicComponent** or a small **DynamicField** renderer for that group.

So: **unknown or unregistered type** → always render **DynamicComponent** so every schema-driven type can be shown without code changes.

---

### 4.4 Page view

- Fetch **GET /store/pages/:slug**.  
- Render page meta (title, description) for SEO.  
- Map over `sections` in order:  
  - If registry has `type.name` → render **Registry[type.name]** with `instance.data`.  
  - Else → render **DynamicComponent** with `instance.data` and `type.fields`.  

Result: **fully dynamic**; new component types created in admin are visible on the storefront without deploying new components, as long as you’re okay with the generic layout. When you want a custom design, add a registry entry for that type name.

---

## PART 5: ADVANCED FEATURES

- **Draft vs publish:** Already in schema (`pages.status`). Admin saves draft; “Publish” sets `status = 'published'`. Store API returns only published; admin can have “Preview” that loads draft by slug with auth.

- **Versioning:** Add `page_versions` (page_id, version number, snapshot of page_components order + instance data or instance ids, created_at). On “Publish”, copy current layout + instance refs (or data) into a new version. Storefront can read “latest version” for a page; admin can “Restore” a version.

- **Scheduled publish:** Add `pages.publish_at` (TIMESTAMPTZ NULL). Job (cron) runs periodically and sets `status = 'published'` where `publish_at <= now()` and status still draft.

- **Global components:** Reusable instances that are “global” (e.g. header/footer). Add `component_instances.is_global` or a separate `global_components` table (page_id NULL, component_instance_id, key). Storefront fetches globals once and merges into layout or a global layout wrapper.

- **SEO metadata per page:** Already in `pages.meta` (JSONB). Admin form: meta title, meta description, og_image, etc. Storefront uses in `<head>` and og tags.

- **Role permissions:** Medusa has roles; restrict CMS by permission, e.g. `cms.page.create`, `cms.page.publish`, `cms.component_type.manage`. In each admin route and API handler, check permission before mutate.

- **Page caching:**  
  - **Store:** Cache GET /store/pages/:slug response (e.g. Redis or HTTP cache) with key `page:slug`; invalidate on page publish or when a reusable instance used on that page is updated.  
  - **Admin:** No long-term cache; or short cache for component type list.

---

## Summary

| Layer        | Responsibility |
|-------------|------------------|
| **DB**      | pages, component_types, component_fields, component_instances, page_components; schema in types+fields, content in instances, layout in page_components. |
| **Admin**   | Component Builder (types + fields), Page Builder (pages + layout + instance forms), Component Library (reusable instances); all forms from schema; drag-and-drop for field order and section order. |
| **API**     | CRUD for types, fields, instances, pages; layout reorder; public endpoint for page by slug (published only). |
| **Storefront** | Fetch page + sections; render via registry (custom components) or DynamicComponent (schema-driven); no hardcoded component list. |

Everything is **schema-driven**: component types and fields define structure; instances hold JSONB content; forms and storefront render from that schema. No hardcoded component types in backend or storefront beyond the single **FieldRenderer** (admin) and **DynamicComponent** (storefront).
