Skip to content

ADR-0003 · Template system scalability

Status: 🟡 Proposed · Depends on: — · Drives: ADR-0004

Context

Template-first is a locked product decision: core, but greenfieldtemplates/template_selections exist in the baseline schema but have zero references in src/; the live app is 100% dedicated per-feature UIs. That prior decision named this "the #1 architecture initiative to sequence before it drives feature design."

The prototype's Templates.dc.html (thoroughly reviewed across two revalidation passes, see Templates review) is a genuinely strong reference for what the real engine needs: a real plan-aware entitlement model (PLANRANK/TPLMETA), a real lifecycle state machine (draft → review → scheduled → published → paused → archived → deprecated with a version log), and — as of the fix just revalidated — a real widget-type security model: five named, schema-typed widget types (widget-calculator, widget-booking, widget-map, widget-leadform, widget-payment) plus one sanitized Custom HTML escape hatch gated behind an independent pending_security_review state, separate from the template's own lifecycle.

Two real gaps remain, unaffected by today's revalidation:

  1. No Claude Designs → real-template import/export contract (Templates Finding 3) — a screen becomes a real template today only by hand-rebuilding it; there's no JSON-schema export/import path.
  2. The block library has no ad/sponsor-slot concept (surfaced during the AdManager review) — AdManager defines a servicecard.footer ad placement with no template-side block type that could ever render it.

This ADR is the actual "what does adopting template-first mean, mechanically" decision the prior product-vision memory flagged as needed before templates drive feature design further.

Options considered

A — Server-rendered JSON-schema block model

Templates stored as an ordered JSON block array ({type, props, plan}[]). A single BlockRenderer React component maps type → component. Entitlement and security are enforced twice: at save-time (an Edge Function validates the prop schema against the block type + plan) and at render-time (a component tree, never raw HTML string interpolation). WIDGETSCHEMA's existing shape (five named widget types with typed props) generalizes directly into this model.

  • For: the security boundary is structural — a BlockRenderer can only ever render known component types with validated props, so there's no string-interpolation surface to sanitize in the first place, for every block except the one deliberate Custom HTML escape hatch.
  • Against: more up-front schema design work than a string-template approach.

B — Handlebars/string-template engine

Templates as Handlebars (or similar) strings with registered helper functions — the approach the greenfield product-vision memory named as a "design-toward target."

  • For: simpler to stand up initially; very flexible authoring.
  • Against: reopens exactly the Custom-HTML-sanitization problem at the engine's core — arbitrary string interpolation is the mechanism, not an escape hatch — instead of solving it once, in one contained block type, the way the just-revalidated fix already does.

C — Hybrid: JSON-schema blocks (A) + one sanitized Custom HTML escape hatch

Most blocks are JSON-schema/component-based per A. Exactly one block type (custom) allows raw HTML, sanitized at write-time with the disclosed policy the prototype now already implements (scripts, event-handler attributes, iframe, object/embed, and external stylesheets stripped before render), gated behind the independent security-review state machine already designed.

  • For: this is already what the prototype converged on, through two rounds of revalidation — a named widget-type system plus one deliberately-contained, sanitized escape hatch. Adopting C is formalizing what's already been designed and verified, not inventing a new approach.
  • Against: none identified beyond A's up-front schema cost.

Recommendation

C. This isn't a new architectural bet — it's naming and generalizing the pattern the Templates screen already arrived at across its security fix and this ADR's own review process. The real engine should define a portable TemplateDocument schema (ordered blocks, each {type, props, plan}, matching WIDGETSCHEMA's shape) consumed by both the real render layer and the save-time validation Edge Function.

Closing the import/export gap (Finding 3)

Define the TemplateDocument JSON schema first — before building any "Import from Claude Designs" UI. Once it exists, a Claude Designs prompt can ask the design tool to emit that JSON schema directly for a finished screen, rather than the real app trying to parse arbitrary .dc.html markup and inline JS (renderVals(), data-props, tsType hints) after the fact — asking the source system to emit a known target schema is far more robust than reverse-engineering a design-tool-specific format.

Closing the ad-slot gap (surfaced post-AdManager review)

Add a type:'sponsored' block to BLOCKLIB/WIDGETSCHEMA itself — a first-party, schema-typed slot ({slotId, fallback}) that calls the same getPromo(slotId, workspace) contract AdManager/manifest.js already use elsewhere (e.g. mobile-console/Home.dc.html's getPromo('home.banner', ws)). This turns "servicecard.footer is a defined slot with no consumer" into "any template can declare a sponsored block, and the Service Card template does" — templates and the ad system become one connected model instead of two.

Consequences

  • The real Edge Function that saves a template must validate the full block array against TemplateDocument (per-block prop schema + plan gate) — this is the "primary write-enforcement layer" the data-access rule already requires, applied to templates specifically.
  • ADR-0004 depends on the sponsored block type existing, so any template (not just AdManager's currently-fixed slot list) can carry a sponsored placement.
  • Templates Finding 2 (the "AI-assisted" creation stub) and Finding 4 (this same ad-slot gap, now addressed here) are otherwise unaffected by this ADR.

Open question for product

Is a template marketplace (users buying/selling templates — raised in the original Claude Designs open questions) in scope for this same phase, or fully roadmap? This affects whether entitlement needs a "purchased" tier in addition to plan-rank, which changes TemplateDocument's save-time validation rules.

  • Tracker: "Templates block library has no ad-slot concept..."
  • Templates review (Findings 1, 3, 4 and both revalidation passes)
  • Product-vision decision #5 (template-first: core but greenfield)