Comfy-Org / Comfy-Org/ComfyUI_frontend
RFC: centralize capability/entitlement resolution instead of deriving it at call sites
- Dominant language
- TypeScript
- Stars
- 2k
- Forks
- 699
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 490
Description
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. https://github.com/Comfy-Org/ComfyUI_frontend/pull/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 https://github.com/Comfy-Org/ComfyUI_frontend/issues/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 #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.
Contributor guide
Assessment
This issue has not been assessed yet.