Staging & sandbox workspaces#3919
Conversation
Expose stagingWorkspaceId on workspaces, set staging environment on create, and add sidebar UI to switch between live and staging.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
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:
📝 WalkthroughWalkthroughAdds WorkspaceEnvironment enum and fields; implements staging/sandbox workspaces and sync; gates payments/payouts and API/UI flows by environment; adds mock providers, copy-to-live actions and UI, workspace deletion/backfill/seed scripts, webhook workspace resolution, and provider/type updates. ChangesWorkspace Environment & Staging Infrastructure
Discounts & rewards, provider updates
APIs & UI gating
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
✨ Finishing Touches🧪 Generate unit tests (beta)
|
…staging-sandbox
Non-live workspaces use mock payout completion in the process cron, skip Stripe payment validation when confirming payouts, and get a demo card from the payment-methods API. Payout updates run before hybrid totals; apply success shows the program environment banner with refreshed banner styling.
Introduce createSandboxWorkspace with trial limits, rename createStagingWorkspace, and apply TRIAL_LIMITS when provisioning staging.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/web/app/(ee)/api/stripe/integration/webhook/customer-created.ts (1)
66-67:createNewCustomer(event)aligns with the resolvedworkspaceforprojectConnectId(so the drift concern for staging/sandbox mapping is unlikely).
resolveWebhookWorkspaceresolves bystripeConnectId: event.accountand, even in test-mode staging fallback, keepsstripeConnectIdequal to the samestripeAccountId.createNewCustomersetsprojectConnectId: event.account, matching the update path’sworkspace.stripeConnectId.
Optional refactor: threadworkspace(orworkspace.stripeConnectId) intocreateNewCustomerfor consistency/guarding against future resolver changes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/`(ee)/api/stripe/integration/webhook/customer-created.ts around lines 66 - 67, The current flow relies on resolveWebhookWorkspace(event.account) matching the projectConnectId used in createNewCustomer(event), but to guard against future resolver changes thread the resolved workspace (or at minimum workspace.stripeConnectId) into createNewCustomer so the create path explicitly uses the same workspace context; update the call site to await createNewCustomer(event, workspace) (or createNewCustomer(event, workspace.stripeConnectId)) and adjust the createNewCustomer signature and internal use of projectConnectId accordingly so both create and update paths use the identical workspace.stripeConnectId value.apps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.ts (1)
37-43: Confirm connect-id source consistency betweencheckout-session-completedandinvoice-paid
apps/web/app/(ee)/api/stripe/integration/webhook/route.tsresolvesworkspaceviaresolveWebhookWorkspace({ stripeAccountId: event.account!, mode }), andresolveWebhookWorkspacelooks upProjectbystripeConnectId: stripeAccountId—soworkspace.stripeConnectIdshould matchevent.accountfor this handler.- In
checkout-session-completed.ts, persistingprojectConnectIdusesstripeAccountIdin one path (projectConnectId: stripeAccountId, ~line 106) andworkspace.stripeConnectIdin another (projectConnectId: workspace.stripeConnectId, ~line 656); given the resolver, these are currently the same, but usingworkspace.stripeConnectIdconsistently would prevent future drift.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/`(ee)/api/stripe/integration/webhook/checkout-session-completed.ts around lines 37 - 43, The handler checkoutSessionCompleted mixes sources for the Stripe connect id when setting projectConnectId (sometimes using stripeAccountId, other times workspace.stripeConnectId); since route.ts resolves workspace via resolveWebhookWorkspace({ stripeAccountId: event.account!, mode }) and Projects are looked up by stripeConnectId, update checkoutSessionCompleted to always use workspace.stripeConnectId when persisting projectConnectId (replace any usage of stripeAccountId for projectConnectId with workspace.stripeConnectId) to keep the connect-id source consistent with invoice-paid and prevent future drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/lib/sandbox/components/workspace-environment-banner.tsx`:
- Around line 12-15: The exit-link currently rebuilds the href using only
usePathname(), dropping the query string; update WorkspaceEnvironmentBanner to
also read the current search params (useSearchParams from next/navigation), and
append them to the computed href so queries are preserved when switching
staging→live. Locate the computation that uses pathname.replace(...) inside
WorkspaceEnvironmentBanner and concatenate the existing search params (if
non-empty) as ?{searchParams.toString()} (or omit the ? when empty); ensure the
replacement logic (the string you remove/replace from pathname) remains
unchanged so only the query string behavior is fixed.
---
Nitpick comments:
In
`@apps/web/app/`(ee)/api/stripe/integration/webhook/checkout-session-completed.ts:
- Around line 37-43: The handler checkoutSessionCompleted mixes sources for the
Stripe connect id when setting projectConnectId (sometimes using
stripeAccountId, other times workspace.stripeConnectId); since route.ts resolves
workspace via resolveWebhookWorkspace({ stripeAccountId: event.account!, mode })
and Projects are looked up by stripeConnectId, update checkoutSessionCompleted
to always use workspace.stripeConnectId when persisting projectConnectId
(replace any usage of stripeAccountId for projectConnectId with
workspace.stripeConnectId) to keep the connect-id source consistent with
invoice-paid and prevent future drift.
In `@apps/web/app/`(ee)/api/stripe/integration/webhook/customer-created.ts:
- Around line 66-67: The current flow relies on
resolveWebhookWorkspace(event.account) matching the projectConnectId used in
createNewCustomer(event), but to guard against future resolver changes thread
the resolved workspace (or at minimum workspace.stripeConnectId) into
createNewCustomer so the create path explicitly uses the same workspace context;
update the call site to await createNewCustomer(event, workspace) (or
createNewCustomer(event, workspace.stripeConnectId)) and adjust the
createNewCustomer signature and internal use of projectConnectId accordingly so
both create and update paths use the identical workspace.stripeConnectId value.
🪄 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: dfe7d507-dab2-4c3c-90c7-89ac3e15ba56
📒 Files selected for processing (14)
apps/web/app/(ee)/api/stripe/integration/webhook/account-application-deauthorized.tsapps/web/app/(ee)/api/stripe/integration/webhook/charge-refunded.tsapps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.tsapps/web/app/(ee)/api/stripe/integration/webhook/coupon-deleted.tsapps/web/app/(ee)/api/stripe/integration/webhook/customer-created.tsapps/web/app/(ee)/api/stripe/integration/webhook/customer-subscription-created.tsapps/web/app/(ee)/api/stripe/integration/webhook/customer-subscription-deleted.tsapps/web/app/(ee)/api/stripe/integration/webhook/customer-updated.tsapps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.tsapps/web/app/(ee)/api/stripe/integration/webhook/promotion-code-updated.tsapps/web/app/(ee)/api/stripe/integration/webhook/route.tsapps/web/app/(ee)/api/stripe/integration/webhook/utils/resolve-webhook-workspace.tsapps/web/app/(ee)/api/stripe/integration/webhook/utils/types.tsapps/web/lib/sandbox/components/workspace-environment-banner.tsx
✅ Files skipped from review due to trivial changes (2)
- apps/web/app/(ee)/api/stripe/integration/webhook/utils/types.ts
- apps/web/app/(ee)/api/stripe/integration/webhook/utils/resolve-webhook-workspace.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/lib/sandbox/mock-payout-completion.ts`:
- Around line 117-121: The call to trackCommissionStatusUpdatesByProgram is
awaited before sendBatchEmail so if it throws the payout emails are skipped;
change the flow to ensure sendBatchEmail always runs by wrapping the tracking
call in a try/catch (or running it non-blocking) and logging the error without
rethrowing, then proceed to await sendBatchEmail as before; reference
trackCommissionStatusUpdatesByProgram (called with commissions, payouts,
newStatus: "paid") and sendBatchEmail so reviewers can find and update the calls
accordingly.
🪄 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: 6b6e4f5d-20d3-453b-8352-ab78e1709daf
📒 Files selected for processing (3)
apps/web/app/(ee)/partners.dub.co/(dashboard)/layout.tsxapps/web/lib/sandbox/mock-payout-completion.tsapps/web/ui/layout/main-nav.tsx
|
@coderabbitai full review please |
|
✅ Actions performedFull review triggered. |
There was a problem hiding this comment.
Actionable comments posted: 6
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/app/api/workspaces/[idOrSlug]/users/route.ts (1)
178-207:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not swallow failures in workspace-member removal flow.
Promise.allSettledis awaited but never inspected, soprojectUsers.delete/restrictedToken.deleteManyfailures can still return 200 and leave inconsistent state.Suggested fix
- await Promise.allSettled([ + const results = await Promise.allSettled([ // Remove the user from the workspace prisma.projectUsers.delete({ where: { userId_projectId: { projectId: workspace.id, userId, }, }, }), @@ workspace.slug === projectUser.user.defaultWorkspace && prisma.user.update({ @@ }), ]); + + const rejected = results.filter((r) => r.status === "rejected"); + if (rejected.length > 0) { + throw new DubApiError({ + code: "internal_server_error", + message: "Failed to fully remove member from workspace.", + }); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/api/workspaces/`[idOrSlug]/users/route.ts around lines 178 - 207, The code currently calls Promise.allSettled for the deletion/update operations (prisma.projectUsers.delete, prisma.restrictedToken.deleteMany, prisma.user.update) but never checks the settled results, which can swallow errors and return 200 on partial failure; replace the Promise.allSettled call with logic that either uses Promise.all (so any rejection bubbles up) or inspects the Promise.allSettled results and throws an error when any result.status === "rejected" (including the rejection reason) so the request fails on partial failures; ensure the check also handles the conditional workspace.slug === projectUser.user.defaultWorkspace branch and surfaces its error if it rejects.
♻️ Duplicate comments (5)
apps/web/lib/sandbox/components/workspace-environment-banner.tsx (1)
12-12:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPreserve query params when building the “Exit staging” link.
Line 68 rebuilds
hreffrompathnameonly, so active query state is dropped when switching workspaces.💡 Minimal fix
-import { usePathname } from "next/navigation"; +import { usePathname, useSearchParams } from "next/navigation"; @@ const pathname = usePathname(); + const searchParams = useSearchParams(); @@ + const query = searchParams.toString(); + const liveWorkspaceHref = liveWorkspace + ? `${pathname.replace(currentWorkspace.slug, liveWorkspace.slug)}${query ? `?${query}` : ""}` + : ""; @@ - href={pathname.replace(currentWorkspace.slug, liveWorkspace.slug)} + href={liveWorkspaceHref}Also applies to: 15-15, 68-68
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/sandbox/components/workspace-environment-banner.tsx` at line 12, The "Exit staging" link in WorkspaceEnvironmentBanner rebuilds href from usePathname() only and drops query parameters; preserve the active query state by reading current search params (via useSearchParams() or equivalent) and appending them to the href when constructing the link (keep the existing pathname value but append '?' + searchParams.toString() when non-empty). Update the code that constructs the href in the WorkspaceEnvironmentBanner component (the variable named href used for the Exit staging link) so query strings are preserved when switching workspaces.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/rewards/group-rewards.tsx (1)
187-196:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winPreserve modifier-click/new-tab behavior in the row click handler.
Line 188 calls
preventDefault()before intent checks, so modified clicks cannot open a new tab/window.💡 Minimal fix
onClick={(e) => { - e.preventDefault(); - if (isClickOnInteractiveChild(e)) return; + if (isClickOnInteractiveChild(e)) return; + if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) { + return; + } + e.preventDefault(); queryParams({ set: { rewardId: reward?.id ?? `new-${event}`,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/rewards/group-rewards.tsx around lines 187 - 196, The row onClick handler currently calls e.preventDefault() before checking intent, which blocks modifier-click/new-tab behavior; update the handler (the onClick arrow, referencing isClickOnInteractiveChild and queryParams) so you first detect and bail out for intentful clicks (if isClickOnInteractiveChild(e) returns true OR e.metaKey || e.ctrlKey || e.shiftKey || e.altKey OR e.button === 1), and only call e.preventDefault() when none of those are true; then proceed to call queryParams with reward?.id ?? `new-${event}` and scroll: false.apps/web/lib/sandbox/create-sandbox-workspace.ts (1)
26-45:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReject partial user resolution before creating the sandbox workspace.
This currently aborts only when zero users are found. If some requested emails are missing, the workspace is still created with a partial member list.
Suggested fix
if (users.length > 0) { usersFound = await prisma.user.findMany({ @@ }); - if (usersFound.length === 0) { - console.error( - "No users found. Please ensure the users exist in the database.", - ); - return; - } + const foundEmails = new Set(usersFound.map((u) => u.email)); + const missingEmails = users + .map((u) => u.email) + .filter((email) => !foundEmails.has(email)); + + if (missingEmails.length > 0) { + throw new Error( + `Cannot create sandbox workspace; missing users: ${missingEmails.join(", ")}`, + ); + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/sandbox/create-sandbox-workspace.ts` around lines 26 - 45, The code in create-sandbox-workspace aborts only when usersFound.length === 0, allowing creation with a partial member list; change the logic after the prisma.user.findMany call (symbols: users, usersFound, prisma.user.findMany) to verify that all requested emails were resolved (compare usersFound.length to users.length), compute which emails are missing, log a clear error listing the missing emails, and return/throw to prevent workspace creation when any requested user is not found.apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts (1)
246-255:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMake staging-workspace creation retryable after partial webhook success.
This is still keyed to the transition edge. If
project.updatesucceeds butcreateStagingWorkspacefails, retries will seeworkspace.plan === newPlanNameand skip creation forever.Suggested fix
- if ( - !workspace.stagingWorkspaceId && - wouldGainPartnerAccess({ - currentPlan: workspace.plan, - newPlan: newPlanName, - }) - ) { + const targetPlanHasPartnerAccess = wouldGainPartnerAccess({ + currentPlan: "free", + newPlan: newPlanName, + }); + + if (!workspace.stagingWorkspaceId && targetPlanHasPartnerAccess) { await createStagingWorkspace(workspace.id); console.log(`Created staging workspace for workspace ${workspace.id}.`); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/`(ee)/api/stripe/webhook/utils/update-workspace-plan.ts around lines 246 - 255, The current check uses wouldGainPartnerAccess({currentPlan: workspace.plan, newPlan: newPlanName}) so if workspace.plan already equals newPlanName (e.g. project.update succeeded but createStagingWorkspace failed) the branch never runs on retry; change the guard to create staging whenever the target plan grants partner access and there is no stagingWorkspaceId (i.e. if (!workspace.stagingWorkspaceId && planGrantsPartnerAccess(newPlanName)) ...), or replace wouldGainPartnerAccess with a helper that only checks the new plan's entitlement; keep using createStagingWorkspace(workspace.id) so retries will succeed if prior creation failed. Ensure you reference workspace.stagingWorkspaceId, workspace.plan, newPlanName, wouldGainPartnerAccess and createStagingWorkspace in the modification.apps/web/lib/sandbox/mock-payout-completion.ts (1)
117-121:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDon't let activity-log failures block payout notification emails.
trackCommissionStatusUpdatesByProgramis awaited beforesendBatchEmail. If tracking throws, payouts are already committed but partner emails are skipped.Consider wrapping in a try/catch to log and continue:
🛠️ Suggested fix
- await trackCommissionStatusUpdatesByProgram({ - commissions, - payouts, - newStatus: "paid", - }); + try { + await trackCommissionStatusUpdatesByProgram({ + commissions, + payouts, + newStatus: "paid", + }); + } catch (error) { + console.error("Failed to track commission status updates", { + invoiceId: invoice.id, + error, + }); + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/sandbox/mock-payout-completion.ts` around lines 117 - 121, The call to trackCommissionStatusUpdatesByProgram is awaited before sendBatchEmail so if tracking throws partner notification emails are skipped; wrap the trackCommissionStatusUpdatesByProgram invocation in a try/catch around the await, log the error (using the existing logger or console) inside the catch, and continue execution so sendBatchEmail still runs for the committed payouts; keep the same arguments and ensure the catch does not rethrow.
🧹 Nitpick comments (2)
packages/email/src/components/environment-banner.tsx (1)
17-20: 💤 Low valueConsider distinct styling for sandbox environment.
Currently, only
staginggets amber styling (Line 19), while all other non-production environments (includingsandbox) get blue. If sandbox should have its own visual identity, add a third condition.🎨 Proposed enhancement
<Section className={cn( "mb-4 rounded-lg px-4 py-2", - environment === "staging" ? "bg-amber-200" : "bg-blue-200", + environment === "staging" + ? "bg-amber-200" + : environment === "sandbox" + ? "bg-blue-200" + : "bg-purple-200", )} >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/email/src/components/environment-banner.tsx` around lines 17 - 20, The conditional styling in EnvironmentBanner uses a two-way ternary on the environment variable (in the className call using cn) so only "staging" gets amber and every other non-production (including "sandbox") falls back to blue; update the conditional to explicitly handle a "sandbox" case (e.g., a third branch that returns a distinct utility class such as a different bg- color) so sandbox has its own visual identity, keeping the check for "staging" and the fallback for production/others intact.apps/web/lib/sandbox/mock-payment-provider.ts (1)
1-20: 💤 Low valueInconsistent payment method types in mock provider.
SANDBOX_PAYMENT_METHODdeclarestype: "card"with card details, butretrievePaymentMethodalways returnstype: "us_bank_account". If these are meant to represent different test scenarios, consider documenting the intent or aligning the types for consistency.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/lib/sandbox/mock-payment-provider.ts` around lines 1 - 20, SANDBOX_PAYMENT_METHOD and MockPaymentProvider.retrievePaymentMethod return inconsistent payment types (card vs us_bank_account); update retrievePaymentMethod in MockPaymentProvider so it returns a matching card-typed object (including card.brand and card.last4) when the id matches SANDBOX_PAYMENT_METHOD.id, or else add branching to return different fixtures based on id/flag and document the behavior; reference SANDBOX_PAYMENT_METHOD, MockPaymentProvider.retrievePaymentMethod, and mockPaymentProvider when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/app/api/workspaces/`[idOrSlug]/billing/payment-methods/route.ts:
- Around line 24-26: The POST handler in apps/web/.../payment-methods/route.ts
currently creates real Stripe sessions for non-production workspaces; add the
same environment guard used for GET (check workspace.environment !==
WorkspaceEnvironment.production) at the top of the exported POST function and
return the sandbox response (e.g., NextResponse.json([SANDBOX_PAYMENT_METHOD])
or appropriate sandbox status) instead of creating Stripe sessions when not in
production; ensure you reference WorkspaceEnvironment.production,
SANDBOX_PAYMENT_METHOD and the POST handler so the early-return mirrors the GET
behavior.
In `@apps/web/app/api/workspaces/`[idOrSlug]/invites/reset/route.ts:
- Line 2: The helper throwIfStagingWorkspace currently only blocks
WorkspaceEnvironment.staging but the invite reset must also block sandbox
workspaces; update the guard logic inside throwIfStagingWorkspace (or
create/rename a new helper) to check for WorkspaceEnvironment.sandbox as well
(e.g., treat sandbox the same as staging) so that route handlers like the reset
invite route call a helper that prevents both staging and sandbox environments.
Ensure you reference the throwIfStagingWorkspace function/method and adjust any
callers if you rename or split the helper.
In `@apps/web/lib/sandbox/components/program-environment-banner.tsx`:
- Line 17: The banner's tailwind z-index string in
program-environment-banner.tsx ("fixed left-0 right-0 top-0 z-50 ...") places it
above the modal layer; change that z-50 to z-30 (to match
workspace-environment-banner) so the banner no longer obscures Radix Dialog
overlays/contents (or alternatively coordinate with packages/ui/src/modal.tsx to
raise the modal z to >50 if you prefer the banner to stay on top).
In `@apps/web/lib/zod/schemas/workspaces.ts`:
- Line 187: The WorkspaceSchema currently declares environment as nullable which
contradicts the Prisma workspace model and weakens type safety; update the
schema by making the environment field non-nullable (remove .nullable()) so
environment: z.enum(WorkspaceEnvironment) (or equivalent non-null zod enum)
matches the Prisma defaulted non-null WorkspaceEnvironment type, and ensure any
callers/validators rely on the non-null contract (e.g., places referencing
WorkspaceSchema.environment).
In `@apps/web/scripts/dev/seed-installation.ts`:
- Line 30: The script calls main() directly which drops errors and doesn't
guarantee Prisma disconnection; update the invocation to handle promise
rejection and always call prisma.$disconnect(): call main().catch(err => { /*
log or rethrow err */ }).finally(() => prisma.$disconnect()) or use an async
IIFE that try/catch/finally around await main() so that any thrown error is
logged/handled and prisma.$disconnect() is invoked in the finally block;
reference the main() function and the prisma instance in your change.
- Around line 1-3: The seed script imports prisma (which constructs new
PrismaClient at module scope) before loading environment variables, so
DATABASE_URL can be unset/stale; move the dotenv loader import
("dotenv-flow/config") to the very top of
apps/web/scripts/dev/seed-installation.ts so environment variables are
initialized before importing prisma or any other modules (e.g., references like
prisma and STRIPE_INTEGRATION_ID), ensuring the PrismaClient is created with the
correct env values.
---
Outside diff comments:
In `@apps/web/app/api/workspaces/`[idOrSlug]/users/route.ts:
- Around line 178-207: The code currently calls Promise.allSettled for the
deletion/update operations (prisma.projectUsers.delete,
prisma.restrictedToken.deleteMany, prisma.user.update) but never checks the
settled results, which can swallow errors and return 200 on partial failure;
replace the Promise.allSettled call with logic that either uses Promise.all (so
any rejection bubbles up) or inspects the Promise.allSettled results and throws
an error when any result.status === "rejected" (including the rejection reason)
so the request fails on partial failures; ensure the check also handles the
conditional workspace.slug === projectUser.user.defaultWorkspace branch and
surfaces its error if it rejects.
---
Duplicate comments:
In `@apps/web/app/`(ee)/api/stripe/webhook/utils/update-workspace-plan.ts:
- Around line 246-255: The current check uses
wouldGainPartnerAccess({currentPlan: workspace.plan, newPlan: newPlanName}) so
if workspace.plan already equals newPlanName (e.g. project.update succeeded but
createStagingWorkspace failed) the branch never runs on retry; change the guard
to create staging whenever the target plan grants partner access and there is no
stagingWorkspaceId (i.e. if (!workspace.stagingWorkspaceId &&
planGrantsPartnerAccess(newPlanName)) ...), or replace wouldGainPartnerAccess
with a helper that only checks the new plan's entitlement; keep using
createStagingWorkspace(workspace.id) so retries will succeed if prior creation
failed. Ensure you reference workspace.stagingWorkspaceId, workspace.plan,
newPlanName, wouldGainPartnerAccess and createStagingWorkspace in the
modification.
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/rewards/group-rewards.tsx:
- Around line 187-196: The row onClick handler currently calls
e.preventDefault() before checking intent, which blocks modifier-click/new-tab
behavior; update the handler (the onClick arrow, referencing
isClickOnInteractiveChild and queryParams) so you first detect and bail out for
intentful clicks (if isClickOnInteractiveChild(e) returns true OR e.metaKey ||
e.ctrlKey || e.shiftKey || e.altKey OR e.button === 1), and only call
e.preventDefault() when none of those are true; then proceed to call queryParams
with reward?.id ?? `new-${event}` and scroll: false.
In `@apps/web/lib/sandbox/components/workspace-environment-banner.tsx`:
- Line 12: The "Exit staging" link in WorkspaceEnvironmentBanner rebuilds href
from usePathname() only and drops query parameters; preserve the active query
state by reading current search params (via useSearchParams() or equivalent) and
appending them to the href when constructing the link (keep the existing
pathname value but append '?' + searchParams.toString() when non-empty). Update
the code that constructs the href in the WorkspaceEnvironmentBanner component
(the variable named href used for the Exit staging link) so query strings are
preserved when switching workspaces.
In `@apps/web/lib/sandbox/create-sandbox-workspace.ts`:
- Around line 26-45: The code in create-sandbox-workspace aborts only when
usersFound.length === 0, allowing creation with a partial member list; change
the logic after the prisma.user.findMany call (symbols: users, usersFound,
prisma.user.findMany) to verify that all requested emails were resolved (compare
usersFound.length to users.length), compute which emails are missing, log a
clear error listing the missing emails, and return/throw to prevent workspace
creation when any requested user is not found.
In `@apps/web/lib/sandbox/mock-payout-completion.ts`:
- Around line 117-121: The call to trackCommissionStatusUpdatesByProgram is
awaited before sendBatchEmail so if tracking throws partner notification emails
are skipped; wrap the trackCommissionStatusUpdatesByProgram invocation in a
try/catch around the await, log the error (using the existing logger or console)
inside the catch, and continue execution so sendBatchEmail still runs for the
committed payouts; keep the same arguments and ensure the catch does not
rethrow.
---
Nitpick comments:
In `@apps/web/lib/sandbox/mock-payment-provider.ts`:
- Around line 1-20: SANDBOX_PAYMENT_METHOD and
MockPaymentProvider.retrievePaymentMethod return inconsistent payment types
(card vs us_bank_account); update retrievePaymentMethod in MockPaymentProvider
so it returns a matching card-typed object (including card.brand and card.last4)
when the id matches SANDBOX_PAYMENT_METHOD.id, or else add branching to return
different fixtures based on id/flag and document the behavior; reference
SANDBOX_PAYMENT_METHOD, MockPaymentProvider.retrievePaymentMethod, and
mockPaymentProvider when making the change.
In `@packages/email/src/components/environment-banner.tsx`:
- Around line 17-20: The conditional styling in EnvironmentBanner uses a two-way
ternary on the environment variable (in the className call using cn) so only
"staging" gets amber and every other non-production (including "sandbox") falls
back to blue; update the conditional to explicitly handle a "sandbox" case
(e.g., a third branch that returns a distinct utility class such as a different
bg- color) so sandbox has its own visual identity, keeping the check for
"staging" and the fallback for production/others intact.
🪄 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: 2ad010ed-c982-483e-9f73-3c654475c800
📒 Files selected for processing (91)
apps/web/app/(ee)/api/cron/discount-codes/create/queue-batches/route.tsapps/web/app/(ee)/api/cron/discount-codes/create/route.tsapps/web/app/(ee)/api/cron/discount-codes/delete/route.tsapps/web/app/(ee)/api/cron/groups/remap-discount-codes/route.tsapps/web/app/(ee)/api/cron/payouts/charge-succeeded/queue-external-payouts.tsapps/web/app/(ee)/api/cron/payouts/charge-succeeded/route.tsapps/web/app/(ee)/api/cron/payouts/charge-succeeded/send-paypal-payouts.tsapps/web/app/(ee)/api/cron/payouts/process/process-payouts.tsapps/web/app/(ee)/api/cron/payouts/process/updates/route.tsapps/web/app/(ee)/api/cron/program-application-reminder/route.tsapps/web/app/(ee)/api/cron/workspaces/delete/delete-workspace.tsapps/web/app/(ee)/api/stripe/integration/route.tsapps/web/app/(ee)/api/stripe/integration/webhook/account-application-deauthorized.tsapps/web/app/(ee)/api/stripe/integration/webhook/charge-refunded.tsapps/web/app/(ee)/api/stripe/integration/webhook/checkout-session-completed.tsapps/web/app/(ee)/api/stripe/integration/webhook/coupon-deleted.tsapps/web/app/(ee)/api/stripe/integration/webhook/customer-created.tsapps/web/app/(ee)/api/stripe/integration/webhook/customer-subscription-created.tsapps/web/app/(ee)/api/stripe/integration/webhook/customer-subscription-deleted.tsapps/web/app/(ee)/api/stripe/integration/webhook/customer-updated.tsapps/web/app/(ee)/api/stripe/integration/webhook/invoice-paid.tsapps/web/app/(ee)/api/stripe/integration/webhook/promotion-code-updated.tsapps/web/app/(ee)/api/stripe/integration/webhook/route.tsapps/web/app/(ee)/api/stripe/integration/webhook/utils/resolve-webhook-workspace.tsapps/web/app/(ee)/api/stripe/integration/webhook/utils/types.tsapps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.tsapps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/apply/page.tsxapps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/apply/success/page.tsxapps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/header.tsxapps/web/app/(ee)/partners.dub.co/(apply)/[programSlug]/(default)/page.tsxapps/web/app/(ee)/partners.dub.co/(dashboard)/layout.tsxapps/web/app/api/tokens/route.tsapps/web/app/api/user/route.tsapps/web/app/api/workspaces/[idOrSlug]/billing/payment-methods/route.tsapps/web/app/api/workspaces/[idOrSlug]/invites/accept/route.tsapps/web/app/api/workspaces/[idOrSlug]/invites/reset/route.tsapps/web/app/api/workspaces/[idOrSlug]/invites/route.tsapps/web/app/api/workspaces/[idOrSlug]/route.tsapps/web/app/api/workspaces/[idOrSlug]/users/route.tsapps/web/app/app.dub.co/(auth)/invites/[code]/page.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/discounts/group-discounts.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/groups/[groupSlug]/rewards/group-rewards.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/members/page-client.tsxapps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/page-client.tsxapps/web/app/app.dub.co/(dashboard)/layout.tsxapps/web/lib/actions/partners/confirm-payouts.tsapps/web/lib/api/activity-log/track-reward-activity-log.tsapps/web/lib/api/workspaces/delete-workspace.tsapps/web/lib/client-access-check.tsapps/web/lib/discounts/create-discount-code.tsapps/web/lib/discounts/discount-provider-shopify.tsapps/web/lib/discounts/discount-provider-stripe.tsapps/web/lib/discounts/generate-discount-code-for-partner.tsapps/web/lib/fetchers/get-program.tsapps/web/lib/hooks/use-dashboard-banner-visible.tsapps/web/lib/partners/create-stablecoin-payout.tsapps/web/lib/partners/create-stripe-transfer.tsapps/web/lib/plan-capabilities.tsapps/web/lib/sandbox/components/copy-discount-to-live-modal.tsxapps/web/lib/sandbox/components/copy-reward-to-live-modal.tsxapps/web/lib/sandbox/components/program-environment-banner.tsxapps/web/lib/sandbox/components/workspace-environment-banner.tsxapps/web/lib/sandbox/components/workspace-environment-switcher.tsxapps/web/lib/sandbox/copy-discount-to-live.tsapps/web/lib/sandbox/copy-reward-to-live.tsapps/web/lib/sandbox/create-sandbox-workspace.tsapps/web/lib/sandbox/create-staging-workspace.tsapps/web/lib/sandbox/mock-payment-provider.tsapps/web/lib/sandbox/mock-payout-completion.tsapps/web/lib/sandbox/schemas.tsapps/web/lib/sandbox/sync-workspace.tsapps/web/lib/sandbox/throw-if-staging-workspace.tsapps/web/lib/zod/schemas/workspaces.tsapps/web/playwright/workspaces/billing-trial.spec.tsapps/web/scripts/dev/seed-installation.tsapps/web/scripts/migrations/backfill-staging-workspaces.tsapps/web/ui/account/update-default-workspace.tsxapps/web/ui/layout/main-nav.tsxapps/web/ui/layout/sidebar/workspace-dropdown.tsxapps/web/ui/layout/upgrade-banner.tsxapps/web/ui/workspaces/delete-workspace.tsxapps/web/ui/workspaces/upload-logo.tsxapps/web/ui/workspaces/workspace-selector.tsxpackages/email/src/components/environment-banner.tsxpackages/email/src/templates/partner-payout-confirmed.tsxpackages/email/src/templates/partner-payout-processed.tsxpackages/email/src/types.tspackages/prisma/client.tspackages/prisma/schema/workspace.prismapackages/ui/src/icons/nucleo/index.tspackages/ui/src/icons/nucleo/isolated-cube.tsx
| return ( | ||
| <div | ||
| className={cn( | ||
| "fixed left-0 right-0 top-0 z-50 flex h-12 items-center justify-between gap-4 overflow-hidden px-6 text-neutral-800", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for z-index usage in the codebase
rg -n 'z-\d+' --type=ts --type=tsx -g '!node_modules' | head -50Repository: dubinc/dub
Length of output: 82
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Find exact z-50 usages in TS/TSX
echo "== occurrences of z-50 =="
rg -n 'z-50' -S -g '*.ts' -g '*.tsx' -g '!node_modules/**' .
# 2) Find other numeric z-* usages (Tailwind default pattern)
echo
echo "== occurrences of z-[0-9]+ (numeric z-*) =="
rg -n 'z-[0-9]+' -S -g '*.ts' -g '*.tsx' -g '!node_modules/**' . || true
# 3) Find z-auto / arbitrary z-[...] usages
echo
echo "== occurrences of z-auto / arbitrary z-[...] =="
rg -n 'z-auto|z-\[.*?\]' -S -g '*.ts' -g '*.tsx' -g '!node_modules/**' . || true
# 4) Locate Tailwind config and inspect any custom zIndex scale
echo
echo "== Tailwind config files =="
fd -t f 'tailwind.config.*' . || true
echo
echo "== theme.extend.zIndex (any Tailwind config) =="
rg -n 'zIndex' -S -g 'tailwind.config.*' . || trueRepository: dubinc/dub
Length of output: 49255
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the specific banner line region for context (kept small)
TARGET="apps/web/lib/sandbox/components/program-environment-banner.tsx"
if [ -f "$TARGET" ]; then
echo "== banner file context =="
nl -ba "$TARGET" | sed -n '1,120p'
else
echo "Missing $TARGET"
fiRepository: dubinc/dub
Length of output: 141
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Print relevant snippets around each z-50 usage to understand intent/stacking.
files=(
"apps/web/lib/sandbox/components/program-environment-banner.tsx"
"apps/web/lib/sandbox/components/workspace-environment-banner.tsx"
"apps/web/ui/layout/upgrade-banner.tsx"
"apps/web/ui/layout/main-nav.tsx"
"apps/web/ui/support/chat-bubble.tsx"
"packages/ui/src/modal.tsx"
"packages/ui/src/popover.tsx"
"packages/ui/src/sheet.tsx"
)
for f in "${files[@]}"; do
echo
echo "===== $f ====="
if [ -f "$f" ]; then
# Show first 120 lines and also any line range likely to include the z-50 usage.
# (Avoid huge output; these files should be small.)
sed -n '1,140p' "$f" | head -n 140
else
echo "Missing: $f"
fi
done
echo
echo "===== Tailwind config: check for custom zIndex ====="
tailwind_cfgs=(
"apps/web/tailwind.config.ts"
"packages/embeds/react/tailwind.config.ts"
"packages/tailwind-config/tailwind.config.ts"
"packages/ui/tailwind.config.ts"
)
for tc in "${tailwind_cfgs[@]}"; do
echo
echo "---- $tc (zIndex search) ----"
if [ -f "$tc" ]; then
rg -n "zIndex|theme.*zIndex" "$tc" || true
rg -n "extend.*zIndex" "$tc" || true
else
echo "Missing: $tc"
fi
doneRepository: dubinc/dub
Length of output: 26886
Align banner z-index with modal layers to avoid obscuring dialogs
apps/web/lib/sandbox/components/program-environment-banner.tsx uses z-50, but packages/ui/src/modal.tsx sets the desktop Radix Dialog.Overlay/Dialog.Content to z-40; with both fixed, this banner can stack above the modal backdrop/content. workspace-environment-banner.tsx already uses z-30, so match the sandbox banner (or raise the modal layer) to keep critical UI on top.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/lib/sandbox/components/program-environment-banner.tsx` at line 17,
The banner's tailwind z-index string in program-environment-banner.tsx ("fixed
left-0 right-0 top-0 z-50 ...") places it above the modal layer; change that
z-50 to z-30 (to match workspace-environment-banner) so the banner no longer
obscures Radix Dialog overlays/contents (or alternatively coordinate with
packages/ui/src/modal.tsx to raise the modal z to >50 if you prefer the banner
to stay on top).
Move program copy into createStagingProgram and trigger both steps asynchronously from the Stripe plan upgrade webhook.
Summary by CodeRabbit
New Features
Improvements