Skip to content

Conversation

@Kitenite
Copy link
Contributor

@Kitenite Kitenite commented Oct 12, 2025

Description

Related Issues

Type of Change

  • Bug fix
  • New feature
  • Documentation
  • Refactor
  • Other (please describe):

Testing

Screenshots (if applicable)

Additional Notes


Important

Introduces an optional admin dashboard, updates documentation, and refactors UI component imports to relative paths.

  • Admin Dashboard:
    • Integrated optional admin dashboard as a submodule in .gitmodules and apps/admin.
    • Added dev:admin script in package.json to run the admin app locally.
  • Documentation:
    • Added admin-dashboard.mdx for admin dashboard features and access.
    • Updated index.mdx and meta.json to include admin dashboard in self-hosting docs.
  • Refactor:
    • Changed UI component imports to relative paths in command.tsx, form.tsx, and sidebar.tsx for consistency.

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


Summary by CodeRabbit

  • Chores
    • Integrated an optional Admin app as a submodule.
    • Added a new development script to run the Admin app locally.
  • Refactor
    • Standardized internal UI component imports to relative paths.
    • Minor export formatting cleanups with no behavior change.
  • Documentation
    • Added Self-Hosting Admin Dashboard docs and linked it from the Self-Hosting section.
    • Minor README wording tweak in Getting Started.

@vercel
Copy link

vercel bot commented Oct 12, 2025

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

Project Deployment Preview Comments Updated (UTC)
docs Ready Ready Preview Comment Oct 12, 2025 6:04am
web Ready Ready Preview Comment Oct 12, 2025 6:04am

@supabase
Copy link

supabase bot commented Oct 12, 2025

This pull request has been ignored for the connected project wowaemfasoptxrdjhilu because there are no changes detected in apps/backend/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai
Copy link

coderabbitai bot commented Oct 12, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Added an npm dev script for the admin app, converted several absolute UI imports to relative paths, added an apps/admin Git submodule pinned to a commit, and added docs/pages for an optional Admin Dashboard. No runtime logic or public API signatures changed.

Changes

