Feature paywall + access restriction
patterns specs/patterns/feature-paywall.kmd
Two sibling sub-patterns: (a) **paywall** — what surfaces when a feature is gated by a paid tier the user hasn't subscribed to; (b) **permission restriction** — what surfaces when the user lacks the required role / scope. Modeled after MongoDB LeafyGreen FeatureWalls.
When this pattern applies
Primary triggers
- Build access control UX in a Koder product
All triggers
- Surface a feature gated by paid tier
- Surface a feature gated by role / permission
Specification body
Pattern — Feature paywall / access restriction
Status: v0.1.0 — Draft. Koder Stack today is free-tier only, but the moment a tiered plan or per-tenant entitlement lands this spec is needed.
R1 — Hide vs disable vs paywall vs restriction
| Situation | Pattern |
|---|---|
| Feature exists, but user's tier excludes it | Paywall — show feature with upgrade overlay |
| Feature exists, but user's role lacks permission | Restriction — show feature with "request access" overlay |
| Feature doesn't exist for the user's deployment at all | Hide — feature absent from UI entirely |
| Feature transiently unavailable (server, network) | Disable — show as disabled with explanatory tooltip |
The four are NOT interchangeable. Pick by the decision tree above.
R2 — Paywall anatomy
Overlay on top of the feature surface (semi-transparent backdrop + centered card):
- Lock icon (sparkle-locked variant from icon set).
- Heading:
{Feature name} is part of {Plan name}. - Body: 1–2 sentences explaining the benefit.
- Primary CTA:
Upgrade plan→ billing flow. - Secondary link:
Learn more about plans.
Tone: informative, never pushy. Per
specs/content/voice-and-tone.kmd marketing column.
R3 — Restriction anatomy
Overlay or page-level message (no semi-transparent backdrop — the user should not perceive the feature as "almost reachable"):
- Permission-denied icon (shield).
- Heading:
You don't have access to {feature name}. - Body: 1 sentence:
Ask {admin role} for the {permission slug} permission. - Primary action:
Request access(opens form / email composer with pre-filled context). - Secondary:
Learn about permissions(link to product docs).
No upsell CTA. Per multi-tenancy: NEVER reveal cross-tenant data even
in the restriction message (cross-tenant access = 404, not 403, per
specs/multi-tenancy/contract.kmd).
R4 — Accessibility
- Modal overlay: focus trap (Tab cycles inside; Esc dismisses overlay returning user to safe surface).
- Backdrop click on paywall: dismisses overlay (don't trap user).
- Restriction: no Esc behavior since there's nothing to dismiss; user navigates away normally.
- Live-region announce on appearance: "{Feature name} requires {Plan / Permission}".
R5 — Graceful keyboard nav
- Tab order: heading → body → primary CTA → secondary link → close (if any).
- Enter on primary CTA activates.
- Esc on paywall: dismiss (returns to safe surface).
- Esc on restriction: no-op (user uses their browser/system back).
R6 — Internationalization
All strings translatable per specs/i18n/contract.kmd. Keys
paywall.{feature}.heading|body|primary|secondary and
restriction.{feature}.heading|body|primary|secondary.
Plan and Permission names use their canonical localized form.
Não-escopo
- Billing / entitlement engine (services/foundation/billing concern).
- Permission model itself (services/foundation/identity concern).
- A/B-tested upgrade copy (product concern).
References
specs/auth/oauth-flow.kmdspecs/components/banners.kmdspecs/multi-tenancy/contract.kmd