Toast
ContributorDecisions

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:

  1. Scattered capability semantics - different layers derive enabled differently.
  2. Endpoint/client sprawl - per-domain /api/*/status endpoints and duplicated frontend cache logic.
  3. Partial-config drift - app ID without secret (or vice versa) yields inconsistent behavior.
  4. 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:

  1. Compute runtime capability in server config modules only.
  2. Expose one shared capability endpoint: GET /api/capabilities.
  3. Use one stable capability shape per feature:
    • { enabled: boolean, config: object | null }
  4. Keep config strictly for static public setup (app IDs, public keys), not secrets.
  5. Keep session/token credentials in action endpoints (for example, collaboration token endpoints).
  6. Use one shared frontend capabilities client per app with cache + in-flight dedupe + fallback/retry.
  7. 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

  1. API config layer

    • Reads env.
    • Computes enabled from complete config pairs.
    • Exports typed immutable config.
  2. Capability API layer

    • Publishes consolidated capability state in one response.
    • Returns only static public setup config.
    • Leaves token/session generation to dedicated action endpoints.
  3. Frontend capabilities client layer

    • Fetches /api/capabilities.
    • Caches and deduplicates requests.
    • Supports safe fallback and explicit refresh/retry behavior.
  4. Runtime boundary layer

    • Contexts decide whether collaboration/AI providers/extensions are active.
    • Disabled capabilities do not block local editor readiness/save flows.
  5. UI/command safety layer

    • Controls hide/disable when unavailable.
    • Commands are optional-safe when extensions/providers are absent.

Why This Works

  1. Single source of truth for runtime capability state.
  2. Scales across feature domains without endpoint/client proliferation.
  3. Clear security boundary between static capability config and tokenized actions.
  4. Resilient UX when optional features are unavailable.
  5. 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

On this page