Cohort / File(s) Summary
NPM Scripts
package.json
Added script dev:adminbun --filter @onlook/admin dev.
UI Components — import path adjustments
packages/ui/src/components/command.tsx, packages/ui/src/components/form.tsx, packages/ui/src/components/sidebar.tsx
Replaced absolute alias imports (@/...) with relative imports (./...); minor export-list formatting changes. No behavioral/API changes.
Git submodule (admin app)
.gitmodules, apps/admin
Added submodule entry for apps/admin (URL https://github.com/onlook-dev/admin.git) and pinned subproject commit 77a91b6626671d18d81f2121fcefe1b89559ab22.
Documentation content
README.md, docs/content/docs/self-hosting/admin-dashboard.mdx, docs/content/docs/self-hosting/index.mdx, docs/content/docs/self-hosting/meta.json
Added new Admin Dashboard docs page and index entry; updated meta.json pages to include admin-dashboard; small README wording change in Getting Started.

Sequence Diagram(s)

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

A rabbit taps npm with glee,
"dev:admin" hops off, swift and free.
Imports curl inward, neat and spry,
A submodule nestles, quiet and shy.
Docs take a bow — I munch a carrot, bye! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description retains all template placeholders without any filled content in the Description, Related Issues, Type of Change, or Testing sections, making it incomplete despite the appended auto-generated summary. Please complete the template by providing a clear and concise Description of the changes, linking any Related Issues, selecting the appropriate Type of Change, and detailing the Testing steps performed, then remove all placeholder comments.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title “feat: admin dashboard” concisely and accurately highlights the main feature introduced by this pull request, clearly indicating the addition of an admin dashboard without extraneous details.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/admin-dashboard

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4d14eb4 and 4e7a416.

📒 Files selected for processing (4)
  • README.md (1 hunks)
  • docs/content/docs/self-hosting/admin-dashboard.mdx (1 hunks)
  • docs/content/docs/self-hosting/index.mdx (1 hunks)
  • docs/content/docs/self-hosting/meta.json (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • docs/content/docs/self-hosting/admin-dashboard.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

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

🧹 Nitpick comments (7)
apps/admin/README.md (2)

16-52: Add language specifier to fenced code block.

The directory structure code block should have a language specifier for proper rendering and accessibility.

Apply this diff:

-```
+```text
 apps/admin/
 ├── src/

123-123: Wrap bare URL in markdown link syntax.

For better markdown rendering and accessibility.

Apply this diff:

-4. **Access**: http://localhost:3001
+4. **Access**: <http://localhost:3001>
apps/admin/tsconfig.json (1)

5-9: Remove redundant path aliases.

The configuration defines both @/* and ~/* mapping to ./src/*, which is redundant. Additionally, the /* alias mapping to ./* is overly broad and could cause path resolution confusion with node_modules or other root-level directories.

Apply this diff to simplify the path aliases:

     "paths": {
-        "@/*": ["./src/*"],
-        "/*": ["./*"],
-        "~/*": ["./src/*"]
+        "@/*": ["./src/*"]
     }
apps/admin/src/components/users/edit-rate-limit.tsx (2)

31-41: Replace alert() with toast notifications.

Using alert() for success and error feedback (Lines 36, 39) provides a poor user experience in modern web applications. Consider using a toast notification library or inline UI feedback instead.

Example using a hypothetical toast library:

-            alert(`Successfully added ${data.creditsAdded} credits. New balance: ${data.newLeft}/${max}`);
+            toast.success(`Successfully added ${data.creditsAdded} credits. New balance: ${data.newLeft}/${max}`);
-            alert(`Error: ${error.message}`);
+            toast.error(`Error: ${error.message}`);

46-56: Replace alert() with inline validation feedback.

The validation alert (Line 49) interrupts the user flow. Consider displaying the validation error inline within the dialog, near the input field.

You could add a state variable for the error message and display it conditionally:

const [validationError, setValidationError] = useState<string>('');

const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (creditsToAdd < 1 || creditsToAdd > maxCreditsCanAdd) {
        setValidationError(`Please enter a value between 1 and ${maxCreditsCanAdd}`);
        return;
    }
    setValidationError('');
    updateMutation.mutate({
        rateLimitId,
        creditsToAdd,
    });
};

Then render the error below the input:

{validationError && (
    <p className="text-xs text-destructive">{validationError}</p>
)}
apps/admin/src/components/projects/project-detail.tsx (2)

208-223: Replace confirm() with a proper confirmation dialog.

Using the browser's native confirm() (Line 212) for destructive actions provides a poor user experience and lacks styling consistency. Consider using a proper dialog component from your UI library.

Example using a dialog component:

const [userToRemove, setUserToRemove] = useState<string | null>(null);

// In render:
<Button
    variant="ghost"
    size="sm"
    onClick={() => setUserToRemove(user.id)}
    className="h-8 px-2 text-destructive hover:text-destructive"
>
    <Trash2 className="size-4" />
</Button>

{/* Add a confirmation dialog */}
<AlertDialog open={!!userToRemove} onOpenChange={(open) => !open && setUserToRemove(null)}>
    <AlertDialogContent>
        <AlertDialogHeader>
            <AlertDialogTitle>Remove User</AlertDialogTitle>
            <AlertDialogDescription>
                Are you sure you want to remove {user.name} from this project?
            </AlertDialogDescription>
        </AlertDialogHeader>
        <AlertDialogFooter>
            <AlertDialogCancel>Cancel</AlertDialogCancel>
            <AlertDialogAction onClick={() => {
                removeUserMutation.mutate({
                    projectId,
                    userId: userToRemove!,
                });
                setUserToRemove(null);
            }}>
                Remove
            </AlertDialogAction>
        </AlertDialogFooter>
    </AlertDialogContent>
</AlertDialog>

76-94: Extract repeated date formatting logic.

The date formatting logic is duplicated for createdAt (Lines 78-82) and updatedAt (Lines 87-91). Consider extracting it into a helper function to improve maintainability.

const formatDate = (date: Date | string) => {
    return new Date(date).toLocaleDateString('en-US', {
        month: 'short',
        day: 'numeric',
        year: 'numeric',
    });
};

// Then use:
<span>Created: {formatDate(project.createdAt)}</span>
<span>Updated: {formatDate(project.updatedAt)}</span>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cea4916 and 24104b9.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (41)
  • apps/admin/.env.example (1 hunks)
  • apps/admin/.gitignore (1 hunks)
  • apps/admin/README.md (1 hunks)
  • apps/admin/eslint.config.js (1 hunks)
  • apps/admin/next.config.ts (1 hunks)
  • apps/admin/package.json (1 hunks)
  • apps/admin/postcss.config.js (1 hunks)
  • apps/admin/src/app/api/trpc/[trpc]/route.ts (1 hunks)
  • apps/admin/src/app/layout.tsx (1 hunks)
  • apps/admin/src/app/page.tsx (1 hunks)
  • apps/admin/src/app/projects/[id].backup/page.tsx (1 hunks)
  • apps/admin/src/app/projects/[id]/page.tsx (1 hunks)
  • apps/admin/src/app/projects/page.tsx (1 hunks)
  • apps/admin/src/app/users/[id]/page.tsx (1 hunks)
  • apps/admin/src/app/users/page.tsx (1 hunks)
  • apps/admin/src/components/layout/breadcrumb.tsx (1 hunks)
  • apps/admin/src/components/layout/sidebar.tsx (1 hunks)
  • apps/admin/src/components/projects/add-user-to-project.tsx (1 hunks)
  • apps/admin/src/components/projects/project-detail.tsx (1 hunks)
  • apps/admin/src/components/projects/projects-list.tsx (1 hunks)
  • apps/admin/src/components/users/edit-rate-limit.tsx (1 hunks)
  • apps/admin/src/components/users/user-detail.tsx (1 hunks)
  • apps/admin/src/components/users/users-list.tsx (1 hunks)
  • apps/admin/src/env.ts (1 hunks)
  • apps/admin/src/server/api/root.ts (1 hunks)
  • apps/admin/src/server/api/routers/projects.ts (1 hunks)
  • apps/admin/src/server/api/routers/users.ts (1 hunks)
  • apps/admin/src/server/api/trpc.ts (1 hunks)
  • apps/admin/src/styles/globals.css (1 hunks)
  • apps/admin/src/trpc/helpers.ts (1 hunks)
  • apps/admin/src/trpc/query-client.ts (1 hunks)
  • apps/admin/src/trpc/react.tsx (1 hunks)
  • apps/admin/src/utils/supabase/admin.ts (1 hunks)
  • apps/admin/src/utils/supabase/client/index.ts (1 hunks)
  • apps/admin/src/utils/supabase/server.ts (1 hunks)
  • apps/admin/tailwind.config.ts (1 hunks)
  • apps/admin/tsconfig.json (1 hunks)
  • package.json (1 hunks)
  • packages/ui/src/components/command.tsx (1 hunks)
  • packages/ui/src/components/form.tsx (2 hunks)
  • packages/ui/src/components/sidebar.tsx (3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/admin/src/app/users/[id]/page.tsx
  • apps/admin/src/components/layout/breadcrumb.tsx
  • packages/ui/src/components/command.tsx
  • apps/admin/src/components/projects/add-user-to-project.tsx
  • apps/admin/src/trpc/helpers.ts
  • apps/admin/src/server/api/routers/users.ts
  • apps/admin/src/app/users/page.tsx
  • apps/admin/src/components/layout/sidebar.tsx
  • apps/admin/src/utils/supabase/admin.ts
  • apps/admin/src/trpc/query-client.ts
  • apps/admin/src/app/projects/page.tsx
  • apps/admin/src/trpc/react.tsx
  • apps/admin/next.config.ts
  • apps/admin/src/env.ts
  • apps/admin/src/server/api/root.ts
  • apps/admin/src/utils/supabase/client/index.ts
  • apps/admin/src/app/projects/[id].backup/page.tsx
  • apps/admin/tailwind.config.ts
  • apps/admin/src/components/projects/projects-list.tsx
  • packages/ui/src/components/sidebar.tsx
  • apps/admin/src/components/users/edit-rate-limit.tsx
  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/app/projects/[id]/page.tsx
  • apps/admin/src/components/projects/project-detail.tsx
  • apps/admin/src/app/layout.tsx
  • apps/admin/src/components/users/user-detail.tsx
  • apps/admin/src/app/page.tsx
  • apps/admin/src/components/users/users-list.tsx
  • apps/admin/src/utils/supabase/server.ts
  • apps/admin/src/app/api/trpc/[trpc]/route.ts
  • packages/ui/src/components/form.tsx
  • apps/admin/src/server/api/routers/projects.ts
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/admin/src/app/users/[id]/page.tsx
  • apps/admin/src/components/layout/breadcrumb.tsx
  • packages/ui/src/components/command.tsx
  • apps/admin/src/components/projects/add-user-to-project.tsx
  • apps/admin/src/trpc/helpers.ts
  • apps/admin/src/server/api/routers/users.ts
  • apps/admin/src/app/users/page.tsx
  • apps/admin/src/components/layout/sidebar.tsx
  • apps/admin/src/utils/supabase/admin.ts
  • apps/admin/src/trpc/query-client.ts
  • apps/admin/src/app/projects/page.tsx
  • apps/admin/src/trpc/react.tsx
  • apps/admin/next.config.ts
  • apps/admin/src/env.ts
  • apps/admin/src/server/api/root.ts
  • apps/admin/src/utils/supabase/client/index.ts
  • apps/admin/src/app/projects/[id].backup/page.tsx
  • apps/admin/tailwind.config.ts
  • apps/admin/src/components/projects/projects-list.tsx
  • packages/ui/src/components/sidebar.tsx
  • apps/admin/src/components/users/edit-rate-limit.tsx
  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/app/projects/[id]/page.tsx
  • apps/admin/src/components/projects/project-detail.tsx
  • apps/admin/src/app/layout.tsx
  • apps/admin/src/components/users/user-detail.tsx
  • apps/admin/src/app/page.tsx
  • apps/admin/src/components/users/users-list.tsx
  • apps/admin/src/utils/supabase/server.ts
  • apps/admin/src/app/api/trpc/[trpc]/route.ts
  • packages/ui/src/components/form.tsx
  • apps/admin/src/server/api/routers/projects.ts
🧠 Learnings (20)
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/trpc/helpers.ts : If reading process.env in server-only helpers, limit to deployment variables (e.g., VERCEL_URL, PORT)

Applied to files:

  • apps/admin/src/trpc/helpers.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from src/server/api/trpc.ts and validate inputs with Zod

Applied to files:

  • apps/admin/src/server/api/routers/users.ts
  • apps/admin/src/server/api/trpc.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/trpc/react.tsx : Keep tRPC React client/provider behind a single client boundary in src/trpc/react.tsx

Applied to files:

  • apps/admin/src/trpc/react.tsx
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/trpc/react.tsx : Keep tRPC React client provider(s) behind a client boundary (ensure this provider file is a client component)

Applied to files:

  • apps/admin/src/trpc/react.tsx
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from apps/web/client/src/server/api/root.ts

Applied to files:

  • apps/admin/src/trpc/react.tsx
  • apps/admin/src/server/api/root.ts
  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/app/api/trpc/[trpc]/route.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from src/server/api/root.ts

Applied to files:

  • apps/admin/src/trpc/react.tsx
  • apps/admin/src/server/api/root.ts
  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/app/api/trpc/[trpc]/route.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/next.config.ts : Import ./src/env in Next.js config to enforce env validation at build time

Applied to files:

  • apps/admin/next.config.ts
  • apps/admin/src/env.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/next.config.ts : Import ./src/env in next.config.ts to enforce env validation at build time

Applied to files:

  • apps/admin/next.config.ts
  • apps/admin/src/env.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/env.ts : Define and validate environment variables via t3-oss/env-nextjs in apps/web/client/src/env.ts

Applied to files:

  • apps/admin/src/env.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/env.ts : Define and validate environment variables in src/env.ts via t3-oss/env-nextjs

Applied to files:

  • apps/admin/src/env.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/env.ts : Expose browser environment variables with NEXT_PUBLIC_* and declare them in the client schema

Applied to files:

  • apps/admin/src/env.ts
  • apps/admin/src/utils/supabase/client/index.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/app/**/*.tsx : Do not use process.env in client code; import env from @/env instead

Applied to files:

  • apps/admin/src/env.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Place tRPC routers under apps/web/client/src/server/api/routers/**

Applied to files:

  • apps/admin/src/server/api/root.ts
  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/app/api/trpc/[trpc]/route.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Place tRPC routers under src/server/api/routers/**

Applied to files:

  • apps/admin/src/server/api/root.ts
  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/app/api/trpc/[trpc]/route.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/utils/supabase/client/index.ts : Use the browser Supabase client only in client components

Applied to files:

  • apps/admin/src/utils/supabase/client/index.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from apps/web/client/src/server/api/trpc.ts and validate inputs with Zod

Applied to files:

  • apps/admin/src/server/api/trpc.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/app/{page,layout,route}.tsx : Follow App Router file conventions (page.tsx, layout.tsx, route.ts) within src/app

Applied to files:

  • apps/admin/src/app/page.tsx
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Prefer TailwindCSS-first styling and reuse existing UI components from onlook/ui and local patterns

Applied to files:

  • apps/admin/src/styles/globals.css
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/utils/supabase/server.ts : Use the server-side Supabase client (headers/cookies) only in server components, actions, and route handlers

Applied to files:

  • apps/admin/src/utils/supabase/server.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Use the Supabase server client in server components/actions/routes and the browser client in client components; never pass server‑only clients into client code

Applied to files:

  • apps/admin/src/utils/supabase/server.ts
🧬 Code graph analysis (20)
apps/admin/src/app/users/[id]/page.tsx (2)
apps/admin/src/components/layout/breadcrumb.tsx (1)
  • Breadcrumb (15-40)
apps/admin/src/components/users/user-detail.tsx (1)
  • UserDetail (18-324)
apps/admin/src/components/projects/add-user-to-project.tsx (4)
packages/ui/src/components/popover.tsx (3)
  • Popover (44-44)
  • PopoverTrigger (44-44)
  • PopoverContent (44-44)
packages/ui/src/components/button.tsx (1)
  • Button (57-57)
packages/ui/src/components/command.tsx (6)
  • Command (148-148)
  • CommandInput (152-152)
  • CommandList (154-154)
  • CommandEmpty (150-150)
  • CommandGroup (151-151)
  • CommandItem (153-153)
packages/ui/src/components/select.tsx (5)
  • Select (158-158)
  • SelectTrigger (166-166)
  • SelectValue (167-167)
  • SelectContent (159-159)
  • SelectItem (161-161)
apps/admin/src/server/api/routers/users.ts (7)
packages/db/src/schema/user/user.ts (1)
  • users (12-25)
packages/db/src/schema/user/user-project.ts (1)
  • userProjects (10-23)
packages/db/src/schema/project/project.ts (1)
  • projects (13-32)
packages/db/src/schema/subscription/subscription.ts (1)
  • subscriptions (11-38)
packages/db/src/schema/subscription/product.ts (1)
  • products (6-13)
packages/db/src/schema/subscription/price.ts (1)
  • prices (8-22)
packages/db/src/schema/subscription/rate-limits.ts (1)
  • rateLimits (6-43)
apps/admin/src/app/users/page.tsx (1)
apps/admin/src/components/users/users-list.tsx (1)
  • UsersList (22-248)
apps/admin/src/utils/supabase/admin.ts (2)
apps/admin/src/utils/supabase/client/index.ts (1)
  • createClient (4-10)
apps/admin/src/utils/supabase/server.ts (1)
  • createClient (5-32)
apps/admin/src/app/projects/page.tsx (1)
apps/admin/src/components/projects/projects-list.tsx (1)
  • ProjectsList (28-318)
apps/admin/src/trpc/react.tsx (2)
apps/admin/src/trpc/query-client.ts (1)
  • createQueryClient (4-21)
apps/admin/src/server/api/root.ts (1)
  • AppRouter (16-16)
apps/admin/next.config.ts (1)
.vscode/.debug.script.mjs (1)
  • __dirname (8-8)
apps/admin/src/server/api/root.ts (3)
apps/admin/src/server/api/trpc.ts (2)
  • createTRPCRouter (89-89)
  • createCallerFactory (75-75)
apps/admin/src/server/api/routers/projects.ts (1)
  • projectsRouter (7-279)
apps/admin/src/server/api/routers/users.ts (1)
  • usersRouter (6-282)
apps/admin/src/utils/supabase/client/index.ts (1)
apps/admin/src/utils/supabase/server.ts (1)
  • createClient (5-32)
apps/admin/src/app/projects/[id].backup/page.tsx (2)
apps/admin/src/app/projects/[id]/page.tsx (1)
  • ProjectDetailPage (4-18)
apps/admin/src/components/layout/breadcrumb.tsx (1)
  • Breadcrumb (15-40)
apps/admin/src/components/projects/projects-list.tsx (1)
apps/admin/src/trpc/react.tsx (1)
  • api (23-23)
apps/admin/src/server/api/trpc.ts (3)
apps/admin/src/utils/supabase/client/index.ts (1)
  • createClient (4-10)
apps/admin/src/utils/supabase/server.ts (1)
  • createClient (5-32)
apps/admin/src/utils/supabase/admin.ts (1)
  • createAdminClient (9-20)
apps/admin/src/app/projects/[id]/page.tsx (2)
apps/admin/src/components/layout/breadcrumb.tsx (1)
  • Breadcrumb (15-40)
apps/admin/src/components/projects/project-detail.tsx (1)
  • ProjectDetail (23-234)
apps/admin/src/components/projects/project-detail.tsx (1)
apps/admin/src/components/projects/add-user-to-project.tsx (1)
  • AddUserToProject (35-183)
apps/admin/src/app/layout.tsx (2)
apps/admin/src/trpc/react.tsx (1)
  • TRPCReactProvider (39-55)
apps/admin/src/components/layout/sidebar.tsx (1)
  • Sidebar (13-46)
apps/admin/src/components/users/user-detail.tsx (1)
apps/admin/src/components/users/edit-rate-limit.tsx (1)
  • EditRateLimit (26-122)
apps/admin/src/utils/supabase/server.ts (1)
apps/admin/src/utils/supabase/client/index.ts (1)
  • createClient (4-10)
apps/admin/src/app/api/trpc/[trpc]/route.ts (2)
apps/admin/src/server/api/trpc.ts (1)
  • createTRPCContext (31-48)
apps/admin/src/server/api/root.ts (1)
  • appRouter (10-13)
apps/admin/src/server/api/routers/projects.ts (4)
apps/admin/src/server/api/trpc.ts (2)
  • createTRPCRouter (89-89)
  • adminProcedure (160-182)
packages/db/src/schema/project/project.ts (1)
  • projects (13-32)
packages/db/src/schema/user/user.ts (1)
  • users (12-25)
packages/db/src/schema/user/user-project.ts (1)
  • userProjects (10-23)
🪛 dotenv-linter (3.3.0)
apps/admin/.env.example

[warning] 4-4: [UnorderedKey] The NEXT_PUBLIC_SUPABASE_ANON_KEY key should go before the NEXT_PUBLIC_SUPABASE_URL key

(UnorderedKey)


[warning] 6-6: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

🪛 markdownlint-cli2 (0.18.1)
apps/admin/README.md

16-16: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


123-123: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (20)
packages/ui/src/components/command.tsx (1)

13-13: LGTM! Import path refactor to relative path.

The change from an absolute alias to a relative import is appropriate for this internal UI component reorganization.

package.json (1)

26-26: LGTM! New admin dev script follows existing patterns.

The new dev:admin script is consistent with the monorepo's workspace-filtered script pattern.

apps/admin/.gitignore (1)

1-46: LGTM! Comprehensive gitignore for Next.js admin app.

The ignore patterns appropriately cover dependencies, build artifacts, environment files, and IDE-specific files.

packages/ui/src/components/form.tsx (2)

15-15: LGTM! Import path refactor to relative path.

Consistent with the internal UI module reorganization across the package.


151-151: LGTM! Export formatting cleanup.

The trailing comma removal is a minor stylistic adjustment with no semantic impact.

packages/ui/src/components/sidebar.tsx (2)

8-15: LGTM! Import path consolidation to relative paths.

The refactor from absolute/alias imports to relative paths is appropriate for this internal UI component and aligns with the broader package reorganization.


689-689: LGTM! Export formatting cleanup.

Minor stylistic adjustment with no semantic impact.

apps/admin/src/components/layout/sidebar.tsx (1)

13-46: LGTM! Clean sidebar implementation with proper active state logic.

The active route detection correctly handles both exact matches and nested routes, with appropriate guarding for the root path.

apps/admin/src/utils/supabase/server.ts (1)

5-32: LGTM! Proper server-side Supabase client setup.

The cookie adapter implementation correctly handles the Server Component scenario with the try/catch block. The async/await pattern for cookies() is appropriate for Next.js 15.

Reminder: Based on learnings, ensure this createClient function is only imported and used in Server Components, Server Actions, and Route Handlers—never in Client Components.

apps/admin/README.md (1)

1-199: Excellent comprehensive documentation!

The README provides clear architectural overview, setup instructions, security considerations, and development workflow. Well-structured for both immediate use and future reference.

apps/admin/src/app/projects/page.tsx (1)

1-19: LGTM!

The projects page structure is clean and follows a consistent pattern with proper component composition.

apps/admin/src/utils/supabase/admin.ts (1)

9-20: LGTM!

The admin client configuration is correct. Disabling autoRefreshToken and persistSession is appropriate for service-role clients that bypass RLS. The warning comment about cautious use is helpful.

apps/admin/src/utils/supabase/client/index.ts (1)

4-10: LGTM! Ensure usage in client components only.

The browser client factory correctly uses NEXT_PUBLIC_* environment variables and follows the proper pattern for client-side Supabase usage. Ensure this client is only imported and used in client components (marked with 'use client' directive).

Based on learnings

apps/admin/src/app/projects/[id]/page.tsx (1)

4-4: Update to handle async params in Next.js 15.

Same issue as in apps/admin/src/app/users/[id]/page.tsx: Next.js 15 made route params async, requiring the component to be async and params to be awaited.

Apply this diff:

-export default function ProjectDetailPage({ params }: { params: { id: string } }) {
+export default async function ProjectDetailPage({ params }: { params: Promise<{ id: string }> }) {
+    const { id } = await params;
     return (
         <div className="bg-background">
             <div className="container mx-auto px-4 py-8 space-y-6">
                 <Breadcrumb
                     items={[
                         { label: 'Projects', href: '/projects' },
-                        { label: params.id },
+                        { label: id },
                     ]}
                 />
-                <ProjectDetail projectId={params.id} />
+                <ProjectDetail projectId={id} />
             </div>
         </div>
     );
 }
apps/admin/src/server/api/root.ts (1)

1-24: LGTM!

The tRPC root router configuration follows best practices:

  • Properly aggregates the projects and users routers
  • Exports the AppRouter type for client-side usage
  • Provides a server-side caller factory with helpful JSDoc

The implementation aligns with the learnings for tRPC router organization.

Based on learnings

apps/admin/src/components/layout/breadcrumb.tsx (1)

15-40: LGTM!

The Breadcrumb component is well-structured with appropriate conditional rendering logic for links and static labels. The use of index as a key (Line 21) is acceptable in this context since breadcrumb items don't dynamically reorder.

apps/admin/src/app/layout.tsx (1)

1-27: LGTM!

The root layout properly sets up the admin app with:

  • Correct metadata configuration
  • TRPCReactProvider wrapping for React Query integration
  • Clean layout structure with Sidebar and scrollable main content area

The implementation follows Next.js 15 app directory conventions.

apps/admin/src/trpc/helpers.ts (1)

1-25: LGTM!

The helper functions are well-structured:

  • getBaseUrl() correctly handles browser, production (Vercel), and local environments
  • The links array properly configures logging for development and error cases
  • Environment variable usage follows best practices (deployment variables only)

The implementation aligns with tRPC best practices and environment configuration guidelines.

Based on learnings

apps/admin/src/components/users/users-list.tsx (1)

22-248: LGTM!

The UsersList component is well-implemented with:

  • Proper loading, error, and empty state handling
  • Correct pagination logic that respects server-side page boundaries
  • Appropriate sort handling that resets to page 1 on sort changes
  • Good use of skeleton loaders for a better perceived performance

The component follows React and Next.js best practices.

apps/admin/src/env.ts (1)

1-69: LGTM!

The environment configuration follows best practices:

  • Proper use of @t3-oss/env-nextjs for type-safe environment validation
  • Server-side variables include required Supabase credentials with appropriate validation
  • Client-side variables correctly use NEXT_PUBLIC_* prefix with sensible defaults
  • Manual runtimeEnv destructuring for edge runtime compatibility
  • Skip validation flag for Docker builds
  • Empty string as undefined behavior for stricter validation

The implementation aligns with the learnings for environment variable management in Next.js apps.

Based on learnings

"dev": "next dev --port 3001",
"build": "next build",
"start": "next start --port 3001",
"typecheck": "tsc --noEmit --project tsconfig.json 2>&1 | grep -v 'web/client' || exit 0",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix the typecheck script filter.

The typecheck script filters out errors containing 'web/client', which appears to be copied from another package and doesn't apply to the admin app. This could inadvertently hide legitimate TypeScript errors.

Apply this diff to remove the incorrect filter:

-        "typecheck": "tsc --noEmit --project tsconfig.json 2>&1 | grep -v 'web/client' || exit 0",
+        "typecheck": "tsc --noEmit --project tsconfig.json",
📝 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
"typecheck": "tsc --noEmit --project tsconfig.json 2>&1 | grep -v 'web/client' || exit 0",
"typecheck": "tsc --noEmit --project tsconfig.json",
🤖 Prompt for AI Agents
In apps/admin/package.json around line 10, the "typecheck" script currently
pipes tsc output through a grep that excludes 'web/client' (copied from another
package) which can hide legitimate errors; remove the entire pipe filter and
trailing "|| exit 0" so the script simply runs tsc --noEmit --project
tsconfig.json and fails on real type errors.

Comment on lines 9 to 10
{ label: 'Projects', href: '/' },
{ label: params.id },
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix breadcrumb link target.

The backup page breadcrumb points “Projects” to the admin root ('/'), but the primary page uses '/projects'. This regression breaks navigation back to the projects list. Align the backup route with the main page.

-                        { label: 'Projects', href: '/' },
+                        { label: 'Projects', href: '/projects' },
🤖 Prompt for AI Agents
In apps/admin/src/app/projects/[id].backup/page.tsx around lines 9 to 10, the
breadcrumb entry for "Projects" incorrectly links to '/' instead of the projects
list; update the breadcrumb object's href from '/' to '/projects' so it matches
the main projects page and restores correct navigation back to the projects
list.

import { Breadcrumb } from '@/components/layout/breadcrumb';
import { UserDetail } from '@/components/users/user-detail';

export default function UserDetailPage({ params }: { params: { id: string } }) {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Update to handle async params in Next.js 15.

Next.js 15 made route params async. The current implementation doesn't await params, which may cause TypeScript errors or runtime issues.

Apply this diff to properly handle async params:

-export default function UserDetailPage({ params }: { params: { id: string } }) {
+export default async function UserDetailPage({ params }: { params: Promise<{ id: string }> }) {
+    const { id } = await params;
     return (
         <div className="bg-background">
             <div className="container mx-auto px-4 py-8 space-y-6">
                 <Breadcrumb
                     items={[
                         { label: 'Users', href: '/users' },
-                        { label: params.id },
+                        { label: id },
                     ]}
                 />
-                <UserDetail userId={params.id} />
+                <UserDetail userId={id} />
             </div>
         </div>
     );
 }

Run the following script to verify if other dynamic route pages in the admin app have the same issue:


🏁 Script executed:

#!/bin/bash
# Description: Find all dynamic route pages that may need async params handling

# Search for page.tsx files in dynamic route directories
fd -e tsx 'page\.tsx$' apps/admin/src/app --exec grep -l "params:" {} \;

Length of output: 129


Handle async route params in Next.js 15 dynamic pages

  • Convert apps/admin/src/app/users/[id]/page.tsx:
-export default function UserDetailPage({ params }: { params: { id: string } }) {
+export default async function UserDetailPage({ params }: { params: Promise<{ id: string }> }) {
+  const { id } = await params;
  • Apply the same change to apps/admin/src/app/projects/[id]/page.tsx

Ensures route params are correctly awaited per Next.js 15.

📝 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
export default function UserDetailPage({ params }: { params: { id: string } }) {
export default async function UserDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
🤖 Prompt for AI Agents
In apps/admin/src/app/users/[id]/page.tsx around line 4, the page component must
handle Next.js 15 async route params: change the component to an async function
and await the incoming params before using them (e.g., declare the function
async and do const { id } = await params). Also apply the same change to
apps/admin/src/app/projects/[id]/page.tsx so both dynamic pages properly await
route params per Next.js 15.

Comment on lines 195 to 223
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
window.open(editorUrl, '_blank');
}}
className="h-6 px-2"
>
<Code2 className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">{editorUrl}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
window.open(previewUrl, '_blank');
}}
className="h-6 px-2"
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Harden external sandbox links against tabnabbing

Both sandbox buttons call window.open(url, '_blank') without noopener. That leaves window.opener writable, so a compromised sandbox can redirect the admin dashboard (reverse tabnabbing). Add the noopener,noreferrer feature string (and clear opener defensively) before shipping.

- window.open(editorUrl, '_blank');
+ const editorWindow = window.open(editorUrl, '_blank', 'noopener,noreferrer');
+ editorWindow?.opener && (editorWindow.opener = null);
...
- window.open(previewUrl, '_blank');
+ const previewWindow = window.open(previewUrl, '_blank', 'noopener,noreferrer');
+ previewWindow?.opener && (previewWindow.opener = null);
📝 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
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
window.open(editorUrl, '_blank');
}}
className="h-6 px-2"
>
<Code2 className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">{editorUrl}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
window.open(previewUrl, '_blank');
}}
className="h-6 px-2"
>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
const editorWindow = window.open(
editorUrl,
'_blank',
'noopener,noreferrer'
);
if (editorWindow?.opener) {
editorWindow.opener = null;
}
}}
className="h-6 px-2"
>
<Code2 className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p className="text-xs">{editorUrl}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
const previewWindow = window.open(
previewUrl,
'_blank',
'noopener,noreferrer'
);
if (previewWindow?.opener) {
previewWindow.opener = null;
}
}}
className="h-6 px-2"
>
🤖 Prompt for AI Agents
In apps/admin/src/components/projects/projects-list.tsx around lines 195 to 223,
the two sandbox buttons open external pages using window.open(url, '_blank')
which leaves window.opener writable and allows reverse tabnabbing; change the
calls to include the feature string 'noopener,noreferrer' (e.g. window.open(url,
'_blank', 'noopener,noreferrer')) and defensively clear the opener on the
returned window object (const newWin = window.open(...); if (newWin)
newWin.opener = null;) so that external pages cannot access or redirect the
admin dashboard.

Comment on lines 243 to 287
const isActive = new Date() >= new Date(rateLimit.startedAt) &&
new Date() <= new Date(rateLimit.endedAt);
const usagePercent = ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100;

return (
<div
key={rateLimit.id}
className="p-4 border rounded-lg space-y-3"
>
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<p className="font-medium">
{rateLimit.max - rateLimit.left} / {rateLimit.max} used
</p>
{isActive && <Badge variant="default">Active</Badge>}
</div>
<p className="text-xs text-muted-foreground">
{rateLimit.left} requests remaining
</p>
</div>
<div className="flex items-center gap-2">
{rateLimit.carryOverTotal > 0 && (
<Badge variant="outline">
Carried over {rateLimit.carryOverTotal}x
</Badge>
)}
{isActive && (
<EditRateLimit
rateLimitId={rateLimit.id}
currentLeft={rateLimit.left}
max={rateLimit.max}
userId={userId}
/>
)}
</div>
</div>

{/* Progress bar */}
<div className="space-y-1">
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all"
style={{ width: `${usagePercent}%` }}
/>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard rate-limit progress against zero max.

If a rate limit row has max = 0 (which happens for exhausted or misconfigured limits), the current calculation yields Infinity/NaN, so the inline width style becomes invalid and the bar disappears. Clamp the math to handle zero/negative max values.

-                                const usagePercent = ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100;
+                                const usagePercent =
+                                    rateLimit.max > 0
+                                        ? Math.min(
+                                              100,
+                                              Math.max(0, ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100),
+                                          )
+                                        : 0;
📝 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 isActive = new Date() >= new Date(rateLimit.startedAt) &&
new Date() <= new Date(rateLimit.endedAt);
const usagePercent = ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100;
return (
<div
key={rateLimit.id}
className="p-4 border rounded-lg space-y-3"
>
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<p className="font-medium">
{rateLimit.max - rateLimit.left} / {rateLimit.max} used
</p>
{isActive && <Badge variant="default">Active</Badge>}
</div>
<p className="text-xs text-muted-foreground">
{rateLimit.left} requests remaining
</p>
</div>
<div className="flex items-center gap-2">
{rateLimit.carryOverTotal > 0 && (
<Badge variant="outline">
Carried over {rateLimit.carryOverTotal}x
</Badge>
)}
{isActive && (
<EditRateLimit
rateLimitId={rateLimit.id}
currentLeft={rateLimit.left}
max={rateLimit.max}
userId={userId}
/>
)}
</div>
</div>
{/* Progress bar */}
<div className="space-y-1">
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all"
style={{ width: `${usagePercent}%` }}
/>
const isActive = new Date() >= new Date(rateLimit.startedAt) &&
new Date() <= new Date(rateLimit.endedAt);
const usagePercent =
rateLimit.max > 0
? Math.min(
100,
Math.max(
0,
((rateLimit.max - rateLimit.left) / rateLimit.max) * 100,
),
)
: 0;
return (
<div
key={rateLimit.id}
className="p-4 border rounded-lg space-y-3"
>
<div className="flex items-center justify-between">
<div className="space-y-1">
<div className="flex items-center gap-2">
<p className="font-medium">
{rateLimit.max - rateLimit.left} / {rateLimit.max} used
</p>
{isActive && <Badge variant="default">Active</Badge>}
</div>
<p className="text-xs text-muted-foreground">
{rateLimit.left} requests remaining
</p>
</div>
<div className="flex items-center gap-2">
{rateLimit.carryOverTotal > 0 && (
<Badge variant="outline">
Carried over {rateLimit.carryOverTotal}x
</Badge>
)}
{isActive && (
<EditRateLimit
rateLimitId={rateLimit.id}
currentLeft={rateLimit.left}
max={rateLimit.max}
userId={userId}
/>
)}
</div>
</div>
{/* Progress bar */}
<div className="space-y-1">
<div className="h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary transition-all"
style={{ width: `${usagePercent}%` }}
/>
🤖 Prompt for AI Agents
In apps/admin/src/components/users/user-detail.tsx around lines 243 to 287, the
usagePercent calculation can produce Infinity/NaN when rateLimit.max is 0 or
negative; change the calculation to first check rateLimit.max > 0, compute
usagePercent = ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100 only
when max > 0, otherwise set usagePercent = 0, then clamp usagePercent between 0
and 100 before using it in the inline width style so the progress bar always
receives a valid percentage.

Comment on lines 160 to 181
export const adminProcedure = t.procedure.use(timingMiddleware).use(({ ctx, next }) => {
if (!ctx.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}

if (!ctx.user.email) {
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'User must have an email address to access this resource',
});
}

const adminSupabase = createAdminClient();

return next({
ctx: {
// infers the `session` as non-nullable
user: ctx.user as SetRequiredDeep<User, 'email'>,
db: ctx.db,
supabase: adminSupabase, // Override with admin client
},
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Restrict admin procedures to actual admins

adminProcedure only verifies that a user is logged in with an email, yet it hands out a service-role Supabase client (bypasses all RLS). Today any authenticated user can invoke admin routers and operate with full DB privileges. Add an explicit authorization guard (e.g., check app_metadata.roles or a dedicated flag) before instantiating the admin client, and return FORBIDDEN when the caller isn’t an admin. Until that check exists, this endpoint is effectively public.

🤖 Prompt for AI Agents
In apps/admin/src/server/api/trpc.ts around lines 160 to 181, the adminProcedure
currently only ensures a logged-in user with an email and then creates a
service-role Supabase client; you must enforce an explicit admin authorization
check before instantiating the admin client. Inspect ctx.user.app_metadata.roles
(or a dedicated admin flag on the user) and if the user does not include the
required admin role, throw new TRPCError({ code: 'FORBIDDEN', message: 'Admin
access required' }); only after that check create the adminSupabase and return
next with the elevated supabase client and the narrowed user type. Ensure the
authorization check runs before any admin client creation to avoid leaking
service-role privileges.

Comment on lines 4 to 20
export const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 30 * 1000,
},
dehydrate: {
serializeData: SuperJSON.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
},
hydrate: {
deserializeData: SuperJSON.deserialize,
},
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Move hydrate/dehydrate options to the correct level.

defaultOptions doesn’t include dehydrate/hydrate, so these settings are ignored. As written, we fall back to vanilla JSON serialization, losing SuperJSON round‑tripping (e.g., Dates). Configure them on the QueryClient instead.

 export const createQueryClient = () =>
-    new QueryClient({
-        defaultOptions: {
-            queries: {
-                // With SSR, we usually want to set some default staleTime
-                // above 0 to avoid refetching immediately on the client
-                staleTime: 30 * 1000,
-            },
-            dehydrate: {
-                serializeData: SuperJSON.serialize,
-                shouldDehydrateQuery: (query) =>
-                    defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
-            },
-            hydrate: {
-                deserializeData: SuperJSON.deserialize,
-            },
-        },
-    });
+    new QueryClient({
+        defaultOptions: {
+            queries: {
+                // With SSR, we usually want to set some default staleTime
+                // above 0 to avoid refetching immediately on the client
+                staleTime: 30 * 1000,
+            },
+        },
+        dehydrateOptions: {
+            serializeData: SuperJSON.serialize,
+            shouldDehydrateQuery: (query) =>
+                defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
+        },
+        hydrateOptions: {
+            deserializeData: SuperJSON.deserialize,
+        },
+    });
📝 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
export const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 30 * 1000,
},
dehydrate: {
serializeData: SuperJSON.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
},
hydrate: {
deserializeData: SuperJSON.deserialize,
},
},
export const createQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 30 * 1000,
},
},
dehydrateOptions: {
serializeData: SuperJSON.serialize,
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
},
hydrateOptions: {
deserializeData: SuperJSON.deserialize,
},
});
🤖 Prompt for AI Agents
In apps/admin/src/trpc/query-client.ts around lines 4 to 20, the
dehydrate/hydrate options are mistakenly placed inside defaultOptions (so
SuperJSON serialize/deserialize and the custom shouldDehydrateQuery are
ignored); move dehydrate and hydrate up to the top-level QueryClient config
(alongside defaultOptions), leaving defaultOptions.queries.staleTime as-is, and
keep shouldDehydrateQuery inside the dehydrate option so SuperJSON is actually
used for server->client round‑tripping.

