Skip to content

Conversation

@themavik
Copy link

@themavik themavik commented Feb 9, 2026

Summary

Fixes #3051.

Root cause: createTRPCContext() throws TRPCError({ code: 'UNAUTHORIZED' }) when supabase.auth.getUser() returns an error, which happens for ALL unauthenticated visitors. This means even publicProcedure endpoints fail for visitors on marketing pages. Components like the telemetry provider, top bar auth button, and pricing table call user.get and subscription.get (protected endpoints), generating cascading UNAUTHORIZED errors and retries.

Fix: Three changes:

  1. trpc.tscreateTRPCContext(): Don't throw on auth errors. Instead, set user to null for unauthenticated visitors. protectedProcedure still validates auth via its middleware, so existing protected endpoints are unaffected.

  2. trpc.ts — new optionalAuthProcedure: A new procedure type that passes through the user context as-is (may be null). Use this for endpoints where authentication is optional.

  3. user/user.ts and subscription/subscription.ts: Added getOptional endpoints using optionalAuthProcedure that return null for unauthenticated users instead of throwing. Components on marketing/public pages should use user.getOptional and subscription.getOptional instead of the protected variants.

Changes

  • apps/web/client/src/server/api/trpc.ts:
    • createTRPCContext(): Catch auth errors gracefully, set user to null instead of throwing.
    • New optionalAuthProcedure export for auth-optional endpoints.
  • apps/web/client/src/server/api/routers/user/user.ts:
    • New getOptional query that returns user data if authenticated, null otherwise.
  • apps/web/client/src/server/api/routers/subscription/subscription.ts:
    • New getOptional query that returns subscription data if authenticated, null otherwise.

Migration Guide

Components that currently call user.get or subscription.get on pages accessible to unauthenticated visitors should switch to user.getOptional / subscription.getOptional:

- const user = api.user.get.useQuery();
+ const user = api.user.getOptional.useQuery();

Root Cause Analysis

The tRPC context creation was designed assuming all API requests come from authenticated users. When the app expanded to include marketing pages with shared components (telemetry, auth button, pricing), these components' tRPC calls failed at the context level before reaching any procedure middleware. The protectedProcedure middleware was correctly checking auth, but the context creation was throwing first.

Risk Assessment

  • Low risk for existing endpoints: protectedProcedure still enforces auth via its own middleware. Existing protected endpoints continue to reject unauthenticated requests.
  • publicProcedure now works for unauthenticated users: This is the correct behavior — public procedures should not require auth.
  • Component changes needed: Frontend components on marketing pages need to be updated to use getOptional instead of get. This PR provides the backend support; frontend migration can be done incrementally.

Important

Introduces optionalAuthProcedure to handle unauthenticated users gracefully and adds optional auth endpoints in user and subscription routers.

  • Behavior:
    • createTRPCContext() in trpc.ts: Sets user to null for unauthenticated users instead of throwing an error.
    • Introduces optionalAuthProcedure in trpc.ts for endpoints where authentication is optional.
  • Routers:
    • user.ts: Adds getOptional endpoint using optionalAuthProcedure to return user data or null.
    • subscription.ts: Adds getOptional endpoint using optionalAuthProcedure to return subscription data or null.

This description was created by Ellipsis for 1dad797. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added optional authentication support to subscription and user information endpoints, allowing both authenticated and unauthenticated users to access certain data.
    • Enhanced request handling to gracefully support visitors without forcing authentication, while maintaining security for protected features.

…ev#3051)

Part of the fix for auth-optional components triggering UNAUTHORIZED errors.
…ev#3051)

Part of the fix for auth-optional components triggering UNAUTHORIZED errors.
…ev#3051)

Part of the fix for auth-optional components triggering UNAUTHORIZED errors.
@vercel
Copy link

vercel bot commented Feb 9, 2026

@themavik is attempting to deploy a commit to the Onlook Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

📝 Walkthrough

Walkthrough

Introduce a new optional-authentication procedure and corresponding endpoints to gracefully handle unauthenticated requests. The system now returns null for missing user context instead of throwing auth errors, enabling components like telemetry and pricing displays to function without forced authentication.

Changes

Cohort / File(s) Summary
TRPC Core Auth
apps/web/client/src/server/api/trpc.ts
Modified createTRPCContext to handle auth retrieval errors permissively, assigning null to user on error or absence instead of throwing. Added new optionalAuthProcedure export that passes ctx.user as-is (possibly null) for non-strict authentication requirements.
Optional Auth Endpoints
apps/web/client/src/server/api/routers/user/user.ts, apps/web/client/src/server/api/routers/subscription/subscription.ts
Added getOptional endpoints to both routers using optionalAuthProcedure. User endpoint merges authenticated user metadata with stored user data, returning null if no user present. Subscription endpoint returns null for unauthenticated users; fetches active subscription with relations otherwise.

Sequence Diagram(s)

