Skip to content

fix: stop firing protected tRPC queries on auth-optional surfaces#3109

Open
Abdul-Moiz31 wants to merge 1 commit into
onlook-dev:mainfrom
Abdul-Moiz31:fix/auth-optional-procedure-3051
Open

fix: stop firing protected tRPC queries on auth-optional surfaces#3109
Abdul-Moiz31 wants to merge 1 commit into
onlook-dev:mainfrom
Abdul-Moiz31:fix/auth-optional-procedure-3051

Conversation

@Abdul-Moiz31
Copy link
Copy Markdown

@Abdul-Moiz31 Abdul-Moiz31 commented May 15, 2026

Description

Components that surface on both authenticated and anonymous pages — the telemetry provider (wraps every page), the top bar "Sign In" button (on marketing pages), and the pricing table — call user.get and subscription.get on every render. Both endpoints are protectedProcedure and throw UNAUTHORIZED for anonymous visitors. React Query then retries 3× and refetches on window focus, producing a console flood of failed queries on every marketing-page load.

This PR introduces optionalAuthProcedure and parallel getOptional endpoints that return null for anonymous callers instead of throwing.

Related Issues

closes #3051

Type of Change

  • Bug fix
  • New feature
  • Documentation
  • Refactor
  • Other

Approach

The issue body proposes adding a separate procedure (e.g. optionalAuthProcedure) and parallel endpoints rather than modifying protectedProcedure to allow null ctx.user. The latter would require adding null checks to every existing protected endpoint (~30 of them) and weaken types throughout. This PR follows the issue's recommendation.

Changes

  1. createTRPCContext — treat AuthSessionMissingError (Supabase's "no session" signal) as ctx.user = null instead of throwing. Other errors (malformed JWT, network failures) still surface as UNAUTHORIZED. protectedProcedure still throws downstream, so no protected endpoint becomes accessible to anonymous users.
  2. Add optionalAuthProcedure — same shape as protectedProcedure but does not throw when ctx.user is null. Endpoints opt in explicitly.
  3. Add user.getOptional and subscription.getOptional — same return shape as their get counterparts, but return null (instead of throwing) when there is no authenticated user.
  4. Switch callsites to the new endpoints:
    • apps/web/client/src/components/telemetry-provider.tsx
    • apps/web/client/src/app/_components/top-bar/user.tsx
    • apps/web/client/src/components/ui/pricing-table/index.tsx
    • apps/web/client/src/components/ui/pricing-modal/use-subscription.tsx (shared hook used by FreeCard / ProCard — the indirect path by which subscription.get reaches the public pricing page)

All four callsites already branched on user ?? null / subscription ?? null in their render logic, so consumer behaviour is unchanged when data exists.

What is NOT changed

  • The original user.get / subscription.get remain protectedProcedure. The ~15 other callsites in authenticated routes (/project/[id]/..., /projects/...) continue to use them, where throwing on missing auth is the correct behaviour.
  • protectedProcedure is untouched.

Testing

Reproduction on main (logged out, incognito):

  1. bun dev, open http://localhost:3000 in an Incognito window.
  2. Open DevTools Console, hard refresh.
  3. Observe ~20 failed queries within seconds: user.get, subscription.get, each retried 3× and refetched on focus.

With this PR (logged out, incognito):

  • Two successful queries (user.getOptional, subscription.getOptional) return null.
  • No retries, no console errors, no UNAUTHORIZED.
  • Sign In button renders correctly, pricing cards show signup CTAs.

With this PR (logged in, regression check):

  • Other protected endpoints (user.get from project pages, project.get, etc.) continue to work.
  • user.getOptional / subscription.getOptional return real data.
  • Avatar dropdown, project list, subscription state all render correctly.

Local checks:

  • bun typecheck
  • bun test ✓ (1045/1045)

Files Changed

  • apps/web/client/src/server/api/trpc.ts — context fix + optionalAuthProcedure
  • apps/web/client/src/server/api/routers/user/user.tsgetOptional
  • apps/web/client/src/server/api/routers/subscription/subscription.tsgetOptional
  • apps/web/client/src/components/telemetry-provider.tsx — use getOptional
  • apps/web/client/src/app/_components/top-bar/user.tsx — use getOptional
  • apps/web/client/src/components/ui/pricing-table/index.tsx — use getOptional
  • apps/web/client/src/components/ui/pricing-modal/use-subscription.tsx — use getOptional

Summary by CodeRabbit

  • New Features
    • Unauthenticated users can now access pricing information without signing in
    • Improved support for logged-out user experience across the application
    • Enhanced session handling for anonymous access to select features

Review Change Stack

@vercel vercel Bot temporarily deployed to Preview – docs-onlook May 15, 2026 12:01 Inactive
@vercel
Copy link
Copy Markdown

vercel Bot commented May 15, 2026

@Abdul-Moiz31 is attempting to deploy a commit to the Onlook Team on Vercel.

A member of the Team first needs to authorize it.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs-onlook Skipped Skipped May 15, 2026 0:02am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

📝 Walkthrough

Walkthrough

The PR resolves UNAUTHORIZED errors thrown by components that should support both authenticated and unauthenticated users. It introduces an optional auth infrastructure, two optional backend endpoints, and updates four frontend components to use them.

Changes

Optional Auth Support

Layer / File(s) Summary
Auth infrastructure and optional procedure foundation
apps/web/client/src/server/api/trpc.ts
createTRPCContext now treats Supabase AuthSessionMissingError as a valid anonymous state by returning user: null instead of throwing UNAUTHORIZED. A new optionalAuthProcedure export enables procedures that accept both authenticated and unauthenticated callers without the stricter protectedProcedure checks.
User optional endpoint
apps/web/client/src/server/api/routers/user/user.ts
The user router imports optionalAuthProcedure and adds a getOptional query that returns null for anonymous users, otherwise fetches and returns the user record with the same shape as the protected get endpoint.
Subscription optional endpoint
apps/web/client/src/server/api/routers/subscription/subscription.ts
The subscription router imports optionalAuthProcedure and adds a getOptional query that mirrors the active subscription lookup but returns null for anonymous users instead of requiring authentication.
Frontend components switch to optional queries
apps/web/client/src/app/_components/top-bar/user.tsx, apps/web/client/src/components/telemetry-provider.tsx, apps/web/client/src/components/ui/pricing-table/index.tsx, apps/web/client/src/components/ui/pricing-modal/use-subscription.tsx
AuthButton, TelemetryProvider, PricingTable, and useSubscription switch from required api.user.get.useQuery() and api.subscription.get.useQuery() calls to their optional counterparts, allowing these components to render without throwing authorization errors for unauthenticated users.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A rabbit's tale of optional auth,
No more UNAUTHORIZED wrath!
Unauthenticated souls now pass,
While login gates still hold their class.
Anonymous and logged-in walk together,
Through pricing pages, light as a feather. 🌙

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 accurately summarizes the main change: stopping protected tRPC queries from firing on public/auth-optional surfaces, which is the core fix in this PR.
Description check ✅ Passed The PR description is comprehensive and includes all required template sections: clear description of the problem, related issues (#3051), type of change (bug fix), detailed approach/changes, what is NOT changed, and testing instructions.
Linked Issues check ✅ Passed The PR fully addresses issue #3051 by introducing optionalAuthProcedure, creating parallel getOptional endpoints for user and subscription, updating createTRPCContext to handle AuthSessionMissingError, and switching auth-optional components to use the new endpoints.
Out of Scope Changes check ✅ Passed All changes are directly scoped to addressing #3051: the core tRPC infrastructure changes (trpc.ts), new optional endpoints (user.ts, subscription.ts), and switching the four identified auth-optional callsites. No unrelated changes are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/web/client/src/server/api/trpc.ts (1)

127-135: 💤 Low value

optionalAuthProcedure is structurally identical to publicProcedure.

Both are defined as t.procedure.use(timingMiddleware) and, after the context change above, both expose the same ctx.user: User | null typing. The only thing distinguishing them today is the JSDoc — at the type level and at runtime they are interchangeable. This is fine as a semantic marker for callers, but it means a future refactor of one easily diverges from the other, and reviewers can't tell from a callsite which contract was intended.

Consider one of:

  • Drop optionalAuthProcedure and use publicProcedure for these endpoints (it already documents "you can still access user session data if they are logged in").
  • Keep both but differentiate with a tiny marker middleware so the intent is enforced (e.g., a no-op middleware named optionalAuthMiddleware that future-proofs adding logging/metrics distinct from truly public endpoints).
♻️ Option B sketch — marker middleware to keep the two procedures distinct
+const optionalAuthMiddleware = t.middleware(async ({ next, ctx }) => {
+    // Marker middleware: this procedure may be called by anonymous users.
+    // Endpoints are expected to handle `ctx.user === null` explicitly.
+    return next({ ctx });
+});
+
 /**
  * Optional auth procedure
  *
  * Use this for endpoints that surface on both authenticated and anonymous pages
  * (e.g. marketing, pricing). `ctx.user` is `User | null` — endpoints must handle
  * both cases and typically return `null` for anonymous callers instead of throwing.
  */
-export const optionalAuthProcedure = t.procedure.use(timingMiddleware);
+export const optionalAuthProcedure = t.procedure
+    .use(timingMiddleware)
+    .use(optionalAuthMiddleware);
🤖 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/client/src/server/api/trpc.ts` around lines 127 - 135, The two
procedures optionalAuthProcedure and publicProcedure are identical (both
t.procedure.use(timingMiddleware)), so either remove optionalAuthProcedure and
update callers to use publicProcedure, or keep it but add a tiny no-op
middleware (e.g., optionalAuthMiddleware) and apply it so optionalAuthProcedure
= t.procedure.use(optionalAuthMiddleware).use(timingMiddleware); implement
optionalAuthMiddleware as a named pass-through middleware to preserve
runtime/type distinction and document its intent; update imports/callsites
accordingly (search for optionalAuthProcedure and publicProcedure to change or
keep consistent).
🤖 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/client/src/server/api/routers/subscription/subscription.ts`:
- Line 7: The import currently pulling createTRPCRouter, optionalAuthProcedure,
and protectedProcedure from a relative path ('../../trpc') should be switched to
the project's configured path alias (use the `@/`* or ~/* alias form) so the
module is imported via the src alias instead of a relative path; update the
import statement to use the alias (keeping the same named imports
createTRPCRouter, optionalAuthProcedure, protectedProcedure) so the code
resolves through the project's path-mapping.

In `@apps/web/client/src/server/api/routers/user/user.ts`:
- Line 8: Replace the relative import for the tRPC helpers with the configured
path alias: update the import that currently pulls createTRPCRouter,
optionalAuthProcedure, and protectedProcedure from '../../trpc' to use the alias
that maps to src (e.g. '@/server/api/trpc') so the symbols createTRPCRouter,
optionalAuthProcedure, and protectedProcedure are imported via the path-alias
import instead of a relative path.

---

Nitpick comments:
In `@apps/web/client/src/server/api/trpc.ts`:
- Around line 127-135: The two procedures optionalAuthProcedure and
publicProcedure are identical (both t.procedure.use(timingMiddleware)), so
either remove optionalAuthProcedure and update callers to use publicProcedure,
or keep it but add a tiny no-op middleware (e.g., optionalAuthMiddleware) and
apply it so optionalAuthProcedure =
t.procedure.use(optionalAuthMiddleware).use(timingMiddleware); implement
optionalAuthMiddleware as a named pass-through middleware to preserve
runtime/type distinction and document its intent; update imports/callsites
accordingly (search for optionalAuthProcedure and publicProcedure to change or
keep consistent).
🪄 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: d5af471f-2598-499b-820a-010608820b63

📥 Commits

Reviewing files that changed from the base of the PR and between a242be5 and d7ee10a.

📒 Files selected for processing (7)
  • apps/web/client/src/app/_components/top-bar/user.tsx
  • apps/web/client/src/components/telemetry-provider.tsx
  • apps/web/client/src/components/ui/pricing-modal/use-subscription.tsx
  • apps/web/client/src/components/ui/pricing-table/index.tsx
  • apps/web/client/src/server/api/routers/subscription/subscription.ts
  • apps/web/client/src/server/api/routers/user/user.ts
  • apps/web/client/src/server/api/trpc.ts

import { headers } from 'next/headers';
import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '../../trpc';
import { createTRPCRouter, optionalAuthProcedure, protectedProcedure } from '../../trpc';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Switch this import to a configured path alias

Line 7 uses ../../trpc; this should use the project alias form in src code.

As per coding guidelines: apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*.

🤖 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/client/src/server/api/routers/subscription/subscription.ts` at line
7, The import currently pulling createTRPCRouter, optionalAuthProcedure, and
protectedProcedure from a relative path ('../../trpc') should be switched to the
project's configured path alias (use the `@/`* or ~/* alias form) so the module is
imported via the src alias instead of a relative path; update the import
statement to use the alias (keeping the same named imports createTRPCRouter,
optionalAuthProcedure, protectedProcedure) so the code resolves through the
project's path-mapping.

import { eq } from 'drizzle-orm';
import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '../../trpc';
import { createTRPCRouter, optionalAuthProcedure, protectedProcedure } from '../../trpc';
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use path alias import for tRPC module

Line 8 uses a relative import (../../trpc) in a src file. Please switch this to the configured alias import for consistency and guideline compliance.

As per coding guidelines: apps/web/client/src/**/*.{ts,tsx}: Use path aliases @/* and ~/* for imports that map to apps/web/client/src/*.

🤖 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/client/src/server/api/routers/user/user.ts` at line 8, Replace the
relative import for the tRPC helpers with the configured path alias: update the
import that currently pulls createTRPCRouter, optionalAuthProcedure, and
protectedProcedure from '../../trpc' to use the alias that maps to src (e.g.
'@/server/api/trpc') so the symbols createTRPCRouter, optionalAuthProcedure, and
protectedProcedure are imported via the path-alias import instead of a relative
path.

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.

[bug] Auth-optional components trigger UNAUTHORIZED errors

1 participant