@vercel vercel bot temporarily deployed to Preview – docs October 12, 2025 03:39 Inactive
listProducts: adminProcedure
.query(async ({ ctx }) => {
// Fetch all products with their prices
const productsData = await ctx.db
Copy link
Contributor

Choose a reason for hiding this comment

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

In the listProducts query, products and prices are fetched in separate queries and then merged in-memory. For scalability, consider performing a JOIN or grouping operation in the database query to reduce post-processing in JavaScript.

Copy link

@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: 1

🧹 Nitpick comments (3)
apps/admin/src/components/layout/sidebar.tsx (2)

29-42: Add aria-current for improved accessibility.

Active navigation links should include the aria-current="page" attribute to help screen reader users understand the current page context.

Apply this diff to add the attribute:

                         <Link
                             key={item.name}
                             href={item.href}
+                            aria-current={isActive ? 'page' : undefined}
                             className={cn(
                                 'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
                                 isActive
                                     ? 'bg-primary text-primary-foreground'
                                     : 'text-muted-foreground hover:bg-muted hover:text-foreground'
                             )}
                         >

24-44: Consider adding an aria-label to the navigation landmark.

Adding a descriptive label to the <nav> element helps screen reader users identify the purpose of this navigation region.

Apply this diff:

-            <nav className="flex-1 space-y-1 px-3 py-4">
+            <nav aria-label="Main navigation" className="flex-1 space-y-1 px-3 py-4">
apps/admin/src/server/api/routers/subscriptions.ts (1)

19-33: Group prices by product to avoid O(n²) filters

Each product currently re-filters the entire pricesData array, turning this into quadratic work as your catalog grows. Pre-grouping prices by productId before the map keeps the procedure linear and avoids unnecessary allocations—especially helpful once the admin dashboard is dealing with hundreds of SKUs.

-            const productsWithPrices = productsData.map(product => ({
-                ...product,
-                prices: pricesData.filter(price => price.productId === product.id),
-            }));
+            const pricesByProduct = pricesData.reduce((acc, price) => {
+                const list = acc.get(price.productId);
+                if (list) {
+                    list.push(price);
+                } else {
+                    acc.set(price.productId, [price]);
+                }
+                return acc;
+            }, new Map<string, typeof pricesData[number][]>());
+
+            const productsWithPrices = productsData.map(product => ({
+                ...product,
+                prices: pricesByProduct.get(product.id) ?? [],
+            }));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 24104b9 and b908a03.

📒 Files selected for processing (6)
  • apps/admin/src/app/subscriptions/page.tsx (1 hunks)
  • apps/admin/src/components/layout/sidebar.tsx (1 hunks)
  • apps/admin/src/components/subscriptions/products-list.tsx (1 hunks)
  • apps/admin/src/components/subscriptions/subscriptions-list.tsx (1 hunks)
  • apps/admin/src/server/api/root.ts (1 hunks)
  • apps/admin/src/server/api/routers/subscriptions.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/admin/src/server/api/root.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/admin/src/app/subscriptions/page.tsx
  • apps/admin/src/components/subscriptions/subscriptions-list.tsx
  • apps/admin/src/components/subscriptions/products-list.tsx
  • apps/admin/src/server/api/routers/subscriptions.ts
  • apps/admin/src/components/layout/sidebar.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/admin/src/app/subscriptions/page.tsx
  • apps/admin/src/components/subscriptions/subscriptions-list.tsx
  • apps/admin/src/components/subscriptions/products-list.tsx
  • apps/admin/src/server/api/routers/subscriptions.ts
  • apps/admin/src/components/layout/sidebar.tsx
🧠 Learnings (4)
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from apps/web/client/src/server/api/root.ts

Applied to files:

  • apps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from src/server/api/trpc.ts and validate inputs with Zod

Applied to files:

  • apps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from src/server/api/root.ts

Applied to files:

  • apps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from apps/web/client/src/server/api/trpc.ts and validate inputs with Zod

Applied to files:

  • apps/admin/src/server/api/routers/subscriptions.ts
🧬 Code graph analysis (3)
apps/admin/src/app/subscriptions/page.tsx (2)
apps/admin/src/components/subscriptions/products-list.tsx (1)
  • ProductsList (8-91)
apps/admin/src/components/subscriptions/subscriptions-list.tsx (1)
  • SubscriptionsList (20-165)
apps/admin/src/components/subscriptions/products-list.tsx (1)
packages/db/src/schema/subscription/product.ts (1)
  • products (6-13)
apps/admin/src/server/api/routers/subscriptions.ts (4)
packages/db/src/schema/subscription/product.ts (1)
  • products (6-13)
packages/db/src/schema/subscription/price.ts (1)
  • prices (8-22)
packages/db/src/schema/subscription/subscription.ts (1)
  • subscriptions (11-38)
packages/db/src/schema/user/user.ts (1)
  • users (12-25)
🔇 Additional comments (1)
apps/admin/src/components/layout/sidebar.tsx (1)

8-12: LGTM!

The navigation array is correctly defined at module scope, avoiding unnecessary recreation on each render.

Comment on lines 26 to 27
const isActive = pathname === item.href ||
(item.href !== '/' && pathname.startsWith(item.href));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential false positive in active route matching.

The startsWith check could incorrectly mark routes as active if you add routes like /projects-archive or /users-admin, since '/projects-archive'.startsWith('/projects') returns true.

Apply this diff to fix the edge case:

-                    const isActive = pathname === item.href ||
-                                   (item.href !== '/' && pathname.startsWith(item.href));
+                    const isActive = 
+                        pathname === item.href ||
+                        (item.href !== '/' && pathname.startsWith(item.href + '/'));

This ensures that /projects matches /projects/123 but not /projects-archive.

📝 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 isActive = pathname === item.href ||
(item.href !== '/' && pathname.startsWith(item.href));
const isActive =
pathname === item.href ||
(item.href !== '/' && pathname.startsWith(item.href + '/'));
🤖 Prompt for AI Agents
In apps/admin/src/components/layout/sidebar.tsx around lines 26-27, the
active-route logic using pathname.startsWith(item.href) can produce false
positives for routes like '/projects-archive'; change the check so that a route
is considered active only if pathname equals item.href or it starts with
item.href followed by a '/' (i.e., pathname.startsWith(item.href + '/') ),
keeping the existing special-case for '/' intact so '/projects' matches
'/projects/123' but not '/projects-archive'.

@vercel vercel bot temporarily deployed to Preview – docs October 12, 2025 04:02 Inactive
<Button
variant="link"
className="h-auto p-0 text-xs font-mono mt-1"
onClick={() => window.open(`https://dashboard.stripe.com/products/${data.price.stripeProductId}`, '_blank')}
Copy link
Contributor

Choose a reason for hiding this comment

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

For security best practices, include 'noopener,noreferrer' in the window.open calls. For example: window.open(url, '_blank', 'noopener,noreferrer').

Suggested change
onClick={() => window.open(`https://dashboard.stripe.com/products/${data.price.stripeProductId}`, '_blank')}
onClick={() => window.open(`https://dashboard.stripe.com/products/${data.price.stripeProductId}`, '_blank', 'noopener,noreferrer')}

className="h-auto p-0 text-xs font-mono"
onClick={(e) => {
e.stopPropagation();
window.open(`https://dashboard.stripe.com/products/${product.stripeProductId}`, '_blank');
Copy link
Contributor

Choose a reason for hiding this comment

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

Add 'noopener,noreferrer' to window.open calls here for security. This applies to both the Stripe Product and Stripe Price links.

Suggested change
window.open(`https://dashboard.stripe.com/products/${product.stripeProductId}`, '_blank');
window.open(`https://dashboard.stripe.com/products/${product.stripeProductId}`, '_blank', 'noopener,noreferrer');

<Button
variant="link"
className="h-auto p-0 text-sm font-mono mt-1"
onClick={() => window.open(`https://dashboard.stripe.com/customers/${user.stripeCustomerId}`, '_blank')}
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider adding 'noopener,noreferrer' to the window.open call for the Stripe Customer ID link for improved security.

Suggested change
onClick={() => window.open(`https://dashboard.stripe.com/customers/${user.stripeCustomerId}`, '_blank')}
onClick={() => window.open(`https://dashboard.stripe.com/customers/${user.stripeCustomerId}`, '_blank', 'noopener,noreferrer')}

.limit(1);

if (!priceData[0]) {
throw new Error('Price not found');
Copy link
Contributor

Choose a reason for hiding this comment

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

For more consistent API error handling, consider using TRPCError (e.g. new TRPCError({ code: 'NOT_FOUND', message: 'Price not found' })) instead of throwing a generic Error.

Suggested change
throw new Error('Price not found');
throw new TRPCError({ code: 'NOT_FOUND', message: 'Price not found' });

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b908a03 and 3148cc2.

📒 Files selected for processing (7)
  • apps/admin/src/app/subscriptions/prices/[id]/page.tsx (1 hunks)
  • apps/admin/src/components/subscriptions/price-detail.tsx (1 hunks)
  • apps/admin/src/components/subscriptions/products-list.tsx (1 hunks)
  • apps/admin/src/components/subscriptions/subscriptions-list.tsx (1 hunks)
  • apps/admin/src/components/users/user-detail.tsx (1 hunks)
  • apps/admin/src/server/api/routers/subscriptions.ts (1 hunks)
  • apps/admin/src/server/api/trpc.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/admin/src/app/subscriptions/prices/[id]/page.tsx
  • apps/admin/src/components/users/user-detail.tsx
  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/components/subscriptions/price-detail.tsx
  • apps/admin/src/components/subscriptions/products-list.tsx
  • apps/admin/src/server/api/routers/subscriptions.ts
  • apps/admin/src/components/subscriptions/subscriptions-list.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/admin/src/app/subscriptions/prices/[id]/page.tsx
  • apps/admin/src/components/users/user-detail.tsx
  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/components/subscriptions/price-detail.tsx
  • apps/admin/src/components/subscriptions/products-list.tsx
  • apps/admin/src/server/api/routers/subscriptions.ts
  • apps/admin/src/components/subscriptions/subscriptions-list.tsx
🧠 Learnings (6)
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from src/server/api/trpc.ts and validate inputs with Zod

Applied to files:

  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Use publicProcedure/protectedProcedure from apps/web/client/src/server/api/trpc.ts and validate inputs with Zod

Applied to files:

  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from apps/web/client/src/server/api/root.ts

Applied to files:

  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/root.ts : Export all tRPC routers from src/server/api/root.ts

Applied to files:

  • apps/admin/src/server/api/trpc.ts
  • apps/admin/src/server/api/routers/subscriptions.ts
📚 Learning: 2025-09-16T19:22:52.461Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: CLAUDE.md:0-0
Timestamp: 2025-09-16T19:22:52.461Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Place tRPC routers under src/server/api/routers/**

Applied to files:

  • apps/admin/src/server/api/trpc.ts
📚 Learning: 2025-09-14T01:44:21.209Z
Learnt from: CR
PR: onlook-dev/onlook#0
File: AGENTS.md:0-0
Timestamp: 2025-09-14T01:44:21.209Z
Learning: Applies to apps/web/client/src/server/api/routers/**/*.ts : Place tRPC routers under apps/web/client/src/server/api/routers/**

Applied to files:

  • apps/admin/src/server/api/trpc.ts
🧬 Code graph analysis (5)
apps/admin/src/app/subscriptions/prices/[id]/page.tsx (2)
apps/admin/src/components/layout/breadcrumb.tsx (1)
  • Breadcrumb (15-40)
apps/admin/src/components/subscriptions/price-detail.tsx (1)
  • PriceDetail (23-181)
apps/admin/src/components/users/user-detail.tsx (1)
apps/admin/src/components/users/edit-rate-limit.tsx (1)
  • EditRateLimit (26-122)
apps/admin/src/server/api/trpc.ts (3)
apps/admin/src/utils/supabase/client/index.ts (1)
  • createClient (4-10)
apps/admin/src/utils/supabase/server.ts (1)
  • createClient (5-32)
apps/admin/src/utils/supabase/admin.ts (1)
  • createAdminClient (9-20)
apps/admin/src/components/subscriptions/products-list.tsx (1)
packages/db/src/schema/subscription/product.ts (1)
  • products (6-13)
apps/admin/src/server/api/routers/subscriptions.ts (4)
packages/db/src/schema/subscription/product.ts (1)
  • products (6-13)
packages/db/src/schema/subscription/price.ts (1)
  • prices (8-22)
packages/db/src/schema/subscription/subscription.ts (1)
  • subscriptions (11-38)
packages/db/src/schema/user/user.ts (1)
  • users (12-25)

import { Breadcrumb } from '@/components/layout/breadcrumb';
import { PriceDetail } from '@/components/subscriptions/price-detail';

export default function PriceDetailPage({ params }: { params: { id: string } }) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Await params in Next.js 15 page components.

Next.js 15 changed route params to be asynchronous. The current synchronous access will cause runtime errors. Make the component async and await the params prop.

Based on learnings

Apply this diff:

-export default function PriceDetailPage({ params }: { params: { id: string } }) {
+export default async function PriceDetailPage({ params }: { params: Promise<{ id: string }> }) {
+    const { id } = await params;
     return (
         <div className="bg-background">
             <div className="container mx-auto px-4 py-8 space-y-6">
                 <Breadcrumb
                     items={[
                         { label: 'Subscriptions', href: '/subscriptions' },
-                        { label: 'Price Details', href: `/subscriptions/prices/${params.id}` },
+                        { label: 'Price Details', href: `/subscriptions/prices/${id}` },
                     ]}
                 />
-                <PriceDetail priceId={params.id} />
+                <PriceDetail priceId={id} />
             </div>
         </div>
     );
 }
📝 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
export default function PriceDetailPage({ params }: { params: { id: string } }) {
export default async function PriceDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return (
<div className="bg-background">
<div className="container mx-auto px-4 py-8 space-y-6">
<Breadcrumb
items={[
{ label: 'Subscriptions', href: '/subscriptions' },
{ label: 'Price Details', href: `/subscriptions/prices/${id}` },
]}
/>
<PriceDetail priceId={id} />
</div>
</div>
);
}
🤖 Prompt for AI Agents
In apps/admin/src/app/subscriptions/prices/[id]/page.tsx around line 4, the page
component accesses params synchronously but Next.js 15 provides params
asynchronously; make the component async and await the params prop (e.g., change
the function to "export default async function PriceDetailPage(...)" and use
"const { id } = await params" or similar), then replace all uses of params.id
with the awaited id.

Comment on lines 77 to 79
if (!priceData[0]) {
throw new Error('Price not found');
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use TRPCError for API error responses.

When a price is not found, throwing a generic Error results in a 500 Internal Server Error response. Use TRPCError with code 'NOT_FOUND' to return a proper 404 status and improve API semantics.

Apply this diff:

+import { TRPCError } from '@trpc/server';
             if (!priceData[0]) {
-                throw new Error('Price not found');
+                throw new TRPCError({ code: 'NOT_FOUND', message: 'Price not found' });
             }
📝 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
if (!priceData[0]) {
throw new Error('Price not found');
}
// Add at the top of apps/admin/src/server/api/routers/subscriptions.ts
import { TRPCError } from '@trpc/server';
// …existing code…
if (!priceData[0]) {
throw new TRPCError({ code: 'NOT_FOUND', message: 'Price not found' });
}
🤖 Prompt for AI Agents
In apps/admin/src/server/api/routers/subscriptions.ts around lines 77 to 79, the
code throws a generic Error when no price is found; replace this with a
TRPCError to return a proper 404. Import TRPCError from '@trpc/server' if not
already imported, then throw new TRPCError({ code: 'NOT_FOUND', message: 'Price
not found' }) instead of throw new Error('Price not found'); ensure the import
and throw follow existing project style and types.

Copy link

@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: 0

♻️ Duplicate comments (1)
apps/admin/src/app/products/prices/[id]/page.tsx (1)

4-18: Await params in Next.js 15 page components.

Next.js 15 changed route params to be asynchronous. The current synchronous access will cause runtime errors. Make the component async and await the params prop.

Based on learnings

Apply this diff:

-export default function PriceDetailPage({ params }: { params: { id: string } }) {
+export default async function PriceDetailPage({ params }: { params: Promise<{ id: string }> }) {
+    const { id } = await params;
     return (
         <div className="bg-background">
             <div className="container mx-auto px-4 py-8 space-y-6">
                 <Breadcrumb
                     items={[
                         { label: 'Products', href: '/products' },
-                        { label: 'Price Details', href: `/products/prices/${params.id}` },
+                        { label: 'Price Details', href: `/products/prices/${id}` },
                     ]}
                 />
-                <PriceDetail priceId={params.id} />
+                <PriceDetail priceId={id} />
             </div>
         </div>
     );
 }
🧹 Nitpick comments (6)
apps/admin/src/app/products/page.tsx (1)

3-19: LGTM!

The page component is well-structured and correctly implements a Next.js App Router page. The composition with ProductsList is clean and follows good separation of concerns.

Optional enhancement: Add metadata export.

Consider adding metadata for better browser tab identification:

export const metadata = {
  title: 'Products & Pricing',
  description: 'Manage products and pricing tiers',
};
apps/admin/src/components/deployments/deployments-list.tsx (2)

189-241: Extract hardcoded CodeSandbox URLs to constants.

The CodeSandbox URLs at lines 204 and 224 are hardcoded, which reduces flexibility if URL patterns change or different environments require different base URLs.

Consider extracting these to module-level constants:

+'use client';
+
+const CODESANDBOX_EDITOR_URL = 'https://codesandbox.io/s';
+const CODESANDBOX_PREVIEW_URL_TEMPLATE = (id: string) => `https://${id}-3000.csb.app`;
+
 import { api } from '@/trpc/react';

Then use them in the handlers:

-window.open(`https://codesandbox.io/s/${deployment.sandboxId}`, '_blank');
+window.open(`${CODESANDBOX_EDITOR_URL}/${deployment.sandboxId}`, '_blank');
-window.open(`https://${deployment.sandboxId}-3000.csb.app`, '_blank');
+window.open(CODESANDBOX_PREVIEW_URL_TEMPLATE(deployment.sandboxId), '_blank');

299-313: Consider removing hardcoded locale for date formatting.

The date and time formatting uses hardcoded 'en-US' locale, which doesn't respect user locale preferences. For better internationalization, consider either using the browser's default locale (by passing undefined) or making the locale configurable.

Apply this diff to use the browser's locale:

 <p className="text-sm">
-    {new Date(deployment.createdAt).toLocaleDateString('en-US', {
+    {new Date(deployment.createdAt).toLocaleDateString(undefined, {
         month: 'short',
         day: 'numeric',
         year: 'numeric',
     })}
 </p>
 <p className="text-xs text-muted-foreground">
-    {new Date(deployment.createdAt).toLocaleTimeString('en-US', {
+    {new Date(deployment.createdAt).toLocaleTimeString(undefined, {
         hour: '2-digit',
         minute: '2-digit',
     })}
 </p>
apps/admin/src/server/api/routers/domains.ts (1)

107-114: Consider using optional chaining.

While the non-null assertion on line 113 is safe because line 110 checks for key existence, using optional chaining would be more idiomatic and eliminate the need for the assertion.

Apply this diff:

-                projectsByDomain.get(pd.customDomainId)!.push(pd);
+                projectsByDomain.get(pd.customDomainId)?.push(pd);
apps/admin/src/components/domains/domains-list.tsx (2)

176-185: Remove redundant non-null assertions.

The type guard on line 176 (domain.projects.length > 0 && domain.projects[0]) ensures projects[0] exists, making the non-null assertions on lines 179, 181, and 183 redundant.

Apply this diff:

                             {domain.projects.length > 0 && domain.projects[0] ? (
                                 <div
                                     className="cursor-pointer hover:underline"
-                                    onClick={() => router.push(`/users/${domain.projects[0]!.ownerId}`)}
+                                    onClick={() => router.push(`/users/${domain.projects[0].ownerId}`)}
                                 >
-                                    <p className="font-medium text-sm">{domain.projects[0]!.ownerName}</p>
+                                    <p className="font-medium text-sm">{domain.projects[0].ownerName}</p>
                                     <p className="text-xs text-muted-foreground">
-                                        {domain.projects[0]!.ownerEmail}
+                                        {domain.projects[0].ownerEmail}
                                     </p>
                                 </div>

207-210: Use enum constant instead of string literal.

The status comparison uses the string literal 'active' instead of the ProjectCustomDomainStatus enum. Consider importing and using the enum constant for type safety and consistency with the backend.

Add the import at the top of the file:

+'use client';
+
+import { ProjectCustomDomainStatus } from '@onlook/db/src/schema/domain/custom/project-custom-domain';
 import { api } from '@/trpc/react';

Then update the comparison:

                                             <Badge
-                                                variant={project.status === 'active' ? 'default' : 'secondary'}
+                                                variant={project.status === ProjectCustomDomainStatus.ACTIVE ? 'default' : 'secondary'}
                                                 className="text-xs"
                                             >
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1493aa0 and 3d82a0c.

📒 Files selected for processing (10)
  • apps/admin/src/app/domains/page.tsx (1 hunks)
  • apps/admin/src/app/products/page.tsx (1 hunks)
  • apps/admin/src/app/products/prices/[id]/page.tsx (1 hunks)
  • apps/admin/src/app/subscriptions/page.tsx (1 hunks)
  • apps/admin/src/components/deployments/deployments-list.tsx (1 hunks)
  • apps/admin/src/components/domains/domains-list.tsx (1 hunks)
  • apps/admin/src/components/layout/sidebar.tsx (1 hunks)
  • apps/admin/src/components/subscriptions/products-list.tsx (1 hunks)
  • apps/admin/src/server/api/root.ts (1 hunks)
  • apps/admin/src/server/api/routers/domains.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/admin/src/components/layout/sidebar.tsx
  • apps/admin/src/app/subscriptions/page.tsx
  • apps/admin/src/server/api/root.ts
  • apps/admin/src/components/subscriptions/products-list.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/admin/src/server/api/routers/domains.ts
  • apps/admin/src/app/products/prices/[id]/page.tsx
  • apps/admin/src/app/domains/page.tsx
  • apps/admin/src/app/products/page.tsx
  • apps/admin/src/components/domains/domains-list.tsx
  • apps/admin/src/components/deployments/deployments-list.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/admin/src/server/api/routers/domains.ts
  • apps/admin/src/app/products/prices/[id]/page.tsx
  • apps/admin/src/app/domains/page.tsx
  • apps/admin/src/app/products/page.tsx
  • apps/admin/src/components/domains/domains-list.tsx
  • apps/admin/src/components/deployments/deployments-list.tsx
🧬 Code graph analysis (4)
apps/admin/src/server/api/routers/domains.ts (5)
packages/db/src/schema/domain/custom/domain.ts (1)
  • customDomains (6-12)
packages/db/src/schema/domain/custom/project-custom-domain.ts (1)
  • projectCustomDomains (15-23)
packages/db/src/schema/project/project.ts (1)
  • projects (13-32)
packages/db/src/schema/user/user-project.ts (1)
  • userProjects (10-23)
packages/db/src/schema/user/user.ts (1)
  • users (12-25)
apps/admin/src/app/products/prices/[id]/page.tsx (2)
apps/admin/src/components/layout/breadcrumb.tsx (1)
  • Breadcrumb (15-40)
apps/admin/src/components/subscriptions/price-detail.tsx (1)
  • PriceDetail (23-181)
apps/admin/src/app/domains/page.tsx (1)
apps/admin/src/components/domains/domains-list.tsx (1)
  • DomainsList (24-287)
apps/admin/src/app/products/page.tsx (1)
apps/admin/src/components/subscriptions/products-list.tsx (1)
  • ProductsList (11-119)
🔇 Additional comments (13)
apps/admin/src/app/products/page.tsx (1)

1-1: LGTM!

Clean import using the path alias.

apps/admin/src/components/deployments/deployments-list.tsx (7)

1-31: LGTM!

The component setup follows Next.js 13+ client component patterns correctly, with appropriate state management for pagination, sorting, and search functionality.


33-39: LGTM!

The tRPC query setup correctly passes pagination, sorting, and search parameters.


41-49: LGTM!

The sorting logic correctly toggles order for the same column and resets to page 1, preventing out-of-bounds page numbers after sort changes.


51-79: LGTM!

The helper functions provide sensible mappings with fallback defaults for badge variants and type labels.


81-90: LGTM!

The error handling provides clear visual feedback with an appropriate destructive style.


320-346: LGTM!

The pagination controls correctly use the API's hasPrev/hasNext flags and only display when there are multiple pages.


25-351: Well-structured component with comprehensive state handling.

The component effectively manages loading, error, empty, and data states with appropriate UI feedback. The search, sort, and pagination features are correctly implemented with proper state synchronization.

apps/admin/src/app/domains/page.tsx (1)

3-19: LGTM! Clean page structure.

The page component properly delegates data fetching and rendering to the DomainsList component while maintaining a clean layout structure.

apps/admin/src/server/api/routers/domains.ts (3)

123-125: Owner name fallback logic is well-structured.

The cascading fallback pattern (displayName → first+last → email → empty) provides robust handling of missing data and ensures a displayable name is always available.


8-138: Overall implementation looks solid.

The query structure properly handles pagination, sorting, and search functionality. The join logic correctly fetches owner information, and the response mapping is clear and type-safe.


23-30: No SQL injection risk—the sql template literal parameterizes the search variable Drizzle-ORM automatically binds ${…} values, so the current implementation is safe.

apps/admin/src/components/domains/domains-list.tsx (1)

24-287: Well-structured component with good UX.

The component provides a comprehensive admin interface with proper:

  • Loading and error states
  • Search and sorting functionality
  • Pagination controls
  • Navigation to related entities
  • Empty state handling

@vercel vercel bot temporarily deployed to Preview – docs October 12, 2025 05:11 Inactive
const [open, setOpen] = useState(false);
const utils = api.useUtils();

const removeMutation = api.users.removeSubscription.useMutation({
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using a non-blocking notification (e.g. toast) instead of alert() for success and error messages for better UX.

periodEnd.setMonth(periodEnd.getMonth() + 1);

// Use existing Stripe customer ID or generate a stub one
const stripeCustomerId = user[0].stripeCustomerId || `stub_stripe_customer_id_${Date.now()}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

Using a stub Stripe customer ID when absent is pragmatic; however, ensure this fallback is acceptable for production and consider wrapping the subscription and rate limit inserts in a transaction to avoid partial updates.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/admin/src/components/users/user-detail.tsx (2)

94-105: Harden external link opener.

Opening Stripe in a new tab without 'noopener,noreferrer' leaves the admin window vulnerable to reverse tabnabbing. Pass the appropriate features string to window.open.

-    onClick={() => window.open(`https://dashboard.stripe.com/customers/${user.stripeCustomerId}`, '_blank')}
+    onClick={() =>
+        window.open(
+            `https://dashboard.stripe.com/customers/${user.stripeCustomerId}`,
+            '_blank',
+            'noopener,noreferrer',
+        )
+    }

266-311: Guard rate-limit progress when max is zero.

We still divide by rateLimit.max without checking for zero/negative values, so the progress bar can receive Infinity/NaN widths and disappear. Clamp the calculation around a non-zero max.

-        const usagePercent = ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100;
+        const usagePercent =
+            rateLimit.max > 0
+                ? Math.min(
+                      100,
+                      Math.max(0, ((rateLimit.max - rateLimit.left) / rateLimit.max) * 100),
+                  )
+                : 0;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d82a0c and 05c9405.

📒 Files selected for processing (4)
  • apps/admin/src/components/users/add-subscription.tsx (1 hunks)
  • apps/admin/src/components/users/remove-subscription.tsx (1 hunks)
  • apps/admin/src/components/users/user-detail.tsx (1 hunks)
  • apps/admin/src/server/api/routers/users.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Do not use the any type unless necessary

Files:

  • apps/admin/src/components/users/add-subscription.tsx
  • apps/admin/src/components/users/user-detail.tsx
  • apps/admin/src/server/api/routers/users.ts
  • apps/admin/src/components/users/remove-subscription.tsx
{apps,packages}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Avoid using the any type unless absolutely necessary

Files:

  • apps/admin/src/components/users/add-subscription.tsx
  • apps/admin/src/components/users/user-detail.tsx
  • apps/admin/src/server/api/routers/users.ts
  • apps/admin/src/components/users/remove-subscription.tsx
🧬 Code graph analysis (3)
apps/admin/src/components/users/add-subscription.tsx (1)
packages/db/src/schema/subscription/product.ts (1)
  • products (6-13)
apps/admin/src/components/users/user-detail.tsx (3)
apps/admin/src/components/users/add-subscription.tsx (1)
  • AddSubscription (29-174)
apps/admin/src/components/users/remove-subscription.tsx (1)
  • RemoveSubscription (24-97)
apps/admin/src/components/users/edit-rate-limit.tsx (1)
  • EditRateLimit (26-122)
apps/admin/src/server/api/routers/users.ts (7)
packages/db/src/schema/user/user.ts (1)
  • users (12-25)
packages/db/src/schema/user/user-project.ts (1)
  • userProjects (10-23)
packages/db/src/schema/project/project.ts (1)
  • projects (13-32)
packages/db/src/schema/subscription/subscription.ts (1)
  • subscriptions (11-38)
packages/db/src/schema/subscription/product.ts (1)
  • products (6-13)
packages/db/src/schema/subscription/price.ts (1)
  • prices (8-22)
packages/db/src/schema/subscription/rate-limits.ts (1)
  • rateLimits (6-43)

Comment on lines 303 to 356
// Fetch price to get monthly message limit
const price = await ctx.db
.select()
.from(prices)
.where(eq(prices.id, input.priceId))
.limit(1);

if (price.length === 0 || !price[0]) {
throw new Error('Price not found');
}

// Create subscription with admin/mock Stripe data
const now = new Date();
const periodEnd = new Date(now);
periodEnd.setMonth(periodEnd.getMonth() + 1);

// Use existing Stripe customer ID or generate a stub one
const stripeCustomerId = user[0].stripeCustomerId || `stub_stripe_customer_id_${Date.now()}`;
const stripeSubscriptionItemId = `si_admin_${Date.now()}`;

const newSubscription = await ctx.db
.insert(subscriptions)
.values({
userId: input.userId,
productId: input.productId,
priceId: input.priceId,
stripeCustomerId,
stripeSubscriptionId: `sub_admin_${Date.now()}`,
stripeSubscriptionItemId,
status: SubscriptionStatus.ACTIVE,
stripeCurrentPeriodStart: now,
stripeCurrentPeriodEnd: periodEnd,
})
.returning();

if (!newSubscription[0]) {
throw new Error('Failed to create subscription');
}

// Create rate limit for the subscription
const carryOverKey = crypto.randomUUID();
await ctx.db
.insert(rateLimits)
.values({
userId: input.userId,
subscriptionId: newSubscription[0].id,
startedAt: now,
endedAt: periodEnd,
max: price[0].monthlyMessageLimit,
left: price[0].monthlyMessageLimit,
carryOverKey,
carryOverTotal: 0,
stripeSubscriptionItemId,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate price/product relationship before inserting.

Right now we only check that the priceId exists. Because we never assert that the chosen price belongs to the supplied productId, callers can create subscriptions whose productId and priceId point to unrelated rows. That breaks billing assumptions and conflicts with how we model prices per product. Add an explicit guard (and ideally read the product to verify existence) before persisting the subscription.

-    if (price.length === 0 || !price[0]) {
-        throw new Error('Price not found');
-    }
+    if (price.length === 0 || !price[0]) {
+        throw new Error('Price not found');
+    }
+
+    if (price[0].productId !== input.productId) {
+        throw new Error('Selected price does not belong to the specified product');
+    }
📝 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
// Fetch price to get monthly message limit
const price = await ctx.db
.select()
.from(prices)
.where(eq(prices.id, input.priceId))
.limit(1);
if (price.length === 0 || !price[0]) {
throw new Error('Price not found');
}
// Create subscription with admin/mock Stripe data
const now = new Date();
const periodEnd = new Date(now);
periodEnd.setMonth(periodEnd.getMonth() + 1);
// Use existing Stripe customer ID or generate a stub one
const stripeCustomerId = user[0].stripeCustomerId || `stub_stripe_customer_id_${Date.now()}`;
const stripeSubscriptionItemId = `si_admin_${Date.now()}`;
const newSubscription = await ctx.db
.insert(subscriptions)
.values({
userId: input.userId,
productId: input.productId,
priceId: input.priceId,
stripeCustomerId,
stripeSubscriptionId: `sub_admin_${Date.now()}`,
stripeSubscriptionItemId,
status: SubscriptionStatus.ACTIVE,
stripeCurrentPeriodStart: now,
stripeCurrentPeriodEnd: periodEnd,
})
.returning();
if (!newSubscription[0]) {
throw new Error('Failed to create subscription');
}
// Create rate limit for the subscription
const carryOverKey = crypto.randomUUID();
await ctx.db
.insert(rateLimits)
.values({
userId: input.userId,
subscriptionId: newSubscription[0].id,
startedAt: now,
endedAt: periodEnd,
max: price[0].monthlyMessageLimit,
left: price[0].monthlyMessageLimit,
carryOverKey,
carryOverTotal: 0,
stripeSubscriptionItemId,
});
// Fetch price to get monthly message limit
const price = await ctx.db
.select()
.from(prices)
.where(eq(prices.id, input.priceId))
.limit(1);
if (price.length === 0 || !price[0]) {
throw new Error('Price not found');
}
// Ensure the price belongs to the specified product
if (price[0].productId !== input.productId) {
throw new Error('Selected price does not belong to the specified product');
}
// Create subscription with admin/mock Stripe data
const now = new Date();
const periodEnd = new Date(now);
periodEnd.setMonth(periodEnd.getMonth() + 1);
🤖 Prompt for AI Agents
In apps/admin/src/server/api/routers/users.ts around lines 303 to 356, the code
fetches a price by priceId but does not verify that the price belongs to the
provided productId (and does not check product existence), allowing
subscriptions to be created with mismatched product/price; fix by querying the
prices table for a row where id = input.priceId AND productId = input.productId
(or fetch the product separately and assert it exists), throw a clear error if
no matching row is returned, and only proceed to insert the subscription and
rate limit when the price/product relationship is validated.

Comment on lines 323 to 356
const newSubscription = await ctx.db
.insert(subscriptions)
.values({
userId: input.userId,
productId: input.productId,
priceId: input.priceId,
stripeCustomerId,
stripeSubscriptionId: `sub_admin_${Date.now()}`,
stripeSubscriptionItemId,
status: SubscriptionStatus.ACTIVE,
stripeCurrentPeriodStart: now,
stripeCurrentPeriodEnd: periodEnd,
})
.returning();

if (!newSubscription[0]) {
throw new Error('Failed to create subscription');
}

// Create rate limit for the subscription
const carryOverKey = crypto.randomUUID();
await ctx.db
.insert(rateLimits)
.values({
userId: input.userId,
subscriptionId: newSubscription[0].id,
startedAt: now,
endedAt: periodEnd,
max: price[0].monthlyMessageLimit,
left: price[0].monthlyMessageLimit,
carryOverKey,
carryOverTotal: 0,
stripeSubscriptionItemId,
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wrap subscription + rate-limit writes in a transaction.

The subscription insert and the follow-up rate-limit insert need to succeed or fail together. If the second insert throws (constraint violations, transient DB failure, etc.), we are left with a subscription row without its corresponding rate limit. Use a DB transaction so both operations commit atomically.

-    const newSubscription = await ctx.db
-        .insert(subscriptions)
-        .values({ ... })
-        .returning();
-    ...
-    await ctx.db
-        .insert(rateLimits)
-        .values({ ... });
+    const newSubscription = await ctx.db.transaction(async (tx) => {
+        const [createdSubscription] = await tx
+            .insert(subscriptions)
+            .values({ ... })
+            .returning();
+
+        if (!createdSubscription) {
+            throw new Error('Failed to create subscription');
+        }
+
+        await tx.insert(rateLimits).values({ ...createdSubscription dependent values... });
+
+        return createdSubscription;
+    });

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/admin/src/server/api/routers/users.ts around lines 323 to 356, the
subscription insert and the subsequent rate-limit insert must be performed
inside a single DB transaction so they commit or roll back together; update the
code to open a transaction with ctx.db (e.g., ctx.db.transaction or the
project's transaction helper), perform the subscription insert and await its
result, then perform the rate-limit insert using the returned subscription id,
and finally commit the transaction; ensure any thrown error causes the
transaction to roll back and propagate, and adjust return/usage to use the
inserted subscription/rate-limit from the transaction result (avoid leaving
partial data on failure).

- Created private repository at onlook-dev/admin
- Removed admin code from main repository
- Added admin as git submodule at apps/admin
- Supports open-core model with private admin dashboard

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Explain admin module is optional for self-hosting
- Provide contact information for admin access
- Clarify main app works without admin module

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Kitenite and others added 2 commits October 12, 2025 16:46
- Create comprehensive admin dashboard guide
- Document features: user, subscription, rate limit management
- Add deployment details and contact information
- Link from self-hosting index page

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@Kitenite Kitenite merged commit fb0e150 into main Oct 12, 2025
3 of 7 checks passed
@Kitenite Kitenite deleted the feat/admin-dashboard branch October 12, 2025 06:09
@coderabbitai coderabbitai bot mentioned this pull request Oct 14, 2025
5 tasks
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