ADR-010: Config and Capabilities
ADR-010: Config and Capabilities
Status
Accepted
Context
Feature availability in Toast depends on runtime configuration, but capability checks were becoming fragmented across API controllers, admin clients, and docs.
This creates repeated problems:
- Scattered capability semantics - different layers derive
enableddifferently. - Endpoint/client sprawl - per-domain
/api/*/statusendpoints and duplicated frontend cache logic. - Partial-config drift - app ID without secret (or vice versa) yields inconsistent behavior.
- Runtime/build confusion - build-time env checks leak into runtime gating.
We need one architecture that works for current features (TipTap collaboration/AI) and scales to future domains (email, storage, etc.).
Decision
Adopt a unified configuration and capability architecture:
- Compute runtime capability in server config modules only.
- Expose one shared capability endpoint:
GET /api/capabilities. - Use one stable capability shape per feature:
{ enabled: boolean, config: object | null }
- Keep
configstrictly for static public setup (app IDs, public keys), not secrets. - Keep session/token credentials in action endpoints (for example, collaboration token endpoints).
- Use one shared frontend capabilities client per app with cache + in-flight dedupe + fallback/retry.
- Fail closed for optional/pro features, fail open for core editor usage.
Architecture
Capability flow
Environment
↓
API Config Layer (typed, immutable)
↓
Capability API (GET /api/capabilities)
↓
Shared Frontend Capabilities Client
↓
Runtime Boundaries (contexts/providers/extensions)
↓
UI + Commands (hide/disable/guarded execution)Layer responsibilities
-
API config layer
- Reads env.
- Computes
enabledfrom complete config pairs. - Exports typed immutable config.
-
Capability API layer
- Publishes consolidated capability state in one response.
- Returns only static public setup config.
- Leaves token/session generation to dedicated action endpoints.
-
Frontend capabilities client layer
- Fetches
/api/capabilities. - Caches and deduplicates requests.
- Supports safe fallback and explicit refresh/retry behavior.
- Fetches
-
Runtime boundary layer
- Contexts decide whether collaboration/AI providers/extensions are active.
- Disabled capabilities do not block local editor readiness/save flows.
-
UI/command safety layer
- Controls hide/disable when unavailable.
- Commands are optional-safe when extensions/providers are absent.
Why This Works
- Single source of truth for runtime capability state.
- Scales across feature domains without endpoint/client proliferation.
- Clear security boundary between static capability config and tokenized actions.
- Resilient UX when optional features are unavailable.
- Operational clarity between build/install requirements and runtime capability.
Alternatives Considered
1) Per-domain status endpoints (/api/collaboration/status, /api/email/status, ...)
Rejected because it duplicates endpoint contracts, frontend cache logic, and retry behavior per domain.
2) Client-side runtime capability from build env (import.meta.env)
Rejected because it drifts from actual server/runtime capability and couples runtime behavior to build-time values.
3) Infer capability from token endpoint failures only
Rejected because it degrades UX (late failure) and prevents proactive UI gating.
Consequences
Positive
- Unified capability contract and API entrypoint.
- Reusable frontend capability cache logic.
- Clear distinction between static capability and per-request credentials.
- Easier extension to new capability domains.
Negative
- Requires disciplined routing/contract governance.
- Adds a small upfront abstraction layer for simple features.
Trade-offs Accepted
We accept modest abstraction overhead to prevent long-term entropy from fragmented capability patterns.
Scope and Non-goals
In scope
- Shared capability contract and endpoint.
- Shared frontend capability client pattern.
- Runtime gating architecture for optional features.
Out of scope
- Full migration of all configuration modules to shared packages.
- Tenant-specific capability policy system.
- Replacing existing token/action endpoints.
Current Implementation (TipTap)
- API config:
apps/api/src/config/tiptap.ts,apps/api/src/config/app-config.ts - Unified capability endpoint:
apps/api/src/routes/capabilities/routes.ts - TipTap token endpoints:
apps/api/src/routes/collaboration/routes.ts - Frontend shared capability client:
apps/admin/src/lib/capabilities.ts - Runtime boundaries:
apps/admin/src/stores/scoped/EditorSessionProvider.tsx(scoped session store + capability-gated runtime setup)
References
- Issue: TryGhost/Toast#468
- ADR-004:
docs/decisions/004-service-repository-architecture.md - Pattern:
docs/patterns/config.md