docs: add platform vision ADR and Sprint 16 project editor design spec

Records the multi-tenant marketplace platform architecture as ADR-0001
(bootstrap-driven, config-only frontend) with a source-backed fact pack,
and writes the approved Sprint 16 design for extending the existing
project editor with Languages/Navigation tabs, an HTML editor, and a
client-side draft/publish flow.
This commit is contained in:
sdarbinyan
2026-07-13 04:01:48 +04:00
parent ee269a9e33
commit 637ae28d47
3 changed files with 248 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
---
id: ADR-0001
title: Multi-tenant marketplace platform vision and config-driven architecture
status: active
date: 2026-07-13
tags: ["architecture", "philosophy", "multi-tenant", "bootstrap"]
---
## Context
This is not a single marketplace — it is a multi-tenant platform powering unlimited
marketplaces (e.g. electronics.example.com, books.example.com) from one codebase.
Every marketplace is configured from the backend via a bootstrap configuration
(`GET /bootstrap`). No marketplace-specific code may exist in the frontend.
## Decision
- The frontend (Angular 20, standalone components, Signals, RxJS, SCSS) is a pure
renderer. It owns render, navigation, interaction, validation, animations only.
- The backend (ASP.NET Core REST API) owns branding, pages, layouts, languages,
homepage, navigation, categories, products, footer, static pages, payment
configuration, and enabled features.
- Flow: Bootstrap → Runtime Provider → Configuration Store → Renderer → Widgets.
Nothing depends on build-time environments; everything depends on runtime
configuration.
- Bootstrap contains only data needed before the app starts (name, logo, colors,
languages, footer pages, homepage layout, navigation, enabled widgets). It must
never contain products, orders, cart, or users.
- Widgets never own page spacing — only their own internal layout. The renderer
owns sections, spacing, and page width.
- Homepage is composed from a configurable, ordered list of sections (Section
Engine): Hero, Categories, Featured Products, Banner, Latest Products, Custom
HTML, Newsletter, etc.
- All layouts (homepage, PLP, etc.) must be backend-configurable without frontend
changes.
- All user-facing text is translatable via a `translations.{lang}` shape, not a
flat `title` field. Adding/removing a supported language must automatically
expose/remove translation fields across all translatable objects, generically —
never per-field hardcoding.
- Static pages (About Us, Privacy, Terms, Contacts, Return Policy, Delivery,
custom pages) are backend-delivered HTML, multilingual, and drive the footer.
- Admin and storefront share a domain but are fully separate applications: the
marketplace bundle never ships admin code and vice versa. Bootstrap is public;
Admin is protected by JWT + roles/permissions + tenant isolation (Super Admin,
Marketplace Admin, Moderator, Editor, Support, Customer).
## Coding rules
- Never hardcode marketplace data or introduce marketplace-specific conditionals.
- Never use environment flags to drive UI — everything is config-driven.
- Keep components small; prefer composition and reusable widgets; never
duplicate layouts.
- Business logic lives in services/facades, not components.
- Prefer Signals and standalone components.
- Every new feature ships with docs: frontend docs, backend contract, bootstrap
updates, API examples, migration notes if needed.
## Guiding question
Before implementing anything: "Will this still make sense after 50 marketplaces
and 100 developers?" If not, redesign before coding.
## Consequences
Any feature (including the Sprint 16 Project Editor) must edit the same Bootstrap
model the storefront consumes — no parallel/duplicate configuration models are
permitted anywhere in the platform.

View File