sequenceDiagram
    participant Component as Auth-Optional Component
    participant Procedure as optionalAuthProcedure
    participant Context as createTRPCContext
    participant Endpoint as getOptional Endpoint
    participant DB as Database

    rect rgba(200, 150, 100, 0.5)
        Note over Component,DB: Unauthenticated User Flow
        Component->>Procedure: Call endpoint (no auth token)
        Procedure->>Context: Request context creation
        Context-->>Procedure: ctx.user = null (no error thrown)
        Procedure->>Endpoint: Pass ctx with user = null
        Endpoint->>Endpoint: Check ctx.user is null
        Endpoint-->>Component: Return null gracefully
    end

    rect rgba(100, 150, 200, 0.5)
        Note over Component,DB: Authenticated User Flow
        Component->>Procedure: Call endpoint (with auth token)
        Procedure->>Context: Request context creation
        Context->>DB: Fetch authenticated user
        DB-->>Context: User data retrieved
        Context-->>Procedure: ctx.user populated
        Procedure->>Endpoint: Pass ctx with valid user
        Endpoint->>DB: Query user/subscription data
        DB-->>Endpoint: Return data
        Endpoint-->>Component: Return composed result
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Hop, hop, hop through auth's new way,
Where optional means guests can stay,
No errors chase the unaware,
Just nulls returned with tender care,
The endpoints dance, components play,
A kinder API today! 🎉

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding optionalAuthProcedure to prevent UNAUTHORIZED errors on public pages, directly addressing the core fix.
Description check ✅ Passed The description is comprehensive and well-structured, covering root cause analysis, the fix with code references, migration guide, risk assessment, and changes across three files. It follows the repository template structure.
Linked Issues check ✅ Passed The PR fully implements the objectives from issue #3051: introduces optionalAuthProcedure, adds getOptional endpoints in user and subscription routers, and modifies createTRPCContext to handle auth errors gracefully without affecting protectedProcedure.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #3051: trpc.ts context and procedure changes, plus getOptional endpoints in user and subscription routers. No unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

No actionable comments were generated in the recent review. 🎉

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

153-170: Consider whether optionalAuthProcedure adds value over publicProcedure.

With the context change above (Lines 34–41), publicProcedure already exposes a nullable ctx.user. The new optionalAuthProcedure middleware only strips supabase/headers from the context and coalesces ctx.user ?? null (already null-safe). This makes optionalAuthProcedure functionally near-identical to publicProcedure but with a narrower context.

If the intent is purely semantic (signaling "this endpoint may or may not have a user"), that's fine — but it's worth a brief comment clarifying why this isn't just publicProcedure. If you do want to keep it, consider also dropping supabase from the protectedProcedure context for consistency, since it does the same context narrowing.

apps/web/client/src/server/api/routers/subscription/subscription.ts (1)

48-75: Extract shared subscription-fetching logic to reduce duplication with get.

Lines 48–75 duplicate nearly all of the get query (Lines 20–47). Consider extracting the shared logic into a helper:

♻️ Suggested refactor
+async function fetchActiveSubscription(db: typeof import('@onlook/db/src/client').db, userId: string) {
+    const subscription = await db.query.subscriptions.findFirst({
+        where: and(
+            eq(subscriptions.userId, userId),
+            eq(subscriptions.status, SubscriptionStatus.ACTIVE),
+        ),
+        with: {
+            product: true,
+            price: true,
+        },
+    });
+
+    if (!subscription) {
+        return null;
+    }
+
+    let scheduledPrice = null;
+    if (subscription.scheduledPriceId) {
+        scheduledPrice = await db.query.prices.findFirst({
+            where: eq(prices.id, subscription.scheduledPriceId),
+        }) ?? null;
+    }
+
+    return fromDbSubscription(subscription, scheduledPrice);
+}

Then both get and getOptional call fetchActiveSubscription(ctx.db, userId).

apps/web/client/src/server/api/routers/user/user.ts (1)

29-48: Same duplication concern — extract shared user-fetching logic.

getOptional (Lines 29–48) duplicates get (Lines 12–28). Extract the shared body into a helper, e.g.:

♻️ Suggested refactor
+function fetchUserData(db: typeof import('@onlook/db/src/client').db, authUser: SupabaseUser) {
+    return db.query.users.findFirst({
+        where: eq(users.id, authUser.id),
+    }).then(user => {
+        const { displayName, firstName, lastName } = getUserName(authUser);
+        return user ? fromDbUser({
+            ...user,
+            firstName: user.firstName ?? firstName,
+            lastName: user.lastName ?? lastName,
+            displayName: user.displayName ?? displayName,
+            email: user.email ?? authUser.email,
+            avatarUrl: user.avatarUrl ?? authUser.user_metadata.avatarUrl,
+        }) : null;
+    });
+}

Then:

 get: protectedProcedure.query(async ({ ctx }) => {
-    const authUser = ctx.user;
-    const user = await ctx.db.query.users.findFirst({ ... });
-    ...
-    return userData;
+    return fetchUserData(ctx.db, ctx.user);
 }),
 getOptional: optionalAuthProcedure.query(async ({ ctx }) => {
     if (!ctx.user) return null;
-    const authUser = ctx.user;
-    ...
-    return userData;
+    return fetchUserData(ctx.db, ctx.user);
 }),

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.

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