Skip to content

Conversation

@slyguy5646
Copy link

@slyguy5646 slyguy5646 commented Dec 21, 2025

This example shows a utilization of Flowglad in a pay-as-you-go scenario. The use case in this example is a chat bot that uses one credit per message generated. Users can buy 100 of these credits at a time for $100 ($1 each).

Users start on the free/default plan with the ability to buy these 100 credit topups.

Some assumptions I made:

  1. There isn't a pricing page since users aren't buying before they use the product. Instead, they're buying while they use it so purchasing is made clear to the user when they need to buy credits within the main chat page.
  2. I originally had a progress bar for the credit usage but removed it since I don't think it makes sense for this specific use case since there isn't a quota like in a capped usage subscription. Instead there's just a simple count of credits remaining.

Here's what the main chat looks like:

Screenshot 2025-12-20 at 11 55 39 PM

Summary by CodeRabbit

  • New Features

    • Full auth (sign-up/sign-in), account navbar, billing flows (usage meters, top-ups, subscription cancellation), usage-event and health endpoints, and pay-as-you-go pricing plan.
  • UI

    • Reusable UI primitives, carousel/tooltip/dropdown components, global theme, dashboard/chat skeleton, client-side chat with generate/top-up flows and loading states.
  • Documentation

    • README, MIT license, and environment example added.
  • Chores

    • Project tooling: ESLint, Prettier, TypeScript, package and build configs, ignore rules, and formatting defaults.

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

@CLAassistant
Copy link

CLAassistant commented Dec 21, 2025

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link

coderabbitai bot commented Dec 21, 2025

Walkthrough

Adds a complete "pay-as-you-go" Next.js example: project tooling, Drizzle/Postgres schema and client, BetterAuth server/client, Flowglad per-customer wiring, API routes (auth, session, flowglad proxy, usage-events, health), middleware, providers, UI primitives, pages, and utilities.

Changes

