Skip to content

[Feature] Capability system for feature gating #214

@Polliog

Description

@Polliog

Feature Description

Introduce a capability system that gates feature availability through a single check: capabilities.has(organizationId, capabilityName). In the open-source distribution, capabilities resolve from a static configuration (default: all enabled). Downstream distributions can plug in alternative resolvers without modifying the core.

Problem/Use Case

Logtide already has features that some operators want to enable conditionally — for example, longer retention windows, stricter audit logging, or limits on the number of saved searches per organization. Today there's no consistent way to express "this feature is available for org X but not org Y". As more such features land, scattering ad-hoc checks (env vars, hardcoded constants, DB columns) creates inconsistency and makes the codebase hard to extend.

A capability system gives every feature gate the same shape, which means:

  • Operators can configure their own restriction policies without forking
  • The codebase becomes self-documenting about which features are gated
  • Downstream platforms (e.g. a hosted service) can resolve capabilities from external sources (subscription state, license file, etc.) without patching the core

Proposed Solution

A capabilities module with a single interface:

interface CapabilityResolver {
  has(organizationId: string, capability: CapabilityName): Promise<boolean>
  list(organizationId: string): Promise<Record<CapabilityName, boolean>>
}

The default resolver reads from config/capabilities.yaml (or env vars), with all capabilities enabled by default for self-hosted users. The resolver is registered at boot time and is replaceable.

Initial capability namespace:

  • retention.extended — retention windows beyond the default cap
  • audit.immutable — append-only audit log storage
  • auth.sso — SAML/OIDC authentication providers
  • alerts.unlimited — no cap on the number of active alert rules
  • dashboards.unlimited — no cap on saved dashboards
    These are placeholders — most map to features that will land progressively. The point of v1.0 is to ship the primitive and the first two or three real consumers, not to populate the full list.

Usage at callsites:

if (!(await capabilities.has(ctx.organizationId, 'retention.extended'))) {
  throw new CapabilityError('retention.extended')
}

Alternatives Considered

  • Boolean columns on the organization table. Works but doesn't scale: every new feature is a migration, and there's no way to centralize the resolution logic (e.g. caching, external lookup). Rejected.
  • Environment variables only. Fine for global toggles, but can't express per-organization policy and doesn't compose with multi-tenant deployments. Rejected as the sole mechanism, but env vars remain a valid backend for the default resolver.
  • Skip and add gating ad-hoc per feature. Cheaper today, more expensive every time we add a gateable feature. The whole point of doing this at v1.0 is amortizing the cost.

Implementation Details (Optional)

  • Capability names are string literals in a TypeScript union type, so callsites get autocomplete and the linter catches typos.
  • Resolvers can cache. The default resolver reads config once at boot and serves from memory; no I/O on the hot path.
  • The list of all capability names lives in one file (src/capabilities/registry.ts) so contributors discover what exists before adding new ones.
  • A GET /api/capabilities endpoint returns the resolved set for the current org, which the frontend uses to conditionally render UI (hide buttons for disabled features instead of showing them and erroring on click).
  • Reject the temptation to overload this for user-level permissions — that's RBAC, a different problem.

Priority

  • Critical - Blocking my usage of Logtide
  • High - Would significantly improve my workflow
  • Medium - Nice to have
  • Low - Minor enhancement

Target Users

  • Self-hosted operators wanting to enforce organization-level policies (e.g. retention caps for free internal tenants)
  • Maintainers needing a consistent pattern for shipping features that have tiered availability
  • Downstream platforms requiring an extension point to gate features by subscription state

Contribution

  • I would like to work on implementing this feature

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions