Skip to content

feat(mobile,trpc,auth): gate mobile behind a paid plan - #6411

Merged
saddlepaddle merged 1 commit into
mainfrom
mobile-paywall
Aug 13, 2026
Merged

saddlepaddle merged 1 commit into
mainfrom
mobile-paywall

Conversation

@saddlepaddle

@saddlepaddle saddlepaddle commented Aug 13, 2026 •

Copy link
Copy Markdown
Collaborator

Mobile launches as a Pro perk (user decision — limits users while we iterate, avoids clawing back free access later). Externally-billed subscription gating with no IAP, per guideline 3.1.3(b) multiplatform services — the Netflix/Slack pattern.

The gate is one wall, twice

  • Client: a new Stack.Protected tier in mobile's RootLayout — signed-in + not-pending-deletion + no paid plan → paywall screen. plan already rides the session via customSession, so no extra query.
  • Server: one protectedProcedure middleware — mobile client + no plan on the active org → FORBIDDEN, allowlisting only account self-management (user.me/deleteAccount/reactivateAccount/myOrganizations). Keyed on the x-superset-client header, which mobile now actually sends (the header parsing existed but no client ever adopted it — this also turns on the per-client API telemetry for mobile). Other clients are never gated.

No per-feature plan checks anywhere — nothing to remember as the product grows.

The paywall screen

Steering-safe copy ("Manage your plan from the Superset desktop app" — no external purchase link, valid on every storefront), plus: org switcher (a member whose active org is their free personal org but who belongs to a paid org can switch right there — otherwise real customers would hit a dead wall), Refresh, Contact support, Log out, and Delete account (grace-period flow) so guideline 5.1.1(v) survives the gate.

App Store review access

  • Credential sign-in enabled in production (emailAndPassword.enabled always; disableSignUp outside dev/preview so nobody can register)
  • Quiet "Sign in with email" link on the sign-in screen (prompt-based, same pattern as the dev options)
  • bun run db:seed-review (APP_REVIEW_EMAIL/APP_REVIEW_PASSWORD) idempotently provisions the demo account: credential login, onboarded, Pro-subscribed personal org (subscription row only, no Stripe charge)
  • store.config.json review-notes/demoRequired update rides chore(mobile,marketing): app store submission metadata and privacy policy #6408; the credentials themselves go into ASC's App Review sign-in fields by hand, never the repo

Known residuals

The relay/host path is not plan-gated (client gate covers the UX; a determined free user scripting the relay directly isn't a launch concern). Desktop/web remain ungated by design.

Testing

  • turbo typecheck (mobile, trpc, auth) + lint green
  • Full paywall→upgrade→refresh loop exercisable on TestFlight with the seeded review account

Summary by cubic

Gate Superset Mobile behind a paid plan and add a paywall screen. Previously any signed-in user could use mobile; now only sessions with a paid plan on the active org can proceed, and unpaid sessions land on the paywall. Desktop/web behavior is unchanged.

  • Client: RootLayout adds a Stack.Protected tier keyed on session.plan; unpaid sessions route to paywall (no extra query; plan is in the session).
  • Server: protectedProcedure forbids mobile without a plan except user.me, user.deleteAccount, user.reactivateAccount, and user.myOrganizations. Gating is keyed by the x-superset-client header, which the mobile client now sends as mobile/<version> (also enables per-client API telemetry).
  • Paywall: org switcher (to move to a paid org), Refresh, Contact support, Log out, and Delete account. Copy avoids external purchase links (App Store 3.1.3(b)); deletion remains accessible (5.1.1(v)).
  • Sign-in: Quiet email/password option added to the sign-in screen. Credential sign-in is enabled in production; sign-up remains disabled outside dev/preview.

Rollout

  • Create the App Review demo account in each environment: APP_REVIEW_EMAIL=… APP_REVIEW_PASSWORD=… bun run db:seed-review (idempotent), then add credentials in App Store Connect review details.
  • No schema/data migrations. Other clients are not gated.

Written for commit caf83ad. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added a Pro subscription paywall with plan messaging, organization switching, support contact, sign-out, and account deletion.
    • Added email and password sign-in with loading states and clearer error messages.
    • Added account deletion with confirmation, progress feedback, and automatic sign-out.
  • Access & Authentication
    • Paid-plan checks now guide eligible mobile users to protected areas or the paywall.
    • Email/password sign-in is available in production.
  • Improvements
    • Mobile requests now report the app version for improved service compatibility.

Mobile is a Pro surface: unpaid sessions land on a paywall screen
(org switcher for members whose active org is unpaid, refresh, support,
sign out, and account deletion so 5.1.1(v) survives the gate). The gate
is one wall — a RootLayout tier client-side plus one protectedProcedure
middleware keyed on the new x-superset-client header that mobile now
sends (which also lights up the dormant per-client API telemetry).
Credential sign-IN is enabled in production for the App Store review
demo account (sign-up stays dev/preview-only), reachable via a quiet
sign-in link; db:seed-review provisions the account idempotently.
@coderabbitai

coderabbitai Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds mobile paywall routing and controls, email/password sign-in, account deletion handling, client version headers, server-side mobile plan enforcement, and a review-account seeding script.

Changes

Mobile authentication and paywall

Layer / File(s) Summary
Server authentication and paywall enforcement
packages/auth/src/server.ts, packages/trpc/src/trpc.ts
Email/password sign-in is always enabled. Unpaid mobile sessions can access only allowlisted account and organization procedures.
Review account provisioning
packages/auth/src/seed-review-account.ts, package.json
The seed script creates or updates a verified review account, personal organization, onboarding state, and active Pro subscription.
Mobile authentication and client headers
apps/mobile/lib/trpc/client.ts, apps/mobile/screens/(auth)/sign-in/...
The mobile client sends its application version. Sign-in supports email/password prompts, loading state, and error reporting.
Mobile paywall routing and account actions
apps/mobile/screens/RootLayout/RootLayout.tsx, apps/mobile/screens/paywall/..., apps/mobile/hooks/useDeleteAccount/..., apps/mobile/app/paywall.tsx, apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx
Authenticated users without a paid plan are routed to the paywall. The paywall supports organization switching, session refresh, support contact, sign-out, and account deletion.

Estimated code review effort: 4 (Complex) | ~45 minutes

Mergeability Score: 🟠 High · up to caf83

The mobile plan gate can currently grant access to an unpaid organization or deny access to a paid one, while the review-account seed can attach Pro access to the wrong organization. These authorization and review-readiness issues, along with exposed password entry and logged review email data, make the PR unsafe to merge until corrected.

Sequence Diagram(s)

sequenceDiagram
  participant RootLayout
  participant PaywallScreen
  participant MobileTRPCClient
  participant tRPCMiddleware
  participant AuthAPI
  RootLayout->>PaywallScreen: route unpaid authenticated session
  PaywallScreen->>MobileTRPCClient: request account or organization action
  MobileTRPCClient->>tRPCMiddleware: send mobile request with client version
  tRPCMiddleware->>AuthAPI: execute allowlisted procedure
  AuthAPI-->>PaywallScreen: return action result
Loading

Possibly related PRs

  • superset-sh/superset#6395: Both changes modify mobile account deletion, including the deletion hook and account settings integration.

Suggested reviewers: kitenite

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title uses conventional commit format and clearly summarizes the primary mobile and tRPC paid-plan gating change.
Description check ✅ Passed The description explains the change, rationale, testing, rollout, residuals, and review-account setup in sufficient detail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mobile-paywall

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
apps/mobile/screens/paywall/PaywallScreen.tsx (1)

45-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the shared deletion grace-period constant.

Line 45 duplicates the 30 day retention period. Import ACCOUNT_DELETION_GRACE_DAYS, as AccountSettingsScreen does, so policy updates keep both deletion notices 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 `@apps/mobile/screens/paywall/PaywallScreen.tsx` at line 45, Update the
deletion notice in PaywallScreen to interpolate the shared
ACCOUNT_DELETION_GRACE_DAYS constant instead of hardcoding 30, importing and
using it consistently with AccountSettingsScreen.
🤖 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/`(auth)/sign-in/components/EmailSignInLink/EmailSignInLink.tsx:
- Around line 25-29: Update AlertPromptModule and PromptOptions to support an
isSecureTextEntry setting on the created UITextField, then enable that setting
in the password prompt within EmailSignInLink. Preserve existing prompt behavior
for callers that do not request secure input.

In `@packages/auth/src/seed-review-account.ts`:
- Around line 59-92: The membership lookup in the review-account seeding flow
must target the deterministic review organization instead of any organization
owned by the user. Use the `${user.id.slice(0, 8)}-review` slug to find or
create that organization, query membership for its organization ID, and use that
same ID when checking and inserting the active Pro subscription.
- Line 32: Update the review-account creation logging in seedReviewAccount to
omit the APP_REVIEW_EMAIL value from both creation and related log messages. Log
only a generic event or status while preserving the existing success and failure
behavior.

In `@packages/trpc/src/trpc.ts`:
- Around line 135-146: Update the middleware around the plan check to resolve
the requested organization and establish activeOrganizationId before evaluating
payment access. Load the plan for that resolved organization rather than relying
on ctx.session.session.plan, then enforce the existing
MOBILE_PAYWALL_ALLOWED_PROCEDURES restriction using the resolved plan for both
paid and unpaid session cases.

---

Nitpick comments:
In `@apps/mobile/screens/paywall/PaywallScreen.tsx`:
- Line 45: Update the deletion notice in PaywallScreen to interpolate the shared
ACCOUNT_DELETION_GRACE_DAYS constant instead of hardcoding 30, importing and
using it consistently with AccountSettingsScreen.
🪄 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: 9cb07c3c-d23e-41a6-b74e-5e91a4efe330

📥 Commits

Reviewing files that changed from the base of the PR and between f716a19 and caf83ad.

📒 Files selected for processing (15)
  • apps/mobile/app/paywall.tsx
  • apps/mobile/hooks/useDeleteAccount/index.ts
  • apps/mobile/hooks/useDeleteAccount/useDeleteAccount.ts
  • apps/mobile/lib/trpc/client.ts
  • apps/mobile/screens/(auth)/sign-in/SignInScreen.tsx
  • apps/mobile/screens/(auth)/sign-in/components/EmailSignInLink/EmailSignInLink.tsx
  • apps/mobile/screens/(auth)/sign-in/components/EmailSignInLink/index.ts
  • apps/mobile/screens/(authenticated)/settings/account/AccountSettingsScreen.tsx
  • apps/mobile/screens/RootLayout/RootLayout.tsx
  • apps/mobile/screens/paywall/PaywallScreen.tsx
  • apps/mobile/screens/paywall/index.ts
  • package.json
  • packages/auth/src/seed-review-account.ts
  • packages/auth/src/server.ts
  • packages/trpc/src/trpc.ts

Comment on lines +25 to +29
const password = await prompt({
title: "Sign in with email",
message: `Password for ${email}`,
confirmText: "Sign in",
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -t f . apps/mobile/modules/alert-prompt | sort
rg -n -C 5 'PromptOptions|secure|password|prompt\(' apps/mobile/modules/alert-prompt

Repository: superset-sh/superset

Length of output: 3467


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- native prompt implementation ---'
cat -n apps/mobile/modules/alert-prompt/ios/AlertPromptModule.swift

printf '%s\n' '--- TypeScript API ---'
cat -n apps/mobile/modules/alert-prompt/src/index.ts

printf '%s\n' '--- component ---'
cat -n 'apps/mobile/screens/(auth)/sign-in/components/EmailSignInLink/EmailSignInLink.tsx'

printf '%s\n' '--- prompt usages ---'
rg -n -C 6 'prompt\(' apps/mobile --glob '*.{ts,tsx}'

Repository: superset-sh/superset

Length of output: 20811


Mask the password input. AlertPromptModule creates a plain UITextField, and PromptOptions has no secure-input option. Add isSecureTextEntry support and enable it for the password prompt at EmailSignInLink.tsx:25.

🤖 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/`(auth)/sign-in/components/EmailSignInLink/EmailSignInLink.tsx
around lines 25 - 29, Update AlertPromptModule and PromptOptions to support an
isSecureTextEntry setting on the created UITextField, then enable that setting
in the password prompt within EmailSignInLink. Preserve existing prompt behavior
for callers that do not request secure input.

.insert(users)
.values({ name: "App Review", email, emailVerified: true })
.returning();
console.log(`Created review user: ${email}`);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Do not write the review email address to logs.

APP_REVIEW_EMAIL is personal data. These messages can persist in shared CI and deployment logs. Log the event state without the email address.

Proposed fix
- console.log(`Created review user: ${email}`);
+ console.log("Created review user");
...
- console.log(`Review account ready: ${email} (onboarded, pro)`);
+ console.log("Review account ready (onboarded, pro)");

Also applies to: 94-94

🤖 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/seed-review-account.ts` at line 32, Update the
review-account creation logging in seedReviewAccount to omit the
APP_REVIEW_EMAIL value from both creation and related log messages. Log only a
generic event or status while preserving the existing success and failure
behavior.

Source: Linters/SAST tools

Comment on lines +59 to +92
let membership = await db.query.members.findFirst({
where: eq(members.userId, user.id),
});
if (!membership) {
// Raw user inserts bypass the user-create database hook, so the
// personal org has to be created explicitly.
await auth.api.createOrganization({
body: {
name: "App Review Team",
slug: `${user.id.slice(0, 8)}-review`,
userId: user.id,
},
});
membership = await db.query.members.findFirst({
where: eq(members.userId, user.id),
});
}
if (!membership) throw new Error("review user has no organization");

const activeSubscription = await db.query.subscriptions.findFirst({
where: and(
eq(subscriptions.referenceId, membership.organizationId),
eq(subscriptions.status, "active"),
),
});
if (!activeSubscription) {
await db.insert(subscriptions).values({
plan: "pro",
referenceId: membership.organizationId,
status: "active",
billingInterval: "monthly",
seats: 1,
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Bind the subscription to the review organization.

findFirst can select any organization when the review user has multiple memberships. The script can then insert a Pro subscription for an unrelated organization. That organization receives paid mobile access because packages/auth/src/server.ts derives session.plan from its active subscription.

Find or create the review organization by its deterministic review slug. Query the membership for that organization only. Insert the subscription with that organization 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 `@packages/auth/src/seed-review-account.ts` around lines 59 - 92, The
membership lookup in the review-account seeding flow must target the
deterministic review organization instead of any organization owned by the user.
Use the `${user.id.slice(0, 8)}-review` slug to find or create that
organization, query membership for its organization ID, and use that same ID
when checking and inserting the active Pro subscription.

Comment thread packages/trpc/src/trpc.ts
Comment on lines +135 to +146
.use(async ({ ctx, path, next }) => {
if (
ctx.client?.product === "mobile" &&
!ctx.session.session.plan &&
!MOBILE_PAYWALL_ALLOWED_PROCEDURES.has(path)
) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Superset Mobile requires a paid plan.",
});
}
return next();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Evaluate the plan after organization selection.

A paid session can send ORGANIZATION_HEADER for an unpaid organization. This middleware allows the request from the paid session plan. The following middleware then sets that unpaid organization as activeOrganizationId.

An unpaid session also cannot access a paid organization selected by the header. Resolve the requested organization first. Then load and enforce the plan for the resolved 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 `@packages/trpc/src/trpc.ts` around lines 135 - 146, Update the middleware
around the plan check to resolve the requested organization and establish
activeOrganizationId before evaluating payment access. Load the plan for that
resolved organization rather than relying on ctx.session.session.plan, then
enforce the existing MOBILE_PAYWALL_ALLOWED_PROCEDURES restriction using the
resolved plan for both paid and unpaid session cases.

@github-actions

github-actions Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch

Thank you for your contribution! 🎉

@saddlepaddle
saddlepaddle merged commit 7b1844a into main Aug 13, 2026
19 checks passed
saddlepaddle added a commit that referenced this pull request Aug 13, 2026
…6413)

* Revert "feat(mobile,trpc,auth): gate mobile behind a paid plan (#6411)"

This reverts commit 7b1844a.

* revert(mobile,trpc,auth): back out the pro-plan gate pending testing

Reverts #6411 (paywall, prod credential sign-in, seed script, client
header) and restores the ungated review posture in store.config.js
(demoRequired false, SIWA-instant-account notes). The dynamic config
and version pin from #6412 stay. The gate lives on in the
mobile-paywall branch for iteration.
@Kitenite Kitenite mentioned this pull request Aug 16, 2026
3 tasks done

This branch was previously deployed

1 inactive deployment
preview — caf83ad7 Deployed Aug 13, 2026 by saddlepaddle via Deploy Marketing #7581
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant