Repository navigation
feat(paywall): gate relay + automations + remote workspaces, dither preview - #3850
Conversation
…review
Tightens the Pro paywall around three free-leaking surfaces and refreshes
the paywall preview shader.
UI gates
- Relay (Settings → Security): turning on "Allow remote workspaces to
access this device" now goes through gateFeature(REMOTE_WORKSPACES).
Off-toggling stays ungated and keeps the new on/off confirm dialog.
- Automations: sidebar entry is visible to free users (was hidden); click
routes through gateFeature(AUTOMATIONS). Direct URLs to /automations
redirect via a new layout-level guard.
- Remote / cloud workspaces in the v2-workspaces list: open + add-to-sidebar
on remote-device / cloud rows are gated. Local rows are unaffected. The
workspace page itself is intentionally not gated — server-side
host.checkAccess already returns paidPlan: false, so unpaid users hit a
data-layer block instead of an accidental modal lock-out.
Server-side defense (zero new round-trips)
- findOrgMembership now LEFT JOINs subscriptions in the same statement, so
callers gating on plan don't pay an extra query. Adds
requireActiveOrgMembershipWithSubscription sibling.
- automation.create / setEnabled / runNow refuse non-paid orgs.
- host.checkAccess returns { allowed, paidPlan }; relay's existing
5-min LRU caches the combined boolean — no per-request API spam.
- dispatchAutomation joins subscriptions into resolveTargetHost and
short-circuits unpaid runs with a recorded skip.
Paywall preview
- Replaces MeshGradient with a DitheredBackground (shaders-react Dithering
warp/4x4) ported from marketing, matching the new visual direction.
- Adds AutomationsDemo + RemoteWorkspacesDemo; drops obsolete
CloudWorkspacesDemo / IntegrationsDemo.
- Reorders PRO_FEATURES to remote-workspaces → automations →
team-collaboration → tasks → mobile-app and tightens the
remote-workspaces description so feature-switching no longer shifts
the modal height.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe changes introduce paywall gating for automations and remote workspaces features by replacing UI demo components, updating feature constants, and enforcing subscription/paid plan verification throughout the frontend and backend layers. Demo components swap from integrations/cloud-workspaces to automations/remote-workspaces, with corresponding database queries, TRPC endpoints, and API routes now validating paid plan status. Changes
Sequence DiagramsequenceDiagram
participant User as User
participant UI as Frontend UI
participant TRPC as TRPC Backend
participant DB as Database
rect rgba(100, 150, 200, 0.5)
Note over User,DB: Accessing Automations Feature
User->>UI: Navigate to automations
UI->>TRPC: Call automations route with user context
TRPC->>DB: requireActiveOrgMembershipWithSubscription(userId, orgId)
DB-->>TRPC: Return membership + subscription
alt Subscription is active/paid
TRPC->>UI: Route allowed, render layout
UI-->>User: Display automations content
else Subscription missing/inactive/free
TRPC-->>UI: Return FORBIDDEN error
UI->>User: Redirect to workspace list (gateFeature flow)
end
end
rect rgba(150, 100, 200, 0.5)
Note over User,DB: Running Automation (Paid Check)
User->>UI: Click "Run Now" on automation
UI->>TRPC: automationRouter.runNow()
TRPC->>DB: Verify subscription via requirePaidSubscription
alt Organization has paid/active subscription
TRPC->>DB: dispatchAutomation()
DB-->>TRPC: Dispatch with paidPlan: true
TRPC-->>UI: Success response
else No paid subscription
DB-->>TRPC: Return skipped_unpaid outcome
TRPC-->>UI: Error: feature requires paid plan
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds UI and server-side paywall gates for Relay, Automations, and Remote Workspaces, replacing the
Confidence Score: 4/5Safe to merge with the dispatch status mismatch resolved; the DB inconsistency will silently corrupt run-history data for free-plan orgs. One P1 finding (wrong skipped_offline status written to automation_runs for plan-blocked runs) and one P2 (uncached negative relay results). The P1 causes observable data corruption in the run history table that will mislead users and operators, warranting a 4 rather than 5. packages/trpc/src/router/automation/dispatch.ts (status mismatch) and apps/relay/src/access.ts (uncached denials).
|
| Filename | Overview |
|---|---|
| packages/trpc/src/router/automation/dispatch.ts | Adds plan check in resolveTargetHost and short-circuits unpaid automations, but calls recordSkipped (which always writes status: skipped_offline) while returning dispatch_failed — incorrect status in DB for plan-blocked runs. |
| apps/relay/src/access.ts | Correctly extends access check to require paidPlan, but only caches positive (ok = true) results — free-plan members bypass the LRU and hit the API on every relay check. |
| packages/db/src/utils/membership.ts | Refactors findOrgMembership to LEFT JOIN subscriptions in the same query, returning { membership, subscription } — eliminates round-trips for plan-gated callers; query is correct and limited to 1 row. |
| packages/trpc/src/router/automation/automation.ts | Gates create, setEnabled (enable-path only), and runNow behind requirePaidSubscription; correctly uses the new requireActiveOrgMembershipWithSubscription to avoid extra round-trips. |
| packages/trpc/src/router/host/host.ts | Extends checkAccess to LEFT JOIN subscriptions and return paidPlan alongside allowed; clean and additive change. |
| apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/layout.tsx | New layout guard for /automations routes that redirects free users and opens paywall; useCurrentPlan falls back to session plan during load so paid users avoid false redirects. |
| apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsx | Adds gateFeature to handleOpen and handleAddToSidebar for non-local-device workspaces; local-device rows pass through unconditionally as intended. |
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/trpc/src/router/automation/dispatch.ts
Line: 61-68
Comment:
**Wrong DB status recorded for unpaid-plan blocks**
`recordSkipped` hard-codes `status: "skipped_offline"` in every call (line 309 of the current file), so this new free-plan short-circuit writes `skipped_offline` to `automation_runs` even though the reason is a plan restriction, not a downed host. Any dashboard or alert that filters on `skipped_offline` to count "offline host" events will be silently inflated, and the run history shown to users will display a misleading "offline" reason for what was actually a paywall block.
Either add a new `"skipped_unpaid"` status to the `automationRuns` schema and pass it through `recordSkipped`, or change the return `status` here to `"skipped_offline"` to be consistent with the DB record — the latter is the minimal fix but the former is cleaner for observability.
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: apps/relay/src/access.ts
Line: 19-23
Comment:
**Negative results are never cached, defeating the anti-spam goal for free-plan members**
Only `ok = true` is written to the LRU cache. A free-plan org member (`allowed: true, paidPlan: false`) will see `ok = false` on every relay check and therefore hit the `host.checkAccess` API on every call — the PR description's "at most once per 5 min" guarantee only holds for paid users. Before this PR, non-members also experienced uncached denials, but this introduces a new class of legitimately-authenticated users (free-plan members) who will spam the endpoint.
Consider caching the combined boolean regardless of its value, or use a `Map<string, boolean>` so the cached entry can hold `false` explicitly.
How can I resolve this? If you propose a fix, please make it concise.Reviews (1): Last reviewed commit: "feat(paywall): gate relay + automations ..." | Re-trigger Greptile
| const { host, paidPlan } = resolved; | ||
| if (!paidPlan) { | ||
| const error = "automations require a Pro plan"; | ||
| const inserted = await recordSkipped( | ||
| automation, | ||
| scheduledFor, | ||
| host.machineId, | ||
| error, |
There was a problem hiding this comment.
Wrong DB status recorded for unpaid-plan blocks
recordSkipped hard-codes status: "skipped_offline" in every call (line 309 of the current file), so this new free-plan short-circuit writes skipped_offline to automation_runs even though the reason is a plan restriction, not a downed host. Any dashboard or alert that filters on skipped_offline to count "offline host" events will be silently inflated, and the run history shown to users will display a misleading "offline" reason for what was actually a paywall block.
Either add a new "skipped_unpaid" status to the automationRuns schema and pass it through recordSkipped, or change the return status here to "skipped_offline" to be consistent with the DB record — the latter is the minimal fix but the former is cleaner for observability.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/trpc/src/router/automation/dispatch.ts
Line: 61-68
Comment:
**Wrong DB status recorded for unpaid-plan blocks**
`recordSkipped` hard-codes `status: "skipped_offline"` in every call (line 309 of the current file), so this new free-plan short-circuit writes `skipped_offline` to `automation_runs` even though the reason is a plan restriction, not a downed host. Any dashboard or alert that filters on `skipped_offline` to count "offline host" events will be silently inflated, and the run history shown to users will display a misleading "offline" reason for what was actually a paywall block.
Either add a new `"skipped_unpaid"` status to the `automationRuns` schema and pass it through `recordSkipped`, or change the return `status` here to `"skipped_offline"` to be consistent with the DB record — the latter is the minimal fix but the former is cleaner for observability.
How can I resolve this? If you propose a fix, please make it concise.| const ok = result.allowed && result.paidPlan; | ||
| if (ok) { | ||
| allowedCache.set(key, true); | ||
| } | ||
| return result.allowed; | ||
| return ok; |
There was a problem hiding this comment.
Negative results are never cached, defeating the anti-spam goal for free-plan members
Only ok = true is written to the LRU cache. A free-plan org member (allowed: true, paidPlan: false) will see ok = false on every relay check and therefore hit the host.checkAccess API on every call — the PR description's "at most once per 5 min" guarantee only holds for paid users. Before this PR, non-members also experienced uncached denials, but this introduces a new class of legitimately-authenticated users (free-plan members) who will spam the endpoint.
Consider caching the combined boolean regardless of its value, or use a Map<string, boolean> so the cached entry can hold false explicitly.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/relay/src/access.ts
Line: 19-23
Comment:
**Negative results are never cached, defeating the anti-spam goal for free-plan members**
Only `ok = true` is written to the LRU cache. A free-plan org member (`allowed: true, paidPlan: false`) will see `ok = false` on every relay check and therefore hit the `host.checkAccess` API on every call — the PR description's "at most once per 5 min" guarantee only holds for paid users. Before this PR, non-members also experienced uncached denials, but this introduces a new class of legitimately-authenticated users (free-plan members) who will spam the endpoint.
Consider caching the combined boolean regardless of its value, or use a `Map<string, boolean>` so the cached entry can hold `false` explicitly.
How can I resolve this? If you propose a fix, please make it concise.
🧹 Preview Cleanup CompleteThe following preview resources have been cleaned up:
Thank you for your contribution! 🎉 |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/DitheredBackground/DitheredBackground.tsx (1)
1-33: Consider consolidating duplicatedDitheredBackgroundlogic into one shared component.This implementation matches the local version in
apps/desktop/src/renderer/routes/_authenticated/components/V1MigrationSummaryModal/V1MigrationSummaryModal.tsx(Line 245-274). Centralizing it would reduce drift when tuning shader props.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/DitheredBackground/DitheredBackground.tsx` around lines 1 - 33, Duplicate DitheredBackground logic should be consolidated into a single shared component: extract the current DitheredBackground function (the component using lazy import of Dithering, props interface DitheredBackgroundProps, and the JSX with Suspense/Dithering) into a common/shared UI module and export it; then update the other copy in V1MigrationSummaryModal to import and use that shared DitheredBackground instead of its local implementation. Ensure the shared component preserves the same props (colors, className) and the exact Dithering props (colorBack, colorFront, shape, type, speed, className, minPixelRatio) so behavior remains identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/layout.tsx`:
- Around line 14-18: The effect in the layout uses gateFeature (from usePaywall)
which is recreated each render, causing the dependency array to retrigger the
side effects and navigation repeatedly; add a one-shot guard via useRef (e.g.,
called hasRunRef) and check it at the start of the useEffect so the body runs
only once when allowed is false, call gateFeature(GATED_FEATURES.AUTOMATIONS, ()
=> {}) as before, and change navigate({ to: "/v2-workspaces" }) to navigate({
to: "/v2-workspaces", replace: true }) to avoid stacking history entries; update
the dependency array to still include allowed but rely on the ref to prevent
repeat runs.
In `@packages/trpc/src/router/automation/dispatch.ts`:
- Around line 62-71: The unpaid-plan branch records the run via recordSkipped
(which sets automation_runs.status = "skipped_offline") but returns status
"dispatch_failed", causing a mismatch; update the return in the unpaid-plan
branch in dispatch.ts to return the persisted run status (use inserted?.status
?? "skipped_offline" or explicitly "skipped_offline") and keep runId as
inserted?.id ?? null so the returned status aligns with the automation_runs
record created by recordSkipped.
---
Nitpick comments:
In
`@apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/DitheredBackground/DitheredBackground.tsx`:
- Around line 1-33: Duplicate DitheredBackground logic should be consolidated
into a single shared component: extract the current DitheredBackground function
(the component using lazy import of Dithering, props interface
DitheredBackgroundProps, and the JSX with Suspense/Dithering) into a
common/shared UI module and export it; then update the other copy in
V1MigrationSummaryModal to import and use that shared DitheredBackground instead
of its local implementation. Ensure the shared component preserves the same
props (colors, className) and the exact Dithering props (colorBack, colorFront,
shape, type, speed, className, minPixelRatio) so behavior remains identical.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: baab9e0a-7853-4bbe-8af3-042a6d1e0d58
📒 Files selected for processing (25)
apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/FeaturePreview.tsxapps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/AutomationsDemo/AutomationsDemo.tsxapps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/AutomationsDemo/index.tsapps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/CloudWorkspacesDemo/CloudWorkspacesDemo.tsxapps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/CloudWorkspacesDemo/index.tsapps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/DitheredBackground/DitheredBackground.tsxapps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/DitheredBackground/index.tsapps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/IntegrationsDemo.tsxapps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/index.tsapps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/RemoteWorkspacesDemo/RemoteWorkspacesDemo.tsxapps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/RemoteWorkspacesDemo/index.tsapps/desktop/src/renderer/components/Paywall/constants.tsapps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/layout.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarHeader/DashboardSidebarHeader.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspaces/components/V2WorkspacesList/components/V2WorkspaceRow/V2WorkspaceRow.tsxapps/desktop/src/renderer/routes/_authenticated/settings/integrations/components/IntegrationsSettings/IntegrationsSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/security/components/SecuritySettings/SecuritySettings.tsxapps/relay/src/access.tspackages/db/src/utils/membership.tspackages/trpc/src/router/automation/automation.tspackages/trpc/src/router/automation/dispatch.tspackages/trpc/src/router/host/host.tspackages/trpc/src/router/integration/utils.tspackages/trpc/src/router/organization/organization.tspackages/trpc/src/router/utils/active-org.ts
💤 Files with no reviewable changes (4)
- apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/index.ts
- apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/CloudWorkspacesDemo/index.ts
- apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/IntegrationsDemo/IntegrationsDemo.tsx
- apps/desktop/src/renderer/components/Paywall/components/FeaturePreview/components/CloudWorkspacesDemo/CloudWorkspacesDemo.tsx
There was a problem hiding this comment.
1 issue found across 25 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/db/src/utils/membership.ts">
<violation number="1" location="packages/db/src/utils/membership.ts:44">
P2: The LEFT JOIN may match multiple subscriptions per org (e.g., both an "active" and a "trialing" row during transitions). Without an explicit `ORDER BY`, `.limit(1)` returns a non-deterministic row. Consider adding an ordering preference (e.g., `active` before `trialing`, or most recently created) to ensure consistent results.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Restores the original `findOrgMembership` / `verifyOrgMembership` shapes so non-gating callers (organization, integration utils) keep their old return types, and moves the subscription join into dedicated siblings used only by plan-gating procedures. - findOrgMembershipWithSubscription / verifyOrgMembershipWithSubscription are the new joined helpers. - requireActiveOrgMembershipWithSubscription routes through the new verify*WithSubscription helper. - Reverts incidental rename churn in organization.ts.
- automation_runs: add `skipped_unpaid` status so paywall blocks don't inflate the `skipped_offline` metric. `recordSkipped` now takes the status as a parameter; dispatchAutomation uses it for the unpaid path and returns a matching `skipped_unpaid` outcome (was: persisting `skipped_offline` while returning `dispatch_failed` — both wrong). Includes drizzle migration 0040. - findOrgMembershipWithSubscription: deterministic ORDER BY (active before trialing, then most recent) so a brief active+trialing overlap doesn't return a non-deterministic row. - automations layout: useRef one-shot guard + replace navigation so the paywall + redirect don't re-fire on rerender (gateFeature isn't memoized) and don't stack history entries.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/db/src/utils/membership.ts (1)
58-61: Add a stable tie-breaker to make ordering fully deterministic.Line 60 can still tie for two rows with the same
statusandcreatedAt, which can produce nondeterministic row selection underlimit(1).Proposed refinement
.orderBy( sql`case ${subscriptions.status} when 'active' then 0 when 'trialing' then 1 else 2 end`, desc(subscriptions.createdAt), + // Add a final stable key (e.g., primary key) to break timestamp ties: + desc(subscriptions.id), )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/db/src/utils/membership.ts` around lines 58 - 61, The ordering can still tie when subscriptions.status and subscriptions.createdAt are equal; update the orderBy to include a stable tie-breaker (the subscription primary key or another unique column) so results are deterministic under limit(1). Concretely, after the existing case expression and createdAt ordering in the query that uses orderBy(...), add a deterministic secondary order key such as subscriptions.id (or subscriptions.uuid) with a fixed direction so identical status+createdAt rows have a consistent order when selecting the first row.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/trpc/src/router/automation/dispatch.ts`:
- Around line 225-253: The LEFT JOIN against subscriptions in the dbWs.select
that builds the row for v2Hosts (using v2Hosts, subscriptions, dbWs.select, and
later evaluated by isPaidPlan/isActiveSubscriptionStatus) can return an
arbitrary subscription when multiple active/trialing rows exist; make the
subscription selection deterministic by either adding an ORDER BY on a
subscriptions timestamp (e.g., subscriptions.createdAt or
subscriptions.updatedAt) or by changing the query to fetch all matching
subscriptions for the organization and then selecting the intended one in code
(e.g., newest/most-recent status) before computing paidPlan; update the
select/join to include that deterministic ordering or the post-query selection
logic so paidPlan gating is unambiguous.
---
Nitpick comments:
In `@packages/db/src/utils/membership.ts`:
- Around line 58-61: The ordering can still tie when subscriptions.status and
subscriptions.createdAt are equal; update the orderBy to include a stable
tie-breaker (the subscription primary key or another unique column) so results
are deterministic under limit(1). Concretely, after the existing case expression
and createdAt ordering in the query that uses orderBy(...), add a deterministic
secondary order key such as subscriptions.id (or subscriptions.uuid) with a
fixed direction so identical status+createdAt rows have a consistent order when
selecting the first row.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: a0c0b9bb-b20b-4bc4-be95-7ae15938530f
📒 Files selected for processing (9)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/components/PreviousRunsList/PreviousRunsList.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/layout.tsxpackages/db/drizzle/0040_automation_run_skipped_unpaid_status.sqlpackages/db/drizzle/meta/0040_snapshot.jsonpackages/db/drizzle/meta/_journal.jsonpackages/db/src/schema/enums.tspackages/db/src/utils/membership.tspackages/trpc/src/router/automation/automation.tspackages/trpc/src/router/automation/dispatch.ts
✅ Files skipped from review due to trivial changes (3)
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/$automationId/components/PreviousRunsList/PreviousRunsList.tsx
- packages/db/drizzle/meta/_journal.json
- packages/db/drizzle/0040_automation_run_skipped_unpaid_status.sql
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/desktop/src/renderer/routes/_authenticated/_dashboard/automations/layout.tsx
- packages/trpc/src/router/automation/automation.ts
Filters automation evaluate-cron to skip rows whose org isn't on a paid plan, so unpaid automations never enqueue, never enter dispatch, and never write a run row. This is the primary fix; the dispatch-side `skipped_unpaid` path stays as defense-in-depth (the subscription join is already paid for by host resolution, so it's free). Also simplifies the deterministic ORDER BY across the three subscription joins (membership / dispatch / host.checkAccess) to plain `desc(subscriptions.createdAt)` — pick the newest matching row, drop the contrived case-expression preference.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/db/src/utils/membership.ts`:
- Around line 27-31: The docstring for the function that mirrors
findOrgMembership but also fetches a subscription (referenced as "Same as
`findOrgMembership` but pulls the org's currently-paying subscription") is
misleading because the SQL join only filters status IN (active, trialing) and
can return free plans; update the docstring to say it returns the org's "active
or trialing subscription (may be free)" or, if you intended to only return
paying plans, add an extra predicate to the join/query to exclude free plans
(e.g., WHERE plan_type != 'free' or similar) and update the function
name/comment accordingly; make the change in the function that constructs the
membership+subscription join (the variant of findOrgMembership that returns
subscription).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 17ab845e-6b47-4fc9-8d06-101411789541
📒 Files selected for processing (4)
apps/api/src/app/api/automations/evaluate/route.tspackages/db/src/utils/membership.tspackages/trpc/src/router/automation/dispatch.tspackages/trpc/src/router/host/host.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/trpc/src/router/host/host.ts
- packages/trpc/src/router/automation/dispatch.ts
| /** | ||
| * Same as `findOrgMembership` but pulls the org's currently-paying subscription | ||
| * in the same statement, so callers gating on plan don't need a second | ||
| * round-trip. `subscription` is null when the org has no active/trialing row. | ||
| */ |
There was a problem hiding this comment.
Docstring overstates paid-plan filtering.
Line 28 says this returns the org’s “currently-paying subscription”, but the join only filters status IN (active, trialing) and can return a free plan. Please tighten wording or add a plan predicate if “paying” is intended.
Suggested doc fix
- * Same as `findOrgMembership` but pulls the org's currently-paying subscription
+ * Same as `findOrgMembership` but pulls the org's current active/trialing subscription
* in the same statement, so callers gating on plan don't need a second
- * round-trip. `subscription` is null when the org has no active/trialing row.
+ * round-trip. `subscription` is null when the org has no active/trialing row.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Same as `findOrgMembership` but pulls the org's currently-paying subscription | |
| * in the same statement, so callers gating on plan don't need a second | |
| * round-trip. `subscription` is null when the org has no active/trialing row. | |
| */ | |
| /** | |
| * Same as `findOrgMembership` but pulls the org's current active/trialing subscription | |
| * in the same statement, so callers gating on plan don't need a second | |
| * round-trip. `subscription` is null when the org has no active/trialing row. | |
| */ |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/db/src/utils/membership.ts` around lines 27 - 31, The docstring for
the function that mirrors findOrgMembership but also fetches a subscription
(referenced as "Same as `findOrgMembership` but pulls the org's currently-paying
subscription") is misleading because the SQL join only filters status IN
(active, trialing) and can return free plans; update the docstring to say it
returns the org's "active or trialing subscription (may be free)" or, if you
intended to only return paying plans, add an extra predicate to the join/query
to exclude free plans (e.g., WHERE plan_type != 'free' or similar) and update
the function name/comment accordingly; make the change in the function that
constructs the membership+subscription join (the variant of findOrgMembership
that returns subscription).
…patch
Now that the cron evaluate-step filters unpaid orgs out of the enqueue
batch, the dispatch-side unpaid check only fires in a tiny race window
between SELECT and dispatch. That doesn't justify a new DB enum +
migration: defense-in-depth bails before any work, but no longer writes
a run row.
- Reverts enum + 0040 migration.
- Reverts recordSkipped to its single-status form.
- Unpaid path returns { status: "skipped_unpaid", runId: null } without
a DB write (status mirrors the runtime-only "conflict" outcome).
…ription joins Replaces 5 inline ["active", "trialing"] arrays with the shared constant from @superset/shared/billing so adding a new active-equivalent status (e.g. past_due_grace) only needs one edit instead of grepping across queries.
…review (ParadevAI#3850) * feat(paywall): gate relay + automations + remote workspaces, dither preview Tightens the Pro paywall around three free-leaking surfaces and refreshes the paywall preview shader. UI gates - Relay (Settings → Security): turning on "Allow remote workspaces to access this device" now goes through gateFeature(REMOTE_WORKSPACES). Off-toggling stays ungated and keeps the new on/off confirm dialog. - Automations: sidebar entry is visible to free users (was hidden); click routes through gateFeature(AUTOMATIONS). Direct URLs to /automations redirect via a new layout-level guard. - Remote / cloud workspaces in the v2-workspaces list: open + add-to-sidebar on remote-device / cloud rows are gated. Local rows are unaffected. The workspace page itself is intentionally not gated — server-side host.checkAccess already returns paidPlan: false, so unpaid users hit a data-layer block instead of an accidental modal lock-out. Server-side defense (zero new round-trips) - findOrgMembership now LEFT JOINs subscriptions in the same statement, so callers gating on plan don't pay an extra query. Adds requireActiveOrgMembershipWithSubscription sibling. - automation.create / setEnabled / runNow refuse non-paid orgs. - host.checkAccess returns { allowed, paidPlan }; relay's existing 5-min LRU caches the combined boolean — no per-request API spam. - dispatchAutomation joins subscriptions into resolveTargetHost and short-circuits unpaid runs with a recorded skip. Paywall preview - Replaces MeshGradient with a DitheredBackground (shaders-react Dithering warp/4x4) ported from marketing, matching the new visual direction. - Adds AutomationsDemo + RemoteWorkspacesDemo; drops obsolete CloudWorkspacesDemo / IntegrationsDemo. - Reorders PRO_FEATURES to remote-workspaces → automations → team-collaboration → tasks → mobile-app and tightens the remote-workspaces description so feature-switching no longer shifts the modal height. * refactor(trpc): split membership-with-subscription into its own helper Restores the original `findOrgMembership` / `verifyOrgMembership` shapes so non-gating callers (organization, integration utils) keep their old return types, and moves the subscription join into dedicated siblings used only by plan-gating procedures. - findOrgMembershipWithSubscription / verifyOrgMembershipWithSubscription are the new joined helpers. - requireActiveOrgMembershipWithSubscription routes through the new verify*WithSubscription helper. - Reverts incidental rename churn in organization.ts. * fix(automations,paywall): address PR review - automation_runs: add `skipped_unpaid` status so paywall blocks don't inflate the `skipped_offline` metric. `recordSkipped` now takes the status as a parameter; dispatchAutomation uses it for the unpaid path and returns a matching `skipped_unpaid` outcome (was: persisting `skipped_offline` while returning `dispatch_failed` — both wrong). Includes drizzle migration 0040. - findOrgMembershipWithSubscription: deterministic ORDER BY (active before trialing, then most recent) so a brief active+trialing overlap doesn't return a non-deterministic row. - automations layout: useRef one-shot guard + replace navigation so the paywall + redirect don't re-fire on rerender (gateFeature isn't memoized) and don't stack history entries. * perf(automations): drop unpaid orgs at the cron, not in dispatch Filters automation evaluate-cron to skip rows whose org isn't on a paid plan, so unpaid automations never enqueue, never enter dispatch, and never write a run row. This is the primary fix; the dispatch-side `skipped_unpaid` path stays as defense-in-depth (the subscription join is already paid for by host resolution, so it's free). Also simplifies the deterministic ORDER BY across the three subscription joins (membership / dispatch / host.checkAccess) to plain `desc(subscriptions.createdAt)` — pick the newest matching row, drop the contrived case-expression preference. * refactor(automations): drop skipped_unpaid enum, bail silently in dispatch Now that the cron evaluate-step filters unpaid orgs out of the enqueue batch, the dispatch-side unpaid check only fires in a tiny race window between SELECT and dispatch. That doesn't justify a new DB enum + migration: defense-in-depth bails before any work, but no longer writes a run row. - Reverts enum + 0040 migration. - Reverts recordSkipped to its single-status form. - Unpaid path returns { status: "skipped_unpaid", runId: null } without a DB write (status mirrors the runtime-only "conflict" outcome). * refactor(billing): use ACTIVE_SUBSCRIPTION_STATUSES constant in subscription joins Replaces 5 inline ["active", "trialing"] arrays with the shared constant from @superset/shared/billing so adding a new active-equivalent status (e.g. past_due_grace) only needs one edit instead of grepping across queries.
Summary
automations/layout.tsx.host.checkAccessalready refuses unpaid tunnels.findOrgMembershipLEFT JOINssubscriptionsso plan checks ride the existing membership read. NewrequireActiveOrgMembershipWithSubscriptionsibling, used inautomation.create / setEnabled / runNow.host.checkAccessreturns{ allowed, paidPlan }(relay's 5-min LRU caches the combined boolean — no per-request API spam).dispatchAutomationjoins subscriptions intoresolveTargetHostand short-circuits unpaid runs with a recorded skip.MeshGradientfor aDitheredBackground(shaders-reactDitheringshape="warp"type="4x4") ported from marketing. AddsAutomationsDemo+RemoteWorkspacesDemo, drops obsoleteCloudWorkspacesDemo/IntegrationsDemo. ReordersPRO_FEATURESto remote-workspaces → automations → team-collaboration → tasks → mobile-app, tightens the remote-workspaces copy so feature-switching no longer shifts the modal height.Test plan
/settings/billing/plans./automationsand/automations/<id>→ paywall + redirect to/v2-workspaces.+(add to sidebar) on those rows → paywall. Local rows still open + add instantly./v2-workspace/<id>does not open the paywall (intentional). Page renders but host data fails to load (relay refuses tunnel).host.checkAccessreturnspaidPlan: false;automation.createreturns 403; QStash dispatch for a free org records a skipped run before relay/host calls.automation.createandhost.checkAccessissue the same number of round-trips before/after this change (subscription join rides the existing membership/access query).host.checkAccessfires at most once per 5 min per (token, hostId).bun run lint:fix && bun run typecheckclean.Summary by cubic
Gates relay, automations, and remote workspaces behind Pro, updates the paywall preview with a dithered background, and filters unpaid automations before enqueue; dispatch still bails without writing runs. Standardizes active-subscription checks using
ACTIVE_SUBSCRIPTION_STATUSES.New Features
@paper-design/shaders-react; added Automations and Remote Workspaces demos; removed obsolete demos; modal height stays steady.automationsandremote-workspaces; integrations settings open directly for all.Refactors
findOrgMembershipWithSubscriptionandrequireActiveOrgMembershipWithSubscription(select newest active/trialing subscription).create,setEnabled(when enabling), andrunNowrequire a paid plan; the evaluate cron only includes paid orgs; dispatch short-circuits unpaid orgs withskipped_unpaidand no run row.host.checkAccessreturns{ allowed, paidPlan }; relay requires both and caches for 5 minutes per (token, hostId).ACTIVE_SUBSCRIPTION_STATUSESfor consistent “active” checks.Written for commit cd3a9a7. Summary will update on new commits. Review in cubic
Summary by CodeRabbit
Release Notes
New Features
Chores