Skip to content

RFC: centralize capability/entitlement resolution instead of deriving it at call sites #14864

Description

@christian-byrne

Captured from the incident-95 local-credits thread (2026-08-02). This design position was argued at length by @huntcsg and agreed in principle by @christian-byrne, and until now existed only in Slack. #14591 shipped a deliberately narrow slice of it; this issue records the rest so the direction is not lost.

Problem / Goal

A user's ability to do something is currently derived at each call site by combining several independent signals. Hunter's enumeration of what actually determines a capability today:

  • subscription tier level
  • feature flag
  • default entitlement (possibly a function of tier)
  • manually overridden entitlement
  • distribution (cloud, local, desktop)
  • role
  • JWT scope

And soon, per-object capabilities: canEdit, canDelete, canExecute on individual resources.

Every call site that recombines these itself is an opportunity to get it wrong. The P0 that started this thread is the worked example: isActiveSubscription is forced true off cloud, so a call site that reasonably read it as "this user has a subscription" produced a dead button on local builds. See also #14862 for three duplicate top-up gates that have since drifted apart.

Hunter's framing of the end state, quoted so it does not get paraphrased away:

I kind of think we want to move to a place where frontend ALWAYS calls the host (cloud.comfy.org or localhost:8188 or whatever), and core api itself can be trusted to respond back with what this particular instance supports.

The complication he raised against a purely backend-authoritative model: the frontend serves multiple genuinely different backends, and comfy-api / the cloud backend is not the local backend. So the resolution point cannot simply be "ask cloud".

Christian's counter-position, which #14591 implements: the combinatorial explosion should be enumerated once in a single module rather than spread across call sites, with app code only ever asking canUseCapability(x). That is code organization, not a change in where configuration lives, and it makes a later move toward API-supplied entitlements a small mechanical refactor rather than a rewrite.

Also unresolved from the same thread, and worth deciding explicitly: should a local-only account be modelled as free tier at all? Today creating an account locally provisions a cloud-backed account which reports subscription_tier: FREE. Deep Mehta confirmed everyone with a cloud account is free tier and that local users get nothing from it. If the semantic answer is "a local-only user is not a free-tier user", the current API shape is a standing source of this bug class.

Proposed Solution

Decide between, or sequence, these:

  1. Keep resolution in the frontend but in exactly one module (the PoC/RFC: Billing policy state for subscription capabilities #14591 direction), with call sites reduced to capability checks.
  2. Move resolution behind a host-served endpoint so each instance reports its own capabilities, with the frontend always talking to its own host.
  3. A hybrid: host-served entitlement facts, frontend-owned mapping from facts to UI capabilities.

Whichever is chosen needs an answer for per-object capabilities, since those cannot be a single global map.

Acceptance Criteria

  • A written decision (ADR under docs/adr/) naming the chosen resolution point and why.
  • The decision explicitly addresses per-object capabilities and the multiple-backends constraint.
  • The local-only-account-as-free-tier question is answered yes or no, with the API implication recorded.
  • Follow-up issues exist for whatever migration the decision implies.

Metadata

Metadata

Assignees

Labels

Public APIAffects or interacts with the public API surface (affecting custom node or extension authors)area:uiGeneral user interface and experience improvements

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions