Repository navigation
feat(mobile,trpc,auth,db): grace-period account deletion with reactivation - #6395
Conversation
App Store guideline 5.1.1(v) requires apps with account creation to offer in-app account deletion. Adds a user.deleteAccount mutation — cancels active Stripe subscriptions on organizations where the user is the sole member, deletes those orgs, then deletes the user (sessions, accounts, and memberships cascade via FK) — and a destructive confirm flow in Settings → Account. better-auth's built-in deleteUser was not used: it requires a <1-day-fresh session, which 30-day OAuth sessions fail.
|
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:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAccount deletion now uses a 30-day recovery period. The server marks accounts as pending deletion, revokes credentials, and restricts access. Mobile, desktop, and web screens support deletion, reactivation, purge timing, support contact, and logout. ChangesAccount deletion lifecycle
Estimated code review effort: 4 (Complex) | ~60 minutes Mergeability Score: ⚪ Minimal · up to The account-deletion grace-period and reactivation changes are merge-ready after normal checks and review; no actionable merge-blocking risk remains. Sequence Diagram(s)sequenceDiagram
participant AccountSettings
participant UserRouter
participant Database
participant RouteGuard
participant RecoveryScreen
AccountSettings->>UserRouter: deleteAccount()
UserRouter->>Database: Set deletedAt and revoke credentials
UserRouter-->>AccountSettings: Return success
RouteGuard->>RecoveryScreen: Render pending-deletion view
RecoveryScreen->>UserRouter: reactivateAccount()
UserRouter->>Database: Clear deletedAt within grace period
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 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 |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/mobile/screens/`(authenticated)/settings/account/AccountSettingsScreen.tsx:
- Around line 17-36: Update handleDeleteAccount so selecting the initial “Delete
account” action presents a second destructive confirmation alert before invoking
deleteAccount(). Keep the existing cancellation behavior and deletion-error
alert, and call deleteAccount only from the second confirmation’s destructive
action.
In `@packages/trpc/src/router/user/user.ts`:
- Around line 65-90: Update the organization cleanup flow around the membership
count query and organizations deletion to prevent deleting an organization that
gains another member: use an atomic, lock-protected database operation or
persisted retryable cleanup state that revalidates membership immediately before
deletion. Move Stripe cancellation out of any long-lived database transaction,
and persist or retry its cleanup state so Stripe or database failures do not
leave earlier deletions committed while reporting a failed user deletion.
Preserve the final users deletion only after organization cleanup is safely
recorded or completed.
- Around line 77-83: Update the active subscription cleanup around
stripeClient.subscriptions.list to iterate through all paginated results using
autoPagingEach or the Stripe async iterator, cancelling every active
subscription before organization deletion. Preserve the existing cancellation
behavior for each subscription.
🪄 Autofix
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 Plus
Run ID: da64057e-38c9-4e01-9d41-e2ef8cc89f7c
📒 Files selected for processing (4)
apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsxapps/mobile/screens/(authenticated)/settings/hooks/useDeleteAccount/index.tsapps/mobile/screens/(authenticated)/settings/hooks/useDeleteAccount/useDeleteAccount.tspackages/trpc/src/router/user/user.ts
| const handleDeleteAccount = () => { | ||
| Alert.alert( | ||
| "Delete account?", | ||
| "This permanently deletes your account, along with any workspaces and data in organizations where you are the only member. This cannot be undone.", | ||
| [ | ||
| { style: "cancel", text: "Cancel" }, | ||
| { | ||
| style: "destructive", | ||
| text: "Delete account", | ||
| onPress: () => { | ||
| deleteAccount().catch(() => { | ||
| Alert.alert( | ||
| "Could not delete account", | ||
| "Something went wrong. Try again, or contact support@superset.sh.", | ||
| ); | ||
| }); | ||
| }, | ||
| }, | ||
| ], | ||
| ); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Add the required second destructive confirmation.
This flow has one confirmation dialog. The Cancel and Delete account buttons do not create two confirmation steps.
Show a second destructive alert before calling deleteAccount(). This prevents an accidental permanent deletion and satisfies the PR objective.
Proposed change
onPress: () => {
- deleteAccount().catch(() => {
- Alert.alert(
- "Could not delete account",
- "Something went wrong. Try again, or contact support@superset.sh.",
- );
- });
+ Alert.alert(
+ "Delete account permanently?",
+ "This action cannot be undone.",
+ [
+ { style: "cancel", text: "Cancel" },
+ {
+ style: "destructive",
+ text: "Delete permanently",
+ onPress: () =>
+ deleteAccount().catch(() => {
+ Alert.alert(
+ "Could not delete account",
+ "Something went wrong. Try again, or contact support@superset.sh.",
+ );
+ }),
+ },
+ ],
+ );
},📝 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.
| const handleDeleteAccount = () => { | |
| Alert.alert( | |
| "Delete account?", | |
| "This permanently deletes your account, along with any workspaces and data in organizations where you are the only member. This cannot be undone.", | |
| [ | |
| { style: "cancel", text: "Cancel" }, | |
| { | |
| style: "destructive", | |
| text: "Delete account", | |
| onPress: () => { | |
| deleteAccount().catch(() => { | |
| Alert.alert( | |
| "Could not delete account", | |
| "Something went wrong. Try again, or contact support@superset.sh.", | |
| ); | |
| }); | |
| }, | |
| }, | |
| ], | |
| ); | |
| const handleDeleteAccount = () => { | |
| Alert.alert( | |
| "Delete account?", | |
| "This permanently deletes your account, along with any workspaces and data in organizations where you are the only member. This cannot be undone.", | |
| [ | |
| { style: "cancel", text: "Cancel" }, | |
| { | |
| style: "destructive", | |
| text: "Delete account", | |
| onPress: () => { | |
| Alert.alert( | |
| "Delete account permanently?", | |
| "This action cannot be undone.", | |
| [ | |
| { style: "cancel", text: "Cancel" }, | |
| { | |
| style: "destructive", | |
| text: "Delete permanently", | |
| onPress: () => | |
| deleteAccount().catch(() => { | |
| Alert.alert( | |
| "Could not delete account", | |
| "Something went wrong. Try again, or contact support@superset.sh.", | |
| ); | |
| }), | |
| }, | |
| ], | |
| ); | |
| }, | |
| }, | |
| ], | |
| ); |
🤖 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/mobile/screens/`(authenticated)/settings/account/AccountSettingsScreen.tsx
around lines 17 - 36, Update handleDeleteAccount so selecting the initial
“Delete account” action presents a second destructive confirmation alert before
invoking deleteAccount(). Keep the existing cancellation behavior and
deletion-error alert, and call deleteAccount only from the second confirmation’s
destructive action.
| for (const membership of memberships) { | ||
| const [memberCount] = await db | ||
| .select({ value: count() }) | ||
| .from(members) | ||
| .where(eq(members.organizationId, membership.organizationId)); | ||
| if ((memberCount?.value ?? 0) > 1) continue; | ||
|
|
||
| const organization = await db.query.organizations.findFirst({ | ||
| where: eq(organizations.id, membership.organizationId), | ||
| columns: { id: true, stripeCustomerId: true }, | ||
| }); | ||
| if (organization?.stripeCustomerId) { | ||
| const activeSubscriptions = await stripeClient.subscriptions.list({ | ||
| customer: organization.stripeCustomerId, | ||
| status: "active", | ||
| }); | ||
| for (const subscription of activeSubscriptions.data) { | ||
| await stripeClient.subscriptions.cancel(subscription.id); | ||
| } | ||
| } | ||
| await db | ||
| .delete(organizations) | ||
| .where(eq(organizations.id, membership.organizationId)); | ||
| } | ||
|
|
||
| await db.delete(users).where(eq(users.id, userId)); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Make organization cleanup atomic and failure-safe.
A member can join an organization after Line 70 and before Line 87. The mutation can then delete an organization that became shared.
If a later Stripe or database operation fails, earlier organization deletions remain committed while the mutation reports failure. The mobile client then shows a failure alert after partial account deletion.
Lock each organization against membership changes during the member check and database deletion. Use a persisted deletion workflow or retryable cleanup state for Stripe side effects. Do not keep a database transaction open during Stripe requests.
🤖 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/trpc/src/router/user/user.ts` around lines 65 - 90, Update the
organization cleanup flow around the membership count query and organizations
deletion to prevent deleting an organization that gains another member: use an
atomic, lock-protected database operation or persisted retryable cleanup state
that revalidates membership immediately before deletion. Move Stripe
cancellation out of any long-lived database transaction, and persist or retry
its cleanup state so Stripe or database failures do not leave earlier deletions
committed while reporting a failed user deletion. Preserve the final users
deletion only after organization cleanup is safely recorded or completed.
| const activeSubscriptions = await stripeClient.subscriptions.list({ | ||
| customer: organization.stripeCustomerId, | ||
| status: "active", | ||
| }); | ||
| for (const subscription of activeSubscriptions.data) { | ||
| await stripeClient.subscriptions.cancel(subscription.id); | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the declared Stripe SDK version and existing pagination patterns.
fd -a '^package\.json$' . -E node_modules -0 |
xargs -0 rg -n -C2 '"stripe"|`@superset/auth`'
rg -n -C3 'subscriptions\.list|autoPagingEach|starting_after|has_more' packages appsRepository: superset-sh/superset
Length of output: 5304
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- user deletion mutation ---'
sed -n '45,105p' packages/trpc/src/router/user/user.ts
printf '%s\n' '--- existing organization deletion hook ---'
sed -n '535,570p' packages/auth/src/server.ts
printf '%s\n' '--- Stripe package metadata ---'
rg -n -C3 '"stripe": "20\.4\.1"|stripe@20\.4\.1|subscriptions\.list' bun.lock* package.json packages appsRepository: superset-sh/superset
Length of output: 6179
🌐 Web query:
Stripe Node.js SDK 20.4.1 subscriptions.list autoPagingEach pagination has_more documentation
💡 Result:
In the Stripe Node.js SDK, the autoPagingEach method automates the process of traversing all pages in a list request, abstracting away the need for you to manually manage the has_more flag or pagination cursors (starting_after/ending_before) [1][2][3]. When you call.autoPagingEach on a list method (such as stripe.subscriptions.list), the SDK handles the underlying API logic for you [2][4]: 1. Internal Pagination: The SDK performs an initial request and inspects the has_more property in the response [5][6][7]. 2. Automatic Fetching: If has_more is true, the SDK automatically executes subsequent API calls to fetch the next page, using the appropriate cursor to maintain continuity [5][6][7]. 3. Iteration: It yields each object to your provided callback function [2][8]. 4. Termination: The iteration stops automatically when the API returns a response where has_more is false [6][7]. You do not need to check has_more yourself when using autoPagingEach; the SDK manages this state internally to ensure you retrieve all objects matching your initial query parameters [1][2][7]. Usage Example: await stripe.subscriptions.list({ limit: 100 }).autoPagingEach(async (subscription) => { // The SDK handles pagination; this function runs for every subscription. console.log(subscription.id); // Return false to break the iteration early if needed. if (shouldStop) { return false; } }); If you are using a modern Node.js environment that supports async iterators, you can also use a for-await-of loop, which provides the same auto-pagination functionality: for await (const subscription of stripe.subscriptions.list({ limit: 100 })) { console.log(subscription.id); } The has_more property is a standard part of the Stripe API list response object, which the SDK's auto-pagination helper uses to determine if another request is necessary [5][6][7]. When manually handling pagination, you would typically check has_more to decide whether to make another request using the ID of the last object as the starting_after cursor [7].
Citations:
- 1: https://docs.stripe.com/api/pagination/auto?lang=node
- 2: https://github.com/stripe/stripe-node?tab=readme-ov-file
- 3: https://docs.stripe.com/api/pagination
- 4: https://docs.stripe.com/api/pagination/auto
- 5: https://github.com/stripe/stripe-node/blob/625f1988/src/autoPagination.ts
- 6: https://github.com/stripe/stripe-node/blob/47cdb5eb/src/autoPagination.ts
- 7: https://docs.stripe.com/pagination
- 8: https://github.com/stripe/stripe-node/blob/master/README.md
Paginate active subscriptions before deleting the organization.
stripeClient.subscriptions.list() returns a paginated list, but this loop cancels only the current page. Use autoPagingEach or an async iterator to cancel every active subscription.
🤖 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/trpc/src/router/user/user.ts` around lines 77 - 83, Update the
active subscription cleanup around stripeClient.subscriptions.list to iterate
through all paginated results using autoPagingEach or the Stripe async iterator,
cancelling every active subscription before organization deletion. Preserve the
existing cancellation behavior for each subscription.
🧹 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: 4
🤖 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 `@packages/auth/src/server.ts`:
- Around line 139-155: Replace getSessionFromCtx in the before hook with
getAuthoritativeSessionFromCtx so pending-deletion checks use current account
state; apply the same authoritative lookup to every other pending-deletion
access-control boundary. Preserve the allowed-path bypass and existing FORBIDDEN
response.
In `@packages/trpc/src/router/user/user.ts`:
- Around line 103-106: Update user.deleteAccount so the database update sets
deletedAt only when it is currently NULL, preserving the original timestamp for
accounts already pending deletion. Ensure repeated deletion requests still
return success without extending the recovery period.
- Around line 84-94: Update the alternate-owner count in the ownership check to
join the users table and filter with isNull(users.deletedAt), while retaining
the existing organization, owner-role, and excluding-current-user predicates.
Keep the zero-count behavior unchanged so only active alternate owners prevent
deletion.
- Around line 103-113: Wrap the user soft-delete and all credential cleanup
operations in a single dbWs.transaction, including the users update, sessions
deletion, oauthAccessTokens deletion, and oauthRefreshTokens deletion. Use the
transaction handle for every query so any failure rolls back the deletedAt
update and prior deletions.
🪄 Autofix
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 Plus
Run ID: e95d52a4-f44b-491a-89cf-ddd56136cfbf
📒 Files selected for processing (16)
apps/mobile/app/account-pending-deletion.tsxapps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsxapps/mobile/screens/RootLayout/RootLayout.tsxapps/mobile/screens/account-pending-deletion/AccountPendingDeletionScreen.tsxapps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/index.tsapps/mobile/screens/account-pending-deletion/hooks/useDaysUntilPurge/useDaysUntilPurge.tsapps/mobile/screens/account-pending-deletion/index.tspackages/auth/src/server.tspackages/db/drizzle/0066_add_user_deleted_at.sqlpackages/db/drizzle/meta/0066_snapshot.jsonpackages/db/drizzle/meta/_journal.jsonpackages/db/src/schema/auth.tspackages/shared/src/constants.tspackages/trpc/src/router/admin/admin.tspackages/trpc/src/router/user/user.tspackages/trpc/src/trpc.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx
| hooks: { | ||
| before: createAuthMiddleware(async (ctx) => { | ||
| if ( | ||
| PENDING_DELETION_ALLOWED_PATH_PREFIXES.some((prefix) => | ||
| ctx.path.startsWith(prefix), | ||
| ) | ||
| ) { | ||
| return; | ||
| } | ||
| const session = await getSessionFromCtx(ctx); | ||
| if ((session?.user as { deletedAt?: Date | null })?.deletedAt) { | ||
| throw new APIError("FORBIDDEN", { | ||
| message: "Account is pending deletion.", | ||
| }); | ||
| } | ||
| }), | ||
| }, |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- server.ts relevant sections ---'
sed -n '1,230p' packages/auth/src/server.ts
printf '%s\n' '--- auth/session/deletion references ---'
rg -n -S \
'cookieCache|getSessionFromCtx|PENDING_DELETION_ALLOWED_PATH_PREFIXES|deletedAt|pending deletion|delete.*session|session.*delete' \
packages/authRepository: superset-sh/superset
Length of output: 8212
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- package versions ---'
rg -n -S '"better-auth"|better-auth@' package.json bun.lockb bun.lock packages --glob 'package.json' --glob 'bun.lock*' 2>/dev/null || true
printf '%s\n' '--- deletion and session-related implementation ---'
sed -n '820,930p' packages/auth/src/server.ts
rg -n -S \
'delete.*account|account.*delete|deleted_at|deletedAt|deleteMany|revoke|session' \
packages apps --glob '*.{ts,tsx,js,jsx}' --glob '!**/node_modules/**' | head -300
printf '%s\n' '--- auth schema ---'
fd -i 'auth' packages/db
rg -n -S 'deletedAt|deleted_at|sessions|users' packages/db --glob '*.{ts,tsx}'Repository: superset-sh/superset
Length of output: 49109
🌐 Web query:
Better Auth cookieCache maxAge revoked sessions remain active documentation getSessionFromCtx disable cache
💡 Result:
In Better Auth, the cookieCache feature improves performance by storing session data in a signed, short-lived cookie to avoid frequent database lookups [1][2]. Because this data is stored in the client's browser, revoked sessions may remain active on other devices until the cache duration (maxAge) expires [1][3]. The server cannot directly invalidate or delete cookies stored on other client devices [2][3]. To manage or mitigate this behavior, you can use the following approaches: Immediate Revocation Strategies If immediate session revocation is required, you can: 1. Disable cookieCache entirely in your configuration [1][2]. 2. Set a significantly shorter maxAge (e.g., 60 seconds) to balance performance and security [1][2]. 3. Use disableCookieCache: true for sensitive operations or specific API calls to force a fresh database lookup [1][3]. Handling Cookie Cache with getSessionFromCtx When calling getSessionFromCtx, you can force the server to bypass the cache and validate the session against the database by passing the disableCookieCache: true option [4][3]. Important Implementation Note: When using disableCookieCache: true, ensure that your environment correctly handles the updated session headers. In some server-side contexts (like certain middleware or specific internal API patterns), the set-cookie header generated by the refresh may be discarded if the call is not configured to return and forward headers to the final client response [5][6]. To ensure the client-side cookie is actually updated, you may need to ensure the calling context handles the headers explicitly, such as by using returnHeaders: true when invoking session-related methods [5][6].
Citations:
- 1: https://better-auth.com/docs/concepts/session-management
- 2: https://raw.githubusercontent.com/better-auth/better-auth/refs/heads/main/docs/content/docs/concepts/session-management.mdx
- 3: https://github.com/better-auth/better-auth/blob/main/docs/content/docs/concepts/session-management.mdx
- 4: https://github.com/better-auth/better-auth/blob/main/packages/better-auth/src/api/routes/session.ts
- 5: getSessionFromCtx discards session_data cookie refresh — JWE session expires after maxAge with no renewal better-auth/better-auth#8763
- 6: Cookie cache not updated from auth.api.getSession better-auth/better-auth#4231
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- all repository references to pending deletion ---'
rg -n -S 'deletedAt|deleted_at|pending deletion|pending-deletion|delete account|account deletion|deleteAccount' . \
--glob '!bun.lock' --glob '!**/.git/**' --glob '!**/node_modules/**' | head -300
printf '%s\n' '--- auth schema and relevant migrations ---'
sed -n '1,75p' packages/db/src/schema/auth.ts
rg -n -S 'deleted_at|deletedAt' packages/db/drizzle packages/auth
printf '%s\n' '--- Better Auth 1.6.22 session implementation references ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://raw.githubusercontent.com/better-auth/better-auth/v1.6.22/packages/better-auth/src/api/routes/session.ts \
-o "$tmpdir/session.ts"
curl -fsSL https://raw.githubusercontent.com/better-auth/better-auth/v1.6.22/packages/better-auth/src/api/index.ts \
-o "$tmpdir/api-index.ts" || true
rg -n -C 8 -S 'disableCookieCache|cookieCache|getSessionFromCtx|session' "$tmpdir"Repository: superset-sh/superset
Length of output: 50377
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- account deletion flow ---'
sed -n '50,150p' packages/trpc/src/router/user/user.ts
sed -n '70,135p' packages/trpc/src/trpc.ts
printf '%s\n' '--- Better Auth 1.6.22 cookie-cache decision path ---'
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://raw.githubusercontent.com/better-auth/better-auth/v1.6.22/packages/better-auth/src/api/routes/session.ts \
-o "$tmpdir/session.ts"
curl -fsSL https://raw.githubusercontent.com/better-auth/better-auth/v1.6.22/packages/better-auth/src/api/context.ts \
-o "$tmpdir/context.ts" || true
rg -n -C 12 -S \
'sessionDataPayload|cookieCache|disableCookieCache|internalAdapter\.getSession|getSessionFromCtx|sessionDataCookie' \
"$tmpdir/session.ts" "$tmpdir/context.ts" | head -240
printf '%s\n' '--- getSessionFromCtx declaration and implementation ---'
curl -fsSL https://raw.githubusercontent.com/better-auth/better-auth/v1.6.22/packages/better-auth/src/api/index.ts \
-o "$tmpdir/index.ts"
rg -n -C 12 -S 'getSessionFromCtx|disableCookieCache' "$tmpdir/index.ts"Repository: superset-sh/superset
Length of output: 20708
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://raw.githubusercontent.com/better-auth/better-auth/v1.6.22/packages/better-auth/src/api/routes/session.ts \
-o "$tmpdir/session.ts"
printf '%s\n' '--- getSessionFromCtx exact implementation ---'
sed -n '527,590p' "$tmpdir/session.ts"
printf '%s\n' '--- cookie-cache return and database fallback ---'
sed -n '213,360p' "$tmpdir/session.ts"
printf '%s\n' '--- auth middleware dispatch/session context setup ---'
for path in \
packages/better-auth/src/api/dispatch.ts \
packages/better-auth/src/api/middlewares/index.ts \
packages/better-auth/src/api/middlewares/session.ts; do
url="https://raw.githubusercontent.com/better-auth/better-auth/v1.6.22/$path"
if curl -fsSL "$url" -o "$tmpdir/$(basename "$path")"; then
printf '%s\n' "--- $path ---"
rg -n -C 8 -S 'context\.session|session.*context|getSession' "$tmpdir/$(basename "$path")" | head -160
fi
doneRepository: superset-sh/superset
Length of output: 9461
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://raw.githubusercontent.com/better-auth/better-auth/v1.6.22/packages/better-auth/src/api/routes/session.ts \
-o "$tmpdir/session.ts"
printf '%s\n' '--- getSessionFromCtx continuation ---'
sed -n '540,610p' "$tmpdir/session.ts"
printf '%s\n' '--- database fallback after cookie-cache branch ---'
sed -n '350,525p' "$tmpdir/session.ts"
printf '%s\n' '--- static behavioral verifier ---'
python3 - <<'PY'
from pathlib import Path
import re
server = Path("packages/auth/src/server.ts").read_text()
deletion = Path("packages/trpc/src/router/user/user.ts").read_text()
trpc = Path("packages/trpc/src/trpc.ts").read_text()
assert re.search(r"cookieCache:\s*\{\s*enabled:\s*true,\s*maxAge:\s*60 \* 5", server, re.S)
assert "const session = await getSessionFromCtx(ctx);" in server
assert ".set({ deletedAt: new Date() })" in deletion
assert "await db.delete(sessions).where(eq(sessions.userId, userId));" in deletion
assert "ctx.session.user.deletedAt" in trpc
# Model the Better Auth 1.6.22 branch observed in session.ts:
# a valid cookie payload is returned before the database lookup unless
# disableCookieCache is true.
def session_source_uses_cookie_cache(cookie_valid: bool, cache_enabled: bool,
disable_cookie_cache: bool) -> bool:
return cookie_valid and cache_enabled and not disable_cookie_cache
assert session_source_uses_cookie_cache(True, True, False)
assert not session_source_uses_cookie_cache(True, True, True)
print("stale cookie cache can bypass the hook's current session lookup; strict lookup requires disableCookieCache.")
PYRepository: superset-sh/superset
Length of output: 8089
🏁 Script executed:
#!/bin/bash
set -eu
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://raw.githubusercontent.com/better-auth/better-auth/v1.6.22/packages/better-auth/src/api/routes/session.ts \
-o "$tmpdir/session.ts"
printf '%s\n' '--- Better Auth authoritative-session helper ---'
sed -n '590,690p' "$tmpdir/session.ts"
printf '%s\n' '--- all references to authoritative session in the pinned release ---'
rg -n -C 8 -S 'getAuthoritativeSessionFromCtx|isStateful' "$tmpdir/session.ts"Repository: superset-sh/superset
Length of output: 5307
Use an authoritative session for pending-deletion checks.
getSessionFromCtx(ctx) can return the five-minute cached user. After user.deleteAccount sets deletedAt and deletes sessions, the hook can still see deletedAt: null and permit restricted endpoints. Use getAuthoritativeSessionFromCtx(ctx), or query auth.users.deleted_at by user ID. Apply the same authoritative check to every pending-deletion access-control boundary.
🤖 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/auth/src/server.ts` around lines 139 - 155, Replace
getSessionFromCtx in the before hook with getAuthoritativeSessionFromCtx so
pending-deletion checks use current account state; apply the same authoritative
lookup to every other pending-deletion access-control boundary. Preserve the
allowed-path bypass and existing FORBIDDEN response.
| const [otherOwners] = await db | ||
| .select({ value: count() }) | ||
| .from(members) | ||
| .where( | ||
| and( | ||
| eq(members.organizationId, ownership.organizationId), | ||
| eq(members.role, "owner"), | ||
| ne(members.userId, userId), | ||
| ), | ||
| ); | ||
| if ((otherOwners?.value ?? 0) === 0) { |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- target file ---'
sed -n '1,180p' packages/trpc/src/router/user/user.ts
printf '%s\n' '--- relevant schema definitions ---'
rg -n -C 5 'deletedAt|members|users' packages/trpc packages -g '*.ts' -g '*.tsx' | head -n 300
printf '%s\n' '--- delete/reactivate implementations and call sites ---'
rg -n -C 12 'deleteAccount|reactivateAccount' packages -g '*.ts' -g '*.tsx' | head -n 400Repository: superset-sh/superset
Length of output: 32542
Count only active alternate owners.
A pending-deletion owner remains an owner member. Each owner can therefore delete their account while the other pending owner is counted. The organization can then have members but no active owner.
Join users and require isNull(users.deletedAt) in this count.
🤖 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/trpc/src/router/user/user.ts` around lines 84 - 94, Update the
alternate-owner count in the ownership check to join the users table and filter
with isNull(users.deletedAt), while retaining the existing organization,
owner-role, and excluding-current-user predicates. Keep the zero-count behavior
unchanged so only active alternate owners prevent deletion.
| await db | ||
| .update(users) | ||
| .set({ deletedAt: new Date() }) | ||
| .where(eq(users.id, userId)); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Do not extend the recovery period on repeated deletion.
user.deleteAccount is allowed for pending-deletion accounts. Line 105 overwrites the original deletedAt value on every call. A user can repeatedly invoke this procedure and indefinitely extend the 30-day recovery period that reactivateAccount checks.
Set deletedAt only when it is currently NULL. Return success without changing the timestamp for an already pending account.
🤖 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/trpc/src/router/user/user.ts` around lines 103 - 106, Update
user.deleteAccount so the database update sets deletedAt only when it is
currently NULL, preserving the original timestamp for accounts already pending
deletion. Ensure repeated deletion requests still return success without
extending the recovery period.
| await db | ||
| .update(users) | ||
| .set({ deletedAt: new Date() }) | ||
| .where(eq(users.id, userId)); | ||
| await db.delete(sessions).where(eq(sessions.userId, userId)); | ||
| await db | ||
| .delete(oauthAccessTokens) | ||
| .where(eq(oauthAccessTokens.userId, userId)); | ||
| await db | ||
| .delete(oauthRefreshTokens) | ||
| .where(eq(oauthRefreshTokens.userId, userId)); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -e
file=packages/trpc/src/router/user/user.ts
printf '%s\n' '--- target file outline ---'
ast-grep outline "$file" --match 'deleteAccount' --view expanded || true
printf '%s\n' '--- target implementation ---'
cat -n "$file" | sed -n '1,180p'
printf '%s\n' '--- database client and schema references ---'
rg -n --glob '*.{ts,tsx}' 'deleteAccount|oauthAccessTokens|oauthRefreshTokens|sessions|transaction\(' packagesRepository: superset-sh/superset
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- database client definitions ---'
fd -i 'client' packages/db packages | head -80
rg -n --glob '*.ts' 'export (const|type).*db|drizzle\(|transaction\s*=' packages/db packages/trpc | head -120
printf '%s\n' '--- auth schema around affected tables ---'
cat -n packages/db/src/schema/auth.ts | sed -n '1,85p;245,310p'
printf '%s\n' '--- representative transactions using this client ---'
cat -n packages/trpc/src/router/v2-host/v2-host.ts | sed -n '110,175p'
cat -n packages/db/src/utils/sql.ts | sed -n '1,65p'
printf '%s\n' '--- package scripts and database package metadata ---'
fd -i 'package.json' . --exec sh -c 'case "$1" in *packages/db/*|*packages/trpc/*) echo "--- $1"; sed -n "/\"scripts\"/,/}/p" "$1" | head -40;; esac' sh {} \;Repository: superset-sh/superset
Length of output: 13521
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- database clients ---'
cat -n packages/db/src/client.ts | sed -n '1,90p'
printf '%s\n' '--- full affected auth-table relationships ---'
cat -n packages/db/src/schema/auth.ts | sed -n '35,125p;180,310p'
printf '%s\n' '--- all user deletion implementations ---'
cat -n packages/trpc/src/router/admin/admin.ts | sed -n '1,150p'
rg -n -C 8 --glob '*.ts' 'deletedAt|delete\(users\)|deleteAccount' packages/trpc packages/auth packages/db
printf '%s\n' '--- transaction style with db (not dbWs) ---'
rg -n -C 4 --glob '*.ts' 'db\.transaction|dbWs\.transaction' packages | head -180Repository: superset-sh/superset
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re
router = Path("packages/trpc/src/router/user/user.ts").read_text()
client = Path("packages/db/src/client.ts").read_text()
body = router.split("deleteAccount:", 1)[1].split("reactivateAccount:", 1)[0]
calls = re.findall(r"await\s+db(?:\s*\n\s*)?(?:\.update|\.delete)", body)
print("deleteAccount uses db calls:", len(calls))
print("deleteAccount contains transaction:", ".transaction(" in body)
print("deleteAccount affected operations:")
for match in re.finditer(r"await\s+db\s*(?:\n\s*)?\.(update|delete)\(([^)]+)", body):
print(f" {match.group(1)}({match.group(2).strip()})")
print("db exports neon-http client:", "export const db = drizzle" in client)
print("dbWs exports pooled client:", "export const dbWs = drizzleWs" in client)
PYRepository: superset-sh/superset
Length of output: 425
Make the deletion transition atomic.
Use dbWs.transaction for the user update and all three credential deletions. If a deletion fails, the transaction must roll back the deletedAt update so the account state and credential revocation remain consistent.
🤖 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/trpc/src/router/user/user.ts` around lines 103 - 113, Wrap the user
soft-delete and all credential cleanup operations in a single dbWs.transaction,
including the users update, sessions deletion, oauthAccessTokens deletion, and
oauthRefreshTokens deletion. Use the transaction handle for every query so any
failure rolls back the deletedAt update and prior deletions.
…ling-correct purge Deactivated (pending-deletion) users are hidden from member search, assignee pickers/writes, and the Electric users shape; the members settings page shows them greyed with a Deactivated badge so owners can remove them and free the seat. organization.listMembers narrows its user select (it returned the entire users row to every teammate) and filters deactivated members by default behind an includeDeactivated escape hatch. Desktop and web gain pending-deletion screens with reactivation, mirroring mobile. admin.deleteUser now decrements Stripe seat quantity for shared orgs, since the member-row FK cascade never fires afterRemoveMember.
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
packages/trpc/src/router/admin/admin.ts (1)
62-69: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winSelect the seat item by price ID.
@better-auth/stripesupports multiple subscription line items.stripeSub.items.data[0]is positional and can update the wrong item if an add-on is present. Match the configured seat price ID or persist the Stripe subscription-item ID before changingquantity.🤖 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/trpc/src/router/admin/admin.ts` around lines 62 - 69, Update the subscription quantity logic around stripeSub.items.data and stripeClient.subscriptions.update to select the line item matching the configured seat price ID, rather than assuming the first item is the seat item. Use that matched item’s ID for the quantity update, while preserving the existing proration behavior; if the implementation already persists a subscription-item ID, reuse it instead.apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx (1)
129-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract the shared
TeamMembermapper.Lines 129-151 duplicate the mapping and sorting in
apps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsxlines 51-73. This PR addeddeletedAtto both copies, which shows the cost of the duplication. Move the mapper next to theTeamMembertype inapps/desktop/src/renderer/routes/_authenticated/settings/members/types.tsand call it from both components.♻️ Suggested helper
// apps/desktop/src/renderer/routes/_authenticated/settings/members/toTeamMembers.ts export function toTeamMembers( membersData: OrganizationMemberWithUser[] | undefined, organizationId: string, ): TeamMember[] { return (membersData ?? []) .map((m) => ({ memberId: m.id, userId: m.userId, organizationId, role: m.role as OrganizationRole, createdAt: m.createdAt, name: m.user.name, email: m.user.email, image: m.user.image, deletedAt: m.user.deletedAt, })) .sort((a, b) => { const priorityDiff = getRoleSortPriority(a.role) - getRoleSortPriority(b.role); if (priorityDiff !== 0) return priorityDiff; return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); }); }const members: TeamMember[] = useMemo(() => { if (!activeOrganizationId) return []; - return (membersData ?? []) - .map((m) => ({ - memberId: m.id, - ... - })) - .sort((a, b) => { ... }); + return toTeamMembers(membersData, activeOrganizationId); }, [membersData, activeOrganizationId]);🤖 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/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx` around lines 129 - 151, Extract the duplicated TeamMember mapping and sorting into a shared toTeamMembers helper adjacent to the TeamMember type, preserving all mapped fields including deletedAt and the existing role/creation-date ordering. Replace the local mapping logic in both OrganizationSettings and MembersSettings with calls to this helper, keeping the inactive-organization empty-list behavior in OrganizationSettings.apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx (1)
12-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicated purge-countdown arithmetic in the desktop and web recovery screens. Both screens declare a local
DAY_MSand recompute the purge date fromACCOUNT_DELETION_GRACE_DAYS. The shared root cause is the absence of one grace-period helper next to the constant inpackages/shared. The mobileuseDaysUntilPurgehook is a third copy of the same math.
apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx#L12-L31: remove the localDAY_MSand thepurgeAt/daysRemainingcomputation, then call the shared helper with thedeletedAtprop.apps/web/src/app/account-pending-deletion/page.tsx#L15-L43: remove the localDAY_MSand the nestedMath.max/Math.ceilexpression, then call the shared helper whensession?.user.deletedAtis set.🤖 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/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx` around lines 12 - 31, Introduce a shared grace-period countdown helper next to ACCOUNT_DELETION_GRACE_DAYS in packages/shared, then reuse it in both recovery screens. In apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx lines 12-31, remove DAY_MS and the local purgeAt/daysRemaining arithmetic and call the helper with deletedAt. In apps/web/src/app/account-pending-deletion/page.tsx lines 15-43, remove DAY_MS and the nested Math.max/Math.ceil calculation and call the helper when session?.user.deletedAt is available.
🤖 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/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx`:
- Around line 14-31: Normalize the session’s deletedAt value at the auth
boundary before passing it to PendingDeletionScreen, ensuring the component
receives a Date. Alternatively, widen PendingDeletionScreen’s deletedAt prop to
Date | string and retain the existing new Date(deletedAt) conversion when
calculating purgeAt.
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx`:
- Line 141: Update the listMembers call in OrganizationSettings to pass {
includeDeactivated: true } so deactivated members are included, then render a
“Deactivated” badge in the organization table whenever member.deletedAt is set.
In `@apps/web/src/proxy.ts`:
- Around line 31-33: Update the isApiRoute classification in proxy to use the
existing matchesRouteOrChild helper for /api and /trpc instead of raw startsWith
checks, reusing the route-boundary behavior covered by proxy-routes.test.ts.
Export the helper from proxy-routes.ts if needed, while preserving the existing
deleted-user and public-route flow.
- Around line 29-33: Update the authentication enforcement around isApiRoute in
proxy.ts so pending-deletion sessions are blocked across all authenticated API
and direct apps/api paths, not only through protectedProcedure. Reuse shared
enforcement for jwtProcedure and direct auth.api.getSession handlers, while
preserving unauthenticated/public routes and the reactivate recovery mutation as
the only intended exceptions.
In `@packages/trpc/src/router/admin/admin.ts`:
- Around line 49-54: Update the subscription lookup in the seat-reconciliation
flow to accept both "active" and "trialing" statuses. In
packages/trpc/src/router/admin/admin.ts lines 81-87, remove the active-status
filter when listing subscriptions for deletion and use Stripe auto-pagination so
every non-canceled subscription is canceled.
---
Nitpick comments:
In
`@apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx`:
- Around line 12-31: Introduce a shared grace-period countdown helper next to
ACCOUNT_DELETION_GRACE_DAYS in packages/shared, then reuse it in both recovery
screens. In
apps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsx
lines 12-31, remove DAY_MS and the local purgeAt/daysRemaining arithmetic and
call the helper with deletedAt. In
apps/web/src/app/account-pending-deletion/page.tsx lines 15-43, remove DAY_MS
and the nested Math.max/Math.ceil calculation and call the helper when
session?.user.deletedAt is available.
In
`@apps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsx`:
- Around line 129-151: Extract the duplicated TeamMember mapping and sorting
into a shared toTeamMembers helper adjacent to the TeamMember type, preserving
all mapped fields including deletedAt and the existing role/creation-date
ordering. Replace the local mapping logic in both OrganizationSettings and
MembersSettings with calls to this helper, keeping the inactive-organization
empty-list behavior in OrganizationSettings.
In `@packages/trpc/src/router/admin/admin.ts`:
- Around line 62-69: Update the subscription quantity logic around
stripeSub.items.data and stripeClient.subscriptions.update to select the line
item matching the configured seat price ID, rather than assuming the first item
is the seat item. Use that matched item’s ID for the quantity update, while
preserving the existing proration behavior; if the implementation already
persists a subscription-item ID, reuse it instead.
🪄 Autofix
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 Plus
Run ID: 9b64537b-bd02-4393-8d70-4f83f7efb895
📒 Files selected for processing (15)
apps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/TasksTopBar/components/CreateTaskDialog/components/CreateTaskAssigneePicker/CreateTaskAssigneePicker.tsxapps/desktop/src/renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/shared/AssigneeMenuItems.tsxapps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/PendingDeletionScreen.tsxapps/desktop/src/renderer/routes/_authenticated/components/PendingDeletionScreen/index.tsapps/desktop/src/renderer/routes/_authenticated/layout.tsxapps/desktop/src/renderer/routes/_authenticated/settings/members/components/MembersSettings/MembersSettings.tsxapps/desktop/src/renderer/routes/_authenticated/settings/members/types.tsapps/desktop/src/renderer/routes/_authenticated/settings/organization/components/OrganizationSettings/OrganizationSettings.tsxapps/electric-proxy/src/where.tsapps/web/src/app/account-pending-deletion/page.tsxapps/web/src/proxy.tspackages/trpc/src/router/admin/admin.tspackages/trpc/src/router/organization/members.tspackages/trpc/src/router/organization/organization.tspackages/trpc/src/router/task/task.ts
| const subscription = await db.query.subscriptions.findFirst({ | ||
| where: and( | ||
| eq(subscriptions.referenceId, membership.organizationId), | ||
| eq(subscriptions.status, "active"), | ||
| ), | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect local subscription status definitions and all Stripe status filters.
rg -n -C 4 --glob '*.{ts,tsx}' \
'status:\s*"active"|subscriptions\.list|subscriptions\.cancel|subscriptions\.update|status.*trialing' \
packages appsRepository: superset-sh/superset
Length of output: 24706
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- admin router ---'
sed -n '1,125p' packages/trpc/src/router/admin/admin.ts
printf '%s\n' '--- subscription schema and status usage ---'
rg -n -C 5 --glob '*.{ts,tsx}' \
'subscriptions\s*=|stripeSubscriptionId|type .*Subscription|status.*trialing|isActiveSubscriptionStatus|inArray\(subscriptions\.status' \
packages
printf '%s\n' '--- organization deletion and seat-update flows ---'
sed -n '560,610p' packages/auth/src/server.ts
sed -n '680,735p' packages/auth/src/server.ts
sed -n '770,820p' packages/auth/src/server.tsRepository: superset-sh/superset
Length of output: 40223
🌐 Web query:
Stripe API subscription list status parameter trialing active canceling subscriptions cancel documentation
💡 Result:
In the Stripe API, the status parameter for listing subscriptions allows you to filter returned results based on the subscription's current state [1]. By default, if no status is provided, the API returns only subscriptions that have not been canceled [1]. The following are the behaviors and details for the status parameter when listing subscriptions: Status Parameter Options - Specific Statuses: You can pass any of the valid subscription status strings to filter by that specific state [1]. The available status values are: trialing, active, past_due, canceled, unpaid, incomplete, incomplete_expired, and paused [2][3]. - canceled: Passing status=canceled returns all canceled subscriptions, including those associated with deleted customers [1]. - ended: Passing status=ended returns subscriptions that are canceled, as well as those that have expired due to incomplete payment [1]. - all: Passing status=all returns subscriptions of all statuses [1]. Default Behavior If you do not specify a status parameter in your request, the API defaults to returning all subscriptions that have not been canceled [1]. Note on Status Definitions - trialing: The subscription is currently in a trial period [2][4]. It will transition to active once the trial ends [2]. - active: The subscription is currently in an active state, generally after a successful first payment [2]. - canceled: A terminal status for a subscription [1]. For further implementation details, you can refer to the official Stripe API reference for listing subscriptions [1] and the subscription object definition [4].
Citations:
- 1: https://docs.stripe.com/api/subscriptions/list
- 2: https://docs.stripe.com/api/subscriptions/object?api-version=2025-02-24.acacia
- 3: https://github.com/stripe/stripe-go/blob/85683b51/subscription.go
- 4: https://docs.stripe.com/api/subscriptions/object
🌐 Web query:
site:docs.stripe.com/api/subscriptions/cancel Stripe cancel subscription trialing past_due unpaid incomplete paused
💡 Result:
When you cancel a Stripe subscription, regardless of its current status (such as trialing, past_due, unpaid, incomplete, or paused), the subscription status transitions to canceled [1][2]. Canceling a subscription immediately stops the customer from being charged again for that subscription [1][3]. By default, Stripe also stops the automatic collection of all finalized invoices for that customer upon cancellation to prevent unexpected payment attempts, though you can manually resume collection if needed [1]. The subscription object becomes largely immutable after cancellation, although you can still update its metadata and cancellation_details [1][2]. Depending on your requirements, you can optionally set parameters when canceling: - invoice_now: Set to true to generate a final invoice for un-invoiced metered usage and pending proration items [1][3]. - prorate: Set to true to generate a proration invoice item that credits unused time until the end of the period [2][3]. If you do not cancel immediately, you may alternatively set a subscription to cancel at the end of the current billing period, in which case any pending prorations are collected at that time [1].
Citations:
- 1: https://docs.stripe.com/api/subscriptions/cancel
- 2: https://docs.stripe.com/api/subscriptions/cancel?api-version=2024-06-20
- 3: https://docs.stripe.com/api/subscriptions/cancel?api-version=2025-11-17.preview
🌐 Web query:
site:docs.stripe.com/api/subscriptions/list Stripe list subscriptions auto-pagination default non-canceled limit
💡 Result:
When calling the Stripe API to list subscriptions, the default behavior includes only subscriptions that have not been canceled [1]. You can include canceled subscriptions by explicitly setting the status parameter to canceled [1]. Regarding pagination, the limit parameter defines the number of objects returned per page, with a default value of 10 and a maximum allowed value of 100 [1]. Stripe's official client libraries handle auto-pagination automatically when using iterator methods or equivalent list helpers, iterating through the entire collection as needed based on these API constraints [1].
Citations:
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- shared billing definition ---'
cat -n packages/shared/src/billing.ts
printf '%s\n' '--- membership subscription join ---'
sed -n '28,60p' packages/db/src/utils/membership.ts
printf '%s\n' '--- Stripe subscription creation/status synchronization ---'
rg -n -C 8 --glob '*.{ts,tsx}' \
'stripeSubscriptionId|subscription\.status|status:\s*(stripe|subscription)|subscriptions\.(create|update|list|cancel)' \
packages/auth/src packages/trpc/src/router/billing packages/shared/srcRepository: superset-sh/superset
Length of output: 16989
🌐 Web query:
Stripe subscription cancellation API supported statuses trialing past_due unpaid incomplete paused
💡 Result:
The Stripe subscription cancellation API does not impose explicit restrictions on the current status of a subscription in order to initiate a cancellation [1][2][3]. You can cancel subscriptions that are in various statuses, including trialing, active, past_due, unpaid, incomplete, and paused [4][5][6]. When you call the subscription cancellation API, the subscription moves to the canceled status [1][2][7]. Once a subscription is in the canceled status, it becomes largely immutable, meaning you can no longer update the subscription's core configuration, with the limited exception of its metadata and cancellation_details [1][2][3]. For context, the following are the primary statuses a Stripe subscription can hold: incomplete, incomplete_expired, trialing, active, past_due, canceled, unpaid, and paused [4][5][6]. While the cancellation API is generally available, keep in mind that canceling a subscription stops the generation of further invoices [1][3]. Stripe documentation also notes that you may want to check for unpaid invoices before allowing a cancellation if you wish to manage outstanding balances [2][7].
Citations:
- 1: https://docs.stripe.com/api/subscriptions/cancel
- 2: https://docs.stripe.com/api/subscriptions/cancel?api-version=2025-07-30.basil
- 3: https://docs.stripe.com/billing/subscriptions/cancel
- 4: https://docs.stripe.com/api/subscriptions/object
- 5: https://docs.stripe.com/api/subscriptions/object?api-version=2025-02-24.acacia
- 6: https://docs.stripe.com/api/subscriptions/object?api-version=2026-02-25.preview
- 7: https://docs.stripe.com/api/subscriptions/cancel?api-version=2025-11-17.preview
🌐 Web query:
Stripe list subscriptions default non-canceled pagination auto-pagination
💡 Result:
When listing subscriptions in the Stripe API, the default behavior is to return only subscriptions that have not been canceled [1][2]. You do not need to apply specific filters to exclude canceled subscriptions, as this happens automatically unless you explicitly request otherwise (for example, by setting status=canceled or status=all) [1][3]. For retrieving large sets of data, Stripe's official client libraries provide auto-pagination helpers [4][5]. These helpers abstract away the manual process of making multiple API calls, managing cursors (starting_after/ending_before), and checking the has_more property [5][6]. Key points for auto-pagination: 1. Availability: Auto-pagination is supported in all official Stripe SDKs (e.g., Python, Node.js, Ruby, Java, PHP,.NET, Go) [4]. It is not available for raw curl or CLI requests, as those perform single HTTP requests [4]. 2. Usage: You initiate a list call with your required parameters (such as the default non-canceled behavior) and then invoke the library's specific auto-pagination method on the result [4]. Examples include: - Node.js: Use for await on the list response [4]. - Python: Call auto_paging_iter on the returned object [4]. - Java: Call autoPagingIterable on the result [4]. - Ruby: Call auto_paging_each on the returned list [4]. - Go: Auto-pagination is enabled by default in the SDK [4]. By using these helpers, you can iterate through all available subscriptions matching your criteria seamlessly [4][6]. Manual pagination is only required if you are not using an official SDK or prefer to handle the cursor logic yourself [4][6].
Citations:
- 1: https://docs.stripe.com/api/subscriptions/list
- 2: https://docs.stripe.com/api/subscriptions/list?api-version=2026-03-25.dahlia
- 3: https://docs.stripe.com/api/subscriptions/list?api-version=2024-06-20
- 4: https://docs.stripe.com/api/pagination/auto
- 5: https://docs.stripe.com/api/pagination
- 6: https://docs.stripe.com/pagination
Handle trialing subscriptions and paginate cancellation.
- Include
"trialing"when selecting a subscription for seat reconciliation. - Omit
status: "active"when listing subscriptions for deletion, and use Stripe auto-pagination so every non-canceled subscription is canceled.
📍 Affects 1 file
packages/trpc/src/router/admin/admin.ts#L49-L54(this comment)packages/trpc/src/router/admin/admin.ts#L81-L87
🤖 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/trpc/src/router/admin/admin.ts` around lines 49 - 54, Update the
subscription lookup in the seat-reconciliation flow to accept both "active" and
"trialing" statuses. In packages/trpc/src/router/admin/admin.ts lines 81-87,
remove the active-status filter when listing subscriptions for deletion and use
Stripe auto-pagination so every non-canceled subscription is canceled.
… web Delete account moves into desktop settings (Account) and a new web /settings/account page, matching mobile; owners also get Delete organization in desktop org settings behind a type-the-name confirm. Deleting the active org follows the leave-organization pattern — setActive the next org, refetch the session, invalidate, and re-enter the root gates — since better-auth nulls the active org server-side and the UI otherwise keeps rendering the deleted one. Confirm-dialog copy trimmed to one sentence everywhere.
The v2 sidebar has no separate Members page — OrganizationSettings is the member-management surface, so it must opt into includeDeactivated and badge pending-deletion members, or their held seat is invisible.
43f5b30 to
8e96c3b
Compare
…deletionRequestedAt The column marks when deletion was requested — the account is recoverable for the whole grace window and, in the tombstone design, no row is ever deleted at all. deletedAt is reserved for the terminal marker that ships with the tombstone purge. Avoids Slack's deleted-means-deactivated trap; migration regenerated (0066).
Why
App Store guideline 5.1.1(v) requires in-app account deletion — the last hard review blocker for the iOS submission. Implements the industry-standard grace-period pattern (Meta/X/Discord-style; Apple explicitly permits deletion that "takes time to complete"). Full design + enforcement/visibility maps: internal artifact "Account Deletion Design".
Lifecycle
Active → pending → restored (or purged later). One column drives it:
auth.users.deletion_requested_at(migration 0066) — named for what it is: the account is recoverable for the whole grace window, so nothing is "deleted" yet.deletedAtis reserved for the terminal tombstone marker in the follow-up. 30-day window viaACCOUNT_DELETION_GRACE_DAYSin @superset/shared.user.deleteAccount): blocked if sole owner of an org with other members. Otherwise: setdeletionRequestedAt, revoke all sessions + issued OAuth access/refresh tokens (CLI/MCP die immediately). Orgs and billing untouched so recovery is total.Stack.Protectedtier, desktop_authenticatedgate, web proxy redirect) with countdown + Reactivate.user.reactivateAccount): window-checked; clears the mark; everything exactly as it was.admin.deleteUserfor now. Note: the purge mechanics in this PR are the interim cascade version and are superseded by the tombstone design (anonymize-in-place +deletedAtterminal marker + external-residue purges) specified for the follow-up PR — acceptable here because purge is manual and ≥30 days away.Enforcement (a pending-deletion user can only: see status, reactivate, sign out)
protectedProcedureguard; allowlistuser.me/deleteAccount/reactivateAccounthooks.before: 403 any session withdeletionRequestedAtexcept sign-in/callback/get-session/sign-outVisibility (what coworkers see)
Slack-style: hidden from member search (MCP/CLI/Slack agent included), new task assignments, the Electric
auth.usersshape, andorganization.listMembersby default — which also now narrows its user select (it previously returned the entire users row, emails included, to every teammate). Member-management surfaces (desktop Organization page + Members page) opt in viaincludeDeactivated: trueand render a greyed Deactivated badge so owners can see the held seat and free it via the normal removal flow.admin.deleteUserdecrements Stripe seats for shared orgs (FK cascade never firesafterRemoveMember).Deletion initiation on desktop + web (not just mobile)
/settings/accountpage (sign out + delete account)Follow-ups (specified in design doc)
tombstone purge (
deletedAtterminal marker, anonymize-in-place, unassign open tasks, GDPR-required external residue: PostHog person / Resend contact / Blob avatar) · purge cron · admin deleted-users view · deletion emails · optional org-deletion friction (Linear's 48h cancellable window)Testing
turbo typecheckacross trpc, auth, db, shared, mobile, desktop, web, electric-proxy +bun run lintgreen