feat(billing): gate programmatic workflow execution behind a paid plan#5036
Conversation
Block free-plan accounts (hosted only) from running workflows programmatically: API-key/public execute, MCP server, A2A agent server, generic webhooks, and cross-origin chat embeds. Returns 402 (403 for chat embeds) with an upgrade message; provider webhooks, session/browser runs, internal-JWT executor traffic, and self-hosted are unaffected. - Add isApiExecutionEntitled / isWorkspaceApiExecutionEntitled gate helpers - Gate the execute, mcp/serve, a2a/serve, webhooks/trigger, and chat routes - Deploy modal: show an upgrade prompt on the API/MCP/A2A tabs for free users - Upgrade page: rename Pro feature to 'Deploy workflows as APIs'; API endpoint rate-limit row shows 0 for Free
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview API enforcement: Workflow Product UI: Deploy modal API/MCP/A2A tabs show Tests cover entitlement helpers, embed rules, and route 402/403 behavior. Reviewed by Cursor Bugbot for commit 9687476. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
@greptile review |
Greptile SummaryThis PR gates programmatic workflow execution (API key, public API, MCP server, A2A agent server, generic webhooks, cross-origin chat embeds) behind a paid plan on hosted deployments, returning 402 for free-plan callers. Provider webhooks (Slack, GitHub, etc.), session-based runs, and self-hosted deployments are explicitly left unaffected.
Confidence Score: 4/5Safe to merge for security correctness; one UI/API mismatch in the deploy modal should be addressed before the dark-rollout flag is used in production. All API routes correctly gate on the workspace billed account and return 402. The one concrete defect is in the deploy modal: its gate condition reads apps/sim/app/workspace/.../deploy-modal/deploy-modal.tsx — the Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Incoming request] --> B{Auth type?}
B -->|Session / Internal JWT| Z[Proceed - not gated]
B -->|API key| C[isWorkspaceApiExecutionEntitled]
B -->|Public API| C
B -->|MCP serve| C
B -->|A2A serve| D[isApiExecutionEntitled via billedUserId]
B -->|Generic webhook| C
B -->|Cross-origin chat embed| C
C --> E{isBillingEnabled AND isFreeApiDeploymentGateEnabled?}
D --> E
E -->|No - self-hosted or gate off| Z
E -->|Yes| F[getWorkspaceBilledAccountUserId]
F -->|null| Z2[Fail-open - allow]
F -->|userId| G[getHighestPrioritySubscription]
G -->|paid plan| Z
G -->|free / null| H[Return 402 / 403]
style H fill:#f66,color:#fff
style Z fill:#6a6,color:#fff
style Z2 fill:#fa6,color:#fff
Reviews (6): Last reviewed commit: "feat(billing): add FREE_API_DEPLOYMENT_G..." | Re-trigger Greptile |
These pre-existing tests exercise gated routes with API-key/generic-webhook paths; on hosted (CI) the gate now returns 402. Mock the gate as entitled by default and add a generic-webhook 402 coverage case.
|
@greptile review |
Greptile SummaryThis PR gates programmatic workflow execution (API key, public execute, MCP, A2A, generic webhooks, cross-origin chat embeds) behind a paid plan on hosted deployments, returning 402/403 for free-tier accounts. New
Confidence Score: 4/5The core billing helpers and most route integrations are correct and well-tested; the two points of concern are an error-response regression in the multi-webhook fan-out path and a visual flash in the deploy modal that does not affect backend enforcement. The webhook fan-out loop silently swallows 402 responses when multiple generic webhooks are all blocked, returning a misleading 500 to the caller. This is a real, reproducible failure: any credential-set fan-out with every entry being a blocked generic webhook triggers it. The execute route also uses the workflow creator's user ID for the public-API billing check rather than the workspace billing account, which could mismatch after org membership changes. apps/sim/app/api/webhooks/trigger/[path]/route.ts — the fan-out billing gate; apps/sim/app/api/workflows/[id]/execute/route.ts — the public-API billing identity. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Incoming programmatic request] --> B{Route type?}
B -->|API key / public execute| C[execute/route.ts]
B -->|MCP server| D[mcp/serve/route.ts]
B -->|A2A agent| E[a2a/serve/route.ts]
B -->|Webhook| F[webhooks/trigger/route.ts]
B -->|Chat embed| G[chat/utils.ts assertChatEmbedAllowed]
C --> H{isApiExecutionEntitled userId}
D --> I{isWorkspaceApiExecutionEntitled workspaceId}
E --> J{isApiExecutionEntitled billedUserId}
F --> K{provider === generic?}
G --> L{isHosted AND cross-origin?}
K -->|yes| M{isWorkspaceApiExecutionEntitled workspaceId}
K -->|no provider webhook| N[Allow]
H -->|not entitled| O[402]
I -->|not entitled| O
J -->|not entitled| O
M -->|not entitled single webhook| O
M -->|not entitled multi webhook| P[continue - may produce 500]
L -->|yes| Q{isWorkspaceApiExecutionEntitled}
H -->|entitled| R[Execute]
I -->|entitled| R
J -->|entitled| R
M -->|entitled| R
Q -->|not entitled| S[403]
Q -->|entitled| R
L -->|same origin| R
Reviews (2): Last reviewed commit: "test(billing): mock api-execution gate i..." | Re-trigger Greptile |
- execute route: gate on the workflow's workspace billed account (like MCP/A2A/ webhooks/chat) instead of the caller/creator's personal plan, so a paid workspace is never 402'd because an individual is on free - webhooks: all-generic-all-free fan-out now returns 402, not a 500 'No webhooks processed' fallback - deploy modal: hold the gate closed until the subscription query resolves (isFree(undefined) is true) to avoid flashing the upgrade wall at paid users
|
@greptile review |
|
Addressed the Bugbot/Greptile findings in 57a0f47:
Intentionally left as-is:
|
|
Re-review on 57a0f47 re-raised a few items — status: Already fixed in 57a0f47 (stale re-raises):
Fail-open findings (api-access.ts L39, chat utils L87) — not a real leak, leaving as-is: |
The paywall should follow billing enforcement, not the hostname. Keying off isHosted would still 402 free users on a hosted deployment with BILLING_ENABLED unset. Switch the server gate (api-access, chat embed) to isBillingEnabled and the deploy-modal UI to the client NEXT_PUBLIC_BILLING_ENABLED flag (matching the Inbox paywall), so a billing-disabled deployment skips the gate entirely.
|
@greptile review |
Gate the programmatic-execution paywall behind a dedicated backend env flag (combined with BILLING_ENABLED), off by default, so it can ship dark and be enabled per-deployment after a backend sanity check. Backend only — the deploy modal UI is unchanged.
|
@greptile review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 9687476. Configure here.
|
Both Bugbot items on the latest commit are re-raises of already-addressed findings (Bugbot re-reviews the full diff each push):
No new changes needed. |

Summary
execute, MCP server, A2A agent server, generic ("custom") webhooks, and cross-origin chat embeds. Returns 402 (403 for chat embeds) with an upgrade message.isApiExecutionEntitled/isWorkspaceApiExecutionEntitledhelpers (hosted-only, roll up org subs) gate each route inline.0for Free.Type of Change
Testing
Tested manually (curl: free API key → 402 on execute/MCP/A2A; generic webhook with valid token → 402; cross-origin chat → 403; first-party/session unaffected). Added/updated unit + route tests.
bun run lintandbun run check:api-validation:strictpass.Checklist