Cohort / File(s) Summary
Project config & tooling
pay-as-you-go/.eslintrc.js, pay-as-you-go/eslint.config.mjs, pay-as-you-go/.prettierrc, pay-as-you-go/bunfig.toml, pay-as-you-go/tsconfig.json, pay-as-you-go/package.json, pay-as-you-go/.gitignore, pay-as-you-go/next.config.js, pay-as-you-go/postcss.config.mjs
Added linting, formatting, TypeScript, Bun/package manifest, gitignore, Next.js and PostCSS/Tailwind configurations.
Docs & license
pay-as-you-go/README.md, pay-as-you-go/LICENSE
New README with setup and MIT license.
Env example
pay-as-you-go/.env.example
New environment variables template (DATABASE_URL, BETTER_AUTH_SECRET, NEXT_PUBLIC_BASE_URL, PORT, FLOWGLAD_SECRET_KEY).
Drizzle / migrations & meta
pay-as-you-go/drizzle.config.ts, pay-as-you-go/drizzle/0000_clammy_ulik.sql, pay-as-you-go/drizzle/meta/*
Drizzle config, initial SQL migration creating auth tables, and migration metadata snapshot/journal.
DB client & schema
pay-as-you-go/src/server/db/client.ts, pay-as-you-go/src/server/db/schema.ts
PG pool with connection reuse and a typed Drizzle schema for users, sessions, accounts, verifications.
Auth (server & client) + routes
pay-as-you-go/src/lib/auth.ts, pay-as-you-go/src/lib/auth-client.ts, pay-as-you-go/src/app/api/auth/[...all]/route.ts, pay-as-you-go/src/app/api/auth/session/route.ts
BetterAuth server config (Drizzle adapter), client auth helper, and Next.js route handlers for auth and session.
Flowglad & billing
pay-as-you-go/pricing.yaml, pay-as-you-go/src/lib/flowglad.ts, pay-as-you-go/src/lib/billing-helpers.ts, pay-as-you-go/src/app/api/flowglad/[...path]/route.ts, pay-as-you-go/src/app/api/usage-events/route.ts
Pricing model, Flowglad server factory that authenticates via session, price lookup helper, Flowglad proxy route, and POST /api/usage-events with validation and error handling.
Middleware & providers
pay-as-you-go/src/middleware.ts, pay-as-you-go/src/components/providers.tsx
Middleware enforcing auth (with matcher exclusions) and React Query + Flowglad provider wrappers tied to session state.
Pages & layout
pay-as-you-go/src/app/layout.tsx, pay-as-you-go/src/app/page.tsx, pay-as-you-go/src/app/home-client.tsx, pay-as-you-go/src/app/sign-in/page.tsx, pay-as-you-go/src/app/sign-up/page.tsx, pay-as-you-go/src/app/globals.css
Root layout with providers, HomeClient chat/usage UI, sign-in/sign-up pages, and global theme CSS.
Components & UI primitives
pay-as-you-go/src/components/*, pay-as-you-go/src/components/ui/*
Added app components (Navbar, DashboardSkeleton) and many UI primitives (Badge, Button, Card, Carousel, DropdownMenu, Input, Progress, ScrollArea, Skeleton, Switch, Tooltip, etc.).
Utilities & hooks
pay-as-you-go/src/lib/utils.ts, pay-as-you-go/src/lib/auth-client.ts, pay-as-you-go/src/hooks/use-mobile.ts, pay-as-you-go/src/lib/billing-helpers.ts
Added cn class merger, client auth helper, useMobile hook, and billing helper utility.
Health check
pay-as-you-go/src/app/api/health/route.ts
GET /api/health endpoint returning { status: 'ok' }.
Next ambient types & UI config
pay-as-you-go/next-env.d.ts, pay-as-you-go/components.json
Next.js ambient declarations and ShadCN UI config.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant U as User (Browser)
    participant App as Next.js UI
    participant API as /api/usage-events
    participant Auth as BetterAuth
    participant Flow as Flowglad
    participant DB as PostgreSQL (Drizzle)

    Note over U,App: User triggers usage (Generate / Top-up)
    U->>App: POST usage request (usageMeterSlug, amount, transactionId)
    App->>API: POST /api/usage-events
    API->>Auth: auth.api.getSession(headers)
    Auth->>DB: query session -> user
    DB-->>Auth: user record
    Auth-->>API: session with userId
    API->>Flow: getBilling(customerExternalId = userId)
    Flow->>DB: fetch pricing & subscriptions
    DB-->>Flow: billing data
    Flow-->>API: billing + subscriptions
    API->>API: findUsagePriceByMeterSlug -> priceSlug
    API->>Flow: createUsageEvent({ subscriptionId, priceSlug, amount, transactionId })
    Flow->>DB: persist usage event
    DB-->>Flow: confirm
    Flow-->>API: usageEvent
    API-->>App: 200 { usageEvent }
    App->>U: update UI / refresh billing
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • joeysabs

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.08% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Pay As You Go Example' directly and clearly describes the main objective of the PR: adding a complete pay-as-you-go example project. It is concise, specific, and accurately reflects the primary change.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

🧹 Nitpick comments (20)
pay-as-you-go/tsconfig.json (1)

3-3: Consider a more modern ECMAScript target.

The ES2017 target is conservative for a Next.js 15.5 project in 2025. Modern Next.js projects typically target ES2020 or later, which reduces unnecessary transpilation and produces more efficient code.

🔎 Suggested change
-    "target": "ES2017",
+    "target": "ES2020",
pay-as-you-go/next.config.js (1)

3-5: Consider removing ESLint bypass during builds.

Setting ignoreDuringBuilds: true allows ESLint errors to slip into production builds. Unless there's a specific reason to bypass linting, it's better to fix linting issues and keep build-time validation enabled.

🔎 Suggested change
-  eslint: {
-    ignoreDuringBuilds: true
-  },
pay-as-you-go/.eslintrc.js (2)

37-42: Consider allowing limited type assertions for third-party library interop.

The assertionStyle: 'never' configuration (line 40) completely bans type assertions (as and <>). While type narrowing is preferred, this can be overly restrictive when working with third-party libraries or complex type scenarios where assertions are the only practical solution.

🔎 Suggested relaxation
    '@typescript-eslint/consistent-type-assertions': [
      'error',
      {
-        assertionStyle: 'never'
+        assertionStyle: 'as',
+        objectLiteralTypeAssertions: 'never'
      }
    ],

This allows as assertions where truly needed while still preventing risky object literal assertions.


71-72: Explicit return types on all functions may impact developer velocity.

Requiring explicit return types (line 71) on every function, including simple callbacks and arrow functions, can significantly slow down development. TypeScript's inference is generally reliable for most cases.

🔎 Suggested alternative

Consider using explicit-module-boundary-types only (which you already have on line 72) and removing or downgrading explicit-function-return-type to 'warn', or limit it to exported functions:

-    '@typescript-eslint/explicit-function-return-type': 'error',
+    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/explicit-module-boundary-types': 'error',

This maintains type safety at module boundaries while allowing inference for internal functions.

pay-as-you-go/src/app/page.tsx (1)

3-5: Remove unnecessary async keyword.

The Home function is declared async but doesn't perform any asynchronous operations. While Next.js 15 App Router supports async Server Components for data fetching, the async keyword here is misleading since no await is used.

🔎 Proposed fix
-export default async function Home() {
+export default function Home() {
  return <HomeClient />;
}
pay-as-you-go/.env.example (1)

25-27: Replace empty quoted string with a placeholder value.

The NEXT_PUBLIC_CREDIT_TOPUP_PRICE_ID is set to an empty quoted string "", which could cause confusion. Environment variable examples typically use descriptive placeholder values to guide users.

🔎 Suggested improvement
 # the price id of the Message Topup product, 
 # can be found in the Flowglad dashboard
-NEXT_PUBLIC_CREDIT_TOPUP_PRICE_ID=""
+NEXT_PUBLIC_CREDIT_TOPUP_PRICE_ID=price_abc123_your_topup_price_id
+

This change:

  • Provides a clearer placeholder value indicating the expected format
  • Adds a blank line at the end per dotenv best practices
pay-as-you-go/drizzle/meta/0000_snapshot.json (1)

110-110: Consider enabling Row-Level Security (RLS) for production.

All tables have isRLSEnabled: false. For a production application with user data, consider enabling PostgreSQL Row-Level Security to enforce data access policies at the database level, providing defense-in-depth against unauthorized access.

This is particularly important for:

  • users table: Users should only access their own data
  • sessions table: Sessions should be restricted to their owner
  • accounts table: Account data should be user-scoped
  • verifications table: Verification tokens should be protected

Also applies to: 193-193, 259-259, 310-310

pay-as-you-go/src/app/sign-up/page.tsx (2)

36-41: Add required attribute to the name input for consistency.

The email and password inputs have required attributes, but the name input does not. This inconsistency could allow form submission with an empty name, which may cause server-side validation errors or unexpected behavior.

🔎 Proposed fix
         <input
           value={name}
           onChange={(e) => setName(e.target.value)}
           placeholder="Name"
           className="w-full rounded border px-3 py-2"
+          required
         />

17-30: Consider wrapping the async call in try/finally for robustness.

The current pattern relies on authClient.signUp.email callbacks to handle all outcomes. If an unexpected error occurs that bypasses the onError callback, the loading state would still be cleared but no error would be displayed.

🔎 Proposed fix
   async function onSubmit(e: React.FormEvent) {
     e.preventDefault();
     setLoading(true);
     setError(null);
-    await authClient.signUp.email(
-      { name, email, password, callbackURL: '/' },
-      {
-        onError: (ctx) => setError(ctx.error.message),
-        onSuccess: () => router.push('/'),
-        onRequest: () => {},
-      }
-    );
-    setLoading(false);
+    try {
+      await authClient.signUp.email(
+        { name, email, password, callbackURL: '/' },
+        {
+          onError: (ctx) => setError(ctx.error.message),
+          onSuccess: () => router.push('/'),
+          onRequest: () => {},
+        }
+      );
+    } catch (err) {
+      setError(err instanceof Error ? err.message : 'An unexpected error occurred');
+    } finally {
+      setLoading(false);
+    }
   }
pay-as-you-go/src/components/navbar.tsx (2)

39-48: Consider using invalidateQueries instead of clear.

queryClient.clear() removes all cached queries, which may include data from unrelated parts of the application. If the intent is only to invalidate session-related queries, use invalidateQueries with a specific query key filter instead.

🔎 Alternative approach
 async function handleSignOut() {
-  await queryClient.clear();
+  await queryClient.invalidateQueries(); // or use specific query keys
   await authClient.signOut({
     fetchOptions: {
       onSuccess: () => {
         router.push('/sign-in');
       },
     },
   });
 }

111-120: Hardcoded locale may not match user preferences.

The date formatting uses 'en-US' locale. Consider using undefined to respect the user's browser locale, or leverage a localization strategy if i18n is planned.

🔎 Proposed change
 const cancellationDate = currentSubscription?.cancelScheduledAt
   ? new Date(currentSubscription.cancelScheduledAt).toLocaleDateString(
-      'en-US',
+      undefined, // Use browser's default locale
       {
         year: 'numeric',
         month: 'long',
         day: 'numeric',
       }
     )
   : null;
pay-as-you-go/src/app/sign-in/page.tsx (1)

35-50: Add autocomplete attributes for better UX and password manager support.

Adding autocomplete attributes helps password managers and improves accessibility.

🔎 Proposed enhancement
 <input
   type="email"
   value={email}
   onChange={(e) => setEmail(e.target.value)}
   placeholder="Email"
   className="w-full rounded border px-3 py-2"
   required
+  autoComplete="email"
 />
 <input
   type="password"
   value={password}
   onChange={(e) => setPassword(e.target.value)}
   placeholder="Password"
   className="w-full rounded border px-3 py-2"
   required
+  autoComplete="current-password"
 />
pay-as-you-go/drizzle/0000_clammy_ulik.sql (1)

1-50: Consider adding indexes on foreign key columns for query performance.

The accounts.user_id and sessions.user_id columns have foreign key constraints but no indexes. PostgreSQL doesn't automatically create indexes on foreign key columns, which can impact performance on JOIN queries and CASCADE deletes.

🔎 Suggested index additions
 ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
-ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
+ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
+CREATE INDEX "accounts_user_id_idx" ON "accounts" ("user_id");--> statement-breakpoint
+CREATE INDEX "sessions_user_id_idx" ON "sessions" ("user_id");
pay-as-you-go/drizzle.config.ts (1)

11-13: Consider failing explicitly when DATABASE_URL is missing.

The empty string fallback process.env.DATABASE_URL ?? '' will cause cryptic connection errors during migrations. An explicit check would provide clearer feedback.

🔎 Suggested improvement
+const databaseUrl = process.env.DATABASE_URL;
+if (!databaseUrl) {
+  throw new Error('DATABASE_URL environment variable is required');
+}
+
 export default defineConfig({
   out: './drizzle',
   schema: './src/server/db/schema.ts',
   dialect: 'postgresql',
   dbCredentials: {
-    url: process.env.DATABASE_URL ?? '',
+    url: databaseUrl,
   },
 });
pay-as-you-go/src/lib/billing-helpers.ts (3)

24-24: Use strict equality operators.

Using loose equality (==) is not idiomatic in TypeScript. Prefer strict equality (===) for predictable comparisons.

-    if (!pricingModel?.usageMeters || !purchases || purchases.length == 0)
+    if (!pricingModel?.usageMeters || !purchases || purchases.length === 0)

32-35: Use strict equality and extract magic number to a constant.

Two concerns here:

  1. Use !== instead of != for consistent strict equality.
  2. The hardcoded 100 (credits per purchase) should be a named constant for clarity and maintainability.
🔎 Proposed fix
+const CREDITS_PER_TOPUP = 100;
+
 export function computeMessageUsageTotal(
   ...
-      if (purchase.priceId != topUpPriceId || purchase.status != 'paid')
+      if (purchase.priceId !== topUpPriceId || purchase.status !== 'paid')
         continue;
 
-      total += purchase.quantity * 100;
+      total += purchase.quantity * CREDITS_PER_TOPUP;

39-41: Silent error swallowing may hide bugs.

Catching all errors and returning 0 silently can mask genuine issues. Consider logging the error in development or re-throwing unexpected errors to aid debugging.

pay-as-you-go/src/components/ui/carousel.tsx (1)

78-89: Keyboard navigation doesn't adapt to vertical orientation.

For vertical carousels, users would expect ArrowUp/ArrowDown keys rather than ArrowLeft/ArrowRight. Consider adapting keyboard handling based on orientation.

🔎 Proposed fix
   const handleKeyDown = React.useCallback(
     (event: React.KeyboardEvent<HTMLDivElement>) => {
-      if (event.key === 'ArrowLeft') {
+      const prevKey = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp';
+      const nextKey = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown';
+      if (event.key === prevKey) {
         event.preventDefault();
         scrollPrev();
-      } else if (event.key === 'ArrowRight') {
+      } else if (event.key === nextKey) {
         event.preventDefault();
         scrollNext();
       }
     },
-    [scrollPrev, scrollNext]
+    [orientation, scrollPrev, scrollNext]
   );
pay-as-you-go/src/server/db/schema.ts (1)

17-28: Consider adding indexes for frequently queried columns.

The sessions and accounts tables reference userId as a foreign key, and sessions.token is likely used for lookups. Adding indexes on these columns can improve query performance as the data grows.

// Example using Drizzle indexes (can be added to table definitions or separately)
import { index } from 'drizzle-orm/pg-core';

// On sessions table:
// index('sessions_user_id_idx').on(sessions.userId)
// index('sessions_token_idx').on(sessions.token)

// On accounts table:
// index('accounts_user_id_idx').on(accounts.userId)

Also applies to: 30-50

pay-as-you-go/src/app/api/usage-events/route.ts (1)

127-137: Consider sanitizing error messages in production.

Returning error.message directly may expose internal implementation details. For a production system, consider logging the full error server-side and returning a generic message to clients.

🔎 Proposed fix
   } catch (error) {
+    console.error('Usage event creation failed:', error);
     return NextResponse.json(
       {
-        error:
-          error instanceof Error
-            ? error.message
-            : 'Failed to create usage event',
+        error: 'Failed to create usage event',
       },
       { status: 500 }
     );
   }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c21aaa and 0335c35.

⛔ Files ignored due to path filters (14)
  • pay-as-you-go/bun.lock is excluded by !**/*.lock
  • pay-as-you-go/public/file.svg is excluded by !**/*.svg
  • pay-as-you-go/public/globe.svg is excluded by !**/*.svg
  • pay-as-you-go/public/images/flowglad.png is excluded by !**/*.png
  • pay-as-you-go/public/images/mock-video-3.gif is excluded by !**/*.gif
  • pay-as-you-go/public/images/unsplash-1.jpg is excluded by !**/*.jpg
  • pay-as-you-go/public/images/unsplash-2.jpg is excluded by !**/*.jpg
  • pay-as-you-go/public/images/unsplash-3.jpg is excluded by !**/*.jpg
  • pay-as-you-go/public/images/unsplash-4.jpg is excluded by !**/*.jpg
  • pay-as-you-go/public/images/unsplash-5.jpg is excluded by !**/*.jpg
  • pay-as-you-go/public/next.svg is excluded by !**/*.svg
  • pay-as-you-go/public/vercel.svg is excluded by !**/*.svg
  • pay-as-you-go/public/window.svg is excluded by !**/*.svg
  • pay-as-you-go/src/app/icon.ico is excluded by !**/*.ico
📒 Files selected for processing (53)
  • pay-as-you-go/.env.example (1 hunks)
  • pay-as-you-go/.eslintrc.js (1 hunks)
  • pay-as-you-go/.gitignore (1 hunks)
  • pay-as-you-go/.prettierrc (1 hunks)
  • pay-as-you-go/LICENSE (1 hunks)
  • pay-as-you-go/README.md (1 hunks)
  • pay-as-you-go/bunfig.toml (1 hunks)
  • pay-as-you-go/components.json (1 hunks)
  • pay-as-you-go/drizzle.config.ts (1 hunks)
  • pay-as-you-go/drizzle/0000_clammy_ulik.sql (1 hunks)
  • pay-as-you-go/drizzle/meta/0000_snapshot.json (1 hunks)
  • pay-as-you-go/drizzle/meta/_journal.json (1 hunks)
  • pay-as-you-go/eslint.config.mjs (1 hunks)
  • pay-as-you-go/next-env.d.ts (1 hunks)
  • pay-as-you-go/next.config.js (1 hunks)
  • pay-as-you-go/package.json (1 hunks)
  • pay-as-you-go/postcss.config.mjs (1 hunks)
  • pay-as-you-go/pricing.yaml (1 hunks)
  • pay-as-you-go/src/app/api/auth/[...all]/route.ts (1 hunks)
  • pay-as-you-go/src/app/api/auth/session/route.ts (1 hunks)
  • pay-as-you-go/src/app/api/flowglad/[...path]/route.ts (1 hunks)
  • pay-as-you-go/src/app/api/health/route.ts (1 hunks)
  • pay-as-you-go/src/app/api/usage-events/route.ts (1 hunks)
  • pay-as-you-go/src/app/globals.css (1 hunks)
  • pay-as-you-go/src/app/home-client.tsx (1 hunks)
  • pay-as-you-go/src/app/layout.tsx (1 hunks)
  • pay-as-you-go/src/app/page.tsx (1 hunks)
  • pay-as-you-go/src/app/sign-in/page.tsx (1 hunks)
  • pay-as-you-go/src/app/sign-up/page.tsx (1 hunks)
  • pay-as-you-go/src/components/dashboard-skeleton.tsx (1 hunks)
  • pay-as-you-go/src/components/navbar.tsx (1 hunks)
  • pay-as-you-go/src/components/providers.tsx (1 hunks)
  • pay-as-you-go/src/components/ui/badge.tsx (1 hunks)
  • pay-as-you-go/src/components/ui/button.tsx (1 hunks)
  • pay-as-you-go/src/components/ui/card.tsx (1 hunks)
  • pay-as-you-go/src/components/ui/carousel.tsx (1 hunks)
  • pay-as-you-go/src/components/ui/dropdown-menu.tsx (1 hunks)
  • pay-as-you-go/src/components/ui/input.tsx (1 hunks)
  • pay-as-you-go/src/components/ui/progress.tsx (1 hunks)
  • pay-as-you-go/src/components/ui/scroll-area.tsx (1 hunks)
  • pay-as-you-go/src/components/ui/skeleton.tsx (1 hunks)
  • pay-as-you-go/src/components/ui/switch.tsx (1 hunks)
  • pay-as-you-go/src/components/ui/tooltip.tsx (1 hunks)
  • pay-as-you-go/src/hooks/use-mobile.ts (1 hunks)
  • pay-as-you-go/src/lib/auth-client.ts (1 hunks)
  • pay-as-you-go/src/lib/auth.ts (1 hunks)
  • pay-as-you-go/src/lib/billing-helpers.ts (1 hunks)
  • pay-as-you-go/src/lib/flowglad.ts (1 hunks)
  • pay-as-you-go/src/lib/utils.ts (1 hunks)
  • pay-as-you-go/src/middleware.ts (1 hunks)
  • pay-as-you-go/src/server/db/client.ts (1 hunks)
  • pay-as-you-go/src/server/db/schema.ts (1 hunks)
  • pay-as-you-go/tsconfig.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (25)
pay-as-you-go/src/app/api/auth/session/route.ts (2)
pay-as-you-go/src/app/api/health/route.ts (1)
  • GET (3-5)
pay-as-you-go/src/lib/auth.ts (1)
  • auth (12-27)
pay-as-you-go/src/components/ui/skeleton.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
pay-as-you-go/src/app/sign-up/page.tsx (1)
pay-as-you-go/src/lib/auth-client.ts (1)
  • authClient (5-8)
pay-as-you-go/src/components/ui/input.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
pay-as-you-go/src/app/api/auth/[...all]/route.ts (1)
pay-as-you-go/src/lib/auth.ts (1)
  • auth (12-27)
pay-as-you-go/src/app/home-client.tsx (4)
pay-as-you-go/src/lib/auth-client.ts (1)
  • authClient (5-8)
pay-as-you-go/src/components/dashboard-skeleton.tsx (1)
  • DashboardSkeleton (9-67)
pay-as-you-go/src/lib/billing-helpers.ts (1)
  • computeMessageUsageTotal (18-42)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
pay-as-you-go/src/components/dashboard-skeleton.tsx (2)
pay-as-you-go/src/components/ui/card.tsx (4)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardContent (91-91)
pay-as-you-go/src/components/ui/skeleton.tsx (1)
  • Skeleton (13-13)
pay-as-you-go/src/app/api/usage-events/route.ts (3)
pay-as-you-go/src/lib/auth.ts (1)
  • auth (12-27)
pay-as-you-go/src/lib/flowglad.ts (1)
  • flowglad (5-23)
pay-as-you-go/src/lib/billing-helpers.ts (1)
  • findUsagePriceByMeterSlug (51-74)
pay-as-you-go/src/components/ui/button.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
pay-as-you-go/src/app/api/flowglad/[...path]/route.ts (1)
pay-as-you-go/src/lib/auth.ts (1)
  • auth (12-27)
pay-as-you-go/src/lib/flowglad.ts (1)
pay-as-you-go/src/lib/auth.ts (1)
  • auth (12-27)
pay-as-you-go/src/components/ui/progress.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
pay-as-you-go/src/app/sign-in/page.tsx (2)
pay-as-you-go/src/lib/auth-client.ts (1)
  • authClient (5-8)
pay-as-you-go/src/components/ui/button.tsx (1)
  • Button (61-61)
pay-as-you-go/src/app/layout.tsx (2)
pay-as-you-go/src/components/providers.tsx (2)
  • ReactQueryProvider (28-36)
  • FlowgladProviderWrapper (38-51)
pay-as-you-go/src/components/navbar.tsx (1)
  • Navbar (23-173)
pay-as-you-go/src/app/page.tsx (1)
pay-as-you-go/src/app/home-client.tsx (1)
  • HomeClient (26-359)
pay-as-you-go/src/middleware.ts (1)
pay-as-you-go/src/lib/auth.ts (1)
  • auth (12-27)
pay-as-you-go/src/lib/auth.ts (2)
pay-as-you-go/src/server/db/client.ts (1)
  • db (24-24)
pay-as-you-go/src/server/db/schema.ts (1)
  • betterAuthSchema (62-67)
pay-as-you-go/src/components/providers.tsx (1)
pay-as-you-go/src/lib/auth-client.ts (1)
  • authClient (5-8)
pay-as-you-go/src/components/ui/badge.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
pay-as-you-go/src/components/ui/tooltip.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
pay-as-you-go/src/app/api/health/route.ts (1)
pay-as-you-go/src/app/api/auth/session/route.ts (1)
  • GET (4-15)
pay-as-you-go/src/components/ui/card.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
pay-as-you-go/src/components/ui/switch.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
pay-as-you-go/src/components/ui/carousel.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
pay-as-you-go/src/components/ui/dropdown-menu.tsx (1)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
🪛 dotenv-linter (4.0.0)
pay-as-you-go/.env.example

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

(EndingBlankLine)


[warning] 27-27: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🪛 markdownlint-cli2 (0.18.1)
pay-as-you-go/README.md

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

(MD040, fenced-code-language)

🔇 Additional comments (46)
pay-as-you-go/LICENSE (1)

1-22: MIT License is appropriate for this example project.

The license is complete, correctly formatted, and properly attributes copyright to Flowglad Inc. This is a standard choice for open-source examples.

pay-as-you-go/bunfig.toml (1)

1-2: LGTM!

The Bun configuration appropriately disables automatic installation, ensuring explicit control over dependency management.

pay-as-you-go/.prettierrc (1)

1-8: LGTM!

The Prettier configuration follows standard conventions and provides consistent formatting rules for the project.

pay-as-you-go/next-env.d.ts (1)

1-6: LGTM!

This is a standard Next.js type declaration file that should not be manually edited. The reference to typed routes aligns with Next.js 15.5 capabilities.

pay-as-you-go/postcss.config.mjs (1)

1-7: LGTM!

The PostCSS configuration correctly integrates the modern Tailwind CSS plugin.

pay-as-you-go/tsconfig.json (1)

11-14: LGTM!

Excellent strict type-checking flags that enhance type safety, including exactOptionalPropertyTypes, noUncheckedIndexedAccess, and noImplicitOverride.

pay-as-you-go/next.config.js (1)

6-13: LGTM!

The image optimization configuration appropriately restricts remote sources to media.giphy.com, providing security through explicit allowlisting.

pay-as-you-go/.gitignore (1)

1-98: LGTM! Comprehensive ignore patterns.

The .gitignore covers all necessary patterns including dependencies, build outputs, environment files, IDE artifacts, OS files, logs, and caches. The explicit enforcement of Bun as the package manager (lines 95-98) is clearly documented.

pay-as-you-go/src/app/globals.css (1)

1-122: LGTM! Modern theming setup with oklch color space.

The CSS configuration properly integrates Tailwind with custom CSS variables for theming. The use of oklch color space (lines 48-113) provides better perceptual uniformity across light and dark modes. The custom dark mode variant (line 4) and theme inline block (lines 6-44) are well-structured.

pay-as-you-go/.eslintrc.js (1)

1-144: Overall: Strict but maintainable ESLint configuration.

The configuration enforces high code quality standards with comprehensive TypeScript rules, import ordering, and consistency requirements. While some rules are quite strict (assertions ban, explicit return types), they reflect a deliberate choice for maximum type safety. The test file overrides (lines 134-143) appropriately relax rules where needed.

pay-as-you-go/src/app/api/health/route.ts (1)

1-5: LGTM! Clean health check endpoint.

The health check route is correctly implemented following Next.js 15 App Router conventions. The synchronous GET handler appropriately returns a simple status payload.

pay-as-you-go/drizzle/meta/_journal.json (1)

1-13: LGTM! Standard Drizzle migration journal.

This is an auto-generated Drizzle Kit journal file recording the initial migration (tag "0000_clammy_ulik"). The structure is correct for Drizzle version 7 with PostgreSQL dialect.

pay-as-you-go/src/app/api/auth/session/route.ts (1)

4-15: Session endpoint implementation is functional.

The route correctly uses BetterAuth's session API and follows Next.js App Router conventions. With the addition of error logging (see previous comment), this implementation will be production-ready.

pay-as-you-go/src/lib/utils.ts (1)

4-6: LGTM!

The cn utility correctly combines clsx for conditional class composition with twMerge for Tailwind CSS class deduplication. This is the standard pattern used by shadcn/ui components.

pay-as-you-go/pricing.yaml (2)

1-11: Verify the plan isDefault flag.

The plan-level isDefault is set to false (line 1), while the Free Plan product is marked as default: true (line 17). In a pay-as-you-go model, users typically start on the default plan automatically. Confirm whether isDefault: false at the plan level is intentional or if it should be true to auto-enroll new users.


51-71: Verify pricing calculation matches intended rate.

The Message Topup product has a unitPrice: 10000 (=$100.00 in cents) and grants 100 message credits via the 100_messages feature. This results in $1.00 per credit, which matches the PR description. However, ensure this pricing aligns with your business model and that the NEXT_PUBLIC_CREDIT_TOPUP_PRICE_ID environment variable references this price's actual ID from your Flowglad dashboard.

pay-as-you-go/src/components/ui/skeleton.tsx (1)

3-11: LGTM!

The Skeleton component follows best practices with proper prop spreading, className merging via cn, and uses the data-slot attribute for potential styling hooks. The implementation is clean and reusable.

pay-as-you-go/drizzle/meta/0000_snapshot.json (1)

71-76: Verify password hashing in application code.

The accounts table includes a password column stored as text. Ensure that passwords are properly hashed (e.g., using bcrypt, argon2, or scrypt) before being stored in this column. Storing plaintext passwords would be a critical security vulnerability.

Since this is a schema snapshot and the actual hashing should occur in application code, verify that BetterAuth or your authentication logic properly hashes passwords before database insertion.

pay-as-you-go/src/app/api/auth/[...all]/route.ts (1)

1-4: LGTM!

The authentication route properly uses BetterAuth's Next.js adapter to expose GET and POST handlers. This follows the recommended pattern for integrating BetterAuth with Next.js App Router.

pay-as-you-go/components.json (1)

1-22: LGTM!

The shadcn/ui configuration is properly set up with:

  • React Server Components enabled (rsc: true)
  • TypeScript support (tsx: true)
  • CSS variables for theming (cssVariables: true)
  • Clear path aliases matching project structure
  • Lucide icons integration

This configuration supports the UI component library introduced in this PR.

pay-as-you-go/src/lib/auth-client.ts (1)

5-8: LGTM!

The auth client configuration correctly uses the NEXT_PUBLIC_BASE_URL environment variable with an empty string fallback. The empty string default enables relative URLs, allowing the application to work across different environments (localhost, staging, production) without hardcoding the base URL.

pay-as-you-go/src/server/db/client.ts (1)

1-24: LGTM!

The database client setup follows the standard pattern for Next.js applications with Drizzle ORM. The global singleton pattern correctly prevents connection pool exhaustion during hot module replacement in development, while production instances create fresh pools as expected. The fail-fast validation of DATABASE_URL is appropriate.

pay-as-you-go/src/components/ui/button.tsx (1)

1-61: LGTM!

This is a well-implemented Button component following the shadcn/ui pattern. The CVA configuration provides comprehensive variant and size options, and the asChild prop correctly enables composition via Radix Slot. The TypeScript typing is accurate.

pay-as-you-go/src/components/ui/progress.tsx (1)

1-32: LGTM!

The Progress component correctly wraps the Radix UI primitive with appropriate styling and transform-based animation. The fallback value || 0 properly handles undefined/null cases.

pay-as-you-go/src/app/layout.tsx (1)

26-41: LGTM!

The root layout correctly composes the provider hierarchy with ReactQueryProvider wrapping FlowgladProviderWrapper, which aligns with dependency requirements. The font configuration and metadata export follow Next.js 15 conventions.

Minor note: The async keyword on line 26 is currently unused but harmless.

pay-as-you-go/src/components/dashboard-skeleton.tsx (1)

9-66: LGTM!

The skeleton structure effectively mirrors the dashboard layout with appropriate placeholders for usage meters, credit top-ups, content area, and input section. The comments documenting each section are helpful for maintainability.

pay-as-you-go/src/components/ui/input.tsx (1)

1-21: LGTM!

The Input component follows the shadcn/ui pattern with comprehensive styling for various states (focus, disabled, aria-invalid, file inputs, dark mode). The implementation is clean and properly typed.

pay-as-you-go/src/components/navbar.tsx (1)

122-171: Good implementation of the navigation dropdown with billing integration.

The component correctly handles multiple states (loading, error, cancellation pending) and provides appropriate user feedback with tooltips and disabled states.

pay-as-you-go/src/app/sign-in/page.tsx (1)

16-29: Form submission logic is correct.

The handler properly prevents default, manages loading state, and handles both success and error paths. The setLoading(false) at line 28 executes regardless of outcome, which is correct since the auth callbacks run synchronously within the await.

pay-as-you-go/src/app/api/flowglad/[...path]/route.ts (1)

6-17: Route handler correctly integrates Flowglad with BetterAuth session.

The implementation properly retrieves the authenticated user ID from the session and passes it to Flowglad. The error handling at line 14 will result in an appropriate failure response for unauthenticated requests.

Note: The req parameter in getCustomerExternalId appears unused. If it's not required by the type signature, consider removing it or prefixing with underscore (_req) to indicate intentional non-use.

pay-as-you-go/src/components/providers.tsx (2)

7-26: Query client setup follows recommended TanStack Query patterns.

The SSR-aware singleton pattern and 30-second staleTime default are appropriate for a Next.js application to avoid refetching immediately on client hydration.


38-51: Good reactive session integration with Flowglad.

Using authClient.useSession() ensures loadBilling updates reactively when the session state changes, which handles cases where the session becomes available after initial render.

pay-as-you-go/src/lib/auth.ts (1)

7-27: LGTM!

The BetterAuth configuration is well-structured:

  • Proper environment variable validation at module load
  • Clean Drizzle adapter integration with explicit schema mapping
  • The @ts-expect-error comment appropriately documents the type incompatibility with exactOptionalPropertyTypes
pay-as-you-go/src/components/ui/switch.tsx (1)

8-34: LGTM!

Well-implemented Switch component following shadcn/ui conventions with proper Radix UI integration, ref forwarding, and accessibility support via the underlying primitive.

pay-as-you-go/src/lib/billing-helpers.ts (1)

51-73: LGTM!

The findUsagePriceByMeterSlug function is well-structured with proper null checks and clean logic for mapping meter slugs to prices.

pay-as-you-go/src/components/ui/scroll-area.tsx (1)

1-58: LGTM!

Clean ScrollArea implementation following shadcn/ui patterns with proper Radix primitive wrapping, orientation support, and consistent styling conventions.

pay-as-you-go/src/components/ui/carousel.tsx (2)

91-106: LGTM!

Proper effect cleanup for event listeners prevents memory leaks. The setApi effect and event subscription patterns are correctly implemented.


175-233: LGTM!

Navigation buttons have proper accessibility with sr-only text, disabled states, and clean positioning logic for both orientations.

pay-as-you-go/src/components/ui/badge.tsx (1)

1-46: LGTM!

Well-structured Badge component following shadcn/ui conventions with proper variant support, asChild pattern, and accessibility styling.

pay-as-you-go/src/server/db/schema.ts (1)

3-15: LGTM!

The users table schema is well-defined with proper constraints, unique email, and consistent timestamp handling with timezone support.

pay-as-you-go/src/components/ui/tooltip.tsx (1)

1-61: LGTM!

Well-structured Radix UI tooltip wrapper following the shadcn/ui pattern. The components correctly forward props, use data-slot attributes for styling hooks, and implement appropriate animation classes for open/close states.

pay-as-you-go/src/app/api/usage-events/route.ts (1)

42-121: LGTM!

The billing flow is well-structured with proper null checks at each step. Good use of early returns with appropriate HTTP status codes, and the comments explaining the single subscription assumption are helpful.

pay-as-you-go/src/components/ui/card.tsx (1)

1-92: LGTM!

Clean implementation of the Card component library following shadcn/ui conventions. Components properly forward props, use data-slot attributes for styling hooks, and compose classNames correctly via the cn utility.

pay-as-you-go/src/app/home-client.tsx (2)

288-312: LGTM on message rendering.

Using array indices as keys is acceptable here since messages are append-only and not reordered. The auto-scroll anchor pattern is clean.


107-110: Empty string input would bypass validation.

messageInput is string | null, so an empty string "" is truthy and passes the !messageInput check, potentially allowing submission of empty messages.

🔎 Proposed fix
   const handleGenerateMessage = async () => {
-    if (!messageInput || messageGenerationsRemaining === 0) {
+    if (!messageInput?.trim() || messageGenerationsRemaining === 0) {
       return;
     }

Likely an incorrect or invalid review comment.

pay-as-you-go/src/components/ui/dropdown-menu.tsx (1)

1-257: LGTM!

Comprehensive Radix UI dropdown menu wrapper implementation following shadcn/ui conventions. Components properly handle variants (default/destructive), inset options, and include all necessary sub-menu support with appropriate icons.

Comment on lines +40 to +47
CREATE TABLE "verifications" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now(),
"updated_at" timestamp with time zone DEFAULT now()
);
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

Inconsistent nullability for created_at/updated_at in verifications table.

The verifications table allows NULL for created_at and updated_at (lines 45-46), while all other tables enforce NOT NULL on these timestamp columns. This inconsistency may cause issues if code assumes these fields are always present.

🔎 Suggested fix
 CREATE TABLE "verifications" (
 	"id" text PRIMARY KEY NOT NULL,
 	"identifier" text NOT NULL,
 	"value" text NOT NULL,
 	"expires_at" timestamp with time zone NOT NULL,
-	"created_at" timestamp with time zone DEFAULT now(),
-	"updated_at" timestamp with time zone DEFAULT now()
+	"created_at" timestamp with time zone DEFAULT now() NOT NULL,
+	"updated_at" timestamp with time zone DEFAULT now() NOT 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
CREATE TABLE "verifications" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now(),
"updated_at" timestamp with time zone DEFAULT now()
);
CREATE TABLE "verifications" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
🤖 Prompt for AI Agents
In pay-as-you-go/drizzle/0000_clammy_ulik.sql around lines 40 to 47, the
created_at and updated_at columns are missing NOT NULL unlike other tables;
update their definitions to DEFAULT now() NOT NULL so both timestamps are
non-nullable and consistent with the rest of the schema, ensuring any migrations
or table-creation SQL are updated accordingly.

Comment on lines +1 to +20
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import prettier from "eslint-config-prettier";

const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
prettier,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);

export default eslintConfig;
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

Critical: Conflicting ESLint configurations - strict rules not being applied.

The project has both eslint.config.mjs (flat config) and .eslintrc.js (legacy config). ESLint v9+ prioritizes flat config and will ignore the .eslintrc.js file entirely. This means all the strict TypeScript rules, import ordering, and code quality checks defined in .eslintrc.js are currently not being enforced.

The flat config here only includes Next.js presets and Prettier, lacking the comprehensive TypeScript strictness from .eslintrc.js.

🔎 Resolution options

Option 1: Remove flat config and use legacy config

# Delete this file to use .eslintrc.js
rm eslint.config.mjs

Option 2: Migrate all rules to flat config (recommended for future compatibility)

Convert the rules from .eslintrc.js into this flat config format. This requires significant work but aligns with ESLint's future direction.

Option 3: Use ESLINT_USE_FLAT_CONFIG=false

Set the environment variable to force legacy config, but this is a temporary workaround.

"engines": {
"node": ">=18.18.0"
},
"preinstall": "bunx --bun only-allow bun",
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

preinstall should be inside scripts object.

The preinstall key at the top level has no effect. Package lifecycle hooks must be defined inside the scripts object to execute.

🔎 Proposed fix
   "engines": {
     "node": ">=18.18.0"
   },
-  "preinstall": "bunx --bun only-allow bun",
   "scripts": {
+    "preinstall": "bunx --bun only-allow bun",
     "dev": "next dev",
🤖 Prompt for AI Agents
In pay-as-you-go/package.json around line 10, the "preinstall" key is at the top
level and therefore not executed; move the "preinstall": "bunx --bun only-allow
bun" entry into the "scripts" object (creating "scripts" if absent) so it
becomes a proper lifecycle hook, and remove the top-level "preinstall" key after
relocating it.

Comment on lines +22 to +68
"dependencies": {
"@flowglad/nextjs": "0.15.0",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-query": "^5.90.5",
"better-auth": "^1.3.32",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.44.7",
"embla-carousel-autoplay": "^8.6.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.548.0",
"next": "15.5.6",
"pg": "^8.16.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/pg": "^8.15.5",
"@types/react": "^19",
"@types/react-dom": "^19",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"dotenv": "^17.2.3",
"drizzle-kit": "^0.31.5",
"eslint": "^9",
"eslint-config-next": "15.5.6",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unused-imports": "^4.0.0",
"prettier": "^3.2.5",
"tailwindcss": "^4",
"tw-animate-css": "^1.4.0",
"typescript": "^5"
},
"overrides": {
"esbuild": "0.18.20"
}
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

The esbuild override to 0.18.20 introduces a known vulnerability and should be removed.

drizzle-kit 0.31.5 depends on esbuild ^0.19.7, making the override to 0.18.20 a downgrade. More critically, esbuild versions before 0.25.0 contain a known vulnerability (GHSA-67mh-4wv8-2f99), and drizzle-kit 0.31.4/0.31.5 depends on @esbuild-kit/esm-loader which has a nested dependency on vulnerable esbuild versions. Rather than protecting against compatibility issues, this override exposes the project to CVEs. Remove the override and rely on drizzle-kit's version resolution, or upgrade drizzle-kit to a future version that removes the deprecated @esbuild-kit/esm-loader dependency.

🤖 Prompt for AI Agents
In pay-as-you-go/package.json lines 22-68 the "overrides" forcing esbuild to
0.18.20 introduces a known vulnerability and downgrades esbuild below what
drizzle-kit expects; remove the "overrides" entry for esbuild (or alternatively
upgrade drizzle-kit to a version that no longer depends on
@esbuild-kit/esm-loader) and then reinstall dependencies (npm/yarn install) and
run an audit/lockfile update to verify esbuild resolves to a non-vulnerable
>=0.25.0 version.

Comment on lines +10 to +31
export function useMobile(breakpoint: number = 768): boolean {
const [isMobile, setIsMobile] = useState<boolean>(false);

useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < breakpoint);
};

// Check on mount
checkMobile();

// Add event listener
window.addEventListener('resize', checkMobile);

// Cleanup
return () => {
window.removeEventListener('resize', checkMobile);
};
}, [breakpoint]);

return isMobile;
}
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

SSR hydration mismatch risk.

The hook initializes isMobile to false and updates it in useEffect, which only runs on the client. This creates a hydration mismatch when the server renders false but the client needs true, potentially causing React warnings and layout shifts on initial load.

Additionally, the resize listener fires on every resize event without debouncing, which can impact performance during rapid window resizing.

🔎 Proposed improvements

1. Suppress hydration warning for this known client-only value:

 export function useMobile(breakpoint: number = 768): boolean {
-  const [isMobile, setIsMobile] = useState<boolean>(false);
+  const [isMobile, setIsMobile] = useState<boolean>(() => {
+    // Return false during SSR to match initial client render
+    if (typeof window === 'undefined') return false;
+    return window.innerWidth < breakpoint;
+  });

   useEffect(() => {
+    // Suppress hydration warning - this value is inherently client-side
     const checkMobile = () => {
       setIsMobile(window.innerWidth < breakpoint);
     };

-    // Check on mount
-    checkMobile();
+    // Initial check (will match useState initial value on client)
+    checkMobile();

     // Add event listener
     window.addEventListener('resize', checkMobile);

2. Add debouncing for better performance:

+'use client';
+
+import { useEffect, useState } from 'react';
+
+// Simple debounce helper
+function debounce<T extends (...args: any[]) => any>(
+  func: T,
+  wait: number
+): (...args: Parameters<T>) => void {
+  let timeout: NodeJS.Timeout;
+  return (...args: Parameters<T>) => {
+    clearTimeout(timeout);
+    timeout = setTimeout(() => func(...args), wait);
+  };
+}
+
 export function useMobile(breakpoint: number = 768): boolean {
   const [isMobile, setIsMobile] = useState<boolean>(() => {
     if (typeof window === 'undefined') return false;
     return window.innerWidth < breakpoint;
   });

   useEffect(() => {
-    const checkMobile = () => {
+    const checkMobile = debounce(() => {
       setIsMobile(window.innerWidth < breakpoint);
-    };
+    }, 150);

     checkMobile();
     window.addEventListener('resize', checkMobile);

Comment on lines +5 to +23
export const flowglad = (customerExternalId: string) => {
return new FlowgladServer({
customerExternalId,
getCustomerDetails: async (customerExternalId: string) => {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session?.user) {
throw new Error('User not authenticated');
}

return {
email: session.user.email || '',
name: session.user.name || '',
};
},
});
};
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

Refactor to use the customerExternalId parameter in the callback

The getCustomerDetails callback receives customerExternalId as a parameter but doesn't use it. The FlowgladServer API expects this parameter to be used to fetch customer details from your database. Replace the session-based lookup with a database query using the provided ID, following the documented integration pattern.

🤖 Prompt for AI Agents
In pay-as-you-go/src/lib/flowglad.ts around lines 5 to 23, the
getCustomerDetails callback ignores the passed customerExternalId and instead
fetches the session; change it to query your customer database using the
provided customerExternalId (e.g., call your repository/ORM function like
getCustomerByExternalId(customerExternalId)), await the result, throw a clear
error if not found, and return the customer's email and name from that DB
record; remove the session-based lookup and ensure proper error handling and
typing for the database call.

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

♻️ Duplicate comments (1)
pay-as-you-go/README.md (1)

106-106: Add language specifier to fenced code block.

The code fence should specify a language for proper syntax highlighting and markdown linting compliance.

🔎 Suggested fix
-```
+```text
 ├── src/
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0335c35 and 9bb1e29.

📒 Files selected for processing (1)
  • pay-as-you-go/README.md (1 hunks)
🧰 Additional context used
🪛 LanguageTool
pay-as-you-go/README.md

[grammar] ~72-~72: Ensure spelling is correct
Context: ...OPUP_PRICE_ID`** - The price id of your topup product - This can be found in the Fl...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
pay-as-you-go/README.md

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

(MD040, fenced-code-language)

🔇 Additional comments (1)
pay-as-you-go/README.md (1)

1-135: Overall documentation quality is strong; major issues from prior review have been resolved.

The pricing model description (line 4) and directory name reference (line 49) have been corrected to accurately reflect the pay-as-you-go billing example. The documentation is comprehensive, clearly structured, and provides step-by-step setup instructions. Environment variable descriptions, tech stack, and project structure are well-documented and align with the PR objectives.

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)
pay-as-you-go/src/lib/billing-helpers.ts (1)

27-27: Environment variable validation needed (previously flagged).

The non-null assertion on NEXT_PUBLIC_CREDIT_TOPUP_PRICE_ID was already flagged in a previous review. If this env var is undefined, the comparison on line 32 will fail silently, causing incorrect calculations.

🧹 Nitpick comments (4)
pay-as-you-go/.env.example (1)

25-27: Polish comment capitalization and add trailing blank line.

The comment for the Flowglad price ID should be capitalized to match the style of other comments in the file. Additionally, the file is missing a trailing blank line.

🔎 Proposed fix
  # the price id of the Message Topup product, 
  # can be found in the Flowglad dashboard
  NEXT_PUBLIC_CREDIT_TOPUP_PRICE_ID=price_your_price_id_here
+ 

Also apply:

- # the price id of the Message Topup product,
+ # The price ID of the Message Topup product,
pay-as-you-go/src/lib/billing-helpers.ts (3)

32-32: Prefer strict equality operators.

Use !== instead of != for type-safe comparisons. Loose equality (!=) can lead to unexpected type coercion.

🔎 Proposed fix
-      if (purchase.priceId != topUpPriceId || purchase.status != 'paid')
+      if (purchase.priceId !== topUpPriceId || purchase.status !== 'paid')

39-40: Consider logging errors before swallowing them.

The empty catch block silently suppresses all errors, making debugging difficult. Consider logging the error (at minimum) before returning the fallback value.

🔎 Proposed enhancement
-  } catch {
+  } catch (error) {
+    console.error('Error computing message usage total:', error);
     return 0;
   }

58-60: Optional: Remove redundant type annotations.

The explicit type annotations meter: UsageMeter, product: Product, and price: Price are unnecessary since TypeScript infers these types from pricingModel.usageMeters and pricingModel.products. Removing them reduces noise.

🔎 Proposed simplification
   const meterIdBySlug = new Map(
-    pricingModel.usageMeters.map((meter: UsageMeter) => [meter.slug, meter.id])
+    pricingModel.usageMeters.map((meter) => [meter.slug, meter.id])
   );
   
   // Find price by meter ID
   const usagePrice = pricingModel.products
-    .flatMap((product: Product) => product.prices ?? [])
+    .flatMap((product) => product.prices ?? [])
     .find(
-      (price: Price) =>
+      (price) =>
         price.type === 'usage' && price.usageMeterId === usageMeterId
     );

Also applies to: 66-71

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9bb1e29 and fd6abab.

📒 Files selected for processing (2)
  • pay-as-you-go/.env.example (1 hunks)
  • pay-as-you-go/src/lib/billing-helpers.ts (1 hunks)
🧰 Additional context used
🪛 dotenv-linter (4.0.0)
pay-as-you-go/.env.example

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

(EndingBlankLine)

🔇 Additional comments (1)
pay-as-you-go/src/lib/billing-helpers.ts (1)

51-74: Well-structured usage price lookup.

The function correctly maps usage meter slugs to IDs and searches for matching usage prices. The defensive null checks and optional chaining are appropriate.

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 (4)
pay-as-you-go/README.md (1)

103-120: Add language specifier to fenced code block.

The code block starting at line 103 should include a language identifier for proper syntax highlighting and markdown linting compliance.

🔎 Proposed fix
-```
+```text
 ├── src/
pay-as-you-go/src/components/dashboard-skeleton.tsx (1)

1-1: Remove the extra semicolon.

There's a duplicate semicolon after the 'use client' directive.

pay-as-you-go/src/app/home-client.tsx (2)

44-59: Potential infinite loop from billing object in dependency array.

The billing object from useBilling() may be a new reference on each render, causing this effect to run repeatedly. Consider extracting stable primitives or using a ref to track the previous state.


229-249: Missing TooltipContent for the purchase button tooltip.

The Tooltip wrapper around the purchase button has no TooltipContent, so hovering won't display any tooltip text. Either add content or remove the tooltip wrapper.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd6abab and a3f1358.

📒 Files selected for processing (5)
  • pay-as-you-go/.env.example (1 hunks)
  • pay-as-you-go/README.md (1 hunks)
  • pay-as-you-go/src/app/home-client.tsx (1 hunks)
  • pay-as-you-go/src/components/dashboard-skeleton.tsx (1 hunks)
  • pay-as-you-go/src/lib/billing-helpers.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pay-as-you-go/src/lib/billing-helpers.ts
🧰 Additional context used
🧬 Code graph analysis (1)
pay-as-you-go/src/components/dashboard-skeleton.tsx (2)
pay-as-you-go/src/components/ui/card.tsx (4)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardContent (91-91)
pay-as-you-go/src/components/ui/skeleton.tsx (1)
  • Skeleton (13-13)
🪛 dotenv-linter (4.0.0)
pay-as-you-go/.env.example

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

(EndingBlankLine)

🪛 markdownlint-cli2 (0.18.1)
pay-as-you-go/README.md

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

(MD040, fenced-code-language)

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

♻️ Duplicate comments (2)
pay-as-you-go/src/app/home-client.tsx (2)

41-56: [Duplicate] Potential infinite loop from billing object in dependency array.

This issue was previously flagged and remains unresolved. The billing object from useBilling() may be a new reference on each render, causing this effect to run repeatedly and trigger unnecessary API calls.

Based on learnings, the past review comment suggested destructuring stable primitives (loaded, reload) from the billing object and using those in the dependency array instead.


226-246: [Duplicate] Missing TooltipContent for the purchase button tooltip.

This issue was previously flagged and remains unresolved. The Tooltip wrapper around the purchase button (lines 226-246) has no TooltipContent child, so hovering won't display any tooltip text.

Based on learnings, the past review comment suggested either adding TooltipContent with appropriate text or removing the unused tooltip wrapper entirely.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3f1358 and 8a76759.

📒 Files selected for processing (1)
  • pay-as-you-go/src/app/home-client.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
pay-as-you-go/src/app/home-client.tsx (8)
pay-as-you-go/src/lib/auth-client.ts (1)
  • authClient (5-8)
pay-as-you-go/src/components/dashboard-skeleton.tsx (1)
  • DashboardSkeleton (9-66)
pay-as-you-go/src/components/ui/card.tsx (4)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardContent (91-91)
pay-as-you-go/src/components/ui/tooltip.tsx (3)
  • Tooltip (61-61)
  • TooltipTrigger (61-61)
  • TooltipContent (61-61)
pay-as-you-go/src/components/ui/button.tsx (1)
  • Button (61-61)
pay-as-you-go/src/components/ui/scroll-area.tsx (1)
  • ScrollArea (58-58)
pay-as-you-go/src/lib/utils.ts (1)
  • cn (4-6)
pay-as-you-go/src/components/ui/input.tsx (1)
  • Input (21-21)

Comment on lines 88 to 160
const handleGenerateMessage = async () => {
if (!messageInput || messageGenerationsRemaining === 0) {
return;
}

setIsGenerating(true);
setGenerateError(null);

const msgs = messages;

setMessages([
...msgs,
{
type: 'user',
content: messageInput || '',
},
]);

setMessageInput(null);

try {
// Generate a unique transaction ID for idempotency
const transactionId = `message_${Date.now()}_${Math.random().toString(36).substring(7)}`;

const response = await fetch('/api/usage-events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
usageMeterSlug: 'message_credits',
amount: 1,
transactionId,
}),
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to create usage event');
}

// Cycle through mock messages
const nextIndex = (currentMessageIndex + 1) % mockMessages.length;
setCurrentMessageIndex(nextIndex);
const nextMessage = mockMessages[nextIndex];
if (nextMessage) {
// add user message again and add response to the chat history (avoids double state update problem)
setMessages([
...msgs,
{
type: 'user',
content: messageInput,
},
{
type: 'assistant',
content: nextMessage,
},
]);
}

// Reload billing data to update usage balances
await billing.reload();
} catch (error) {
setMessages(msgs);
setGenerateError(
error instanceof Error
? error.message
: 'Failed to generate message. Please try again.'
);
} finally {
setIsGenerating(false);
}
};
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

Critical bug: user message content becomes null in final state update.

On Line 106, messageInput is set to null to clear the input field. However, Line 139 still references messageInput when constructing the final chat state, resulting in the user message having null content in the displayed chat history.

🔎 Proposed fix

Save the message content in a local variable before clearing the input:

 const handleGenerateMessage = async () => {
   if (!messageInput || messageGenerationsRemaining === 0) {
     return;
   }

   setIsGenerating(true);
   setGenerateError(null);

   const msgs = messages;
+  const userMessageContent = messageInput;

   setMessages([
     ...msgs,
     {
       type: 'user',
-      content: messageInput || '',
+      content: userMessageContent,
     },
   ]);

   setMessageInput(null);

   try {
     // Generate a unique transaction ID for idempotency
     const transactionId = `message_${Date.now()}_${Math.random().toString(36).substring(7)}`;

     const response = await fetch('/api/usage-events', {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
       },
       body: JSON.stringify({
         usageMeterSlug: 'message_credits',
         amount: 1,
         transactionId,
       }),
     });

     if (!response.ok) {
       const errorData = await response.json();
       throw new Error(errorData.error || 'Failed to create usage event');
     }

     // Cycle through mock messages
     const nextIndex = (currentMessageIndex + 1) % mockMessages.length;
     setCurrentMessageIndex(nextIndex);
     const nextMessage = mockMessages[nextIndex];
     if (nextMessage) {
       // add user message again and add response to the chat history (avoids double state update problem)
       setMessages([
         ...msgs,
         {
           type: 'user',
-          content: messageInput,
+          content: userMessageContent,
         },
         {
           type: 'assistant',
           content: nextMessage,
         },
       ]);
     }

     // Reload billing data to update usage balances
     await billing.reload();
   } catch (error) {
     setMessages(msgs);
     setGenerateError(
       error instanceof Error
         ? error.message
         : 'Failed to generate message. Please try again.'
     );
   } finally {
     setIsGenerating(false);
   }
 };
📝 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 handleGenerateMessage = async () => {
if (!messageInput || messageGenerationsRemaining === 0) {
return;
}
setIsGenerating(true);
setGenerateError(null);
const msgs = messages;
setMessages([
...msgs,
{
type: 'user',
content: messageInput || '',
},
]);
setMessageInput(null);
try {
// Generate a unique transaction ID for idempotency
const transactionId = `message_${Date.now()}_${Math.random().toString(36).substring(7)}`;
const response = await fetch('/api/usage-events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
usageMeterSlug: 'message_credits',
amount: 1,
transactionId,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to create usage event');
}
// Cycle through mock messages
const nextIndex = (currentMessageIndex + 1) % mockMessages.length;
setCurrentMessageIndex(nextIndex);
const nextMessage = mockMessages[nextIndex];
if (nextMessage) {
// add user message again and add response to the chat history (avoids double state update problem)
setMessages([
...msgs,
{
type: 'user',
content: messageInput,
},
{
type: 'assistant',
content: nextMessage,
},
]);
}
// Reload billing data to update usage balances
await billing.reload();
} catch (error) {
setMessages(msgs);
setGenerateError(
error instanceof Error
? error.message
: 'Failed to generate message. Please try again.'
);
} finally {
setIsGenerating(false);
}
};
const handleGenerateMessage = async () => {
if (!messageInput || messageGenerationsRemaining === 0) {
return;
}
setIsGenerating(true);
setGenerateError(null);
const msgs = messages;
const userMessageContent = messageInput;
setMessages([
...msgs,
{
type: 'user',
content: userMessageContent,
},
]);
setMessageInput(null);
try {
// Generate a unique transaction ID for idempotency
const transactionId = `message_${Date.now()}_${Math.random().toString(36).substring(7)}`;
const response = await fetch('/api/usage-events', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
usageMeterSlug: 'message_credits',
amount: 1,
transactionId,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to create usage event');
}
// Cycle through mock messages
const nextIndex = (currentMessageIndex + 1) % mockMessages.length;
setCurrentMessageIndex(nextIndex);
const nextMessage = mockMessages[nextIndex];
if (nextMessage) {
// add user message again and add response to the chat history (avoids double state update problem)
setMessages([
...msgs,
{
type: 'user',
content: userMessageContent,
},
{
type: 'assistant',
content: nextMessage,
},
]);
}
// Reload billing data to update usage balances
await billing.reload();
} catch (error) {
setMessages(msgs);
setGenerateError(
error instanceof Error
? error.message
: 'Failed to generate message. Please try again.'
);
} finally {
setIsGenerating(false);
}
};
🤖 Prompt for AI Agents
In pay-as-you-go/src/app/home-client.tsx around lines 88 to 160, the handler
clears messageInput (line ~106) before the final state update so the user
message becomes null; fix by capturing the current message text into a local
const (e.g., const userMessage = messageInput || '') before calling
setMessageInput(null), then use that local variable when building the new
messages arrays (both the interim and final setMessages calls) so the user
message content is preserved; ensure all places that reference messageInput in
this function use the saved local variable and keep error handling that restores
msgs unchanged.

Copy link
Contributor

@agreea agreea left a comment

Choose a reason for hiding this comment

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

please rework some of this to get it closer in terms of patterns and robustness as we have with generation-based-subscription

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

♻️ Duplicate comments (2)
pay-as-you-go/src/app/home-client.tsx (2)

39-55: Potential infinite loop from billing object in dependency array.

The billing object from useBilling() may be a new reference on each render, causing this effect to run repeatedly. Consider extracting stable primitives (billing.loaded, billing.reload) into separate variables before the effect and using those in the dependency array.


226-243: Missing TooltipContent for the purchase button tooltip.

The Tooltip wrapper around the purchase button has no TooltipContent, so hovering won't display any tooltip text. Either add content or remove the tooltip wrapper.

🧹 Nitpick comments (1)
pay-as-you-go/src/app/home-client.tsx (1)

137-144: Clarify misleading comment.

The comment states "add user message again" but the code only adds the assistant message. The user message was already added on lines 101-107 and remains in the state.

🔎 Proposed fix
-      // add user message again and add response to the chat history (avoids double state update problem)
+      // Add assistant response to the chat history
       setMessages((msgs) => [
         ...msgs,
         {
           type: 'assistant',
           content: nextMessage,
         },
       ]);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86c89e7 and 67c2adb.

📒 Files selected for processing (1)
  • pay-as-you-go/src/app/home-client.tsx

@slyguy5646
Copy link
Author

slyguy5646 commented Dec 23, 2025

@agreea I cleaned up the hacky double state thing and made some of the usage balance variables similar to how they are in the generation-based example.

I initially removed everything related to subscriptions (pricing page, checking if a user has the right plan, progress bar, etc.) from this since the free/default "plan" is a singular payment product in this example and not treated by Flowglad as a "subscription". I can work around this and add some of these things back but just wanted to clarify what you meant by patterns and robustness in this case since this example differs from the others in terms of the subscription/single product classification.

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.

3 participants