Skip to content

Codex/redesign core product pages for saas#21

Merged
AutomatosAI merged 5 commits intomainfrom
codex/redesign-core-product-pages-for-saas
Jan 15, 2026
Merged

Codex/redesign core product pages for saas#21
AutomatosAI merged 5 commits intomainfrom
codex/redesign-core-product-pages-for-saas

Conversation

@AutomatosAI
Copy link
Copy Markdown
Owner

@AutomatosAI AutomatosAI commented Jan 15, 2026

Summary by CodeRabbit

  • New Features

    • Clerk-based sign-in/sign-up UI, new admin & tenant profile pages, and a webhook to sync identity events.
  • Bug Fixes & Improvements

    • Streamlined authentication/session flows, clearer role-based redirects, simplified store login, and minor UI spacing/layout refinements.
  • Refactor

    • Large migration from legacy auth to Clerk with consolidation of auth helpers and removal of legacy session/password endpoints and wrappers.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 15, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

Replaces NextAuth with Clerk across auth, API routes, pages, middleware, and components; removes several NextAuth routes and auth helpers; refactors Doctor Green integration and DrGreen client; introduces Clerk webhook handler and many Clerk/seed scripts; performs large Prisma schema changes and UI adjustments.

Changes

Cohort / File(s) Summary of changes / attention
Auth removal & helper
nextjs_space/lib/auth.ts, nextjs_space/components/session-provider.tsx, nextjs_space/types/next-auth.d.ts, nextjs_space/app/api/auth/[...nextauth]/route.ts
Removed NextAuth config, session provider and typings; deleted NextAuth API route.
Clerk migration (API routes & pages)
nextjs_space/app/api/** (many), nextjs_space/app/**/page.tsx (tenant-admin, super-admin, store, auth pages), nextjs_space/lib/auth-helper.ts, nextjs_space/app/layout.tsx
Replaced getServerSession/useSession with Clerk currentUser/useUser/getCurrentUser across many handlers and pages; updated auth checks to use Clerk publicMetadata and email-address fields.
Deleted password & admin routes
nextjs_space/app/api/reset-password/route.ts, .../auth/reset-password/confirm/route.ts, .../customer/change-password/route.ts, .../tenant-admin/customers/[id]/reset-password/route.ts, .../super-admin/tenants/[id]/reset-password/route.ts
Entire route implementations removed with no replacements — review callers and UX for missing endpoints.
Dr. Green integration
nextjs_space/lib/doctor-green-api.ts, nextjs_space/lib/drgreen-api-client.ts, nextjs_space/app/api/consultation/submit/route.ts, nextjs_space/app/api/orders/**
API URL default switched to prod; payload shapes and createClient signature changed; country/currency/image logic added; signing behavior updated — verify all consumers and type shapes.
Middleware & routing
nextjs_space/middleware.ts, nextjs_space/next.config.js
Replaced custom middleware with Clerk middleware wrapper and updated route matching and tenant header resolution; Next config updated for image domains/remotePatterns.
Prisma schema refactor
nextjs_space/prisma/schema.prisma
Large schema rework: new email_event_mappings model, added enums, tenant_templates redesign, field/type/constraint changes — requires careful DB migration planning.
Frontend auth pages & components
nextjs_space/app/auth/*, nextjs_space/app/store/*/login/page.tsx, nextjs_space/components/** (AdminSidebar, DashboardSidebar, HeaderProfile, TenantDashboardSidebar, navigation templates)
Old login/signup pages removed; added Clerk SignIn/SignUp and profile components; sidebars updated to use Clerk signOut/useUser.
New Clerk webhook & scripts
nextjs_space/app/api/webhooks/clerk/route.ts, nextjs_space/scripts/seed-*.ts, nextjs_space/scripts/*.ts, nextjs_space/check-user*.ts
New Svix-verified Clerk webhook handler and many seed/debug scripts that require environment secrets and idempotency checks.
Sanitization & presentation
nextjs_space/app/blog/[slug]/page.tsx, nextjs_space/app/the-wire/**, nextjs_space/components/landing/FeaturesSection.tsx
One blog page now renders raw HTML (DOMPurify removed); other minor UI spacing and DOMPurify option tweaks — review XSS exposure.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Middleware
  participant Clerk
  participant API
  participant Database

  Client->>Middleware: HTTP request (Clerk auth headers)
  Middleware->>Clerk: Validate/auth via clerkMiddleware
  Clerk-->>Middleware: Auth result (user or null)
  alt Auth valid
    Middleware->>API: Forward request + x-tenant-* headers
    API->>Clerk: getCurrentUser() / currentUser()
    Clerk-->>API: Clerk user (id, email, publicMetadata)
    API->>Database: Lookup/upsert local user by email or tenantId
    Database-->>API: user/result
    API-->>Client: 200/201 response
  else Auth invalid
    Middleware-->>Client: 401 Unauthorized
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested labels

codex

"I hopped through code with nimble paws,
Swapped old sessions for Clerk's new laws.
Tokens verified, webhooks in tune,
Seeds planted gently under the moon.
A joyful thump — the rabbits cheer, auth is new this spring!"

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title describes a redesign of core product pages, but the changeset is primarily a comprehensive migration from NextAuth to Clerk authentication with numerous file deletions, API restructuring, and related infrastructure updates. Revise the title to accurately reflect the main change, such as 'Migrate authentication from NextAuth to Clerk across all routes and components' or 'Implement Clerk-based authentication and remove NextAuth integration'.
Docstring Coverage ⚠️ Warning Docstring coverage is 28.40% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
  • 📝 Generate docstrings


📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a398c3 and 5382d9a.

📒 Files selected for processing (2)
  • nextjs_space/app/api/orders/route.ts
  • nextjs_space/app/api/tenant-admin/customers/[id]/route.ts

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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: 10

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (15)
nextjs_space/app/api/super-admin/audit-logs/route.ts (1)

18-26: Add input validation for pagination parameters.

parseInt can return NaN for invalid input, which would propagate to Prisma's take and skip options and cause unexpected behavior. Consider validating and clamping the values.

Suggested fix
     const { searchParams } = new URL(req.url);
-    const page = parseInt(searchParams.get("page") || "1");
-    const limit = parseInt(searchParams.get("limit") || "50");
+    const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10) || 1);
+    const limit = Math.min(100, Math.max(1, parseInt(searchParams.get("limit") || "50", 10) || 50));
nextjs_space/components/shop/ClientOnboarding.tsx (1)

92-103: Email default value won't populate due to async Clerk loading.

useForm's defaultValues are evaluated once at initialization. Since Clerk loads asynchronously, user is likely undefined on first render, resulting in an empty email field that never updates even after user becomes available.

Additionally, isLoaded is destructured but unused.

🔧 Proposed fix using useEffect to reset form when user loads
  const { user, isLoaded, isSignedIn } = useUser();

  const personalForm = useForm<PersonalDetails>({
    resolver: zodResolver(personalDetailsSchema),
    defaultValues: formData.personal || {
      firstName: "",
      lastName: "",
-     email: user?.primaryEmailAddress?.emailAddress || "",
+     email: "",
      phone: "",
      dateOfBirth: "",
    },
  });
+
+  // Populate email when Clerk user data becomes available
+  useEffect(() => {
+    if (isLoaded && user?.primaryEmailAddress?.emailAddress && !formData.personal?.email) {
+      personalForm.setValue("email", user.primaryEmailAddress.emailAddress);
+    }
+  }, [isLoaded, user, personalForm, formData.personal?.email]);

Alternatively, consider showing a loading skeleton until isLoaded is true to ensure user data is available before rendering the form.

nextjs_space/app/api/super-admin/tenants/route.ts (1)

121-133: Remove adminPassword from the API contract; it's extracted, required, but discarded.

You're collecting a password and then replacing it with a generated placeholder, which is a sensitive-data anti-pattern and misleads clients about the API contract. Since the code comment explicitly states Clerk handles authentication, remove adminPassword from the destructuring, validation check, and required fields.

🛠️ Suggested fix
     const {
       businessName,
       subdomain,
       countryCode,
       adminEmail,
       adminFirstName,
       adminLastName,
-      adminPassword,
     } = body;

-    if (!businessName || !subdomain || !adminEmail || !adminPassword) {
+    if (!businessName || !subdomain || !adminEmail) {
       return NextResponse.json(
         { error: "Missing required fields" },
         { status: 400 },
       );
     }
nextjs_space/app/tenant-admin/audit-logs/page.tsx (1)

52-66: Add isLoaded and isSignedIn guards before fetching audit logs.

The useEffect at line 64 calls fetchLogs() immediately without waiting for Clerk's useUser() to finish loading. Since user loading is async, the API request fires before authentication is ready, causing premature 401 responses or false empty states. Per Clerk's documented best practices, always check isLoaded first, then isSignedIn, before making authenticated API calls.

♻️ Required fix
-  const { user } = useUser();
+  const { user, isLoaded, isSignedIn } = useUser();
   const [logs, setLogs] = useState<AuditLog[]>([]);
   const [pagination, setPagination] = useState<PaginationInfo>({
     page: 1,
     limit: 50,
     total: 0,
     totalPages: 0,
   });
   const [loading, setLoading] = useState(true);
   const [actionFilter, setActionFilter] = useState<string>("");
   const [entityFilter, setEntityFilter] = useState<string>("");

   useEffect(() => {
+    if (!isLoaded) return;
+    if (!isSignedIn) {
+      setLogs([]);
+      setLoading(false);
+      return;
+    }
     fetchLogs();
-  }, [pagination.page, actionFilter, entityFilter]);
+  }, [isLoaded, isSignedIn, pagination.page, actionFilter, entityFilter]);
nextjs_space/app/tenant-admin/page.tsx (1)

28-50: Guard missing email to prevent Prisma from selecting an arbitrary user.

When email is undefined, Prisma drops the filter and can return the first user, which can accidentally bind the wrong tenant. Fail fast if no email is present.

🔧 Suggested guard
-  const email = user.emailAddresses[0]?.emailAddress;
+  const email =
+    user.primaryEmailAddress?.emailAddress ??
+    user.emailAddresses[0]?.emailAddress;
+  if (!email) {
+    redirect("/sign-in");
+  }
   const localUser = await prisma.users.findFirst({
-    where: { email: email },
+    where: { email },
     include: {
       tenants: {
nextjs_space/app/api/store/[slug]/orders/submit/route.ts (1)

51-70: Add email validation before passing to webhook.

The user.email from getCurrentUser() uses optional chaining (user.emailAddresses[0]?.emailAddress), so it could be undefined. Currently, this potentially undefined value is passed directly to the webhook payload without validation. Webhook consumers may fail or misbehave if they expect a valid email. Validate that user.email exists before the triggerWebhook call, or handle the case where it's missing.

nextjs_space/app/api/tenant-admin/orders/[id]/admin-notes/route.ts (1)

15-42: Guard against missing Clerk email before tenant lookup.

If user.emailAddresses[0] is undefined, Prisma can end up querying without an email filter, risking cross-tenant access or a 500. Fail fast when email is missing.

🛠️ Proposed fix
-    const email = user.emailAddresses[0]?.emailAddress;
+    const email = user.emailAddresses[0]?.emailAddress;
+    if (!email) {
+      return NextResponse.json({ error: "Email required" }, { status: 400 });
+    }
     const role = (user.publicMetadata.role as string) || "";
@@
-    const localUser = await prisma.users.findFirst({
-      where: { email: email },
+    const localUser = await prisma.users.findFirst({
+      where: { email },
       include: { tenants: true },
     });
nextjs_space/app/api/tenant-admin/posts/route.ts (1)

62-71: Fix authorId to use the local database user ID, not the Clerk user ID.

authorId is being set to user.id from getCurrentUser(), which returns the raw Clerk user ID (format: user_xxxxx). However, the posts.authorId column is a foreign key to the local users table. The database user record's id field depends on how users are synced:

  • The Clerk webhook (line 107 in app/api/webhooks/clerk/route.ts) creates users with id: user_${clerkId}, resulting in double-prefixed IDs like user_user_xxxxx
  • The seed script uses id: clerkUser.id directly, resulting in user_xxxxx

This inconsistency will cause FK constraint violations when inserting posts. Either:

  1. Standardize user ID creation across all paths (webhook and seed) to use raw Clerk IDs, OR
  2. Look up the local database user and use their ID instead of the Clerk ID
nextjs_space/app/tenant-admin/branding/page.tsx (1)

15-25: Handle potential undefined email before database query.

If user.emailAddresses[0] is undefined, email will be undefined, and the Prisma query will search for email: undefined, which may not behave as expected. Add an early guard for missing email.

Proposed fix
  const email = user.emailAddresses[0]?.emailAddress;
+ if (!email) {
+   redirect('/sign-in');
+ }
+
  const localUser = await prisma.users.findFirst({
    where: { email: email },
nextjs_space/app/api/tenant-admin/posts/[id]/route.ts (1)

117-129: Potential null pointer error for SUPER_ADMIN without local user.

Line 121 uses localUser!.tenantId with a non-null assertion, but a SUPER_ADMIN bypasses the tenant ownership check at line 103 and may not have a corresponding local user record. This would cause a runtime error when checking slug uniqueness.

Proposed fix
    if (validatedData.title && validatedData.title !== existingPost.title) {
      const baseSlug = slugify(validatedData.title);
      let uniqueSlug = baseSlug;
      let counter = 1;
+     const tenantIdForSlug = localUser?.tenantId ?? existingPost.tenantId;
      while (
        await prisma.posts.findFirst({
          where: {
            slug: uniqueSlug,
-           tenantId: localUser!.tenantId,
+           tenantId: tenantIdForSlug,
            NOT: { id },
          },
        })
      ) {
        uniqueSlug = `${baseSlug}-${counter}`;
        counter += 1;
      }
      dataToUpdate.slug = uniqueSlug;
    }
nextjs_space/app/api/tenant-admin/orders/route.ts (1)

2-39: Tenant ID mismatch: Clerk Org ID stored in metadata, but local UUID used in database.

The onboarding flow (/api/onboarding/route.ts line 151) stores the Clerk Org ID (format org_...) to publicMetadata.tenantId, but creates a local tenant with a UUID in the database (line 199). When getCurrentUser() retrieves this metadata and passes it to database queries (orders/route.ts lines 35, 363), the mismatch causes all queries to fail silently with 404s or empty results.

Fix: Update line 151 in /api/onboarding/route.ts to store the local tenantId (UUID) instead of clerkOrg.id:

tenantId: tenantId  // Use the local UUID, not clerkOrg.id

This applies to both GET and PATCH handlers as noted.

nextjs_space/app/api/onboarding/route.ts (1)

146-215: Move Clerk metadata update after local tenant creation and use local UUID.

The code sets publicMetadata.tenantId to clerkOrg.id (format: org_...), but all downstream routes extract this value and use it to query the database where the tenant ID is a UUID. This causes database lookups to fail. Update metadata after creating the local tenant with the correct UUID.

✅ Align metadata with local tenant ID
-    // 4. Update Clerk User Metadata with new Org ID (for easier lookup later)
-    const client = await clerkClient();
-    await client.users.updateUserMetadata(clerkUser.id, {
-      publicMetadata: {
-        role: "TENANT_ADMIN",
-        tenantId: clerkOrg.id
-      }
-    });
...
-    const tenant = await prisma.tenants.create({
+    const tenant = await prisma.tenants.create({
       data: {
         id: tenantId,
         ...
         settings: {
           contactInfo,
           templatePreset: templateId || "modern",
           clerkOrgId: clerkOrg.id
         },
       },
     });
+    // 4. Update Clerk User Metadata with local Tenant UUID
+    const client = await clerkClient();
+    await client.users.updateUserMetadata(clerkUser.id, {
+      publicMetadata: {
+        role: "TENANT_ADMIN",
+        tenantId: tenant.id,
+        clerkOrgId: clerkOrg.id
+      }
+    });
nextjs_space/lib/doctor-green-api.ts (1)

84-88: Dead code: redundant apiKey check.

Line 88 (if (!apiKey) console.warn(...)) will never execute because lines 84-86 already throw an error if !apiKey || !secretKey. This warning is unreachable.

🧹 Proposed fix
  if (!apiKey || !secretKey) {
    throw new Error("MISSING_CREDENTIALS");
  }

-  if (!apiKey) console.warn("Warning: No Dr Green API Key provided");
-
  return callDrGreenAPI(endpoint, {
nextjs_space/app/api/tenant-admin/settings/route.ts (1)

89-105: Critical: Duplicate encryption of drGreenApiKey.

The same encryption logic for drGreenApiKey appears twice (lines 89-96 and 98-105). This will encrypt the key twice if provided, resulting in corrupted/unusable data.

🐛 Proposed fix: Remove duplicate block
     if (drGreenApiKey && drGreenApiKey.trim() !== '') {
       try {
         dataToUpdate.drGreenApiKey = encrypt(drGreenApiKey);
       } catch (e) {
         console.error('Encryption failed:', e);
         throw e;
       }
     }
-
-    if (drGreenApiKey && drGreenApiKey.trim() !== '') {
-      try {
-        dataToUpdate.drGreenApiKey = encrypt(drGreenApiKey);
-      } catch (e) {
-        console.error('Encryption failed:', e);
-        throw e;
-      }
-    }
nextjs_space/templates/healingbuds/components/Navigation.tsx (1)

253-309: Replace stale status checks with Clerk auth state.
status is undefined in this component, so rendering at Line 253/399 will throw. Use isLoaded && isSignedIn from Clerk instead.

🛠️ Proposed fix
-                {status === 'authenticated' ? (
+                {isLoaded && isSignedIn ? (
                   <DropdownMenu>
                     ...
                   </DropdownMenu>
                 ) : (
                   <Link
                     href={`/store/${tenant.subdomain}/login`}
                     ...
                   >
                     <User className="w-6 h-6" />
                   </Link>
                 )}
-                {status === 'authenticated' ? (
+                {isLoaded && isSignedIn ? (
                   <>
                     ...
                   </>
                 ) : (
                   <Link
                     href={`/store/${tenant.subdomain}/login`}
                     ...
                   >
                     Patient Login
                   </Link>
                 )}

Also applies to: 399-432

🤖 Fix all issues with AI agents
In `@nextjs_space/app/api/store/`[slug]/cart/add/route.ts:
- Around line 12-16: The route is passing Clerk's user.id from getCurrentUser()
into addToCart/getCart causing ensureClientId() to fail — switch to using
currentUser(), lookup the application DB user by email (as done in
cart/clear/route.ts), and pass the DB user's id (dbUser.id) into
addToCart/getCart; update both this file (route.ts under cart/add) and
cart/route.ts to replace getCurrentUser() usage with currentUser(), perform
prisma.users.findUnique({ where: { email: user.email } }) (or equivalent) to
obtain dbUser, and forward dbUser.id to ensureClientId()/cart functions.

In `@nextjs_space/app/api/store/`[slug]/orders/[orderId]/route.ts:
- Around line 12-24: The code mixes Clerk ID and local DB user IDs causing
inconsistent lookups; standardize to lookup users by email across order routes:
get the current user via currentUser(), extract email from
user.emailAddresses[0].emailAddress, then always use prisma.users.findFirst({
where: { email } }) to obtain dbUser and use dbUser.id for all order queries
(including in GET /api/store/[slug]/orders, POST /api/store/[slug]/orders/submit
and POST /api/orders) or alternatively ensure every user creation path writes
the Clerk ID into the users table (e.g., clerkId field) and change all queries
to use that clerkId consistently; update references to currentUser(),
emailAddresses, prisma.users.findFirst, dbUser and submitOrder() accordingly so
all routes use the same lookup key.

In `@nextjs_space/app/api/tenant-admin/customers/`[id]/route.ts:
- Around line 22-32: The request lacks a guard for missing email which lets
prisma.users.findFirst({ where: { email } }) run with undefined and match
arbitrary users; add an explicit check for user.emailAddresses[0]?.emailAddress
(the local variable email) and return a 401/400 before calling
prisma.users.findFirst in this route handler and the corresponding GET, PATCH,
DELETE handlers in route.ts; ensure the same guard is applied wherever you use
prisma.users.findFirst with email to prevent cross-tenant access and only
proceed when email is a non-empty string.

In `@nextjs_space/app/api/tenant-admin/settings/test-smtp/route.ts`:
- Around line 15-24: The extracted email variable (const email =
user.emailAddresses[0]?.emailAddress) can be undefined and must be explicitly
guarded before calling prisma.users.findFirst; add a check immediately after
that line to return an error response (401 or 400) if email is falsy, so you
don't call prisma.users.findFirst({ where: { email } }) with undefined which
causes unintended user matches—update the route handler to validate email and
only proceed to the prisma.users.findFirst call when email is present.

In `@nextjs_space/app/api/tenant-admin/webhooks/`[id]/route.ts:
- Around line 22-35: The Prisma lookup uses email which may be undefined and
will remove the filter in Prisma 6.7.0; add an explicit guard that verifies
email (from user.emailAddresses[0]?.emailAddress) is a non-empty string before
calling prisma.users.findFirst in the PATCH handler (route.ts) and return an
error (e.g., 400/401 JSON) if missing, then apply the identical guard to the
DELETE endpoint; update the logic around the localUser/tenantId fetching so
prisma.users.findFirst only runs when email is truthy to avoid accidental
full-table matches.

In `@nextjs_space/app/api/user/profile/route.ts`:
- Around line 45-54: The Prisma update call in prisma.users.update is using the
non-null-asserted user.email (user.email!) which can be undefined at runtime;
change the query to use the stable user.id instead. Locate the
prisma.users.update block in route.ts (the code that builds the where: { ... }
object) and replace where: { email: user.email! } with where: { id: user.id } so
the update uses the always-present Clerk user id; keep the rest of the data
object unchanged.

In `@nextjs_space/app/store/`[slug]/the-wire/[postSlug]/page.tsx:
- Line 6: Restore HTML sanitization for rendered post content: re-enable the
DOMPurify import (DOMPurify from "isomorphic-dompurify") and apply it to
post.content before passing it into the renderer (where dangerouslySetInnerHTML
or similar is used in the page component that renders post.content). Ensure you
call DOMPurify.sanitize(post.content, {SAFE_FOR_TEMPLATES: true} or equivalent
safe config) and use the sanitized string in place of raw post.content so stored
XSS is prevented; update any variable names around post.content rendering to use
the sanitized value.

In `@nextjs_space/app/tenant-admin/seo/page.tsx`:
- Around line 18-24: Guard against undefined email by reading
user.primaryEmailAddress (or validate user.emailAddresses[0]) and
returning/redirecting or throwing if it's missing before calling the DB; replace
the prisma.users.findFirst({ where: { email } }) call with
prisma.users.findUnique({ where: { email } }) using the validated email
variable, and keep using the localUser.tenantId check after the safe lookup.
Apply the same change pattern wherever tenant-admin pages use
user.emailAddresses[0] and findFirst to avoid leaking another tenant's data.

In `@nextjs_space/scripts/seed-clerk-orgs.ts`:
- Around line 93-110: The loop that calls
clerkClient.organizations.createOrganizationMembership is passing the Prisma
primary key (user.id) instead of the Clerk user ID; change the call to use the
Clerk user ID field stored on your user record (e.g., user.clerkId or
user.clerkUserId) when setting userId in createOrganizationMembership, and add a
guard to log and skip users missing that Clerk ID so you don't attempt
membership creation with an invalid identifier; update references in the
tenant.users iteration and the error/log messages accordingly.
- Around line 51-53: The seed uses Prisma user IDs (tenant.users[0]?.id,
adminId, userId) where Clerk expects Clerk user IDs (e.g.,
createOrganization.createdBy); fix by resolving the Clerk ID before calling
Clerk APIs: for each user referenced (tenant.users[0], adminId, userId) query
your users table for a clerkUserId column or lookup the Clerk user by email (via
Clerk SDK) and substitute that clerkUserId into createdBy/adminId/userId; update
the seed-clerk-orgs logic (the block calling createOrganization and where
adminId/userId are assigned) to use the resolved Clerk user IDs instead of
Prisma UUIDs.
🟠 Major comments (28)
nextjs_space/check-user.ts-1-1 (1)

1-1: Debug script needs refactoring before merge.

Several concerns with this utility script:

  1. Code duplication: This file is nearly identical to check-user-pt.ts, differing only by email address. Consider consolidating into a single parameterized script.

  2. Hardcoded PII: Email address is hardcoded in source. Accept it as a CLI argument instead.

  3. No error handling: Unhandled promise rejection if query fails.

  4. Formatting: Single-line code is unreadable.

  5. Location: Other debug scripts are in scripts/ directory (e.g., debug-drgreen-raw.ts, seed-co-admin.ts).

♻️ Suggested consolidated script at `scripts/check-user.ts`
import { prisma } from '../lib/db';

async function checkUser(email: string) {
  try {
    const user = await prisma.users.findUnique({
      where: { email },
      include: { tenants: true },
    });
    
    if (!user) {
      console.log(`No user found with email: ${email}`);
      return;
    }
    
    console.log(JSON.stringify(user, null, 2));
  } catch (error) {
    console.error('Failed to fetch user:', error);
    process.exit(1);
  } finally {
    await prisma.$disconnect();
  }
}

const email = process.argv[2];
if (!email) {
  console.error('Usage: npx ts-node scripts/check-user.ts <email>');
  process.exit(1);
}

checkUser(email);

Usage: npx ts-node scripts/check-user.ts admin@healingbuds.co

nextjs_space/check-user-pt.ts-1-1 (1)

1-1: Remove duplicate script.

This file is a copy of check-user.ts with only the email address changed. Delete this file and use the consolidated parameterized script suggested in the other review comment.

nextjs_space/scripts/seed-real-users.ts-5-18 (1)

5-18: Add environment guard and use stronger seed credentials.

This script seeds admin users with a weak hardcoded password. While this appears to be a development/staging seed script, consider:

  1. Adding an environment check to prevent accidental execution in production
  2. Using a stronger default password or environment variable
🔒 Proposed fix
 async function main() {
+    // Prevent accidental execution in production
+    if (process.env.NODE_ENV === 'production') {
+        console.error('This seed script should not run in production!');
+        process.exit(1);
+    }
+
     const tenantEmail = 'gerard@automatos.app';
     const superEmail = 'gerard161@gmail.com';
-    const password = await hash('admin123', 12);
+    const password = await hash(process.env.SEED_PASSWORD || 'DevAdmin123!', 12);
nextjs_space/app/tenant-admin/the-wire/[id]/page.tsx-32-36 (1)

32-36: Handle missing email address to avoid undefined query behavior.

If the Clerk user has no verified email addresses, email will be undefined, and querying Prisma with { email: undefined } may return unexpected results (e.g., a user with a NULL email).

Consider adding an explicit check:

🛡️ Proposed fix
  // Verify tenant access
  const email = user.emailAddresses[0]?.emailAddress;
+ if (!email) {
+   redirect("/sign-in");
+ }
  const localUser = await prisma.users.findFirst({
    where: { email: email },
    include: { tenants: true },
  });
nextjs_space/scripts/seed-co-admin.ts-5-8 (1)

5-8: Remove hardcoded credentials; load from env and require a strong secret.

Embedding a plaintext password in the repo is a security/compliance risk and encourages weak credentials in real environments. Prefer reading from env and fail fast if missing.

🔐 Proposed fix
 async function main() {
-    const email = 'admin@healingbuds.co';
-    const password = await hash('admin123', 12);
+    const email = process.env.SEED_ADMIN_EMAIL ?? 'admin@healingbuds.co';
+    const rawPassword = process.env.SEED_ADMIN_PASSWORD;
+    if (!rawPassword) {
+        throw new Error('SEED_ADMIN_PASSWORD is required');
+    }
+    const password = await hash(rawPassword, 12);
nextjs_space/scripts/debug_user.ts-21-22 (1)

21-22: Avoid dumping full user records (PII) to logs.
JSON.stringify(users, null, 2) can expose sensitive fields in shared logs. Consider logging only a minimal, redacted view.

🔐 Safer logging
-    console.log("Found Users:", JSON.stringify(users, null, 2));
+    const redacted = users.map((u: any) => ({
+        id: u.id,
+        email: u.email?.replace(/(^.).+(@.*$)/, "$1***$2"),
+        tenantId: u.tenantId
+    }));
+    console.log("Found Users:", JSON.stringify(redacted, null, 2));
nextjs_space/scripts/debug_user.ts-21-32 (1)

21-32: Avoid async forEach — pending queries will be dropped before Prisma disconnect.

forEach doesn't await the async callback, so after forEach completes, main() immediately returns and the finally block disconnects Prisma while tenant queries (line 25) are still pending. Use a for...of loop to ensure all queries complete before disconnect.

✅ Safer loop
-    users.forEach(async (user: any) => {
-        if (user.tenantId) {
-            const tenant = await prisma.tenants.findUnique({
-                where: { id: user.tenantId }
-            });
-            console.log(`User ${user.email} associated with tenant:`, tenant?.businessName);
-        } else {
-            console.log(`User ${user.email} has NO tenantId set.`);
-        }
-    });
+    for (const user of users) {
+        if (user.tenantId) {
+            const tenant = await prisma.tenants.findUnique({
+                where: { id: user.tenantId }
+            });
+            console.log(`User ${user.email} associated with tenant:`, tenant?.businessName);
+        } else {
+            console.log(`User ${user.email} has NO tenantId set.`);
+        }
+    }
nextjs_space/scripts/fix_missing_user.ts-7-11 (1)

7-11: Avoid committing PII/tenant IDs and logging full emails.

Hardcoding real emails/tenant IDs in the repo and logging them verbatim is a compliance/privacy risk. Prefer env/CLI inputs and redact logs.

🔧 Suggested change (env/argv + redacted log)
 async function main() {
-    const email = "gerard161+clerk@gmail.com";
-    const tenantId = "1ae9473f-318d-4c5b-9f1c-e43caf7986cd";
-    const businessName = "clerk";
+    const email = process.env.FIX_USER_EMAIL ?? process.argv[2];
+    const tenantId = process.env.FIX_USER_TENANT_ID ?? process.argv[3];
+    const businessName = process.env.FIX_USER_BUSINESS_NAME ?? "clerk";
+    if (!email || !tenantId) {
+        console.error("Missing email or tenantId. Set FIX_USER_EMAIL/FIX_USER_TENANT_ID or pass argv.");
+        process.exit(1);
+    }
+    const redactedEmail = email.replace(/(^.).*(@.*$)/, "$1***$2");
 
-    console.log(`Restoring missing user: ${email} for tenant: ${tenantId}`);
+    console.log(`Restoring missing user: ${redactedEmail} for tenant: ${tenantId}`);
nextjs_space/scripts/fix_missing_user.ts-13-37 (1)

13-37: Use upsert and avoid storing plaintext password placeholders.

The check-then-create pattern has a race condition window. More importantly, storing the literal string CLERK_MANAGED_ACCOUNT as a plaintext password is a security concern. The schema requires password to be a non-nullable String, so use bcryptjs.hash() (already in your codebase—see seed.ts, seed-real-users.ts) or a hashed placeholder value like in seed-clerk-users.ts.

Replace the check-then-update/create flow with upsert, which is safe since email has a unique constraint:

♻️ Suggested change (upsert + hashed password)
-    const existing = await prisma.users.findUnique({
-        where: { email }
-    });
-
-    if (existing) {
-        console.log("User already exists! Updating tenantId...");
-        await prisma.users.update({
-            where: { email },
-            data: { tenantId }
-        });
-    } else {
-        console.log("Creating new user record...");
-        await prisma.users.create({
-            data: {
-                id: nodeCrypto.randomUUID(),
-                email,
-                password: "CLERK_MANAGED_ACCOUNT",
-                name: businessName,
-                role: "TENANT_ADMIN",
-                tenantId: tenantId,
-                updatedAt: new Date(),
-            }
-        });
-    }
+    console.log("Upserting user record...");
+    await prisma.users.upsert({
+        where: { email },
+        update: { tenantId },
+        create: {
+            id: nodeCrypto.randomUUID(),
+            email,
+            // Use bcryptjs.hash() like seed files, or a hashed placeholder
+            password: await bcrypt.hash('clerk-managed-account', 10),
+            name: businessName,
+            role: "TENANT_ADMIN",
+            tenantId,
+            updatedAt: new Date(),
+        }
+    });
nextjs_space/app/auth/callback/page.tsx-13-27 (1)

13-27: Verify admin role server‑side; do not route based on client‑readable metadata alone.
While publicMetadata cannot be modified from client code, it is still readable by the frontend and should not be the sole basis for routing to protected admin areas. Implement server‑side verification using Clerk's session/auth context (or privateMetadata via backend API) before granting access to admin routes.

nextjs_space/app/auth/callback/page.tsx-15-35 (1)

15-35: Guard email presence before DB lookup and prefer primaryEmailAddress.

Clerk supports phone-only accounts without email addresses. The current code queries the database without verifying that email exists, which can cause mis-routing. Use primaryEmailAddress (the recommended server-side pattern per Clerk documentation) with a fallback, and guard the DB lookup with an email presence check. If no role and no email exist, redirect to a safe fallback.

🛠️ Suggested fix
-    const email = user.emailAddresses[0]?.emailAddress;
+    const email =
+        user.primaryEmailAddress?.emailAddress ??
+        user.emailAddresses[0]?.emailAddress;

@@
-    if (!role) {
-        const localUser = await prisma.users.findFirst({ where: { email } });
-        if (localUser?.role === "SUPER_ADMIN") return redirect("/super-admin");
-        if (localUser?.role === "TENANT_ADMIN") return redirect("/tenant-admin");
-    }
+    if (!role && email) {
+        const localUser = await prisma.users.findFirst({ where: { email } });
+        if (localUser?.role === "SUPER_ADMIN") return redirect("/super-admin");
+        if (localUser?.role === "TENANT_ADMIN") return redirect("/tenant-admin");
+    }
+    if (!role) {
+        return redirect("/");
+    }
nextjs_space/app/api/webhooks/clerk/route.ts-58-59 (1)

58-59: Avoid logging full webhook payload (PII).

Line 59 logs the entire body, which includes emails and names. This is a privacy/compliance risk in production logs.

✅ Proposed fix (reduce logging)
-    console.log("Webhook body:", body);
+    // Avoid logging full payload (PII). Keep minimal metadata only.
+    console.log("Webhook received.");
nextjs_space/app/api/webhooks/clerk/route.ts-77-110 (1)

77-110: Make user sync idempotent with an atomic upsert.

Lines 77-110 use findUnique followed by conditional update/create, which can race under concurrent webhook retries and throw unique constraint errors on the email field. Replace with upsert to make this atomic while still avoiding overwrites of role and tenantId.

Critical: The proposed fix uses role: "CONSUMER", but the schema only allows PATIENT, TENANT_ADMIN, or SUPER_ADMIN. Use role: "PATIENT" instead (the schema default).

✅ Corrected atomic upsert
-        const existingUser = await prisma.users.findUnique({ where: { email } });
-
-        if (existingUser) {
-            await prisma.users.update({
-                where: { email },
-                data: {
-                    name: name || undefined,
-                    firstName: first_name || undefined,
-                    lastName: last_name || undefined,
-                    updatedAt: new Date(),
-                    // We do NOT update role or tenantId here typically, as that's business logic
-                }
-            });
-        } else {
-            await prisma.users.create({
-                data: {
-                    email,
-                    name: name,
-                    firstName: first_name || null,
-                    lastName: last_name || null,
-                    role: "CONSUMER", // Default role
-                    isActive: true,
-                    id: `user_${id}`, // Sync IDs if possible or just use generated
-                    updatedAt: new Date(),
-                }
-            });
-        }
+        await prisma.users.upsert({
+            where: { email },
+            update: {
+                name: name || undefined,
+                firstName: first_name || undefined,
+                lastName: last_name || undefined,
+                updatedAt: new Date(),
+                // Do NOT update role or tenantId here
+            },
+            create: {
+                email,
+                name,
+                firstName: first_name || null,
+                lastName: last_name || null,
+                role: "PATIENT",
+                isActive: true,
+                id: `user_${id}`,
+                updatedAt: new Date(),
+            },
+        });
nextjs_space/app/api/webhooks/clerk/route.ts-31-46 (1)

31-46: Use the raw request body for Svix signature verification.

Lines 32–34 parse the JSON then stringify it. Svix signs the request body as raw bytes (svix-id + "." + svix-timestamp + "." + raw_body), so any re-encoding—including parse-then-stringify—changes the string and breaks signature verification. Call req.text() once to get the raw body, then verify it directly.

Proposed fix
-    const payload = await req.json();
-    const body = JSON.stringify(payload);
+    const body = await req.text();
nextjs_space/app/api/signup/route.ts-3-3 (1)

3-3: Remove unused password validation and plaintext storage

The input password is required in API validation (line 23) but never used—only a plaintext clerk_managed_<uuid> placeholder is stored (lines 59–66). Since authentication is fully handled by Clerk, the password field should be made nullable in the schema to reflect the Clerk-only architecture, and the validation check should be removed to eliminate the confusing dead code.

Suggested fix
-    if (!email || !password || !firstName || !lastName) {
+    if (!email || !firstName || !lastName) {
       return NextResponse.json(
         { error: "Missing required fields" },
         { status: 400 },
       );
     }

-    const placeholderPassword = `clerk_managed_${crypto.randomUUID()}`;

     const user = await prisma.users.create({
       data: {
         email,
-        password: placeholderPassword,
         name: `${firstName} ${lastName}`,
         role: "PATIENT",
         tenantId: tenant.id,
       },
     });

Also requires updating the Prisma schema to make password nullable: password String?

Also applies to: 59–66

nextjs_space/app/api/tenant-admin/orders/[id]/route.ts-2-2 (1)

2-2: Inconsistent authentication pattern and missing role check.

This file uses currentUser() directly and performs a manual DB lookup for tenantId, whereas other tenant-admin routes (e.g., products/reorder/route.ts) use the getCurrentUser() helper from @/lib/auth-helper which extracts tenantId directly from publicMetadata.

More critically, this endpoint lacks a role authorization check. Other tenant-admin routes verify the user has TENANT_ADMIN or SUPER_ADMIN role before proceeding. Without this check, any authenticated Clerk user could potentially access order details.

Proposed fix
 import { NextResponse } from "next/server";
-import { currentUser } from "@clerk/nextjs/server";
+import { getCurrentUser } from "@/lib/auth-helper";
 import { prisma } from "@/lib/db";

 /**
  * GET /api/tenant-admin/orders/[id]
  *
  * Fetch a single order by ID for the authenticated tenant admin.
  * Used for packing slip generation and order detail views.
  */
 export async function GET(
   request: Request,
   { params }: { params: { id: string } },
 ) {
   try {
-    const user = await currentUser();
+    const user = await getCurrentUser();

-    if (!user) {
+    if (
+      !user ||
+      (user.role !== "TENANT_ADMIN" && user.role !== "SUPER_ADMIN")
+    ) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }

-    const email = user.emailAddresses[0]?.emailAddress;
-
-    // Fetch user's tenant
-    const localUser = await prisma.users.findFirst({
-      where: { email: email },
-      select: { tenantId: true },
-    });
-
-    if (!localUser?.tenantId) {
+    if (!user.tenantId) {
       return NextResponse.json(
         { error: "No tenant associated with user" },
         { status: 403 },
       );
     }

     // Fetch order with items and user data
     const order = await prisma.orders.findFirst({
       where: {
         id: params.id,
-        tenantId: localUser.tenantId,
+        tenantId: user.tenantId,
       },

Also applies to: 16-28

nextjs_space/app/api/super-admin/templates/[id]/route.ts-13-19 (1)

13-19: Guard email before audit log; verify Clerk role configuration is server-controlled.

email! at line 89 bypasses the optional chain from line 19—if a user lacks an email address, the audit log receives undefined and fails. Additionally, user.publicMetadata.role depends entirely on your Clerk setup; if this metadata is ever modifiable by clients (through misconfiguration), it opens privilege escalation. Confirm with your Clerk admin that publicMetadata is set only server-side and cannot be tampered with by end users.

🔧 Fix email handling
    const email = user.emailAddresses[0]?.emailAddress;
+
+    if (!email) {
+      return NextResponse.json(
+        { error: "User email missing" },
+        { status: 400 },
+      );
+    }
@@
-      userEmail: email!,
+      userEmail: email,

Also applies to: line 89

nextjs_space/app/tenant-admin/the-wire/page.tsx-20-27 (1)

20-27: Guard against unscoped user lookup when email is missing.

If user.emailAddresses[0]?.emailAddress is undefined, Prisma ignores the email filter and returns an arbitrary user record, exposing another tenant's posts. Use primaryEmailAddress as a fallback and validate the email exists before querying.

🔧 Suggested fix
  const email = user.emailAddresses[0]?.emailAddress;
+  if (!email) {
+    redirect("/sign-in");
+  }
   const localUser = await prisma.users.findFirst({
-    where: { email: email },
+    where: { email },
     include: { tenants: true },
   });

Alternatively, use primaryEmailAddress as a fallback:

-  const email = user.emailAddresses[0]?.emailAddress;
+  const email = user.primaryEmailAddress?.emailAddress ?? user.emailAddresses[0]?.emailAddress;
+  if (!email) {
+    redirect("/sign-in");
+  }
nextjs_space/app/tenant-admin/customers/[id]/page.tsx-24-32 (1)

24-32: Guard email and tenantId before querying customer data.

If email is undefined, Prisma may not filter correctly. Additionally, tenantId can be undefined and the customer query on line 34 relies on optional chaining ...(tenantId && { tenantId }) to conditionally filter, allowing a request without proper tenant isolation if email lookup fails. While line 55-57 provides a post-query validation, fail fast with explicit guards.

🔧 Suggested guard
   let tenantId: string | undefined;
   if (user.publicMetadata.role === "TENANT_ADMIN") {
-    const email = user.emailAddresses[0]?.emailAddress;
+    const email =
+      user.primaryEmailAddress?.emailAddress ??
+      user.emailAddresses[0]?.emailAddress;
+    if (!email) {
+      redirect("/sign-in");
+    }
     const localUser = await prisma.users.findFirst({
-      where: { email: email },
+      where: { email },
       select: { tenantId: true },
     });
+    if (!localUser?.tenantId) {
+      notFound();
+    }
     tenantId = localUser.tenantId;
   }
nextjs_space/app/api/tenant-admin/tenant/route.ts-13-17 (1)

13-17: Add a role gate for this tenant‑admin endpoint.
Right now any authenticated user with a tenantId can access tenant data. Restrict to TENANT_ADMIN/SUPER_ADMIN to avoid exposure.

✅ Suggested authorization check
-    if (!user || !user.tenantId) {
+    if (
+      !user ||
+      !user.tenantId ||
+      !["TENANT_ADMIN", "SUPER_ADMIN"].includes(user.role)
+    ) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }
nextjs_space/app/tenant-admin/settings/page.tsx-18-33 (1)

18-33: Add email validation before the Prisma query to prevent undefined email lookups.
The email is extracted with optional chaining but never validated before being used as the lookup key for tenant data. If the email is undefined, the query behavior becomes unreliable and could fail to properly scope the tenant lookup.

🛡️ Suggested guard
  const email = user.emailAddresses[0]?.emailAddress;
+ if (!email) {
+   redirect("/sign-in");
+ }
  const localUser = await prisma.users.findFirst({
    where: { email },
    include: { tenants: true },
  });
nextjs_space/app/tenant-admin/emails/[id]/page.tsx-20-27 (1)

20-27: Guard missing email before the Prisma lookup to prevent cross‑tenant access.
Clerk's currentUser() can return a user without verified email addresses. If emailAddresses[0]?.emailAddress is undefined, Prisma's findFirst({ where: { email: undefined } }) removes the filter entirely, returning an unfiltered first row. The subsequent code then uses this wrong user's tenant ID to query templates, allowing access to unintended tenant data. Add an explicit email check and redirect before the database query.

🛠️ Suggested hardening
  const email = user.emailAddresses[0]?.emailAddress;
+  if (!email) {
+    redirect("/sign-in");
+  }
   const localUser = await prisma.users.findFirst({
     where: { email: email },
     include: { tenants: true },
   });
nextjs_space/app/api/super-admin/platform-settings/route.ts-112-121 (1)

112-121: Keep GET authorization consistent with POST.

GET only checks user.role. If a super admin exists in the DB but their token metadata is missing, GET will return 403 while POST allows access. Align GET with the dual-check used in POST.

🛠️ Proposed fix
-    // Role check
-    if (user.role !== 'SUPER_ADMIN') {
-      // Optional: db check
-      return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
-    }
+    const dbUser = user.email
+      ? await prisma.users.findUnique({ where: { email: user.email } })
+      : null;
+    if (dbUser?.role !== "SUPER_ADMIN" && user.role !== "SUPER_ADMIN") {
+      return NextResponse.json({ error: "Forbidden" }, { status: 403 });
+    }
nextjs_space/components/admin/HeaderProfile.tsx-25-27 (1)

25-27: Inconsistent sign-out redirect URL.

The signOut redirects to /auth/login, but other files in this PR (e.g., branding/page.tsx line 12, super-admin/layout.tsx line 22) redirect unauthorized users to /sign-in. This inconsistency could cause confusion or 404 errors if /auth/login no longer exists after the Clerk migration.

Proposed fix
    const handleLogout = async () => {
-       await signOut({ redirectUrl: "/auth/login" });
+       await signOut({ redirectUrl: "/sign-in" });
    };
nextjs_space/scripts/seed-clerk-users.ts-17-37 (1)

17-37: Security: Weak hardcoded passwords for admin accounts.

Using admin123 as passwords, even for dev/test seeding, creates security risks if this script is accidentally run in a non-development environment or if these credentials leak. Consider using environment variables or generating random passwords that are logged to console during seeding.

Suggested approach
+const generateSecurePassword = () => {
+    return crypto.randomUUID().replace(/-/g, '').slice(0, 16);
+};
+
 const USERS_TO_SEED = [
     {
         email: 'admin@budstack.io',
-        password: 'admin123',
+        password: process.env.SEED_ADMIN_PASSWORD || generateSecurePassword(),
         firstName: 'Super',
         lastName: 'Admin',
         // ...
     },
     // ...
 ];
nextjs_space/app/api/tenant-admin/email-templates/[id]/route.ts-10-26 (1)

10-26: Inconsistent auth pattern: Use getCurrentUser() helper instead of direct currentUser() call.

This file uses currentUser() directly and performs an email-based database lookup to retrieve tenant info. However, other files in this PR (e.g., email-mappings/route.ts, templates/[id]/activate/route.ts) use the getCurrentUser() helper which already extracts tenantId from Clerk's publicMetadata, avoiding the extra database query.

For consistency and efficiency, consider aligning with the established pattern.

Suggested refactor for GET handler
 import { NextRequest, NextResponse } from "next/server";
-import { currentUser } from "@clerk/nextjs/server";
+import { getCurrentUser } from "@/lib/auth-helper";
 import { prisma } from "@/lib/db";

 export async function GET(
   req: NextRequest,
   { params }: { params: { id: string } },
 ) {
   try {
-    const clerkUser = await currentUser();
+    const user = await getCurrentUser();

     if (
-      !clerkUser ||
-      !["TENANT_ADMIN", "SUPER_ADMIN"].includes(
-        (clerkUser.publicMetadata.role as string) || "",
-      )
+      !user ||
+      !["TENANT_ADMIN", "SUPER_ADMIN"].includes(user.role || "")
     ) {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }

-    const email = clerkUser.emailAddresses[0]?.emailAddress;
-
-    const user = await prisma.users.findFirst({
-      where: { email: email },
-      include: { tenants: true },
-    });
-
-    if (!user?.tenants) {
-      return NextResponse.json({ error: "Tenant not found" }, { status: 404 });
+    const tenantId = user.tenantId;
+    if (!tenantId) {
+      return NextResponse.json({ error: "Tenant not found for user" }, { status: 404 });
     }

     const template = await prisma.email_templates.findFirst({
       where: {
         id: params.id,
-        tenantId: user.tenants.id, // Strict ownership
+        tenantId: tenantId, // Strict ownership
       },
     });

Apply the same pattern to PUT and DELETE handlers.

nextjs_space/app/api/tenant-admin/templates/[id]/activate/route.ts-6-28 (1)

6-28: Fix params type annotation to match Next.js 14.2.35.

The params parameter is incorrectly typed as Promise<{ id: string }> (line 8) and awaited on line 27. In Next.js 14.2.35, params is a synchronous object, not a Promise. The Promise-based params pattern is exclusive to Next.js 15.

While awaiting a non-Promise value returns the value (so runtime behavior may work), the type annotation is incompatible with the project's Next.js version. Change line 8 to { params }: { params: { id: string } } and line 27 to const { id } = params;.

nextjs_space/app/api/tenant-admin/webhooks/[id]/deliveries/route.ts-21-34 (1)

21-34: Add email validation before the Prisma lookup to prevent data leaks.

If user.emailAddresses is empty (e.g., users authenticated via phone or username), email becomes undefined. Prisma omits undefined values from the where clause, causing findFirst() to return any user record instead of filtering by email. This would allow the query to retrieve another tenant's data.

Add an early guard before the Prisma query:

🛠️ Proposed fix
     const email = user.emailAddresses[0]?.emailAddress;
+    if (!email) {
+      return NextResponse.json(
+        { error: "User email not available" },
+        { status: 400 },
+      );
+    }
     const role = (user.publicMetadata.role as string) || "";

     if (role !== "TENANT_ADMIN") {
       return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
     }

     const localUser = await prisma.users.findFirst({
-      where: { email: email },
+      where: { email },
       select: { tenantId: true },
     });

Comment thread nextjs_space/app/api/store/[slug]/cart/add/route.ts Outdated
Comment thread nextjs_space/app/api/store/[slug]/orders/[orderId]/route.ts
Comment thread nextjs_space/app/api/tenant-admin/customers/[id]/route.ts Outdated
Comment thread nextjs_space/app/api/tenant-admin/settings/test-smtp/route.ts
Comment on lines +22 to 35
const email = user.emailAddresses[0]?.emailAddress;
const role = (user.publicMetadata.role as string) || "";

if (role !== "TENANT_ADMIN") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const localUser = await prisma.users.findFirst({
where: { email: email },
select: { tenantId: true },
});

const tenantId = localUser?.tenantId;
if (!tenantId) {
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "route.ts" | grep -E "webhook|tenant-admin" | head -20

Repository: AutomatosAI/budstack-saas

Length of output: 1248


🏁 Script executed:

git ls-files | grep -E "webhook.*route\.ts|tenant-admin.*route\.ts"

Repository: AutomatosAI/budstack-saas

Length of output: 2048


🏁 Script executed:

fd "route.ts" --path "*tenant-admin*webhook*"

Repository: AutomatosAI/budstack-saas

Length of output: 284


🏁 Script executed:

cat -n nextjs_space/app/api/tenant-admin/webhooks/[id]/route.ts

Repository: AutomatosAI/budstack-saas

Length of output: 5633


🏁 Script executed:

rg "where.*undefined" nextjs_space --type ts -A 2 -B 2 | head -50

Repository: AutomatosAI/budstack-saas

Length of output: 51


🌐 Web query:

Prisma undefined value in where clause filter behavior

💡 Result:

Short answer: by default Prisma treats undefined in a where value as “no filter” (the key is removed from the generated query). That means:

  • Passing { field: undefined } is equivalent to not specifying field at all. [1]
  • Operators behave differently when undefined entries are present: AND/NOT with undefined act like “no filter” (return all), but OR with undefined can produce an always-false condition (no results) in some cases. This has caused surprising behavior (see GitHub issue and StackOverflow thread). [2][3]

If you want safer behavior, enable Prisma’s strictUndefinedChecks (preview introduced in v5.20.0) which changes how undefined is handled to avoid accidental query removal. [1][4]

Sources:

  • Prisma docs: Null and undefined in Prisma Client. [1]
  • GitHub issue discussing empty objects / where producing WHERE 1=0. [2]
  • StackOverflow discussion about OR/undefined returning nothing. [3]
  • Prisma discussion/announcement for strictUndefinedChecks (v5.20.0). [4]

🏁 Script executed:

cat -n nextjs_space/package.json | grep -A 5 -B 5 prisma

Repository: AutomatosAI/budstack-saas

Length of output: 1530


🏁 Script executed:

cat nextjs_space/prisma/schema.prisma | head -20

Repository: AutomatosAI/budstack-saas

Length of output: 477


🏁 Script executed:

rg "emailAddresses" nextjs_space --type ts -B 3 -A 3 | head -60

Repository: AutomatosAI/budstack-saas

Length of output: 4105


🌐 Web query:

Clerk currentUser() emailAddresses guaranteed to exist authentication

💡 Result:

Short answer: No — emailAddresses are not universally guaranteed to exist for every authenticated Clerk user. Clerk users can authenticate via phone, OAuth/SAML, Web3, passkeys, etc., so a user may have no email addresses on file. Check user.emailAddresses (or user.primaryEmailAddress) for null/empty and handle that case in your code. [1][2][3]

Sources:

  • Clerk Backend User types (emailAddresses, primaryEmailAddress described). [1]
  • SignIn/PrepareFirstFactor doc (lists auth strategies that don't require email). [2]
  • Guides noting how email addresses are added/verified when using email flows. [3]

Add email validation guard before Prisma lookup (affects both PATCH and DELETE).

With Prisma 6.7.0 (used here), passing undefined to a where clause silently removes the filter, causing where: { email: undefined } to match any user. Since emailAddresses can be undefined when users authenticate via phone, OAuth, or SAML, this risks returning an arbitrary tenant ID. Add an explicit guard before the Prisma query and apply the same fix in the DELETE endpoint.

🐛 Suggested fix
 const email = user.emailAddresses[0]?.emailAddress;
 const role = (user.publicMetadata.role as string) || "";

+if (!email) {
+  return NextResponse.json({ error: "User email not found" }, { status: 401 });
+}
+
 const localUser = await prisma.users.findFirst({
-  where: { email: email },
+  where: { email },
   select: { tenantId: true },
 });
📝 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.

Suggested change
const email = user.emailAddresses[0]?.emailAddress;
const role = (user.publicMetadata.role as string) || "";
if (role !== "TENANT_ADMIN") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const localUser = await prisma.users.findFirst({
where: { email: email },
select: { tenantId: true },
});
const tenantId = localUser?.tenantId;
if (!tenantId) {
const email = user.emailAddresses[0]?.emailAddress;
const role = (user.publicMetadata.role as string) || "";
if (role !== "TENANT_ADMIN") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (!email) {
return NextResponse.json({ error: "User email not found" }, { status: 401 });
}
const localUser = await prisma.users.findFirst({
where: { email },
select: { tenantId: true },
});
const tenantId = localUser?.tenantId;
if (!tenantId) {
🤖 Prompt for AI Agents
In `@nextjs_space/app/api/tenant-admin/webhooks/`[id]/route.ts around lines 22 -
35, The Prisma lookup uses email which may be undefined and will remove the
filter in Prisma 6.7.0; add an explicit guard that verifies email (from
user.emailAddresses[0]?.emailAddress) is a non-empty string before calling
prisma.users.findFirst in the PATCH handler (route.ts) and return an error
(e.g., 400/401 JSON) if missing, then apply the identical guard to the DELETE
endpoint; update the logic around the localUser/tenantId fetching so
prisma.users.findFirst only runs when email is truthy to avoid accidental
full-table matches.

Comment thread nextjs_space/app/api/user/profile/route.ts
Comment thread nextjs_space/app/store/[slug]/the-wire/[postSlug]/page.tsx Outdated
Comment thread nextjs_space/app/tenant-admin/seo/page.tsx
Comment thread nextjs_space/scripts/seed-clerk-orgs.ts Outdated
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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
nextjs_space/app/api/user/profile/route.ts (1)

26-37: Partial address updates will overwrite existing fields with empty strings.

When any address field is provided, the entire address object is rebuilt with empty strings for missing fields. This causes unintended data loss—e.g., if a user only updates city, their existing addressLine1, postalCode, etc. are wiped.

Consider either:

  1. Fetching the existing address first and merging, or
  2. Only including fields that were explicitly provided in the request
🛠️ Proposed fix: Only include provided fields
-    // Build address object if any address fields provided
-    const address =
-      addressLine1 || city || state || postalCode || country
-        ? {
-            addressLine1: addressLine1 || "",
-            addressLine2: addressLine2 || "",
-            city: city || "",
-            state: state || "",
-            postalCode: postalCode || "",
-            country: country || "",
-          }
-        : undefined;
+    // Build address object only with provided fields
+    const addressFields = {
+      ...(addressLine1 !== undefined && { addressLine1 }),
+      ...(addressLine2 !== undefined && { addressLine2 }),
+      ...(city !== undefined && { city }),
+      ...(state !== undefined && { state }),
+      ...(postalCode !== undefined && { postalCode }),
+      ...(country !== undefined && { country }),
+    };
+    const address = Object.keys(addressFields).length > 0 ? addressFields : undefined;
nextjs_space/app/api/tenant-admin/webhooks/[id]/route.ts (1)

2-39: DELETE method is missing email validation before Prisma query — critical issue.

The PATCH method correctly guards against missing email (lines 25–27) before using it for the tenant lookup. The DELETE method extracts the email at line 120 but lacks this guard before calling prisma.users.findFirst({ where: { email: email } }) at line 128. If user.emailAddresses[0]?.emailAddress is undefined, the Prisma query will execute with an undefined email value.

Additionally, line 162 uses a non-null assertion (email!) in the audit log, but the email may legitimately be undefined at that point.

Add an email null check to DELETE matching the PATCH pattern:

Suggested fix for DELETE
const email = user.emailAddresses[0]?.emailAddress;
const role = (user.publicMetadata.role as string) || "";

+   if (!email) {
+     return NextResponse.json({ error: "Email not found" }, { status: 401 });
+   }

if (role !== "TENANT_ADMIN") {

Clerk API usage (currentUser() and publicMetadata.role access) is correct for route handlers.

🤖 Fix all issues with AI agents
In `@nextjs_space/app/api/store/`[slug]/cart/add/route.ts:
- Around line 18-29: The lookup uses user.emailAddresses[0] which may not be the
user's primary email; update the code that sets email (the variable used for
prisma.users.findUnique) to use Clerk's primaryEmailAddress first
(user.primaryEmailAddress?.emailAddress) and fall back to the existing
emailAddresses array if needed (e.g., find the primary entry or use index 0),
then pass that email to prisma.users.findUnique so multi-email users resolve
correctly.

In `@nextjs_space/app/api/store/`[slug]/orders/route.ts:
- Around line 17-24: The code currently uses
user.emailAddresses[0]?.emailAddress which can be non-primary; instead, read
user.primaryEmailAddressId and locate the matching entry in user.emailAddresses
(e.g., find(e => e.id === user.primaryEmailAddressId)) to extract the
authoritative emailAddress, fall back to a verified/primary strategy only if
needed, and then pass that email into prisma.users.findFirst; update the logic
around the user, emailAddresses, primaryEmailAddressId and the
prisma.users.findFirst call in route.ts so you return 401 only when no primary
email is found.

In `@nextjs_space/app/api/tenant-admin/customers/`[id]/route.ts:
- Around line 140-161: The PATCH/DELETE handlers lack a guard for a missing
localUser from prisma.users.findFirst which lets tenant checks using
localUser?.tenantId behave incorrectly; update the PATCH and DELETE flows (same
pattern used in GET) to immediately return a 404 (or 401) when localUser is null
for non-SUPER_ADMINs after calling currentUser() and resolving email/role,
before any tenantId comparisons, so that TENANT_ADMINs without a mapped
localUser cannot proceed; ensure you reference and guard the localUser variable
in the handlers that perform tenant scoping (the sections around the existing
localUser = await prisma.users.findFirst(...) and the subsequent
localUser?.tenantId checks).

In `@nextjs_space/scripts/seed-clerk-orgs.ts`:
- Around line 85-111: The membership creation catch block is checking the wrong
Clerk error codes; update the condition in the catch for
clerkClient.organizations.createOrganizationMembership so it treats the Clerk
error code 'already_a_member_in_organization' as the benign case: inspect the
caught error variable (e) and if e.errors && e.errors[0]?.code ===
'already_a_member_in_organization' log the "User already a member" message,
otherwise rethrow or log the failure as before; keep the existing
clerkUserList/clerkUserId resolution and logging intact.
♻️ Duplicate comments (3)
nextjs_space/app/api/user/profile/route.ts (1)

45-54: LGTM! Previous critical issue resolved.

The update now correctly uses where: { id: user.id } instead of the previously flagged user.email!. This aligns with the codebase pattern and avoids potential runtime errors from undefined email values.

nextjs_space/app/tenant-admin/seo/page.tsx (1)

18-33: Prefer primary email to avoid mismatched tenant lookup.

emailAddresses[0] can select a secondary/unverified email; if local users are keyed by the primary email, multi‑address users may fail lookup and get redirected. Consider primaryEmailAddress first (optionally with a fallback).

🔧 Suggested change
-  const email = user.emailAddresses[0]?.emailAddress;
+  const email =
+    user.primaryEmailAddress?.emailAddress ?? user.emailAddresses[0]?.emailAddress;
nextjs_space/app/api/tenant-admin/webhooks/[id]/route.ts (1)

120-130: Add the missing email guard in DELETE to avoid undefined Prisma filters.
If email is undefined (e.g., non-email auth), where: { email: undefined } can drop the filter and return an arbitrary user, leading to incorrect tenant association and audit attribution. Mirror the PATCH guard here.

🐛 Suggested fix
 const email = user.emailAddresses[0]?.emailAddress;
 const role = (user.publicMetadata.role as string) || "";

+if (!email) {
+  return NextResponse.json({ error: "Email not found" }, { status: 401 });
+}
+
 if (role !== "TENANT_ADMIN") {
   return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
 }

 const localUser = await prisma.users.findFirst({
-  where: { email: email },
+  where: { email },
   select: { tenantId: true },
 });

Please confirm Prisma’s undefined-where behavior for your version.

Prisma Client undefined value in where clause behavior (current version)

Also applies to: 161-163

🧹 Nitpick comments (4)
nextjs_space/scripts/seed-clerk-orgs.ts (1)

11-14: Consider validating required environment variables.

If CLERK_SECRET_KEY is missing or undefined, the script may fail with an unclear error downstream. Adding an early guard improves debuggability for developers running this seed script.

💡 Suggested validation
+if (!process.env.CLERK_SECRET_KEY) {
+    console.error('❌ CLERK_SECRET_KEY is not set. Please check your .env.local file.');
+    process.exit(1);
+}
+
 const clerkClient = createClerkClient({
     secretKey: process.env.CLERK_SECRET_KEY,
     publishableKey: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
 });
nextjs_space/app/api/tenant-admin/settings/test-smtp/route.ts (3)

26-31: Prefer deterministic user lookup and confirm tenant cardinality.

If users.email is unique, findUnique avoids accidental duplicates and is clearer. Also confirm whether users.tenants is 1‑to‑1; if it’s a list, localUser.tenants.settings will fail unless you select a tenant explicitly.

♻️ Proposed change (if email is unique)
-    const localUser = await prisma.users.findFirst({
-      where: { email: email },
+    const localUser = await prisma.users.findUnique({
+      where: { email },
       include: { tenants: true },
     });

Also applies to: 44-44


79-80: Return SMTP verify errors as 400 (align with super‑admin flow).

Connection verify failures are usually configuration issues, so returning a 400 with error details (similar to nextjs_space/app/api/super-admin/test-smtp/route.ts lines 6‑156) gives better UX than a generic 500.

♻️ Proposed change
-    console.log(`[TenantSMTP] Verifying connection for ${localUser.tenants.id}...`);
-    await transporter.verify();
+    console.log(`[TenantSMTP] Verifying connection for ${localUser.tenants.id}...`);
+    try {
+      await transporter.verify();
+    } catch (err: any) {
+      console.error("[TenantSMTP] Connection verification failed:", err);
+      return NextResponse.json(
+        {
+          error: `SMTP connection failed: ${err.message}`,
+          details: { code: err.code, command: err.command },
+        },
+        { status: 400 },
+      );
+    }

83-84: Sanitize fromName/fromEmail before building headers.

These values are tenant‑controlled; stripping CR/LF (and quotes in the name) prevents header injection and reduces send failures if invalid characters slip in. If validation already happens on save, consider reusing that sanitization here.

♻️ Proposed change
-    const fromAddress = smtp.fromEmail
-      ? `"${smtp.fromName || localUser.tenants.businessName}" <${smtp.fromEmail}>`
-      : `"${localUser.tenants.businessName}" <${smtp.user}>`;
+    const safeFromName = (smtp.fromName || localUser.tenants.businessName || "")
+      .replace(/[\r\n"]/g, "")
+      .trim();
+    const safeFromEmail = (smtp.fromEmail || "")
+      .replace(/[\r\n]/g, "")
+      .trim();
+
+    const fromAddress = safeFromEmail
+      ? `"${safeFromName}" <${safeFromEmail}>`
+      : `"${safeFromName}" <${smtp.user}>`;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3892fe3 and f1bdf25.

📒 Files selected for processing (10)
  • nextjs_space/app/api/store/[slug]/cart/add/route.ts
  • nextjs_space/app/api/store/[slug]/orders/route.ts
  • nextjs_space/app/api/store/[slug]/orders/submit/route.ts
  • nextjs_space/app/api/tenant-admin/customers/[id]/route.ts
  • nextjs_space/app/api/tenant-admin/settings/test-smtp/route.ts
  • nextjs_space/app/api/tenant-admin/webhooks/[id]/route.ts
  • nextjs_space/app/api/user/profile/route.ts
  • nextjs_space/app/store/[slug]/the-wire/[postSlug]/page.tsx
  • nextjs_space/app/tenant-admin/seo/page.tsx
  • nextjs_space/scripts/seed-clerk-orgs.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • nextjs_space/app/store/[slug]/the-wire/[postSlug]/page.tsx
  • nextjs_space/app/api/store/[slug]/orders/submit/route.ts
🧰 Additional context used
🧬 Code graph analysis (7)
nextjs_space/app/api/tenant-admin/webhooks/[id]/route.ts (1)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/app/api/tenant-admin/customers/[id]/route.ts (1)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/app/api/store/[slug]/cart/add/route.ts (1)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/app/api/store/[slug]/orders/route.ts (3)
nextjs_space/app/api/orders/customer/route.ts (1)
  • GET (5-49)
nextjs_space/app/api/orders/route.ts (1)
  • GET (180-223)
nextjs_space/app/api/store/[slug]/cart/route.ts (1)
  • GET (7-47)
nextjs_space/app/tenant-admin/seo/page.tsx (1)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/app/api/tenant-admin/settings/test-smtp/route.ts (2)
nextjs_space/app/api/super-admin/test-smtp/route.ts (1)
  • POST (7-157)
nextjs_space/app/api/tenant-admin/settings/route.ts (1)
  • POST (7-144)
nextjs_space/app/api/user/profile/route.ts (2)
nextjs_space/lib/auth-helper.ts (1)
  • getCurrentUser (3-23)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
🔇 Additional comments (12)
nextjs_space/app/api/user/profile/route.ts (1)

7-11: LGTM! Auth migration correctly implemented.

The authentication check using getCurrentUser() follows the established Clerk-based pattern across the codebase.

nextjs_space/app/api/store/[slug]/cart/add/route.ts (1)

70-72: Good: dbUser.id is now used for cart ops.

nextjs_space/app/api/tenant-admin/customers/[id]/route.ts (2)

16-40: Clerk auth + tenant scoping in GET looks solid.
Good guardrails around Line 22–27 (email) and Line 29–31 (role), and the tenantId check in Line 88–90 is correctly scoped to TENANT_ADMIN.

Also applies to: 88-90


216-217: Audit log attribution update is correct.
Using Clerk’s user.id and email here keeps audit trails consistent with the new auth flow.

Also applies to: 320-321

nextjs_space/scripts/seed-clerk-orgs.ts (3)

37-47: Good fix: Admin Clerk ID is now resolved correctly.

The code properly resolves the Clerk user ID via email lookup before passing it to createOrganization. This addresses the previous review feedback about using Prisma IDs where Clerk IDs are expected.


130-137: LGTM!

The script properly handles cleanup with prisma.$disconnect() in the finally block, ensuring the database connection is released regardless of success or failure.


63-80: Error handling for duplicate organization slug is correct.

The error code form_identifier_exists correctly identifies slug conflicts in Clerk's API, and the fallback approach of retrieving the existing organization via admin membership is appropriate.

nextjs_space/app/tenant-admin/seo/page.tsx (1)

1-16: Auth guard looks solid.

Clear authentication/role gating with early redirects.

nextjs_space/app/api/store/[slug]/orders/route.ts (3)

2-2: Clerk import looks good.


40-41: Good switch to dbUser.id for scoping orders.


26-28: The related order endpoints in this namespace all consistently return 404 for missing db users—no alignment issue.

All sibling endpoints within /api/store/[slug]/orders/* (including [orderId]/route.ts and submit/route.ts) return 404 when the db user is not found, matching the behavior in the target file. The endpoint is consistent with its related family and does not represent a breaking change.

Likely an incorrect or invalid review comment.

nextjs_space/app/api/tenant-admin/settings/test-smtp/route.ts (1)

2-24: Auth/role gate is clean and early.

The early unauthorized exits keep the path clear; just verify that admin accounts in Clerk always have publicMetadata.role and a primary email populated so valid users aren’t rejected.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +18 to +29
const email = user.emailAddresses[0]?.emailAddress;
if (!email) {
return NextResponse.json({ error: "Email not found" }, { status: 401 });
}

const dbUser = await prisma.users.findUnique({
where: { email },
});

if (!dbUser) {
return NextResponse.json({ error: "User not found in database" }, { status: 404 });
}
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n nextjs_space/app/api/store/[slug]/cart/add/route.ts | head -50

Repository: AutomatosAI/budstack-saas

Length of output: 1797


🌐 Web query:

Clerk currentUser User object primaryEmailAddress documentation

💡 Result:

Clerk's User object (returned by currentUser/useUser/etc.) includes these primary email fields:

  • primaryEmailAddress — an EmailAddress object or null (information about the user's primary email). [1][2]
  • primaryEmailAddressId — string or null (the EmailAddress resource ID set as primary). [1][2]

Note: there are known typing/shape differences between user objects returned by some helpers (e.g., useUser) and currentUser/currentUser server helpers; see the Clerk GitHub issue discussing missing fields like primaryEmailAddress in some contexts. [3]

Sources:
[1] Clerk — User object reference (primaryEmailAddress, primaryEmailAddressId).
[2] Clerk — Users / managing users guide (description of primary email).
[3] Clerk GitHub issue: useUser vs currentUser user object type differences.


Use primary email to avoid failed lookups for multi-email users.

user.emailAddresses[0] is not guaranteed to be the primary email. For users with multiple registered addresses, this can cause a 404 even when they exist in your database. Clerk's primaryEmailAddress field is available on the currentUser() object; use it with a fallback to ensure correct lookups.

Suggested fix
-    const email = user.emailAddresses[0]?.emailAddress;
+    const email =
+      user.primaryEmailAddress?.emailAddress ??
+      user.emailAddresses[0]?.emailAddress;
     if (!email) {
       return NextResponse.json({ error: "Email not found" }, { status: 401 });
     }
📝 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.

Suggested change
const email = user.emailAddresses[0]?.emailAddress;
if (!email) {
return NextResponse.json({ error: "Email not found" }, { status: 401 });
}
const dbUser = await prisma.users.findUnique({
where: { email },
});
if (!dbUser) {
return NextResponse.json({ error: "User not found in database" }, { status: 404 });
}
const email =
user.primaryEmailAddress?.emailAddress ??
user.emailAddresses[0]?.emailAddress;
if (!email) {
return NextResponse.json({ error: "Email not found" }, { status: 401 });
}
const dbUser = await prisma.users.findUnique({
where: { email },
});
if (!dbUser) {
return NextResponse.json({ error: "User not found in database" }, { status: 404 });
}
🤖 Prompt for AI Agents
In `@nextjs_space/app/api/store/`[slug]/cart/add/route.ts around lines 18 - 29,
The lookup uses user.emailAddresses[0] which may not be the user's primary
email; update the code that sets email (the variable used for
prisma.users.findUnique) to use Clerk's primaryEmailAddress first
(user.primaryEmailAddress?.emailAddress) and fall back to the existing
emailAddresses array if needed (e.g., find the primary entry or use index 0),
then pass that email to prisma.users.findUnique so multi-email users resolve
correctly.

Comment on lines +17 to +24
const email = user.emailAddresses[0]?.emailAddress;
if (!email) {
return NextResponse.json({ error: "Email not found" }, { status: 401 });
}

const dbUser = await prisma.users.findFirst({
where: { email },
});
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "route.ts" | grep -E "store.*orders" | head -20

Repository: AutomatosAI/budstack-saas

Length of output: 241


🏁 Script executed:

cat -n nextjs_space/app/api/store/\[slug\]/orders/route.ts

Repository: AutomatosAI/budstack-saas

Length of output: 1973


🏁 Script executed:

# Search for Clerk imports and user object patterns to understand the type
rg -A 5 -B 5 "user\." nextjs_space/app/api/store/\[slug\]/orders/route.ts

Repository: AutomatosAI/budstack-saas

Length of output: 379


🏁 Script executed:

# Check if Clerk types are used in the project
rg "from.*@clerk" --max-count=10

Repository: AutomatosAI/budstack-saas

Length of output: 8736


🏁 Script executed:

# Check how emailAddresses and primaryEmailAddress are used elsewhere
rg "emailAddresses\[0\]|primaryEmailAddress|primaryEmailAddressId" -B 2 -A 2

Repository: AutomatosAI/budstack-saas

Length of output: 25770


🏁 Script executed:

# Look for Clerk user type definitions or how user object is handled elsewhere
rg "currentUser\(\)" -A 10 | head -100

Repository: AutomatosAI/budstack-saas

Length of output: 6292


🏁 Script executed:

# Search for any Clerk documentation or type imports in the codebase
rg "User|EmailAddress" nextjs_space --type ts --type tsx | grep -i clerk | head -20

Repository: AutomatosAI/budstack-saas

Length of output: 96


🌐 Web query:

Clerk currentUser() server-side primaryEmailAddress API documentation

💡 Result:

Short answer: Clerk's server-side helper currentUser() (e.g., @clerk/nextjs/server currentUser()) returns a Backend API User object that does NOT always include the frontend-only fields like primaryEmailAddress. If you need the user's primaryEmailAddress server-side, call the Clerk Admin/Backend user API (clerk.users.getUser or clerkClient.users.getUser) with the user's id to get the full User object (or fetch /v1/users/:id) which includes primaryEmailAddress. [1][2][3]

References:

  • currentUser() / Backend API docs (explanation of currentUser usage). [1]
  • User object / reference (shows User.primaryEmailAddress and related user fields available via the API). [2]
  • GitHub issue noting the difference between useUser() (frontend) and currentUser() (server) and that primaryEmailAddress may be missing from currentUser(). [3]

If you want, I can paste a minimal server-side example showing how to get primaryEmailAddress with clerk.users.getUser.


🌐 Web query:

Clerk User object emailAddresses primaryEmailAddress verified property

💡 Result:

Short answer:

  • User.emailAddresses is an array of EmailAddress objects (includes both verified and unverified addresses). [1]
  • User.primaryEmailAddress (or User.primaryEmailAddressId) is the single email the user has marked as primary; primaryEmailAddressId holds that EmailAddress id. [1]
  • Each EmailAddress object has a verified property (boolean) that indicates whether that specific email address has been verified via Clerk's verification flow. You can create/ update an EmailAddress with verified or change it via the EmailAddress API. [2][3]

Sources: [1] Clerk User reference, [2] Clerk EmailAddress reference, [3] EmailAddress API (create/update schema).


🏁 Script executed:

# Check if there are any package.json files to see Clerk version being used
find . -name "package.json" -type f -exec grep -l "clerk" {} \; | head -5

Repository: AutomatosAI/budstack-saas

Length of output: 96


🏁 Script executed:

# Look for tsconfig or type definitions that might give us Clerk type info
fd -e "d.ts" | grep -i clerk | head -10

Repository: AutomatosAI/budstack-saas

Length of output: 51


🏁 Script executed:

# Check for any comments in the code about email handling
rg -i "primary.*email|verified.*email" -B 2 -A 2 | head -50

Repository: AutomatosAI/budstack-saas

Length of output: 4265


🏁 Script executed:

# Check Clerk version in package.json
cat nextjs_space/package.json | grep -A 2 -B 2 "@clerk"

Repository: AutomatosAI/budstack-saas

Length of output: 254


🏁 Script executed:

# Look for any examples in the codebase of using clerkClient or fetching full user data server-side
rg "clerkClient\.users|clerk\.users" -B 2 -A 5 | head -80

Repository: AutomatosAI/budstack-saas

Length of output: 4996


🏁 Script executed:

# Check if primaryEmailAddressId is used anywhere in the codebase
rg "primaryEmailAddressId" | head -20

Repository: AutomatosAI/budstack-saas

Length of output: 51


🏁 Script executed:

# Look at the webhook handler to see how it handles user data from Clerk events
cat -n nextjs_space/app/api/webhooks/clerk/route.ts | head -100

Repository: AutomatosAI/budstack-saas

Length of output: 4734


🏁 Script executed:

# Check if primaryEmailAddressId is used anywhere in the codebase with currentUser
rg "currentUser\(\)" -A 20 | rg -B 5 -A 5 "primaryEmailAddressId"

Repository: AutomatosAI/budstack-saas

Length of output: 51


🏁 Script executed:

# Let's also verify what properties are available on user object from currentUser
# by looking at how user is accessed in these server-side routes
rg "const user = await currentUser" -A 30 | head -100

Repository: AutomatosAI/budstack-saas

Length of output: 7136


Use the primary email address, not the first address in the array.

emailAddresses[0] can be non-primary or unverified, which can mis-map users or return false "not found" for valid accounts. The server-side currentUser() object includes primaryEmailAddressId; use it to find the correct email address.

🔧 Suggested adjustment
-    const email = user.emailAddresses[0]?.emailAddress;
+    const email =
+      user.emailAddresses.find(
+        (address) => address.id === user.primaryEmailAddressId,
+      )?.emailAddress ?? user.emailAddresses[0]?.emailAddress;
📝 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.

Suggested change
const email = user.emailAddresses[0]?.emailAddress;
if (!email) {
return NextResponse.json({ error: "Email not found" }, { status: 401 });
}
const dbUser = await prisma.users.findFirst({
where: { email },
});
const email =
user.emailAddresses.find(
(address) => address.id === user.primaryEmailAddressId,
)?.emailAddress ?? user.emailAddresses[0]?.emailAddress;
if (!email) {
return NextResponse.json({ error: "Email not found" }, { status: 401 });
}
const dbUser = await prisma.users.findFirst({
where: { email },
});
🤖 Prompt for AI Agents
In `@nextjs_space/app/api/store/`[slug]/orders/route.ts around lines 17 - 24, The
code currently uses user.emailAddresses[0]?.emailAddress which can be
non-primary; instead, read user.primaryEmailAddressId and locate the matching
entry in user.emailAddresses (e.g., find(e => e.id ===
user.primaryEmailAddressId)) to extract the authoritative emailAddress, fall
back to a verified/primary strategy only if needed, and then pass that email
into prisma.users.findFirst; update the logic around the user, emailAddresses,
primaryEmailAddressId and the prisma.users.findFirst call in route.ts so you
return 401 only when no primary email is found.

Comment thread nextjs_space/app/api/tenant-admin/customers/[id]/route.ts
Comment thread nextjs_space/scripts/seed-clerk-orgs.ts
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
nextjs_space/app/api/tenant-admin/customers/[id]/route.ts (1)

195-200: Handle empty-string name updates explicitly.

The truthy check skips updates when firstName/lastName are empty strings, leaving name stale. If empty strings are valid for “clear,” use !== undefined.

✅ Suggested fix
-        ...(firstName && lastName && { name: `${firstName} ${lastName}` }),
+        ...(firstName !== undefined &&
+          lastName !== undefined && { name: `${firstName} ${lastName}`.trim() }),
🤖 Fix all issues with AI agents
In `@nextjs_space/app/api/orders/route.ts`:
- Line 88: The code is currently trusting clientId from the request body when
building the order (client_id: clientId || dbUser.id), which allows
impersonation; change it to derive client_id from the authenticated user (use
dbUser.id) and do not accept clientId unless validated and authorized—if you
must support an override, validate that the authenticated user has an admin role
(e.g., dbUser.role === 'admin') or that clientId equals dbUser.id before using
it; update the order creation site (where client_id is set) to ignore
unvalidated clientId values and enforce the server-derived dbUser.id (or an
explicitly-authorized override).
- Around line 27-31: The user lookup uses prisma.users.findFirst with only email
which lacks tenant scoping; fetch the tenant context using
getTenantFromRequest(req) (or ensure tenantId is obtained first) and include
tenantId in the findFirst where clause (i.e., prisma.users.findFirst({ where: {
email, tenantId } })) so dbUser is resolved within the tenant boundary; update
the code around prisma.users.findFirst and any flow that depends on dbUser to
use the tenant-scoped lookup.

In `@nextjs_space/app/api/tenant-admin/customers/`[id]/route.ts:
- Around line 22-27: The code currently picks the email with
user.emailAddresses[0], which is fragile; instead resolve the primary email by
matching user.primaryEmailAddressId against user.emailAddresses[*].id and use
that emailAddress value, falling back to user.emailAddresses[0]?.emailAddress if
no match; update the GET handler block where const email is set, and make the
identical change in the PATCH and DELETE handlers so all uses of
user.emailAddresses use the resolved primaryEmailAddressId lookup before falling
back, keeping the existing NextResponse.json({ error: "Email not found" }, {
status: 401 }) behavior when no email is found.
🧹 Nitpick comments (2)
nextjs_space/scripts/seed-clerk-orgs.ts (1)

11-14: Consider validating required environment variables.

If CLERK_SECRET_KEY is missing or undefined, the script will fail with an unclear error from the Clerk API. Adding upfront validation improves debuggability.

♻️ Suggested validation
+if (!process.env.CLERK_SECRET_KEY) {
+    console.error('❌ CLERK_SECRET_KEY is not set. Ensure .env.local is configured.');
+    process.exit(1);
+}
+
 const clerkClient = createClerkClient({
     secretKey: process.env.CLERK_SECRET_KEY,
     publishableKey: process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
 });
nextjs_space/app/api/tenant-admin/customers/[id]/route.ts (1)

157-158: Remove duplicate comment to reduce noise.

The same comment appears twice in a row.

🧹 Cleanup
-    // Fetch local user to get tenantId for authorization
-    // Fetch local user to get tenantId for authorization
+    // Fetch local user to get tenantId for authorization
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1bdf25 and 5a398c3.

📒 Files selected for processing (3)
  • nextjs_space/app/api/orders/route.ts
  • nextjs_space/app/api/tenant-admin/customers/[id]/route.ts
  • nextjs_space/scripts/seed-clerk-orgs.ts
🧰 Additional context used
🧬 Code graph analysis (3)
nextjs_space/scripts/seed-clerk-orgs.ts (2)
nextjs_space/next.config.js (1)
  • path (1-1)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/app/api/tenant-admin/customers/[id]/route.ts (1)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
nextjs_space/app/api/orders/route.ts (1)
nextjs_space/lib/db.ts (1)
  • prisma (34-43)
🔇 Additional comments (7)
nextjs_space/scripts/seed-clerk-orgs.ts (6)

1-7: LGTM!

Environment loading is correctly ordered before the Clerk client import, ensuring CLERK_SECRET_KEY is available during initialization.


16-28: LGTM!

The tenant query correctly includes TENANT_ADMIN users for organization seeding.


37-47: LGTM!

The admin Clerk ID resolution via email lookup is the correct approach, addressing the previous concern about mixing Prisma and Clerk user IDs.


49-81: LGTM!

Organization creation correctly uses the resolved Clerk admin ID, and the fallback lookup via membership list gracefully handles existing organizations.


83-120: LGTM!

Member resolution and error handling are correctly implemented. The already_a_member_in_organization error code matches Clerk's API response for duplicate memberships.


130-137: LGTM!

Script execution pattern is correct with proper error handling and Prisma disconnection in the finally block.

nextjs_space/app/api/orders/route.ts (1)

194-198: Use Clerk’s primary email for DB lookup in GET to avoid mismatched users.

If emailAddresses[0] isn’t guaranteed to be the primary email, this can resolve a different user and surface another account’s orders. Align GET with POST’s primary-email resolution.

🔧 Proposed fix
-    const email = user.emailAddresses[0].emailAddress;
+    const primaryEmail =
+      user.emailAddresses.find(e => e.id === user.primaryEmailAddressId)?.emailAddress;
+    const email = primaryEmail || user.emailAddresses[0]?.emailAddress;

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread nextjs_space/app/api/orders/route.ts Outdated
Comment thread nextjs_space/app/api/orders/route.ts Outdated
Comment on lines +22 to +27
const email = user.emailAddresses[0]?.emailAddress;
const role = (user.publicMetadata.role as string) || "";

if (!email) {
return NextResponse.json({ error: "Email not found" }, { status: 401 });
}
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

🧩 Analysis chain

🌐 Web query:

Clerk currentUser primaryEmailAddressId emailAddresses API documentation recommended access

💡 Result:

Summary — where these fields/methods live and recommended access

  • primaryEmailAddressId: property on the Clerk User object (string | null). Available in both frontend (ClerkJS / client SDK) and backend user objects. Use it to get the ID of the user's primary EmailAddress. [1][3]

  • emailAddresses (list): Clerk exposes the user's email addresses via the EmailAddress resource (e.g., user.emailAddresses()) — returns all email addresses (verified and unverified), including the primary. This endpoint/method is available in SDKs (frontend and backend variants depending on SDK). [6]

  • Changing the primary email: set primaryEmailAddressId (or primaryEmailAddressID in backend updateUser) when updating the user. The email you set must already exist on the user and be verified. Backend updateUser supports notifyPrimaryEmailAddressChanged flag. Use the backend SDK for authoritative changes. [1][2]

Recommended access patterns

  • Read-only display of current user primary email: safe to read currentUser.primaryEmailAddressId (and resolve to the EmailAddress object from user.primaryEmailAddress or emailAddresses list) on the frontend. [1][3]
  • To list or manage all email addresses, or to make authoritative changes (set primary, require verification, send notifications), perform those operations via the Backend SDK / Backend API (updateUser or the backend User/emailAddresses endpoints). This ensures proper security and server-side validation. [2][6]

Sources
[1] Clerk User object (JS SDK) — primaryEmailAddressId, update() params.
[2] Clerk Backend SDK — updateUser() (primaryEmailAddressID, notifyPrimaryEmailAddressChanged).
[3] Clerk Backend User object reference — primaryEmailAddress accessor.
[6] Clerk SDK / API emailAddresses (list) method.


🌐 Web query:

Clerk @clerk/nextjs server currentUser primaryEmailAddress accessor backend User object

💡 Result:

Short answer

  • The server-side currentUser() from @clerk/nextjs/server returns the Backend "User" object but its shape differs from the client-side useUser() result: currentUser() may not include convenience fields like primaryEmailAddress/fullName that the client object exposes. Use the User fields that are present (emailAddresses, primaryEmailAddressId) or fetch the full Backend User via the backend SDK to get the primary-email info. [1][2][3]

How to access the primary email (two options)

  1. If currentUser() includes primaryEmailAddress:
  • Access it directly:
    const user = await currentUser();
    const email = user.primaryEmailAddress?.emailAddress;
  1. If currentUser() does NOT include primaryEmailAddress (safe server approach):
  • Use clerkClient to fetch the full backend User (or lookup from user.emailAddresses):
    import { currentUser, clerkClient } from '@clerk/nextjs/server'
    const me = await currentUser()
    if (!me) return null
    // fetch full backend user by id to ensure all backend fields are present
    const fullUser = await clerkClient.users.getUser(me.id)
    const primary = fullUser.primaryEmailAddress?.emailAddress
    // OR find by id from emailAddresses array:
    const primaryFromList = fullUser.emailAddresses.find(e => e.id === fullUser.primaryEmailAddressId)?.emailAddress

Why this is needed

  • Clerk docs state currentUser() returns a Backend User object but caution about differences vs client user objects; a reported GitHub issue documents the inconsistency (primaryEmailAddress exists in client useUser but not always on server currentUser()). Use clerkClient.users.getUser() when you need the reliable backend fields. [1][2][3][4]

References

  • Clerk currentUser() docs. [1]
  • GitHub issue about different shapes (useUser vs currentUser). [2]
  • Clerk User object reference (primaryEmailAddress, emailAddresses, primaryEmailAddressId). [3]
  • Example using clerkClient to map users and primary email. [4]

🏁 Script executed:

# Verify the file and lines mentioned in the review
fd "route.ts" | grep "tenant-admin/customers"

Repository: AutomatosAI/budstack-saas

Length of output: 179


🏁 Script executed:

cat -n nextjs_space/app/api/tenant-admin/customers/[id]/route.ts | head -50

Repository: AutomatosAI/budstack-saas

Length of output: 1858


🏁 Script executed:

cat -n nextjs_space/app/api/tenant-admin/customers/[id]/route.ts | sed -n '200,250p'

Repository: AutomatosAI/budstack-saas

Length of output: 1655


🏁 Script executed:

cat -n nextjs_space/app/api/tenant-admin/customers/[id]/route.ts | sed -n '155,165p'

Repository: AutomatosAI/budstack-saas

Length of output: 493


🏁 Script executed:

cat -n nextjs_space/app/api/tenant-admin/customers/[id]/route.ts | sed -n '130,155p'

Repository: AutomatosAI/budstack-saas

Length of output: 1009


🏁 Script executed:

cat -n nextjs_space/app/api/tenant-admin/customers/[id]/route.ts | sed -n '250,290p'

Repository: AutomatosAI/budstack-saas

Length of output: 1588


Use Clerk's primaryEmailAddressId to resolve the primary email address.

Using index 0 is fragile—if the email addresses array reorders, a non-primary email may be selected and break local user mapping. Resolve by ID using primaryEmailAddressId and fallback to index 0. Apply the same change in PATCH and DELETE.

🔧 Suggested adjustment (GET block; mirror in PATCH/DELETE)
-    const email = user.emailAddresses[0]?.emailAddress;
+    const primaryEmail =
+      user.emailAddresses.find(
+        (addr) => addr.id === user.primaryEmailAddressId,
+      )?.emailAddress;
+    const email = primaryEmail ?? user.emailAddresses[0]?.emailAddress;
🤖 Prompt for AI Agents
In `@nextjs_space/app/api/tenant-admin/customers/`[id]/route.ts around lines 22 -
27, The code currently picks the email with user.emailAddresses[0], which is
fragile; instead resolve the primary email by matching
user.primaryEmailAddressId against user.emailAddresses[*].id and use that
emailAddress value, falling back to user.emailAddresses[0]?.emailAddress if no
match; update the GET handler block where const email is set, and make the
identical change in the PATCH and DELETE handlers so all uses of
user.emailAddresses use the resolved primaryEmailAddressId lookup before falling
back, keeping the existing NextResponse.json({ error: "Email not found" }, {
status: 401 }) behavior when no email is found.

@AutomatosAI AutomatosAI merged commit 6c3933f into main Jan 15, 2026
1 check was pending
@coderabbitai coderabbitai bot mentioned this pull request Feb 13, 2026
@coderabbitai coderabbitai bot mentioned this pull request Apr 9, 2026
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.

2 participants