@@ -0,0 +1,6 @@
{"id":"PV-20260713T000000Z-0001","subject":"platform","predicate":"is-architected-as","object":"multi-tenant marketplace platform powering unlimited marketplaces from one codebase, driven entirely by backend bootstrap configuration","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"decision","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["architecture","multi-tenant"]}
{"id":"PV-20260713T000000Z-0002","subject":"frontend","predicate":"must-not","object":"contain marketplace-specific code, hardcoded marketplace data, or environment-flag-driven UI","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["frontend","constraint"]}
{"id":"PV-20260713T000000Z-0003","subject":"bootstrap","predicate":"must-only-contain","object":"data needed before app start (branding, languages, homepage layout, navigation, enabled widgets, footer pages) and must never contain products, orders, cart, or users","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["bootstrap","constraint"]}
{"id":"PV-20260713T000000Z-0004","subject":"translatable-fields","predicate":"must-be-modeled-as","object":"generic translations.{lang} map so adding/removing a language automatically exposes/removes translation fields across all translatable objects","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["i18n","constraint"]}
{"id":"PV-20260713T000000Z-0005","subject":"admin-app","predicate":"is-isolated-from","object":"marketplace storefront bundle: admin code never ships to storefront and vice versa, though they may share a domain","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["admin","security"]}
{"id":"PV-20260713T000000Z-0006","subject":"widgets","predicate":"must-not-own","object":"page spacing or page width; the renderer owns sections, spacing, and page width, widgets own only their internal layout","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["widgets","layout"]}

View File

@@ -0,0 +1,175 @@
# Sprint 16 — Marketplace Project Editor MVP
Status: approved for planning
Date: 2026-07-13
Related: [ADR-0001](../../context/adrs/ADR-0001-marketplace-platform-vision.md), [Project-Editor.md](../../Project-Editor.md)
## Goal
A client can open the editor for their marketplace, edit settings, save a
draft, preview, and publish — without touching JSON by hand. The editor edits
the same `BootstrapConfig` the storefront consumes. No parallel/duplicate
configuration model is introduced anywhere in this work.
## Current state (as of this sprint)
A working editor already exists at `/builder`
(`src/app/features/project-editor/`): `ProjectEditorPageComponent` + signal-based
`ProjectEditorFacade` (no NgRx), sections for general, branding, theme, header,
footer, homepage (already has drag-and-drop reordering), widgets,
static-pages (via `StaticPagesEditorComponent`), features, preview. Static
page HTML is edited via a plain `<textarea>`. There is no Languages tab, no
Navigation tab, no Draft/Publish distinction, and no `ProjectValidator`. There
is no separate admin app — admin-ish features are ordinary lazy routes in the
same Angular build as the storefront. There is no `projectId` / multi-project
concept: each domain (tenant) resolves to its own bootstrap via the existing
tenant-resolution mechanism, so "a project" is implicitly the current domain's
tenant.
This sprint extends the existing editor rather than rebuilding it.
## Routing
Single Angular build (no separate admin app this sprint — deferred, see
Out of scope). Flat, non-parameterized routes, since there is no project id:
```
/edit → redirect to /edit/general
/edit/general
/edit/branding
/edit/theme (existing, kept)
/edit/languages (new)
/edit/navigation (new)
/edit/header (existing, kept)
/edit/homepage
/edit/footer
/edit/static-pages
/edit/widgets (existing, kept)
/edit/features (existing, kept)
/edit/preview
```
`/builder` and `/builder/*` redirect to the equivalent `/edit/*` route for
backward compatibility. Actual subdomain hosting (`admin.marketplace.com`)
and a physically separate Angular deployment are a future ADR/sprint — this
sprint only makes the route shape subdomain-ready (flat, no id segment).
Tabs become real child routes (deep-linkable, back-button works) instead of
today's in-component signal switch. `ProjectEditorFacade` is unchanged by
this — it already loads/holds bootstrap independent of which tab is active.
## Languages tab (new)
New `LocaleSyncService` (in `features/project-editor/services/`). It knows
both translation shapes already used in the codebase:
- index-signature maps: `{ [locale: string]: string }`
(`LocalizedTextContent`/`LocalizedHtmlContent`)
- keyed translation records: `Record<string, { title?, html?, seo? }>`
(e.g. `StaticPageTranslationConfig`)
`addLocale(code)` / `removeLocale(code)` walk the known translatable slices of
the in-memory bootstrap draft (static pages, footer nav labels, homepage
widget text props) and add/remove that locale's key generically — not one
`if` per field. `setDefaultLocale(code)` updates
`localization.defaultLocale` (already exists). The tab lists supported
locales, lets you add/remove, and pick the default; removing the current
default is blocked by `ProjectValidator` (see below).
## Navigation tab (new)
Visual nested list over header + footer navigation
(`src/app/shared/models/config/navigation.model.ts`). Add link, remove link,
reorder (buttons, matching the homepage tab's existing pattern — no new
drag-and-drop library), edit label (multilingual, via the same translation
shape as everywhere else), edit URL, "open in new tab" toggle, visibility
toggle, and nested children. Backed by new facade actions on
`ProjectEditorFacade` (`addNavLink`, `removeNavLink`, `reorderNavLink`,
`updateNavLink`) — no new store, same facade.
## Rich HTML editor (new)
New `MarketplaceHtmlEditorComponent`
(`features/project-editor/components/html-editor/`): native
`contentEditable` + a toolbar (bold, italic, underline, bullet/numbered
lists, link, image, table, headings, code view, preview). No new npm
dependency (no Quill/TipTap). Emits raw HTML on change; does not sanitize —
sanitization remains a storefront-render concern, unchanged. Replaces the
`<textarea>` currently in `static-pages-editor.component.html`, and is reused
anywhere else HTML is edited going forward (e.g. footer custom content).
## Draft / Publish
No backend draft/publish API exists yet — confirmed against
`docs/Project-Editor.md`, which only lists *recommended future* endpoints
(`PUT /builder/bootstrap`, `POST /builder/bootstrap/{preview,validate,import}`,
`GET /builder/bootstrap/export`), none of which distinguish draft from
published. This sprint models it client-side only:
- `ProjectDraft` (new, in `models/`): `{ bootstrap: BootstrapConfig, status:
'draft' | 'published', dirty: boolean, lastSavedAt?: string }`.
- **Save** persists the draft using the same local mechanism the editor
already uses for export/import (no new backend call this sprint) and
clears `dirty`.
- **Publish** runs `ProjectValidator` (must pass), then calls
`ConfigService.applyBootstrapOverride()` (the mechanism already used for
live preview) and sets `status = 'published'`.
- **Documented gap for backend**: real draft/publish persistence needs
`PUT /builder/bootstrap/draft` and `POST /builder/bootstrap/publish`
endpoints. Not implemented this sprint; called out explicitly so backend
work can be scheduled.
## Validation (new)
New `ProjectValidator` (in `services/`), pure functions run against the
current draft. Checks: missing logo, no languages configured, invalid
marketplace URL, duplicate static-page slugs, empty homepage (no sections),
homepage section referencing a missing/unregistered widget, duplicate
navigation links (same URL+label pair), invalid color values (theme tab).
Runs on every Save (surfaces warnings, does not block) and on Publish
(blocks with a clear list of failures). Errors surface inline in the
relevant tab plus a summary list.
## Dirty-state
`ProjectEditorFacade` tracks `dirty` via a diff against the last-saved
snapshot. A `CanDeactivate` route guard plus a `beforeunload` listener warn
before navigating away or closing the tab with unsaved changes.
## UI
Card-based layout per tab, sticky Save/Publish bar (shows dirty state and
validation summary), responsive down to tablet width — matching the
existing editor's current visual style, no new design system.
## Architecture summary
Reused as-is: `ProjectEditorFacade`, `ProjectEditorIoService`,
`ProjectEditorPreviewService`, existing section components.
New: `LocaleSyncService`, `ProjectValidator`, `MarketplaceHtmlEditorComponent`,
`ProjectDraft` model, Navigation tab section + facade actions, Languages tab
section + facade actions, `CanDeactivate` dirty-guard.
No `ProjectEditorStore`/`ProjectSerializer` as separate classes — the
existing facade + `ProjectEditorIoService` already cover that responsibility
(store = facade signals; serializer = IO service); introducing parallel
classes would duplicate what's there, which the platform's own coding rules
forbid.
## Out of scope (this sprint)
- Separate Angular app / actual subdomain deployment for admin.
- Real backend draft/publish persistence (documented as a gap above).
- Drag-and-drop for the new Navigation tab (buttons only, like homepage's
existing pattern predates this — homepage itself already has drag-and-drop
from a prior sprint and is left as-is).
- Products, Orders, Users, Dashboards, Analytics (future sprints per the
platform roadmap).
## Documentation
This spec plus updates to `docs/Project-Editor.md` (new tabs, draft/publish
model, new services) and a fact-pack update under
`docs/context/features/project-editor/FACTS.jsonl` are part of the
deliverable, per the platform's documentation rule ("every new feature must
include documentation").