Skip to main content

Theme Tier-Gating Requires Multi-Repo Entitlement Sync

The Insight

Premium themes (Candy Themes for Starlight docs) are gated at subscription tiers. A new theme registration requires synchronized edits across 5 separate files spanning TypeScript, Go, and JSX. A mismatch in any file silently breaks the feature: the theme becomes inaccessible to all tiers, or accessible to tiers that shouldn’t have it. The discovery: Parity between the TS manifest, TS entitlements engine, and Go server-side validation is not enforced by the type system. TypeScript can compile with all union members registered, and all Go tests can pass, while a theme’s entitlement is missing from the Go map. The client offers it; the server rejects it.

Why This Happens

Theme registration lives in two languages, three concerns:
  1. Displaymanifest.ts tracks which themes exist (TypeScript compilation)
  2. Client-side gatingengine.ts tracks which tiers can see which themes (TS type-check)
  3. Server-side validationentitlements.go validates theme selections on PATCH (Go vet)
Neither language’s compiler has visibility into the other. A missing Go entry doesn’t break Go compilation; it only breaks the theme PATCH request at runtime. A missing TS entitlement doesn’t break the manifest import chain.

The Solution

Declare a verification gate that spans both languages:
# Count theme entries in each file; all must be equal
TS_THEMES=$(grep -c 'slug:' manifest.ts)
TS_ENTS=$(grep -c '"theme:' engine.ts)
GO_ENTS=$(grep -c 'TierWhite\|TierMilk\|TierDark' entitlements.go)

test "$TS_THEMES" = "$TS_ENTS" && test "$TS_ENTS" = "$GO_ENTS" && echo "OK" || exit 1
This gate runs after all edits are complete and confirms parity before commit. It is the only artifact that crosses language boundaries.

Key Metrics (Blackjack Theme, 2026-04-24)

  • Files touched: 5 (1 CSS, 1 TS manifest, 1 TS engine, 1 Go map, 1 JSX comment)
  • Parity checks: 3 (union member count = THEMES record = TS entitlements = Go map)
  • CSS variable parity: 21 --sl-* variables (compare: new theme vs. reference theme)
  • Type errors before gate: 1 (missing THEMES.blackjack)
  • Type errors after gate: 0
  • Go vet errors: 0
  • Silent bugs: 0 (gate caught all potential mismatches)
  • candy-themes-tier-gated-registration.doctrine.md — Full pattern for registering a theme across all 5 files.
  • sequential-taskset-gated-execution-pattern.doctrine.md — How to order the 5 edits as gated tasksets so rollback scope is minimized.

Lesson for Future Work

Any feature spanning multiple languages or repos requires an explicit cross-language parity gate in the plan. The gate is a command (exit 0/1), not an assertion. It runs last, before commit, and must be documented in the conventional-commits trailer.