Skip to content

feat: Dev Plans API, Stripe integration, and Code dashboard UI - #1449

Merged
steebchen merged 18 commits into
mainfrom
terragon/add-dev-plans-subscription-ime9td
Jan 15, 2026
Merged

steebchen merged 18 commits into
mainfrom
terragon/add-dev-plans-subscription-ime9td

Conversation

@steebchen

@steebchen steebchen commented Jan 14, 2026 •

Copy link
Copy Markdown
Member

Summary

  • Adds a comprehensive Dev Plans feature set: backend API under /dev-plans, Stripe-based subscriptions, and a Code app UI scaffolding to manage Dev Plans with a live dashboard, usage, and tier changes. Includes authentication scaffolding and shared UI components to support the new flows.
  • This PR wires end-to-end Dev Plans flows from API surface to frontend scaffolding, with admin/security tweaks and migration notes.

Changes

Backend API

  • Added new Dev Plans API under /dev-plans with:
    • GET /personal-org: retrieve or create a personal organization for the user.
    • POST /subscribe: create a Stripe Checkout session for a dev plan tier (lite, pro, max).
    • POST /cancel: cancel active dev plan subscription (end of period).
    • POST /resume: resume cancelled dev plan subscription.
    • POST /change-tier: upgrade/downgrade dev plan tier with prorations.
    • GET /status: query current dev plan status and credits.
  • API routes wired into the main router under /dev-plans.
  • Pro/subscription routing adjustments to align with dev-plan flows (e.g., personal orgs restrictions).

Stripe & Payments

  • DEV_PLAN_PRICES: lite: 29, pro: 79, max: 179.
  • DEV_PLAN_CREDITS_MULTIPLIER: 3 (default) to calculate credits limit per tier.
  • Env vars added for Stripe price IDs:
    • STRIPE_DEV_PLAN_LITE_PRICE_ID
    • STRIPE_DEV_PLAN_PRO_PRICE_ID
    • STRIPE_DEV_PLAN_MAX_PRICE_ID
  • Extended Stripe checkout flow to attach dev_plan metadata and create corresponding transactions and invoices for dev plan starts and renewals.
  • Webhook handling extended to differentiate dev plan subscriptions from Pro subscriptions, updating local org state accordingly.

Database / Models

  • Organization model extended with devPlan fields:
    • devPlan, devPlanCreditsLimit, devPlanCreditsUsed, devPlanBillingCycleStart, devPlanStripeSubscriptionId, devPlanCancelled, devPlanExpiresAt
  • Transactions augmented with dev_plan_* types for dev plan lifecycle events.

API/UI Routing

  • Exposed new API path /dev-plans in apps/api/src/routes/index.ts.
  • Updated organization and subscriptions routes to respect dev plan flows (e.g., Pro plan restrictions for personal orgs).

Frontend (Code app) – Dev Plans UI scaffolding

  • New Code app dashboard pages under apps/code/src/app/dashboard/page.tsx:
    • Displays current dev plan status, credits usage, and a visual usage bar.
    • Actions: Cancel, Resume, and Change Tier with live feedback and loading states.
    • Plan grid to switch tiers (lite, pro, max) with upgrade/downgrade logic.
  • Code app authentication and sign-in flow scaffolding (login/signup pages):
    • Sign in with email or passkey, sign up flow with plan pre-selection, and sign-in/up navigation.
  • Shared UI components for Code app:
    • Button, Form (with fields, validation, and messages), Input, Label, and a small UI utility library.
  • Global styles and layout for Code app (globals.css, layout.tsx) and a minimal providers setup with PostHog integration (optional).
  • Lightweight client utilities for API access and config (config-server.ts, config.tsx) and a server API helper (server-api.ts).
  • Basic integration scaffolding for a Code-hosted UI while backend dev-plans are being wired end-to-end.

UI / UX

  • Dashboard shows current dev plan, billing cycle, and credits consumed vs. limit with a progress bar.
  • Plan cards support a “Most Popular” badge and a clear upgrade/downgrade action flow.
  • Subscriptions flow includes email and in-app feedback via toasts and PostHog events.

Admin / Security tweaks

  • Prevent deletion of personal orgs via API (personal orgs are managed via dev plans).
  • Pro plan subscriptions for personal orgs blocked at API level with clear messaging.

Code App UI Artifacts

  • Added Code app route pages and UI scaffolding to enable a working dev plan dashboard sooner while backend wiring progresses.
  • Includes a dedicated dashboard, login/signup, and shared UI components to support rapid iteration.

How to set up / migrate

  • Ensure environment variables are set for dev plans:
    • STRIPE_DEV_PLAN_LITE_PRICE_ID
    • STRIPE_DEV_PLAN_PRO_PRICE_ID
    • STRIPE_DEV_PLAN_MAX_PRICE_ID
    • DEV_PLAN_CREDITS_MULTIPLIER (defaults to 3)
  • CODE_URL should be configured for success/cancel URLs in Stripe checkout sessions (e.g., code.llmgateway.io mappings).
  • If upgrading from a previous deployment, review the new dev_plan_* fields on organization and related transaction records to populate existing data.

Testing plan

  • Backend
    • Create a personal org for a test user and invoke /dev-plans/personal-org to ensure creation flow works.
    • Start a dev plan via /dev-plans/subscribe for lite, pro, max and verify a Stripe Checkout session URL is returned.
    • Cancel /resume /change-tier /status endpoints behave as expected with correct validations and DB updates.
    • Verify that dev plan lifecycle events are recorded as transactions with correct types.
  • Frontend (Code app UI)
    • Open the Code dashboard and verify a no-active-plan state shows pricing cards with a CTA to sign up.
    • Subscribe to a dev plan via Checkout URL and ensure redirection works post-checkout.
    • Cancel plan, resume, and change tier flows update the UI and show appropriate toasts.
  • Admin / Observability
    • Confirm that PostHog events are emitted for key actions (dev_plan_started, dev_plan_renewed, dev_plan_reactivated, dev_plan_cancel, etc.).

Notes

  • This is a staged rollout; the codebase now includes a Dev Plans API and a Code app UI scaffold for developers. A follow-up PR can fully wire the frontend dashboard to the backend dev-plans service and expand analytics dashboards.
  • Documentation updates forthcoming to cover Dev Plans usage and billing.

🌿 Generated by Terry

📎 Task: https://www.terragonlabs.com/task/63361f9f-9494-46d5-b6f3-1b2a453ac902

Summary by CodeRabbit

  • New Features

    • Development Plans (Lite, Pro, Max): subscribe / cancel / resume / change-tier flows with dedicated credits and billing.
    • New Code app: public landing, signup, login, and dashboard pages for managing plans, API keys, and usage.
  • Behavior Changes

    • Personal organizations treated specially (hidden from normal org lists; team management and Pro subscriptions blocked).
    • Transaction history and credits reporting now include dev plan lifecycle events and credit usage.
  • Chores

    • Local dev config updates and shared integration icons added.

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

- Introduce Dev Plans API routes to manage subscriptions: subscribe, cancel, resume, change tier, and status.
- Implement Dev Plans logic in Stripe webhook handling with custom transaction types.
- Add personal organization concept for Dev Plan users, with restrictions on team and pro features.
- Extend environment variables for Dev Plan Stripe price IDs and credits multiplier.
- Update UI with landing page, dashboard, login, and signup to support Dev Plans.
- Integrate PostHog events for Dev Plan lifecycle tracking.
- Add new TypeScript types, OpenAPI schemas, and frontend utilities for Dev Plans.
- Prevent deletion and team management for personal orgs tied to Dev Plans.
- Maintain separate handling of Dev Plan subscriptions from Pro subscriptions.

This provides a subscription system tailored for individual developers with monthly credit plans.

Co-authored-by: terragon-labs[bot] <terragon-labs[bot]@users.noreply.github.com>
Copilot AI review requested due to automatic review settings January 14, 2026 20:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a comprehensive Dev Plans subscription system with a dedicated Code app for developers. The implementation includes backend API endpoints for managing personal organizations and dev plan subscriptions, Stripe integration for billing, database schema changes to support dev plan features, and a complete Next.js frontend with authentication, dashboard UI, and plan management capabilities.

Changes:

  • Added Dev Plans API with endpoints for personal org management, subscription, cancellation, resumption, tier changes, and status queries
  • Extended organization schema with dev plan fields (isPersonal, devPlan, credits tracking, billing cycle, and cancellation status)
  • Implemented Stripe checkout flow for dev plans with metadata tracking and webhook handling for subscription lifecycle events
  • Created Code app (Next.js) with authentication, landing page, dashboard with plan management UI, and shared UI components
  • Updated worker to prioritize dev plan credits over regular credits for cost deduction

Reviewed changes

Copilot reviewed 40 out of 50 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
apps/api/src/routes/dev-plans.ts New API endpoints for dev plan subscription management
packages/db/src/schema.ts Added dev plan fields to organization table
packages/db/src/types.ts Extended Organization type with devPlan field
apps/worker/src/worker.ts Updated credit deduction logic to use dev plan credits first
apps/gateway/src/chat/chat.ts Added dev plan credit checks before request processing
apps/code/* New Next.js app with auth, dashboard, and plan management UI
apps/api/src/routes/organization.ts Filtered personal orgs from regular UI, blocked deletion
apps/api/src/routes/subscriptions.ts Blocked Pro plan for personal orgs
apps/api/src/routes/team.ts Blocked team management for personal orgs
.env.example Added dev plan environment variables
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +85 to +88
} catch {
console.error(`Server API error for ${method} ${path}`);
return null;
}

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

The error is being caught but not logged. The error details should be logged to help with debugging. Change to catch (error) and log the error object: console.error(\Server API error for ${method} ${path}`, error);`

Copilot uses AI. Check for mistakes.
Comment thread apps/code/next.config.ts
return config;
},
typescript: {
ignoreBuildErrors: true,

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

Setting ignoreBuildErrors: true bypasses TypeScript type checking during builds, which can mask type errors and lead to runtime issues. This should be removed or set to false to enforce type safety.

Suggested change
ignoreBuildErrors: true,
ignoreBuildErrors: false,

Copilot uses AI. Check for mistakes.
Comment thread apps/api/src/routes/dev-plans.ts Outdated
],
allow_promotion_codes: true,
success_url: `${process.env.CODE_URL || "http://localhost:3007"}/dashboard?success=true`,
cancel_url: `${process.env.CODE_URL || "http://localhost:3007"}/dashboard/plans?canceled=true`,

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

The query parameter canceled uses one 'l' spelling, while devPlanCancelled in the schema uses two 'l's. Consider using consistent spelling throughout the codebase. Use either 'canceled' (US English) or 'cancelled' (UK English) consistently.

Suggested change
cancel_url: `${process.env.CODE_URL || "http://localhost:3007"}/dashboard/plans?canceled=true`,
cancel_url: `${process.env.CODE_URL || "http://localhost:3007"}/dashboard/plans?cancelled=true`,

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +36
if (data) {
posthog.identify(data.user.id, {
email: data.user.email,
name: data.user.name,
});
}

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

The PostHog identify call runs on every render when data changes, even if the user hasn't changed. This should be wrapped in a useEffect with proper dependencies to avoid unnecessary re-identification calls.

Copilot uses AI. Check for mistakes.
{},
{
enabled: !!user,
refetchInterval: 5000,

Copilot AI Jan 14, 2026

Copy link

Choose a reason for hiding this comment

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

Polling every 5 seconds (5000ms) may be excessive for dev plan status updates, which typically only change during user actions (subscribe, cancel, etc.). Consider using a longer interval (e.g., 30000ms) or removing automatic polling in favor of manual refetch after mutations.

Suggested change
refetchInterval: 5000,
refetchInterval: 30000,

Copilot uses AI. Check for mistakes.
@coderabbitai

coderabbitai Bot commented Jan 14, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@steebchen has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 22 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4373e5c and 7c60ee1.

⛔ Files ignored due to path filters (4)
  • apps/admin/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/code/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (6)
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/routes/organization.ts
  • apps/api/src/stripe.ts
  • packages/db/src/types.ts
  • packages/shared/src/dev-plans.ts
  • packages/shared/src/index.ts

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

Adds a Dev Plans subscription feature (Lite/Pro/Max) with Stripe Checkout/webhook integration, DB schema and migration updates, API routes for plan lifecycle, frontend "code" app with dashboard/auth, shared icon components, and gateway/worker changes to account for dev-plan credits and personal-org guards.

Changes

Cohort / File(s) Summary
Env & CORS
\.env.example, \.env.unified.example, apps/api/src/index.ts, apps/gateway/src/app.ts, package.json
New CODE_URL, dev-plan Stripe price IDs and DEV_PLAN_CREDITS_MULTIPLIER; added localhost ports (3004,3005) to CORS and origin allowlists; updated build filter to exclude code.
Database & Types
packages/db/src/schema.ts, packages/db/src/types.ts, packages/db/migrations/1768492522_chilly_makkari.sql, packages/db/migrations/meta/_journal.json
Add organization dev-plan columns (isPersonal, devPlan*, credits, billing, subscription id, cancelled, expires_at) and new transaction types for dev_plan lifecycle; migration and journal entry added.
API: Dev Plans
apps/api/src/routes/dev-plans.ts, apps/api/src/routes/index.ts
New /dev-plans route module: endpoints to create/ensure personal org, subscribe (Stripe Checkout), cancel, resume, change-tier, and status; registered in routes index.
API: Stripe & Billing
apps/api/src/stripe.ts
Dev-plan-aware Stripe handlers (checkout.session.completed, invoice.payment_succeeded, subscription.updated/deleted): activate dev plans, set credits limits, record transactions/invoices, manage billing cycle and PostHog events; parallel dev_plan vs pro branches added.
API: Org & Subscriptions Guards
apps/api/src/routes/organization.ts, apps/api/src/routes/subscriptions.ts, apps/api/src/routes/team.ts
Transaction enum extended with dev_plan_* types; block Pro subscription and team management for personal orgs (403); filter personal orgs from normal listings.
Gateway & Worker
apps/gateway/src/chat/chat.ts, apps/gateway/src/lib/rate-limit.spec.ts, apps/worker/src/worker.ts
Credit checks now combine regular + dev-plan credits; added dev-plan-specific exhaustion messages; worker batchProcessLogs deducts dev-plan credits first, then regular credits; tests' mocks updated.
Frontend app: project config
apps/code/* (.gitignore, components.json, eslint.config.mjs, next.config.ts, package.json, postcss.config.mjs, tsconfig.json, public/favicon/site.webmanifest)
New Next.js "code" app scaffold and configs: shadcn/ui, Tailwind/PostCSS, ESLint, Next config (standalone), package scripts/deps, TS config, and manifest.
Frontend app: pages & layout
apps/code/src/app/* (layout.tsx, page.tsx, login/page.tsx, signup/page.tsx, dashboard/page.tsx)
New root layout with metadata and Providers; landing, login (passkey support), signup, and dashboard UI for dev-plan status and actions (subscribe/cancel/resume/change-tier) plus API key management.
Frontend app: styling & components
apps/code/src/app/globals.css, apps/code/src/components/providers.tsx, apps/code/src/components/ui/* (button.tsx,form.tsx,input.tsx,label.tsx,sonner.tsx)
Global theme CSS and Tailwind variables; Providers wiring (QueryClient, PostHog, Theme); UI primitives and form helpers.
Frontend app: hooks & libs
apps/code/src/hooks/useUser.ts, apps/code/src/lib/* (auth-client.ts,config-server.ts,config.tsx,fetch-client.ts,server-api.ts,utils.ts)
Client/server config utilities, auth and fetch clients, typed OpenAPI React Query client, server API helper, useUser hook and update mutation, and cn utility.
Shared UI icons
packages/shared/src/components/integration-icons.tsx, packages/shared/src/components/index.tsx, apps/ui/src/components/integrations/integration-cards.tsx
New shared SVG icon components exported and integration-cards updated to import icons from @llmgateway/shared instead of local SVGs.
Devops / Docker / Supervisord
infra/*, .github/*, infra/split.dockerfile, infra/unified.dockerfile, infra/docker-compose*.yml, .github/start.sh, .github/test-*.sh, .github/workflows/images.yml, infra/supervisord.conf
Add code service wiring (ports, env CODE_URL, Docker build/runtime stages, supervisord process) and include code in CI/workflows and local compose files.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend as Code UI
    participant API as Dev Plans API
    participant Stripe as Stripe
    participant Webhook as API Stripe Webhook
    participant DB as Database

    User->>Frontend: Click "Subscribe" (choose tier)
    Frontend->>API: POST /dev-plans/subscribe (tier)
    API->>Stripe: Create Checkout Session (price id, metadata)
    Stripe-->>API: Session info
    API-->>Frontend: Checkout URL
    Frontend->>Stripe: Redirect user to Checkout
    User->>Stripe: Complete payment
    Stripe->>Webhook: checkout.session.completed
    Webhook->>DB: create/ensure personal org, set devPlan, credits limit, billing cycle, store subscription id
    Webhook->>DB: insert dev_plan_start transaction, generate invoice
    Webhook-->>Stripe: 200 OK
    User->>Frontend: View Dashboard
    Frontend->>API: GET /dev-plans/status
    API->>DB: fetch org dev-plan & credits
    API-->>Frontend: plan status, credits usage, API key
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately summarizes the main change: adding a Dev Plans feature with API, Stripe integration, and a Code dashboard UI.

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


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

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

⚠️ Outside diff range comments (4)
apps/api/src/routes/subscriptions.ts (2)

232-234: Arbitrary sleep is fragile.

Using setTimeout with a fixed 5-second delay to wait for webhook processing is unreliable. If webhooks are slow, the response may return before state is updated; if fast, it adds unnecessary latency. Consider polling for state change or returning immediately with a note that state will update asynchronously.


325-327: Same arbitrary sleep pattern.

Same concern as the cancel flow - the 5-second sleep for webhook processing is unreliable.

apps/api/src/routes/organization.ts (1)

128-137: Potential API schema drift: returning full organization rows while OpenAPI schema omits new fields.

getOrganizations returns uo.organization objects directly, but organizationSchema does not include fields like isPersonal / devPlan*. Clients will receive extra fields not reflected in the contract. Either:

  • explicitly select/pick only organizationSchema fields before c.json, or
  • extend organizationSchema to include the new fields you intend to expose.
apps/api/src/stripe.ts (1)

77-82: Avoid any type for organization.

The organization: any return type violates the coding guideline to avoid any. Use the proper organization type from the schema.

🐛 Proposed fix
+import type { InferSelectModel } from "drizzle-orm";
+
 async function resolveOrganizationFromStripeEvent(eventData: {
 	metadata?: { organizationId?: string };
 	customer?: string;
 	subscription?: string;
 	lines?: { data?: Array<{ metadata?: { organizationId?: string } }> };
-}): Promise<{ organizationId: string; organization: any } | null> {
+}): Promise<{ organizationId: string; organization: InferSelectModel<typeof tables.organization> } | null> {

Based on coding guidelines, any should not be used unless absolutely necessary.

🤖 Fix all issues with AI agents
In `@apps/api/src/routes/dev-plans.ts`:
- Around line 441-463: The resume handler contains a blocking 3-second sleep
after calling stripe.subscriptions.update which should be removed (same fix as
the cancel handler); delete the Promise/setTimeout block following
stripe.subscriptions.update in the resume route so the handler returns
immediately after the update (references: stripe.subscriptions.update and
personalOrg.devPlanStripeSubscriptionId), leaving the existing error handling
and response intact.
- Around line 16-27: DEV_PLAN_PRICES (and its associated type DevPlanTier and
usage in getDevPlanCreditsLimit) are duplicated from stripe.ts; extract the
pricing constants into a shared module (e.g., export const DEV_PLAN_PRICES and
export type DevPlanTier from a new shared file) and update both
apps/api/src/routes/dev-plans.ts and stripe.ts to import them instead of
redefining; ensure getDevPlanCreditsLimit uses the imported DEV_PLAN_PRICES and
DevPlanTier and preserve the existing multiplier logic.
- Around line 354-376: Remove the blocking sleep after calling
stripe.subscriptions.update: delete the new Promise with setTimeout and return
success immediately once
stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
cancel_at_period_end: true }) resolves; keep the existing try/catch and error
logging (logger.error and HTTPException) but do not wait for webhooks in the
request handler—relying on the dashboard polling /status is sufficient.
- Around line 548-560: The code assumes subscription.items.data[0] exists; add a
defensive null/length check before using it (e.g., verify subscription.items and
subscription.items.data are present and length > 0), extract the first item id
into a variable (like firstItemId) and if missing return/throw a clear error or
log and skip the update; update the call in the stripe.subscriptions.update
block to use that validated firstItemId and ensure metadata update still occurs
only after the check.

In `@apps/api/src/routes/organization.ts`:
- Around line 512-518: The deletion guard currently checks
organization?.isPersonal but does not enforce owner-only deletes; update the
delete flow to verify userOrganization.role === "owner" before performing the
soft-delete. In the route handling deletion (where userOrganization and
organization?.isPersonal are checked), add a check that throws new
HTTPException(403, { message: "Only organization owners can delete
organizations." }) when userOrganization.role !== "owner", placing it prior to
the actual soft-delete logic so non-owners cannot trigger deletion.

In `@apps/api/src/stripe.ts`:
- Around line 20-32: DEV_PLAN_PRICES, DevPlanTier, and getDevPlanCreditsLimit
are duplicated and should be centralized: extract these three symbols into a
shared module (e.g., apps/api/src/utils/dev-plan-config.ts or a package-level
module), export DEV_PLAN_PRICES, DevPlanTier, and getDevPlanCreditsLimit from
that module, and update apps/api/src/stripe.ts and
apps/api/src/routes/dev-plans.ts to import them instead of redefining; ensure
the exported getDevPlanCreditsLimit still reads DEV_PLAN_CREDITS_MULTIPLIER from
process.env exactly as before to preserve behavior.

In `@apps/code/.gitignore`:
- Around line 34-35: The .gitignore pattern `.env*` is too broad and will ignore
important example files like `.env.example` that should be committed. Replace
this wildcard with a more specific pattern that ignores only actual environment
variable files, such as `.env` and `.env.local`, while explicitly excluding
example or template files. Adjust the .gitignore so it ignores `.env` files
except those meant as examples or templates.

In `@apps/code/public/favicon/site.webmanifest`:
- Around line 1-3: The manifest's "name" and "short_name" fields are empty; set
them to meaningful, user-facing strings (e.g., full app name for "name" and a
shorter label for "short_name") so the PWA shows a proper title when installed
or added to home screen; update the "name" and "short_name" keys in
site.webmanifest (the manifest JSON) with appropriate localized values if needed
and ensure they are non-empty and trimmed.

In `@apps/code/src/app/login/page.tsx`:
- Around line 77-119: The onSubmit function currently shows duplicate error
toasts because signIn.email's onError callback already displays a toast and the
subsequent if (error) block also shows one; remove the redundant error-handling
block (the if (error) {...} after the signIn.email call) so errors are handled
only in the onError callback, or alternatively remove the onError callback and
rely solely on the returned error — update either signIn.email usage or the if
(error) block accordingly (modify symbols: onSubmit, signIn.email, onError, and
the if (error) block).

In `@apps/code/src/app/page.tsx`:
- Around line 84-86: Confirm whether a free trial is actually implemented; if
not, update the CTA text in apps/code/src/app/page.tsx where the Link and Button
render (e.g., the Link href="/signup" wrapping <Button size="lg">Start Free
Trial</Button> and the other occurrences around lines 202-204) to an accurate
label such as "Get Started" or "Subscribe Now" and ensure the copy is consistent
across all CTA instances; if a free trial does exist, keep the labels but add a
brief qualifier or link to the trial terms in the same components to avoid
misleading users.

In `@apps/code/src/app/signup/page.tsx`:
- Around line 85-93: The code is sending PII (email and name) to PostHog via
posthog.identify and posthog.capture; remove ctx.data.user.email,
ctx.data.user.name, values.email and values.name from those calls and only
identify the user by their unique ID (e.g., posthog.identify(ctx.data.user.id)
with no PII properties) and send non-PII event properties like selectedPlan; if
a user-readable name is required for product features, persist it server-side or
use a hashed/consented value instead, and ensure any intentional PII collection
is documented in the privacy policy.
- Line 12: Update the Zod import to use the project convention by replacing the
non-standard import specifier "zod/v3" with "zod" (i.e., import { z } from
"zod";) in the file that defines the signup page (where the symbol z is
imported); ensure any other imports in this file that reference "zod/v3" are
similarly updated to "zod" for consistency.

In `@apps/code/src/components/ui/form.tsx`:
- Around line 47-57: The null-check for fieldContext is after its usage in
useFormField, causing a crash when fieldContext is falsy; move the guard so the
check for fieldContext (the React.useContext(FormFieldContext) result) occurs
before any access to fieldContext.name or before calling
useFormState/getFieldState with fieldContext.name; ensure the early throw in
useFormField runs immediately after obtaining fieldContext and before computing
formState or calling getFieldState so all uses of fieldContext.name are safe.

In `@apps/code/src/hooks/useUser.ts`:
- Around line 38-50: The useEffect block starting with useEffect(() => { if
(!data?.user || isLoading) { return; } ... }, [data?.user, isLoading, router,
pathname]) is dead code because it contains no side-effects or logic after its
guards; remove this entire useEffect to clean up the hook (delete the effect
that references data?.user, isLoading, router, and pathname). If redirect or
auth-handling behavior was intended, re-implement it explicitly in a new effect
or function instead of leaving this no-op effect.

In `@apps/code/src/lib/server-api.ts`:
- Around line 49-53: The fetchServerData function uses options?: any which
violates the no-any guideline; replace any with the proper OpenAPI request
options type (e.g., the request/options type exported by your openapi-fetch
client or generator) and import that type into this module, then update the
signature of fetchServerData(method: "GET" | "POST" | "PUT" | "DELETE" |
"PATCH", path: keyof paths, options?: OpenApiRequestOptions): Promise<T | null>;
if needed create method-specific overloads or a union of option types for
different HTTP verbs to tighten typing further.
♻️ Duplicate comments (5)
apps/code/next.config.ts (1)

18-20: Remove ignoreBuildErrors: true to enforce type safety.

Setting ignoreBuildErrors: true bypasses TypeScript type checking during builds, allowing type errors to slip into production and potentially cause runtime failures. This undermines the value of TypeScript in the codebase.

Proposed fix
 	typescript: {
-		ignoreBuildErrors: true,
+		ignoreBuildErrors: false,
 	},
apps/code/src/lib/server-api.ts (1)

85-88: Log the error object for debugging.

The caught error is discarded, making it harder to diagnose issues. Capture and log the error details.

Proposed fix
-	} catch {
-		console.error(`Server API error for ${method} ${path}`);
+	} catch (error) {
+		console.error(`Server API error for ${method} ${path}`, error);
 		return null;
 	}
apps/code/src/hooks/useUser.ts (1)

31-36: Move PostHog identify call into a useEffect.

The posthog.identify call runs on every render when data changes. This should be wrapped in a useEffect to avoid unnecessary re-identification calls and to follow React's rules for side effects.

Proposed fix
+	useEffect(() => {
+		if (data?.user) {
+			posthog.identify(data.user.id, {
+				email: data.user.email,
+				name: data.user.name,
+			});
+		}
+	}, [data?.user?.id, data?.user?.email, data?.user?.name, posthog]);
+
-	if (data) {
-		posthog.identify(data.user.id, {
-			email: data.user.email,
-			name: data.user.name,
-		});
-	}
apps/code/src/app/dashboard/page.tsx (1)

62-70: Polling interval is excessive for dev plan status.

Dev plan status changes only occur during explicit user actions (subscribe, cancel, resume). This comment duplicates a prior review.

apps/api/src/routes/dev-plans.ts (1)

263-263: Spelling inconsistency already flagged.

The canceled vs cancelled spelling inconsistency was noted in a prior review.

🧹 Nitpick comments (17)
apps/code/package.json (2)

35-35: Move babel-plugin-react-compiler to devDependencies.

This is a build-time compiler plugin and should not be in runtime dependencies. Placing it in dependencies unnecessarily includes it in production bundles.

♻️ Suggested fix

Move to devDependencies:

 	"dependencies": {
 		...
-		"babel-plugin-react-compiler": "1.0.0",
 		...
 	},
 	"devDependencies": {
+		"babel-plugin-react-compiler": "1.0.0",
 		...
 	}

18-53: Consider standardizing version pinning strategy.

The dependencies mix exact versions (e.g., "next": "16.1.1") with caret ranges (e.g., "clsx": "^2.1.1"). For reproducible builds, consider using a consistent pinning strategy across all dependencies, or rely on a lockfile to enforce determinism.

.env.unified.example (1)

18-25: Consider alphabetical ordering for consistency.

CODE_URL could be placed before DOCS_URL and PLAYGROUND_URL to maintain alphabetical ordering.

📋 Suggested reordering
 UI_URL=http://localhost:3002
-PLAYGROUND_URL=http://localhost:3003
 CODE_URL=http://localhost:3007
 DOCS_URL=http://localhost:3005
+PLAYGROUND_URL=http://localhost:3003
 ADMIN_URL=http://localhost:3006
apps/gateway/src/lib/rate-limit.spec.ts (1)

81-114: Consider extracting a mock organization factory to reduce duplication.

The mock organization object is repeated 6 times across test cases with only minor variations (primarily credits value). A factory function would reduce boilerplate and make test maintenance easier.

♻️ Suggested factory function
function createMockOrganization(overrides: Partial<typeof baseOrg> = {}) {
	return {
		id: "org-1",
		createdAt: new Date(),
		updatedAt: new Date(),
		name: "Test Org",
		billingEmail: "test@example.com",
		billingCompany: null,
		billingAddress: null,
		billingTaxId: null,
		billingNotes: null,
		stripeCustomerId: null,
		stripeSubscriptionId: null,
		credits: "0",
		autoTopUpEnabled: false,
		autoTopUpThreshold: "10",
		autoTopUpAmount: "10",
		plan: "free" as const,
		planExpiresAt: null,
		subscriptionCancelled: false,
		trialStartDate: null,
		trialEndDate: null,
		isTrialActive: false,
		retentionLevel: "retain" as const,
		status: "active" as const,
		referralEarnings: "0",
		isPersonal: false,
		devPlan: "none" as const,
		devPlanCreditsUsed: "0",
		devPlanCreditsLimit: "0",
		devPlanBillingCycleStart: null,
		devPlanStripeSubscriptionId: null,
		devPlanCancelled: false,
		devPlanExpiresAt: null,
		...overrides,
	};
}

// Usage in tests:
vi.mocked(cdb.query.organization.findFirst).mockResolvedValue(
	createMockOrganization({ credits: "10.50" })
);
apps/code/src/hooks/useUser.ts (1)

69-77: Remove redundant options from dependency array.

The dependency array includes options?.redirectTo, options?.redirectWhen, and options. The full options object is redundant since its properties are already listed individually, and including it may cause unnecessary re-runs if the object reference changes.

Proposed fix
 	}, [
 		data?.user,
 		isLoading,
 		error,
 		router,
 		options?.redirectTo,
 		options?.redirectWhen,
-		options,
 	]);
apps/code/src/app/signup/page.tsx (3)

60-62: Guard against PostHog not being initialized.

When posthogKey is not configured, PostHogProvider is not rendered, and usePostHog() may return an uninitialized instance. Calling posthog.capture() in this case could fail silently or throw. Add a guard check.

Proposed fix
 	useEffect(() => {
-		posthog.capture("page_viewed_signup", { plan: selectedPlan });
+		posthog?.capture("page_viewed_signup", { plan: selectedPlan });
 	}, [posthog, selectedPlan]);

100-118: Duplicate error handling.

The onError callback (lines 100-107) and the if (error) check (lines 111-118) both display error toasts. If signUp.email triggers onError and also returns an error object, the user may see duplicate toasts. Consider consolidating error handling.

Proposed fix - remove duplicate
 		const { error } = await signUp.email(
 			{
 				name: values.name,
 				email: values.email,
 				password: values.password,
 			},
 			{
 				onSuccess: (ctx) => {
 					queryClient.clear();
-					posthog.identify(ctx.data.user.id, {
-						email: ctx.data.user.email,
-						name: ctx.data.user.name,
-					});
-					posthog.capture("user_signed_up", {
-						email: values.email,
-						name: values.name,
+					posthog?.identify(ctx.data.user.id);
+					posthog?.capture("user_signed_up", {
 						plan: selectedPlan,
 					});
 					toast.success("Account created", {
 						description:
 							"Please check your email to verify your account before signing in.",
 					});
 					router.push(returnUrl);
 				},
-				onError: (ctx) => {
-					toast.error(ctx.error.message || "Failed to sign up", {
-						style: {
-							backgroundColor: "var(--destructive)",
-							color: "var(--destructive-foreground)",
-						},
-					});
-				},
 			},
 		);
 
 		if (error) {
 			toast.error(error.message || "Failed to sign up", {
 				style: {
 					backgroundColor: "var(--destructive)",
 					color: "var(--destructive-foreground)",
 				},
 			});
 		}

35-43: Login link does not preserve returnUrl.

When users click "Already have an account? Sign in", the returnUrl and plan query params are lost. Consider forwarding them to maintain the intended flow.

Proposed fix
-				<p className="px-8 text-center text-sm text-muted-foreground">
-					<Link
-						href="/login"
-						className="hover:text-primary underline underline-offset-4"
-					>
-						Already have an account? Sign in
-					</Link>
-				</p>
+				<p className="px-8 text-center text-sm text-muted-foreground">
+					<Link
+						href={`/login?returnUrl=${encodeURIComponent(returnUrl)}${selectedPlan ? `&plan=${selectedPlan}` : ""}`}
+						className="hover:text-primary underline underline-offset-4"
+					>
+						Already have an account? Sign in
+					</Link>
+				</p>

Also applies to: 194-199

apps/code/src/components/providers.tsx (1)

36-40: PostHog options object recreated on every render.

The posthogOptions object is created fresh on each render, which could cause unnecessary re-renders of PostHogProvider. Consider memoizing it.

Proposed fix
-	const posthogOptions: Partial<PostHogConfig> | undefined = {
-		api_host: config.posthogHost,
-		capture_pageview: "history_change",
-		autocapture: true,
-	};
+	const posthogOptions = useMemo<Partial<PostHogConfig>>(
+		() => ({
+			api_host: config.posthogHost,
+			capture_pageview: "history_change",
+			autocapture: true,
+		}),
+		[config.posthogHost],
+	);
apps/code/src/components/ui/form.tsx (1)

29-31: Empty object context defaults may cause subtle runtime issues.

Using {} as FormFieldContextValue and {} as FormItemContextValue as default values means accessing properties like name or id outside a provider will return undefined rather than throwing. This could lead to hard-to-debug issues. Consider using null with proper type guards.

Also applies to: 74-76

apps/api/src/routes/team.ts (1)

319-325: Consider extracting the personal org guard to reduce repetition.

The same guard logic appears in addMember, updateMember, and removeMember. You could extract this into a reusable helper.

♻️ Optional: Extract helper function
function assertNotPersonalOrg(organization: { isPersonal?: boolean } | null) {
	if (organization?.isPersonal) {
		throw new HTTPException(403, {
			message:
				"Team management is not available for personal organizations. Please create a regular organization to invite team members.",
		});
	}
}

Then use assertNotPersonalOrg(userOrganization.organization) in each handler.

Also applies to: 470-476

apps/code/src/app/page.tsx (1)

6-49: Plan data should stay in sync with backend pricing configuration.

The hardcoded plan prices and credits match the backend configuration (DEV_PLAN_PRICES and DEV_PLAN_CREDITS_MULTIPLIER=3). However, if backend pricing changes, this frontend data could drift.

Consider fetching plan data from an API endpoint or sharing a constants file to ensure consistency.

apps/api/src/routes/organization.ts (1)

64-91: Keep transaction type enums in sync with the DB/source-of-truth.

You’re extending transactionSchema.type with dev_plan_* values; consider importing a shared enum (or generating from DB schema/types) to prevent future drift between API OpenAPI schema and persisted values.

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

22-47: Avoid hardcoding the canonical site URL in metadataBase (staging/preview correctness).

Consider deriving metadataBase (and openGraph.url) from config/env so previews don’t publish production canonical URLs.

apps/worker/src/worker.ts (1)

653-705: Dev plan eligibility + N+1 query risk inside the transaction.

  • Eligibility: this logic only checks org.devPlan !== "none". If devPlanExpiresAt / devPlanCancelled can be out-of-sync, the worker may incorrectly spend dev plan credits. Either enforce the invariant at write-time (best) or gate here (e.g., devPlanExpiresAt > now && !devPlanCancelled).
  • Perf: await tx.query.organization.findFirst(...) in a loop can become N+1. Consider preloading all org rows for orgCosts.keys() via inArray once.
apps/gateway/src/chat/chat.ts (1)

1788-1809: Stabilize renewal-date formatting and de-duplicate the “available credits” logic.

  • new Date(...).toLocaleDateString() is environment-dependent; prefer a stable format (e.g. ISO date) for error messages.
  • The regularCredits + (devPlanLimit - devPlanUsed) logic is repeated in 3 places; consider a small helper to compute { totalAvailableCredits, devPlanCreditsRemaining, renewalDate } once and reuse.
  • If devPlanExpiresAt / devPlanCancelled can be out-of-sync, consider gating dev plan credits on validity here (or ensure invariants upstream).

Also applies to: 1895-1917, 1946-1959

apps/code/src/app/dashboard/page.tsx (1)

23-46: Hardcoded plan credits may drift from server configuration.

The credits values (87, 237, 447) are derived from price * 3 (the default multiplier), but if DEV_PLAN_CREDITS_MULTIPLIER is changed server-side, this UI will display incorrect values. Consider fetching plan details from an API endpoint or deriving credits from a shared constant.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 84155d7 and 58c6bea.

⛔ Files ignored due to path filters (10)
  • apps/code/public/favicon/android-chrome-192x192.png is excluded by !**/*.png
  • apps/code/public/favicon/android-chrome-512x512.png is excluded by !**/*.png
  • apps/code/public/favicon/apple-touch-icon.png is excluded by !**/*.png
  • apps/code/public/favicon/favicon-16x16.png is excluded by !**/*.png
  • apps/code/public/favicon/favicon-32x32.png is excluded by !**/*.png
  • apps/code/public/favicon/favicon.ico is excluded by !**/*.ico
  • apps/code/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (40)
  • .env.example
  • .env.unified.example
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/routes/index.ts
  • apps/api/src/routes/organization.ts
  • apps/api/src/routes/subscriptions.ts
  • apps/api/src/routes/team.ts
  • apps/api/src/stripe.ts
  • apps/code/.gitignore
  • apps/code/components.json
  • apps/code/eslint.config.mjs
  • apps/code/next.config.ts
  • apps/code/package.json
  • apps/code/postcss.config.mjs
  • apps/code/public/favicon/site.webmanifest
  • apps/code/src/app/dashboard/page.tsx
  • apps/code/src/app/globals.css
  • apps/code/src/app/layout.tsx
  • apps/code/src/app/login/page.tsx
  • apps/code/src/app/page.tsx
  • apps/code/src/app/signup/page.tsx
  • apps/code/src/components/providers.tsx
  • apps/code/src/components/ui/button.tsx
  • apps/code/src/components/ui/form.tsx
  • apps/code/src/components/ui/input.tsx
  • apps/code/src/components/ui/label.tsx
  • apps/code/src/components/ui/sonner.tsx
  • apps/code/src/hooks/useUser.ts
  • apps/code/src/lib/auth-client.ts
  • apps/code/src/lib/config-server.ts
  • apps/code/src/lib/config.tsx
  • apps/code/src/lib/fetch-client.ts
  • apps/code/src/lib/server-api.ts
  • apps/code/src/lib/utils.ts
  • apps/code/tsconfig.json
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/worker/src/worker.ts
  • packages/db/src/schema.ts
  • packages/db/src/types.ts
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/code/src/components/ui/input.tsx
  • apps/code/src/components/ui/sonner.tsx
  • apps/code/src/components/providers.tsx
  • apps/code/src/lib/fetch-client.ts
  • apps/code/src/lib/auth-client.ts
  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/code/src/lib/utils.ts
  • apps/code/src/hooks/useUser.ts
  • apps/code/src/app/layout.tsx
  • apps/code/src/lib/config-server.ts
  • apps/code/src/components/ui/label.tsx
  • apps/code/next.config.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/team.ts
  • apps/code/src/app/dashboard/page.tsx
  • apps/worker/src/worker.ts
  • packages/db/src/schema.ts
  • packages/db/src/types.ts
  • apps/api/src/stripe.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/code/src/lib/config.tsx
  • apps/api/src/routes/subscriptions.ts
  • apps/code/src/app/signup/page.tsx
  • apps/code/src/lib/server-api.ts
  • apps/code/src/app/page.tsx
  • apps/code/src/components/ui/button.tsx
  • apps/code/src/app/login/page.tsx
  • apps/api/src/routes/organization.ts
  • apps/api/src/routes/index.ts
  • apps/code/src/components/ui/form.tsx
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • apps/code/src/components/ui/input.tsx
  • apps/code/src/components/ui/sonner.tsx
  • apps/code/src/components/providers.tsx
  • apps/code/src/lib/fetch-client.ts
  • apps/code/src/lib/auth-client.ts
  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/code/src/lib/utils.ts
  • apps/code/src/hooks/useUser.ts
  • apps/code/package.json
  • apps/code/src/app/layout.tsx
  • apps/code/src/lib/config-server.ts
  • apps/code/tsconfig.json
  • apps/code/src/components/ui/label.tsx
  • apps/code/next.config.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/team.ts
  • apps/code/src/app/dashboard/page.tsx
  • apps/worker/src/worker.ts
  • packages/db/src/schema.ts
  • packages/db/src/types.ts
  • apps/api/src/stripe.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/code/src/lib/config.tsx
  • apps/api/src/routes/subscriptions.ts
  • apps/code/components.json
  • apps/code/src/app/signup/page.tsx
  • apps/code/src/lib/server-api.ts
  • apps/code/src/app/page.tsx
  • apps/code/src/components/ui/button.tsx
  • apps/code/src/app/login/page.tsx
  • apps/api/src/routes/organization.ts
  • apps/api/src/routes/index.ts
  • apps/code/src/components/ui/form.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/code/src/components/ui/input.tsx
  • apps/code/src/components/ui/sonner.tsx
  • apps/code/src/components/providers.tsx
  • apps/code/src/lib/fetch-client.ts
  • apps/code/src/lib/auth-client.ts
  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/code/src/lib/utils.ts
  • apps/code/src/hooks/useUser.ts
  • apps/code/src/app/layout.tsx
  • apps/code/src/lib/config-server.ts
  • apps/code/src/components/ui/label.tsx
  • apps/code/next.config.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/team.ts
  • apps/code/src/app/dashboard/page.tsx
  • apps/worker/src/worker.ts
  • packages/db/src/schema.ts
  • packages/db/src/types.ts
  • apps/api/src/stripe.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/code/src/lib/config.tsx
  • apps/api/src/routes/subscriptions.ts
  • apps/code/src/app/signup/page.tsx
  • apps/code/src/lib/server-api.ts
  • apps/code/src/app/page.tsx
  • apps/code/src/components/ui/button.tsx
  • apps/code/src/app/login/page.tsx
  • apps/api/src/routes/organization.ts
  • apps/api/src/routes/index.ts
  • apps/code/src/components/ui/form.tsx
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/code/src/components/ui/input.tsx
  • apps/code/src/components/ui/sonner.tsx
  • apps/code/src/components/providers.tsx
  • apps/code/src/lib/fetch-client.ts
  • apps/code/src/lib/auth-client.ts
  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/code/src/lib/utils.ts
  • apps/code/src/hooks/useUser.ts
  • apps/code/src/app/layout.tsx
  • apps/code/src/lib/config-server.ts
  • apps/code/src/components/ui/label.tsx
  • apps/code/next.config.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/team.ts
  • apps/code/src/app/dashboard/page.tsx
  • apps/worker/src/worker.ts
  • packages/db/src/schema.ts
  • packages/db/src/types.ts
  • apps/api/src/stripe.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/code/src/lib/config.tsx
  • apps/api/src/routes/subscriptions.ts
  • apps/code/src/app/signup/page.tsx
  • apps/code/src/lib/server-api.ts
  • apps/code/src/app/page.tsx
  • apps/code/src/components/ui/button.tsx
  • apps/code/src/app/login/page.tsx
  • apps/api/src/routes/organization.ts
  • apps/api/src/routes/index.ts
  • apps/code/src/components/ui/form.tsx
**/*.spec.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Unit tests should use *.spec.ts file naming convention

Files:

  • apps/gateway/src/lib/rate-limit.spec.ts
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/team.ts
  • apps/api/src/stripe.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/routes/subscriptions.ts
  • apps/api/src/routes/organization.ts
  • apps/api/src/routes/index.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/team.ts
  • packages/db/src/schema.ts
  • packages/db/src/types.ts
  • apps/api/src/stripe.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/routes/subscriptions.ts
  • apps/api/src/routes/organization.ts
  • apps/api/src/routes/index.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/team.ts
  • apps/api/src/stripe.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/routes/subscriptions.ts
  • apps/api/src/routes/organization.ts
  • apps/api/src/routes/index.ts
packages/db/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Drizzle ORM with latest object syntax for database operations

Files:

  • packages/db/src/schema.ts
  • packages/db/src/types.ts
🧠 Learnings (6)
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to apps/{ui,playground}/**/*.{ts,tsx} : Use Next.js App Router with React Server Components for frontend development

Applied to files:

  • apps/code/src/hooks/useUser.ts
  • apps/code/package.json
  • apps/code/src/app/layout.tsx
  • apps/code/src/app/dashboard/page.tsx
  • apps/code/src/lib/config.tsx
  • apps/code/src/lib/server-api.ts
  • apps/code/src/app/page.tsx
  • apps/code/src/app/login/page.tsx
  • apps/api/src/routes/index.ts
📚 Learning: 2025-12-03T12:42:26.162Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-03T12:42:26.162Z
Learning: Applies to apps/{ui,playground,docs}/**/*.{ts,tsx} : Use `next/link` for links and `next/navigation`'s router for programmatic navigation

Applied to files:

  • apps/code/src/app/layout.tsx
  • apps/code/src/app/dashboard/page.tsx
  • apps/code/src/app/page.tsx
  • apps/code/src/app/login/page.tsx
  • apps/api/src/routes/index.ts
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to apps/{ui,playground}/**/*.{tsx} : Use `next/link` for links and `next/navigation`'s router for programmatic navigation

Applied to files:

  • apps/code/src/app/layout.tsx
  • apps/code/src/app/page.tsx
  • apps/code/src/app/login/page.tsx
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : No unnecessary code comments

Applied to files:

  • apps/code/tsconfig.json
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : Always use top-level `import`, never use require or dynamic imports

Applied to files:

  • apps/code/tsconfig.json
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to **/*.{ts,tsx} : Never use `any` or `as any` unless absolutely necessary in TypeScript code

Applied to files:

  • apps/code/next.config.ts
🧬 Code graph analysis (18)
apps/code/src/components/ui/input.tsx (1)
apps/code/src/lib/utils.ts (1)
  • cn (4-6)
apps/code/src/lib/fetch-client.ts (1)
apps/code/src/lib/config.tsx (1)
  • useAppConfig (25-31)
apps/code/src/lib/auth-client.ts (1)
apps/code/src/lib/config.tsx (1)
  • useAppConfig (25-31)
apps/code/src/lib/utils.ts (1)
apps/docs/lib/cn.ts (1)
  • twMerge (1-1)
apps/code/src/hooks/useUser.ts (3)
apps/api/src/posthog.ts (1)
  • posthog (3-6)
apps/code/src/lib/fetch-client.ts (1)
  • useApi (20-26)
packages/logger/src/index.ts (1)
  • error (153-160)
apps/code/src/components/ui/label.tsx (2)
apps/code/src/lib/utils.ts (1)
  • cn (4-6)
apps/admin/src/components/ui/label.tsx (1)
  • Label (8-22)
apps/gateway/src/chat/chat.ts (2)
packages/db/src/schema.ts (1)
  • organization (109-160)
packages/models/src/models.ts (1)
  • ModelDefinition (181-241)
apps/code/src/app/dashboard/page.tsx (7)
apps/api/src/posthog.ts (1)
  • posthog (3-6)
apps/code/src/lib/auth-client.ts (1)
  • useAuth (18-31)
apps/code/src/lib/fetch-client.ts (1)
  • useApi (20-26)
apps/code/src/hooks/useUser.ts (1)
  • useUser (14-85)
packages/db/src/schema.ts (1)
  • user (41-53)
apps/ui/src/lib/components/use-toast.ts (1)
  • toast (194-194)
apps/code/src/components/ui/button.tsx (1)
  • Button (60-60)
apps/worker/src/worker.ts (1)
packages/db/src/schema.ts (1)
  • organization (109-160)
apps/api/src/stripe.ts (3)
packages/db/src/schema.ts (2)
  • organization (109-160)
  • transaction (189-233)
apps/api/src/utils/invoice.ts (1)
  • generateAndEmailInvoice (192-283)
apps/api/src/utils/email.ts (2)
  • sendTransactionalEmail (38-105)
  • generateSubscriptionCancelledEmailHtml (251-330)
apps/api/src/routes/dev-plans.ts (4)
packages/db/src/db.ts (1)
  • db (21-25)
packages/db/src/index.ts (1)
  • tables (14-16)
apps/api/src/stripe.ts (1)
  • ensureStripeCustomer (34-71)
apps/api/src/routes/payments.ts (1)
  • stripe (13-18)
apps/api/src/routes/subscriptions.ts (1)
packages/db/src/schema.ts (1)
  • organization (109-160)
apps/code/src/lib/server-api.ts (1)
apps/code/src/lib/config-server.ts (1)
  • getConfig (12-24)
apps/code/src/app/page.tsx (1)
apps/code/src/components/ui/button.tsx (1)
  • Button (60-60)
apps/code/src/components/ui/button.tsx (1)
apps/code/src/lib/utils.ts (1)
  • cn (4-6)
apps/code/src/app/login/page.tsx (3)
apps/api/src/posthog.ts (1)
  • posthog (3-6)
apps/code/src/lib/auth-client.ts (1)
  • useAuth (18-31)
apps/code/src/hooks/useUser.ts (1)
  • useUser (14-85)
apps/api/src/routes/index.ts (1)
apps/api/src/routes/dev-plans.ts (1)
  • devPlans (14-14)
apps/code/src/components/ui/form.tsx (2)
apps/code/src/lib/utils.ts (1)
  • cn (4-6)
apps/code/src/components/ui/label.tsx (1)
  • Label (24-24)
🪛 dotenv-linter (4.0.0)
.env.unified.example

[warning] 20-20: [UnorderedKey] The CODE_URL key should go before the PLAYGROUND_URL key

(UnorderedKey)


[warning] 133-133: [UnorderedKey] The STRIPE_DEV_PLAN_MAX_PRICE_ID key should go before the STRIPE_DEV_PLAN_PRO_PRICE_ID key

(UnorderedKey)

.env.example

[warning] 59-59: [UnorderedKey] The CODE_URL key should go before the PLAYGROUND_URL key

(UnorderedKey)


[warning] 199-199: [UnorderedKey] The STRIPE_DEV_PLAN_MAX_PRICE_ID key should go before the STRIPE_DEV_PLAN_PRO_PRICE_ID key

(UnorderedKey)

🔇 Additional comments (46)
apps/code/tsconfig.json (1)

1-35: LGTM!

The TypeScript configuration is well-structured for a Next.js application:

  • Appropriate compiler options for Next.js (moduleResolution: "bundler", jsx: "preserve", strict: true)
  • Correctly includes Next.js generated type definitions (.next/types/**/*.ts)
  • Path alias @/* properly configured for cleaner imports
  • Incremental builds enabled for faster compilation
apps/code/eslint.config.mjs (1)

1-17: LGTM!

The ESLint flat config structure is correct, and the composition of base + React configs is appropriate. Disabling react/no-unescaped-entities is reasonable to reduce noise from apostrophes in JSX text. The ignore pattern for **/v1.d.ts correctly excludes auto-generated type definitions.

One optional consideration: no-console: "off" allows console statements globally. If you'd prefer to catch accidental debug logs in production while still permitting warnings/errors, you could use "no-console": ["warn", { "allow": ["warn", "error"] }] instead—but this is fine as-is for a developer-facing dashboard.

apps/code/public/favicon/site.webmanifest (1)

4-15: Icon files exist and are correctly referenced.

The manifest references to android-chrome-192x192.png and android-chrome-512x512.png are valid—both files are present in apps/code/public/favicon/.

apps/code/package.json (3)

1-9: LGTM!

Package metadata is appropriate for a private monorepo application. The files array correctly scopes the publishable artifacts.


10-17: LGTM!

Scripts are well-structured. The build script's defensive test -d public && check before copying is a good pattern.


67-67: Remove unused zod dependency from devDependencies.

zod is listed in devDependencies but is not used anywhere in the codebase and is not installed in the dependency tree. If it's not required for development or type generation, remove it to keep dependencies clean.

Likely an incorrect or invalid review comment.

apps/code/.gitignore (2)

1-42: Overall structure looks good.

The gitignore patterns are well-organized and cover all the standard Next.js, TypeScript, and Node.js artifacts. The Yarn 2+ configuration with selective allowances for patches, plugins, releases, and versions follows best practices.


16-19: No action needed. The /.next-dev/ directory is intentionally configured in next.config.ts to serve as the build output directory during development mode (distDir: process.env.NODE_ENV === "development" ? ".next-dev" : ".next"), and the corresponding .gitignore entry is correct.

Likely an incorrect or invalid review comment.

apps/code/src/lib/utils.ts (1)

1-6: LGTM!

Clean implementation of the standard cn utility for composing Tailwind classes. The combination of clsx for conditional class handling and twMerge for deduplication is the recommended pattern for shadcn/ui components.

apps/code/src/components/ui/label.tsx (1)

1-24: LGTM!

Well-structured Label component that aligns with the existing pattern in apps/admin/src/components/ui/label.tsx. The use of data-slot="label", consistent disabled state styling, and proper TypeScript typing are all good practices.

.env.unified.example (1)

130-136: Env vars are well documented.

The dev plan configuration is clearly explained. Minor note: alphabetically, STRIPE_DEV_PLAN_MAX_PRICE_ID would come before STRIPE_DEV_PLAN_PRO_PRICE_ID, but this follows the logical tier ordering (lite → pro → max) which is arguably more intuitive.

apps/code/components.json (1)

1-22: LGTM!

Well-configured shadcn/ui setup. The empty tailwind.config is appropriate for Tailwind v4's CSS-first configuration approach. Aliases are properly mapped and consistent with the app structure.

apps/gateway/src/lib/rate-limit.spec.ts (1)

106-114: Remove this suggestion - devPlan does not affect rate limiting behavior.

The rate limiting logic depends only on whether the model is free, the organization's credit balance, and the model's rateLimitKind. The devPlan field is not referenced in the implementation and therefore does not impact rate limits.

Likely an incorrect or invalid review comment.

apps/code/src/components/ui/input.tsx (1)

1-21: LGTM!

Clean implementation following React 19 patterns where ref is a regular prop via ComponentProps. The Tailwind styling is comprehensive with proper state handling (focus-visible, aria-invalid, disabled) and dark mode support.

apps/code/src/components/ui/sonner.tsx (1)

1-27: LGTM!

The implementation correctly integrates Sonner with next-themes and follows the established pattern from apps/ui/src/lib/components/sonner.tsx. The as React.CSSProperties cast is necessary for CSS custom properties.

apps/code/next.config.ts (1)

5-17: Configuration looks reasonable for monorepo standalone deployment.

A minor note: productionBrowserSourceMaps: true exposes source code to end users in production, which may have IP or security implications. Verify this is intentional for the Code app's use case.

apps/code/src/lib/server-api.ts (2)

8-27: LGTM!

The cookie forwarding logic correctly handles both secure and non-secure session tokens, prioritizing the __Secure- prefixed cookie when available.


29-47: LGTM!

The path type helpers provide good compile-time safety by constraining paths to those supporting each HTTP method. The use of any here is at the type level for property existence checks, which is acceptable.

apps/code/src/lib/config-server.ts (1)

1-24: LGTM!

Well-structured configuration module with appropriate typing and sensible defaults for local development. The fallback of apiBackendUrl to apiUrl is a good pattern for environments where they're the same.

apps/code/src/lib/config.tsx (1)

1-31: LGTM!

Clean implementation of the AppConfig context pattern. The provider and hook follow React best practices with proper null safety check and a descriptive error message.

apps/code/src/lib/fetch-client.ts (1)

1-26: LGTM!

Well-structured hooks with proper memoization. The credentials: "include" setting is appropriate for cookie-based authentication across origins.

apps/code/src/lib/auth-client.ts (1)

1-31: LGTM!

Good implementation with proper memoization. The pattern of exposing useSession through the returned object follows the better-auth library's intended usage pattern.

apps/code/src/components/ui/button.tsx (1)

1-60: LGTM!

Well-structured Button component following shadcn/ui conventions. The CVA configuration provides comprehensive variants and sizes, and the asChild pattern with Radix Slot is correctly implemented for composition flexibility.

apps/code/src/hooks/useUser.ts (1)

87-97: LGTM!

The useUpdateUser hook is well-structured with proper cache invalidation on success.

apps/code/src/components/providers.tsx (1)

42-68: LGTM!

The provider composition is well-structured with conditional PostHog rendering and dev-only React Query devtools. The nesting order is appropriate.

apps/code/src/components/ui/form.tsx (2)

110-127: LGTM!

FormControl properly sets up ARIA attributes for accessibility, binding aria-describedby and aria-invalid based on form state.


142-160: LGTM!

FormMessage correctly handles both error messages and custom children, with proper null return when no content exists.

apps/api/src/routes/subscriptions.ts (1)

88-94: LGTM!

The guard correctly blocks Pro subscriptions for personal organizations, directing users to the appropriate Dev Plans flow. The placement before existing checks is appropriate.

apps/api/src/routes/index.ts (1)

9-9: LGTM!

The devPlans route is properly imported and registered following existing patterns. The placement after subscriptions is logical given the related functionality.

Also applies to: 58-58

apps/code/postcss.config.mjs (1)

1-5: LGTM!

Clean PostCSS configuration for Tailwind CSS v4. The @tailwindcss/postcss plugin is the correct approach for v4's CSS-first configuration.

.env.example (1)

196-202: LGTM!

Well-documented environment variables for Dev Plans. The grouping keeps related Stripe price IDs together, and the credits multiplier default of 3 aligns with the PR description (credits = price × 3).

apps/api/src/routes/team.ts (1)

172-178: LGTM!

The guard correctly blocks team management for personal organizations before checking other permissions. The error message helpfully guides users to create a regular organization.

apps/code/src/app/globals.css (3)

1-5: LGTM!

Correct Tailwind CSS v4 setup with @import "tailwindcss" and the @custom-variant dark directive for class-based dark mode toggling.


6-44: LGTM!

The @theme inline block correctly maps CSS custom properties to Tailwind's design token system, enabling utilities like bg-background, text-foreground, etc. The radius calculations provide consistent scaling.


46-113: LGTM!

Well-structured theme using oklch() color space for better perceptual uniformity. Both light (:root) and dark (.dark) themes are comprehensive with consistent token coverage.

apps/code/src/app/page.tsx (1)

51-222: LGTM on overall structure.

Clean landing page implementation using Next.js App Router conventions. The component correctly uses next/link for navigation (per project guidelines), imports the shared Button component, and renders responsive plan cards with appropriate styling for the popular plan.

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

49-59: Providers wiring via getConfig() in RootLayout looks clean.

Nice separation: server config resolved once at the layout and passed into a single Providers entrypoint.

packages/db/src/types.ts (2)

79-87: Organization devPlan typing matches the DB enum; looks good.

The Omit<..., "devPlan"> & { devPlan: ... } pattern keeps the enum narrow and consistent.


107-126: SerializedOrganization timestamp serialization for dev plan fields is consistent.

Adding devPlanBillingCycleStart / devPlanExpiresAt as string | null in the serialized shape matches the existing serialization strategy for timestamps.

packages/db/src/schema.ts (2)

147-159: LGTM! Dev plan fields are well-structured.

The new organization fields follow existing patterns with appropriate defaults and constraints. The devPlanStripeSubscriptionId unique constraint prevents duplicate subscription associations.


208-213: LGTM! Transaction types cover the complete dev plan lifecycle.

The new transaction types (dev_plan_start, dev_plan_upgrade, dev_plan_downgrade, dev_plan_cancel, dev_plan_end, dev_plan_renewal) properly mirror the existing subscription event types and will enable comprehensive audit trails.

apps/code/src/app/dashboard/page.tsx (1)

77-98: LGTM! Subscribe flow handles loading state and errors appropriately.

The mutation correctly manages the subscribing state, validates the checkout URL, and captures analytics before redirecting.

apps/code/src/app/login/page.tsx (1)

34-42: LGTM! Good open redirect protection.

getSafeRedirectUrl properly validates that the redirect URL is a relative path (starts with / but not //), preventing open redirect vulnerabilities.

apps/api/src/stripe.ts (2)

293-388: LGTM! Dev plan checkout session handling is comprehensive.

The handler correctly:

  • Activates the dev plan and sets credits limits
  • Creates transaction records with duplicate prevention
  • Generates and emails invoices with proper error suppression
  • Tracks events in PostHog

1028-1057: LGTM! Dev plan renewal correctly resets credits.

The renewal handler properly resets devPlanCreditsUsed to "0" and updates devPlanBillingCycleStart, ensuring users get their full credit allocation each billing cycle.

apps/api/src/routes/dev-plans.ts (1)

66-142: LGTM! Personal org retrieval/creation logic is solid.

The handler correctly finds existing personal orgs or creates new ones with the necessary associations (user-org relationship, default project). The response properly serializes all dev plan fields.

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

Comment thread apps/api/src/routes/dev-plans.ts Outdated
Comment on lines +354 to +376
try {
await stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
cancel_at_period_end: true,
});

// Wait for webhook to process
await new Promise((resolve) => {
setTimeout(resolve, 3000);
});

return c.json({
success: true,
});
} catch (error) {
logger.error(
"Stripe dev plan cancellation error",
error instanceof Error ? error : new Error(String(error)),
);
throw new HTTPException(500, {
message: "Failed to cancel dev plan subscription",
});
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid blocking sleep to wait for webhooks.

The 3-second setTimeout blocks the request thread and provides no guarantee the webhook has been processed. Webhooks may arrive later, or not at all due to network issues.

Consider returning success immediately after the Stripe API call succeeds, and let the UI poll for the updated status (which it already does via /status endpoint).

🐛 Proposed fix: Remove the sleep
 	try {
 		await stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
 			cancel_at_period_end: true,
 		});

-		// Wait for webhook to process
-		await new Promise((resolve) => {
-			setTimeout(resolve, 3000);
-		});
-
 		return c.json({
 			success: true,
 		});
 	} catch (error) {

The dashboard already polls /status every 5 seconds, so the UI will reflect the cancellation state once the webhook updates the database.

🤖 Prompt for AI Agents
In `@apps/api/src/routes/dev-plans.ts` around lines 354 - 376, Remove the blocking
sleep after calling stripe.subscriptions.update: delete the new Promise with
setTimeout and return success immediately once
stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
cancel_at_period_end: true }) resolves; keep the existing try/catch and error
logging (logger.error and HTTPException) but do not wait for webhooks in the
request handler—relying on the dashboard polling /status is sufficient.

Comment on lines +441 to +463

await stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
cancel_at_period_end: false,
});

// Wait for webhook to process
await new Promise((resolve) => {
setTimeout(resolve, 3000);
});

return c.json({
success: true,
});
} catch (error) {
logger.error(
"Stripe dev plan resume error",
error instanceof Error ? error : new Error(String(error)),
);
throw new HTTPException(500, {
message: "Failed to resume dev plan subscription",
});
}
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same blocking sleep issue in resume handler.

Apply the same fix as the cancel handler - remove the 3-second sleep.

🤖 Prompt for AI Agents
In `@apps/api/src/routes/dev-plans.ts` around lines 441 - 463, The resume handler
contains a blocking 3-second sleep after calling stripe.subscriptions.update
which should be removed (same fix as the cancel handler); delete the
Promise/setTimeout block following stripe.subscriptions.update in the resume
route so the handler returns immediately after the update (references:
stripe.subscriptions.update and personalOrg.devPlanStripeSubscriptionId),
leaving the existing error handling and response intact.

Comment on lines +548 to +560
await stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
items: [
{
id: subscription.items.data[0].id,
price: newPriceId,
},
],
proration_behavior: "create_prorations",
metadata: {
...subscription.metadata,
devPlan: newTier,
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add null check for subscription items.

subscription.items.data[0] assumes the subscription has at least one item. While this should always be true for valid subscriptions, a defensive check prevents potential runtime errors.

🐛 Proposed fix
 		const subscription = await stripe.subscriptions.retrieve(
 			personalOrg.devPlanStripeSubscriptionId,
 		);

+		if (!subscription.items.data[0]) {
+			throw new HTTPException(500, {
+				message: "Invalid subscription: no items found",
+			});
+		}
+
 		// Update subscription with new tier
 		await stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
🤖 Prompt for AI Agents
In `@apps/api/src/routes/dev-plans.ts` around lines 548 - 560, The code assumes
subscription.items.data[0] exists; add a defensive null/length check before
using it (e.g., verify subscription.items and subscription.items.data are
present and length > 0), extract the first item id into a variable (like
firstItemId) and if missing return/throw a clear error or log and skip the
update; update the call in the stripe.subscriptions.update block to use that
validated firstItemId and ensure metadata update still occurs only after the
check.

Comment thread apps/api/src/routes/dev-plans.ts
Comment on lines +85 to +93
posthog.identify(ctx.data.user.id, {
email: ctx.data.user.email,
name: ctx.data.user.name,
});
posthog.capture("user_signed_up", {
email: values.email,
name: values.name,
plan: selectedPlan,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

PII logged to analytics.

Sending email and name to PostHog may have privacy/compliance implications (GDPR, CCPA). Consider whether this is intentional and documented in your privacy policy. If only tracking is needed, the user ID alone should suffice.

Proposed fix - remove PII from event
-				posthog.identify(ctx.data.user.id, {
-					email: ctx.data.user.email,
-					name: ctx.data.user.name,
-				});
-				posthog.capture("user_signed_up", {
-					email: values.email,
-					name: values.name,
-					plan: selectedPlan,
-				});
+				posthog?.identify(ctx.data.user.id);
+				posthog?.capture("user_signed_up", {
+					plan: selectedPlan,
+				});
📝 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
posthog.identify(ctx.data.user.id, {
email: ctx.data.user.email,
name: ctx.data.user.name,
});
posthog.capture("user_signed_up", {
email: values.email,
name: values.name,
plan: selectedPlan,
});
posthog?.identify(ctx.data.user.id);
posthog?.capture("user_signed_up", {
plan: selectedPlan,
});
🤖 Prompt for AI Agents
In `@apps/code/src/app/signup/page.tsx` around lines 85 - 93, The code is sending
PII (email and name) to PostHog via posthog.identify and posthog.capture; remove
ctx.data.user.email, ctx.data.user.name, values.email and values.name from those
calls and only identify the user by their unique ID (e.g.,
posthog.identify(ctx.data.user.id) with no PII properties) and send non-PII
event properties like selectedPlan; if a user-readable name is required for
product features, persist it server-side or use a hashed/consented value
instead, and ensure any intentional PII collection is documented in the privacy
policy.

Comment on lines +47 to +57
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);

if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect guard check ordering in useFormField.

The null check for fieldContext at line 54 happens after fieldContext.name is already used at line 51. This means if fieldContext is falsy, the code will error before reaching the guard. Move the check before usage.

Proposed fix
 const useFormField = () => {
 	const fieldContext = React.useContext(FormFieldContext);
 	const itemContext = React.useContext(FormItemContext);
+
+	if (!fieldContext.name) {
+		throw new Error("useFormField should be used within <FormField>");
+	}
+
 	const { getFieldState } = useFormContext();
 	const formState = useFormState({ name: fieldContext.name });
 	const fieldState = getFieldState(fieldContext.name, formState);
 
-	if (!fieldContext) {
-		throw new Error("useFormField should be used within <FormField>");
-	}
-
 	const { id } = itemContext;
📝 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 useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { getFieldState } = useFormContext();
const formState = useFormState({ name: fieldContext.name });
const fieldState = getFieldState(fieldContext.name, formState);
const { id } = itemContext;
🤖 Prompt for AI Agents
In `@apps/code/src/components/ui/form.tsx` around lines 47 - 57, The null-check
for fieldContext is after its usage in useFormField, causing a crash when
fieldContext is falsy; move the guard so the check for fieldContext (the
React.useContext(FormFieldContext) result) occurs before any access to
fieldContext.name or before calling useFormState/getFieldState with
fieldContext.name; ensure the early throw in useFormField runs immediately after
obtaining fieldContext and before computing formState or calling getFieldState
so all uses of fieldContext.name are safe.

Comment on lines +38 to +50
useEffect(() => {
if (!data?.user || isLoading) {
return;
}

const currentPath = pathname;
const isAuthPage = ["/login", "/signup"].includes(currentPath);
const isLandingPage = currentPath === "/";

if (isAuthPage || isLandingPage) {
return;
}
}, [data?.user, isLoading, router, pathname]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove dead code: this useEffect performs no action.

This effect checks conditions for auth/landing pages but never executes any logic—it just returns early. If redirect prevention logic was intended here, it's incomplete. Otherwise, this should be removed.

Proposed fix: remove the dead effect
-	useEffect(() => {
-		if (!data?.user || isLoading) {
-			return;
-		}
-
-		const currentPath = pathname;
-		const isAuthPage = ["/login", "/signup"].includes(currentPath);
-		const isLandingPage = currentPath === "/";
-
-		if (isAuthPage || isLandingPage) {
-			return;
-		}
-	}, [data?.user, isLoading, router, pathname]);
📝 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
useEffect(() => {
if (!data?.user || isLoading) {
return;
}
const currentPath = pathname;
const isAuthPage = ["/login", "/signup"].includes(currentPath);
const isLandingPage = currentPath === "/";
if (isAuthPage || isLandingPage) {
return;
}
}, [data?.user, isLoading, router, pathname]);
🤖 Prompt for AI Agents
In `@apps/code/src/hooks/useUser.ts` around lines 38 - 50, The useEffect block
starting with useEffect(() => { if (!data?.user || isLoading) { return; } ... },
[data?.user, isLoading, router, pathname]) is dead code because it contains no
side-effects or logic after its guards; remove this entire useEffect to clean up
the hook (delete the effect that references data?.user, isLoading, router, and
pathname). If redirect or auth-handling behavior was intended, re-implement it
explicitly in a new effect or function instead of leaving this no-op effect.

Comment on lines +49 to +53
export async function fetchServerData<T>(
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
path: keyof paths,
options?: any,
): Promise<T | null> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid any for the options parameter.

Per coding guidelines, any should not be used unless absolutely necessary. The options parameter can be typed more strictly using the openapi-fetch request options types.

Suggested improvement
+import type { FetchOptions } from "openapi-fetch";
+
 export async function fetchServerData<T>(
 	method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
 	path: keyof paths,
-	options?: any,
+	options?: FetchOptions<unknown>,
 ): Promise<T | null> {

Alternatively, if more precise typing is needed per method, consider method-specific overloads or a union type based on the path parameter.

📝 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 async function fetchServerData<T>(
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
path: keyof paths,
options?: any,
): Promise<T | null> {
import type { FetchOptions } from "openapi-fetch";
export async function fetchServerData<T>(
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH",
path: keyof paths,
options?: FetchOptions<unknown>,
): Promise<T | null> {
🤖 Prompt for AI Agents
In `@apps/code/src/lib/server-api.ts` around lines 49 - 53, The fetchServerData
function uses options?: any which violates the no-any guideline; replace any
with the proper OpenAPI request options type (e.g., the request/options type
exported by your openapi-fetch client or generator) and import that type into
this module, then update the signature of fetchServerData(method: "GET" | "POST"
| "PUT" | "DELETE" | "PATCH", path: keyof paths, options?:
OpenApiRequestOptions): Promise<T | null>; if needed create method-specific
overloads or a union of option types for different HTTP verbs to tighten typing
further.

Comment thread apps/worker/src/worker.ts
Comment on lines +646 to +704
// Dev plan credits are deducted first, then regular credits
const referralEarnings = new Map<string, Decimal>();

for (const [orgId, totalCost] of orgCosts.entries()) {
if (totalCost.greaterThan(0)) {
const costNumber = totalCost.toNumber();
await tx
.update(organization)
.set({
credits: sql`${organization.credits} - ${costNumber}`,
})
.where(eq(organization.id, orgId));
let remainingCost = totalCost;

logger.debug(
`Deducted ${costNumber} credits from organization ${orgId}`,
);
// Fetch the organization to check for dev plan
const org = await tx.query.organization.findFirst({
where: { id: { eq: orgId } },
});

// First, try to deduct from dev plan credits if available
if (org && org.devPlan !== "none") {
const devPlanCreditsLimit = new Decimal(
org.devPlanCreditsLimit || "0",
);
const devPlanCreditsUsed = new Decimal(
org.devPlanCreditsUsed || "0",
);
const devPlanRemaining =
devPlanCreditsLimit.minus(devPlanCreditsUsed);

if (devPlanRemaining.greaterThan(0)) {
const deductFromDevPlan = Decimal.min(
remainingCost,
devPlanRemaining,
);
const deductNumber = deductFromDevPlan.toNumber();

await tx
.update(organization)
.set({
devPlanCreditsUsed: sql`${organization.devPlanCreditsUsed} + ${deductNumber}`,
})
.where(eq(organization.id, orgId));

logger.debug(
`Deducted ${deductNumber} dev plan credits from organization ${orgId}`,
);

remainingCost = remainingCost.minus(deductFromDevPlan);
}
}

// Deduct any remaining cost from regular credits
if (remainingCost.greaterThan(0)) {
const costNumber = remainingCost.toNumber();
await tx
.update(organization)
.set({
credits: sql`${organization.credits} - ${costNumber}`,
})
.where(eq(organization.id, orgId));

logger.debug(
`Deducted ${costNumber} regular credits from organization ${orgId}`,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Credit precision risk: avoid Decimal -> number for balance updates.

toNumber() can introduce floating rounding (especially with fractional costs). Prefer updating using string/Decimal values so Postgres numeric math stays exact.

Proposed fix (keep arithmetic in numeric/string form)
-							const deductNumber = deductFromDevPlan.toNumber();
+							const deductValue = deductFromDevPlan.toString();

 							await tx
 								.update(organization)
 								.set({
-									devPlanCreditsUsed: sql`${organization.devPlanCreditsUsed} + ${deductNumber}`,
+									devPlanCreditsUsed: sql`${organization.devPlanCreditsUsed} + ${deductValue}`,
 								})
 								.where(eq(organization.id, orgId));

 ...
-						const costNumber = remainingCost.toNumber();
+						const costValue = remainingCost.toString();
 						await tx
 							.update(organization)
 							.set({
-								credits: sql`${organization.credits} - ${costNumber}`,
+								credits: sql`${organization.credits} - ${costValue}`,
 							})
 							.where(eq(organization.id, orgId));
📝 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
// Dev plan credits are deducted first, then regular credits
const referralEarnings = new Map<string, Decimal>();
for (const [orgId, totalCost] of orgCosts.entries()) {
if (totalCost.greaterThan(0)) {
const costNumber = totalCost.toNumber();
await tx
.update(organization)
.set({
credits: sql`${organization.credits} - ${costNumber}`,
})
.where(eq(organization.id, orgId));
let remainingCost = totalCost;
logger.debug(
`Deducted ${costNumber} credits from organization ${orgId}`,
);
// Fetch the organization to check for dev plan
const org = await tx.query.organization.findFirst({
where: { id: { eq: orgId } },
});
// First, try to deduct from dev plan credits if available
if (org && org.devPlan !== "none") {
const devPlanCreditsLimit = new Decimal(
org.devPlanCreditsLimit || "0",
);
const devPlanCreditsUsed = new Decimal(
org.devPlanCreditsUsed || "0",
);
const devPlanRemaining =
devPlanCreditsLimit.minus(devPlanCreditsUsed);
if (devPlanRemaining.greaterThan(0)) {
const deductFromDevPlan = Decimal.min(
remainingCost,
devPlanRemaining,
);
const deductNumber = deductFromDevPlan.toNumber();
await tx
.update(organization)
.set({
devPlanCreditsUsed: sql`${organization.devPlanCreditsUsed} + ${deductNumber}`,
})
.where(eq(organization.id, orgId));
logger.debug(
`Deducted ${deductNumber} dev plan credits from organization ${orgId}`,
);
remainingCost = remainingCost.minus(deductFromDevPlan);
}
}
// Deduct any remaining cost from regular credits
if (remainingCost.greaterThan(0)) {
const costNumber = remainingCost.toNumber();
await tx
.update(organization)
.set({
credits: sql`${organization.credits} - ${costNumber}`,
})
.where(eq(organization.id, orgId));
logger.debug(
`Deducted ${costNumber} regular credits from organization ${orgId}`,
);
}
// Dev plan credits are deducted first, then regular credits
const referralEarnings = new Map<string, Decimal>();
for (const [orgId, totalCost] of orgCosts.entries()) {
if (totalCost.greaterThan(0)) {
let remainingCost = totalCost;
// Fetch the organization to check for dev plan
const org = await tx.query.organization.findFirst({
where: { id: { eq: orgId } },
});
// First, try to deduct from dev plan credits if available
if (org && org.devPlan !== "none") {
const devPlanCreditsLimit = new Decimal(
org.devPlanCreditsLimit || "0",
);
const devPlanCreditsUsed = new Decimal(
org.devPlanCreditsUsed || "0",
);
const devPlanRemaining =
devPlanCreditsLimit.minus(devPlanCreditsUsed);
if (devPlanRemaining.greaterThan(0)) {
const deductFromDevPlan = Decimal.min(
remainingCost,
devPlanRemaining,
);
const deductValue = deductFromDevPlan.toString();
await tx
.update(organization)
.set({
devPlanCreditsUsed: sql`${organization.devPlanCreditsUsed} + ${deductValue}`,
})
.where(eq(organization.id, orgId));
logger.debug(
`Deducted ${deductValue} dev plan credits from organization ${orgId}`,
);
remainingCost = remainingCost.minus(deductFromDevPlan);
}
}
// Deduct any remaining cost from regular credits
if (remainingCost.greaterThan(0)) {
const costValue = remainingCost.toString();
await tx
.update(organization)
.set({
credits: sql`${organization.credits} - ${costValue}`,
})
.where(eq(organization.id, orgId));
logger.debug(
`Deducted ${costValue} regular credits from organization ${orgId}`,
);
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/api/src/routes/dev-plans.ts`:
- Around line 204-228: The org creation block duplicates getPersonalOrg’s
missing transaction handling; extract a shared helper
getOrCreatePersonalOrg(user, db) that runs inside a single transaction (use
db.transaction or the project’s transaction helper) and performs the insert into
tables.organization, tables.userOrganization and tables.project together,
returning the created organization; replace the inline creation code and any
other usages (including where personalOrg is set) with calls to
getOrCreatePersonalOrg to ensure atomicity and avoid partial state on failures.
♻️ Duplicate comments (8)
apps/code/src/app/page.tsx (1)

84-86: "Free Trial" messaging may be inaccurate.

The CTAs reference "Start Free Trial" but the PR objectives don't mention a free trial feature. If there's no actual free trial implementation, update the copy to match the actual flow (e.g., "Get Started").

Also applies to lines 202-204.

apps/code/src/app/dashboard/page.tsx (1)

62-70: Consider reducing polling frequency.

Polling every 5 seconds may be excessive for dev plan status, which typically only changes during user actions. Consider increasing to 30 seconds or using manual refetch after mutations.

apps/api/src/stripe.ts (1)

20-32: Duplicate pricing constants - already flagged.

This duplication has been identified in a previous review. Extract DEV_PLAN_PRICES, DevPlanTier, and getDevPlanCreditsLimit to a shared module to avoid drift between stripe.ts and dev-plans.ts.

apps/api/src/routes/dev-plans.ts (5)

16-27: Duplicate pricing constants - already flagged.

Extract these to a shared module as noted in the previous review.


359-362: Remove blocking sleep - already flagged.

The 3-second sleep blocks the request thread and doesn't guarantee webhook processing. The UI already polls /status.


446-449: Remove blocking sleep - already flagged.

Same issue as the cancel handler.


547-560: Add null check for subscription items - already flagged.


564-566: Type assertion may fail for "none" plan - already flagged.

🧹 Nitpick comments (5)
apps/code/src/app/page.tsx (1)

6-49: Consider extracting shared plan definitions.

The plans array is duplicated in both page.tsx and dashboard/page.tsx. Extract this to a shared constants file to maintain consistency and reduce maintenance burden.

♻️ Suggested extraction

Create a shared file, e.g., apps/code/src/lib/plans.ts:

export const plans = [
	{
		name: "Lite",
		price: 29,
		credits: 87,
		description: "For small dev tasks and getting started",
		features: [
			"$87 in monthly credits",
			"Access to all LLM models",
			"Claude, GPT-4, and more",
			"Credits reset monthly",
		],
		tier: "lite" as const,
	},
	// ... Pro and Max
] as const;

export type PlanTier = (typeof plans)[number]["tier"];

Then import in both pages:

import { plans } from "@/lib/plans";
apps/code/src/app/dashboard/page.tsx (2)

93-94: Empty catch blocks lose error context.

The catch blocks discard error information which makes debugging harder. Consider logging the error or including it in the toast message.

♻️ Proposed fix
-		} catch {
+		} catch (error) {
+			console.error("Failed to start subscription:", error);
 			toast.error("Failed to start subscription");
 		} finally {

Apply similar pattern to handleCancel (line 109), handleResume (line 122), and handleChangeTier (line 141).


23-46: Duplicate plan definitions.

Same plans array exists in page.tsx. See earlier comment about extracting to a shared module.

apps/api/src/routes/dev-plans.ts (1)

568-582: Consider adding error handling for DB update after Stripe change.

If the Stripe subscription update succeeds (lines 548-560) but the database update fails (lines 568-574), there's a temporary inconsistency. While webhooks will eventually sync the state, consider wrapping the DB operations in a try-catch with logging, or relying entirely on webhooks for state sync instead of optimistic local updates.

apps/api/src/stripe.ts (1)

1351-1356: Use dev-plan-specific cancellation email copy.

The dev plan cancellation sends generateSubscriptionCancelledEmailHtml, which contains hardcoded Pro subscription messaging: "Your Pro subscription for ${organizationName} has been cancelled and your organization has been downgraded to the free plan." This template doesn't apply to dev plans.

Create a dev-plan-specific template or modify the existing one to accept a plan parameter for appropriate user messaging.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 58c6bea and fe71aa8.

⛔ Files ignored due to path filters (1)
  • apps/code/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (10)
  • .env.example
  • .env.unified.example
  • apps/api/src/index.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/stripe.ts
  • apps/code/package.json
  • apps/code/src/app/dashboard/page.tsx
  • apps/code/src/app/page.tsx
  • apps/gateway/src/app.ts
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/code/package.json
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/api/src/index.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/gateway/src/app.ts
  • apps/code/src/app/page.tsx
  • apps/code/src/app/dashboard/page.tsx
  • apps/api/src/stripe.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • apps/api/src/index.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/gateway/src/app.ts
  • apps/code/src/app/page.tsx
  • package.json
  • apps/code/src/app/dashboard/page.tsx
  • apps/api/src/stripe.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/api/src/index.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/gateway/src/app.ts
  • apps/code/src/app/page.tsx
  • apps/code/src/app/dashboard/page.tsx
  • apps/api/src/stripe.ts
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/api/src/index.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/gateway/src/app.ts
  • apps/api/src/stripe.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/api/src/index.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/gateway/src/app.ts
  • apps/code/src/app/page.tsx
  • apps/code/src/app/dashboard/page.tsx
  • apps/api/src/stripe.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/api/src/index.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/gateway/src/app.ts
  • apps/api/src/stripe.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/api/src/index.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/gateway/src/app.ts
  • apps/api/src/stripe.ts
{package.json,pnpm-lock.yaml}

📄 CodeRabbit inference engine (CLAUDE.md)

Use pnpm for package management, never use npm or yarn

Files:

  • package.json
🧠 Learnings (5)
📚 Learning: 2025-12-03T12:42:26.162Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-03T12:42:26.162Z
Learning: Applies to apps/{gateway,api}/src/**/*.ts : Use Hono for backend framework in Gateway and API services

Applied to files:

  • apps/gateway/src/app.ts
📚 Learning: 2025-12-03T12:42:26.162Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-03T12:42:26.162Z
Learning: Applies to apps/{ui,playground,docs}/**/*.{ts,tsx} : Use `next/link` for links and `next/navigation`'s router for programmatic navigation

Applied to files:

  • apps/code/src/app/page.tsx
  • apps/code/src/app/dashboard/page.tsx
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to apps/{ui,playground}/**/*.{ts,tsx} : Use Next.js App Router with React Server Components for frontend development

Applied to files:

  • apps/code/src/app/page.tsx
  • apps/code/src/app/dashboard/page.tsx
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to apps/{ui,playground}/**/*.{tsx} : Use `next/link` for links and `next/navigation`'s router for programmatic navigation

Applied to files:

  • apps/code/src/app/page.tsx
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Run `pnpm build` to ensure production builds work after code changes

Applied to files:

  • package.json
🧬 Code graph analysis (3)
apps/code/src/app/page.tsx (1)
apps/code/src/components/ui/button.tsx (1)
  • Button (60-60)
apps/code/src/app/dashboard/page.tsx (5)
apps/api/src/posthog.ts (1)
  • posthog (3-6)
apps/code/src/lib/auth-client.ts (1)
  • useAuth (18-31)
apps/code/src/lib/fetch-client.ts (1)
  • useApi (20-26)
apps/code/src/hooks/useUser.ts (1)
  • useUser (14-85)
apps/ui/src/lib/components/use-toast.ts (1)
  • toast (194-194)
apps/api/src/stripe.ts (6)
apps/api/src/routes/organization.ts (1)
  • organization (11-11)
packages/db/src/schema.ts (2)
  • organization (109-160)
  • transaction (189-233)
packages/db/src/index.ts (1)
  • tables (14-16)
apps/api/src/utils/invoice.ts (1)
  • generateAndEmailInvoice (192-283)
apps/api/src/posthog.ts (1)
  • posthog (3-6)
apps/api/src/utils/email.ts (2)
  • sendTransactionalEmail (38-105)
  • generateSubscriptionCancelledEmailHtml (251-330)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 59-59: [UnorderedKey] The CODE_URL key should go before the PLAYGROUND_URL key

(UnorderedKey)


[warning] 199-199: [UnorderedKey] The STRIPE_DEV_PLAN_MAX_PRICE_ID key should go before the STRIPE_DEV_PLAN_PRO_PRICE_ID key

(UnorderedKey)

.env.unified.example

[warning] 20-20: [UnorderedKey] The CODE_URL key should go before the PLAYGROUND_URL key

(UnorderedKey)


[warning] 133-133: [UnorderedKey] The STRIPE_DEV_PLAN_MAX_PRICE_ID key should go before the STRIPE_DEV_PLAN_PRO_PRICE_ID key

(UnorderedKey)

🔇 Additional comments (14)
package.json (1)

12-12: LGTM!

The addition of --filter=!code correctly excludes the new code app from core builds, consistent with how other frontend apps (ui, docs, playground, admin) are already excluded.

apps/api/src/index.ts (1)

60-61: LGTM — minor clarification on port 3005.

Port 3004 aligns with the new Dev Plans "code" app mentioned in the PR. Could you clarify what service is expected to run on port 3005? Adding a brief comment or documenting the port assignments would help maintainability.

apps/gateway/src/app.ts (1)

76-76: LGTM!

The addition of http://localhost:3004 to the CORS origins is consistent with the new CODE_URL environment variable and enables the new Code app to communicate with the gateway.

.env.unified.example (2)

20-20: LGTM!

The CODE_URL addition is consistent with the new Code app at port 3004.


129-136: LGTM!

The Dev Plans Stripe configuration variables are well-documented with clear comments explaining their purpose and default values.

.env.example (2)

59-59: LGTM!

Consistent with .env.unified.example.


196-202: LGTM!

Dev Plans configuration is consistent with .env.unified.example and well-documented.

apps/code/src/app/page.tsx (1)

51-222: LGTM on component structure.

The landing page is well-structured with clear sections, uses next/link for navigation per coding guidelines, and the pricing grid renders correctly with conditional styling for the popular plan.

apps/code/src/app/dashboard/page.tsx (2)

161-167: LGTM on credit calculations.

The credit usage logic correctly handles edge cases with Math.max(0, ...) and safe percentage calculation when creditsLimit is 0.


48-60: LGTM on authentication flow.

The useUser hook with redirectTo and redirectWhen options properly handles unauthenticated users, and the component correctly uses next/navigation for programmatic navigation. Based on learnings, this follows the recommended pattern.

apps/api/src/stripe.ts (3)

1019-1074: Dev plan renewal logic looks correct.

The renewal detection via devPlanStripeSubscriptionId match and devPlan !== "none" check is sound. The credit reset and transaction recording follow the expected pattern.


1185-1243: Dev plan subscription update handling looks good.

The dual detection mechanism (metadata or subscription ID match) is robust. Transaction records for cancellation events are properly created.


315-323: The Drizzle query syntax used is correct and consistent with the codebase. The { fieldName: { eq: value } } pattern is the established standard throughout the repository (used in stripe.ts, worker.ts, organization.ts, and payments.ts) and aligns with the "latest object syntax" requirement from the coding guidelines. No refactoring needed.

apps/api/src/routes/dev-plans.ts (1)

598-680: Status endpoint implementation looks good.

Auth check, null handling for missing personal org, and credit calculations are all correct.

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

Comment thread apps/api/src/routes/dev-plans.ts Outdated
Comment thread apps/api/src/routes/dev-plans.ts Outdated
steebchen and others added 2 commits January 15, 2026 14:26
…s-subscription-ime9td

# Conflicts:
#	apps/gateway/src/chat/chat.ts
#	apps/gateway/src/lib/rate-limit.spec.ts
#	packages/db/src/schema.ts
#	packages/db/src/types.ts
- Auto-create API key when user has active dev plan
- Show API key in dashboard with copy/show/hide controls
- Link to integration guides from dashboard
- Change usage display from $ amounts to percentage
- Remove free trial mentions (direct paid subscription)
- Remove credits terminology from UI copy
- Update metadata descriptions

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@steebchen steebchen changed the title feat: Implement Dev Plans subscription and dashboard UI feat: Dev Plans API, Stripe integration, and Code dashboard UI Jan 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (2)
.env.example (1)

56-64: Add CODE_URL origin to ORIGIN_URLS (likely required for local dev).

CODE_URL is http://localhost:3004, but ORIGIN_URLS still only includes http://localhost:3002, which can break CORS/auth callbacks when the code app calls the API.

Proposed fix
 ORIGIN_URLS=http://localhost:3002
+# ORIGIN_URLS=http://localhost:3002,http://localhost:3004
+ORIGIN_URLS=http://localhost:3002,http://localhost:3004
apps/gateway/src/chat/chat.ts (1)

1854-1875: Use the same “free model” predicate as other branches (isModelTrulyFree).

This branch checks !(modelInfo as ModelDefinition).free, while the hybrid branch uses isModelTrulyFree(...). Aligning avoids inconsistent credit enforcement across modes.

🤖 Fix all issues with AI agents
In `@apps/api/src/routes/dev-plans.ts`:
- Around line 706-725: The GET /status handler currently performs a DB write by
calling getOrCreatePersonalOrgApiKey when personalOrg.devPlan !== "none" and
uses db.query.project.findFirst without ordering, causing nondeterministic
project selection; change the handler to remove side effects (do not create API
keys on GET) and instead only read existing keys, moving creation to an explicit
POST endpoint (or subscription/webhook flow) that calls
getOrCreatePersonalOrgApiKey, and make project lookup deterministic by replacing
db.query.project.findFirst with a deterministic query (e.g., findFirst with an
orderBy createdAt or lookup by a “Default Project” name/flag) so the code that
currently references personalOrg.devPlan, db.query.project.findFirst, and
getOrCreatePersonalOrgApiKey no longer performs writes on GET and selects a
predictable project.

In `@apps/api/src/stripe.ts`:
- Around line 348-353: The invoice line item description in the lineItems array
mixes currency and credit units; update the description construction (where
description is built using devPlanTier and creditsLimit) to remove the dollar
sign and clearly indicate credits (e.g., "Dev Plan ${devPlanTier.toUpperCase()}
(${creditsLimit} credits included)") so it doesn't read like a dollar amount;
ensure only the description string is changed and leave amount calculation
(amount: (session.amount_total || 0) / 100) untouched.
- Around line 281-304: The code unsafely casts metadata.devPlan to DevPlanTier;
validate metadata.devPlan against the allowed DevPlanTier values before using
it. In the block that sets isDevPlan and devPlanTier, replace the unchecked cast
by checking metadata?.devPlan is one of the known tiers (e.g., via a Set/map or
a type-guard function isValidDevPlanTier(value): value is DevPlanTier) and only
assign devPlanTier when valid; if invalid, log/warn and skip calling
getDevPlanCreditsLimit and the DB update that writes devPlanCreditsLimit/related
fields. Ensure getDevPlanCreditsLimit is called only with a validated
devPlanTier and that you do not persist NaN or unexpected values.
- Around line 1431-1437: The cancellation email send isn't protected and can
surface transient failures to Stripe; wrap the await sendTransactionalEmail call
(the call using sendTransactionalEmail with
generateSubscriptionCancelledEmailHtml and organization.billingEmail) in a
try/catch inside the webhook handler so any errors are caught, logged via
processLogger or similar, and not re-thrown — do not change the handler's
success flow or return value so Stripe sees the webhook as handled even if email
sending fails.
- Around line 1108-1113: Validate organization.devPlan before casting to
DevPlanTier: inside the isDevPlanRenewal block, check that organization.devPlan
is a string and that organization.devPlan in DEV_PLAN_PRICES (and explicitly
exclude "none") before calling getDevPlanCreditsLimit; if the check fails,
handle defensively by logging a warning/error and either set creditsLimit to a
safe default (e.g., 0) or abort the renewal flow to avoid computing NaN. Update
the code paths that use creditsLimit (the existing getDevPlanCreditsLimit call
and subsequent logic) to use the validated value or early-return on invalid
devPlan.

In `@apps/code/src/app/dashboard/page.tsx`:
- Around line 253-255: The remaining-percentage display can go negative when
usagePercentage > 100; update the JSX that renders "{(100 -
usagePercentage).toFixed(0)}% remaining this cycle" to clamp the computed value
to a minimum of 0 (e.g., compute remaining = Math.max(0, 100 - usagePercentage)
and render remaining.toFixed(0)), referring to the usagePercentage variable and
the paragraph that outputs the remaining percentage.
- Around line 157-162: The handleCopyApiKey handler calls
navigator.clipboard.writeText without error handling; wrap the await call in a
try-catch inside handleCopyApiKey (keeping the existing devPlanStatus?.apiKey
guard), toast.success on success and on failure catch the error, log it
(console.error or process logger) and call toast.error with a clear message like
"Failed to copy API key" (optionally include error.message); ensure the function
remains async and does not allow unhandled rejections.
♻️ Duplicate comments (8)
apps/worker/src/worker.ts (1)

690-719: Credit precision risk: avoid Decimal -> number for balance updates.

Converting Decimal to number via toNumber() can introduce floating-point rounding errors for fractional costs. Use .toString() instead and let Postgres handle the numeric arithmetic precisely.

Proposed fix (keep arithmetic in numeric/string form)
-						const deductNumber = deductFromDevPlan.toNumber();
+						const deductValue = deductFromDevPlan.toString();

 						await tx
 							.update(organization)
 							.set({
-								devPlanCreditsUsed: sql`${organization.devPlanCreditsUsed} + ${deductNumber}`,
+								devPlanCreditsUsed: sql`${organization.devPlanCreditsUsed} + ${deductValue}::numeric`,
 							})
 							.where(eq(organization.id, orgId));

 ...

-					const costNumber = remainingCost.toNumber();
+					const costValue = remainingCost.toString();
 					await tx
 						.update(organization)
 						.set({
-							credits: sql`${organization.credits} - ${costNumber}`,
+							credits: sql`${organization.credits} - ${costValue}::numeric`,
 						})
 						.where(eq(organization.id, orgId));
apps/api/src/stripe.ts (1)

20-32: Centralize dev-plan pricing and verify max price (179).

Same duplication/drift risk as apps/api/src/routes/dev-plans.ts.

apps/api/src/routes/dev-plans.ts (5)

16-23: Confirm dev-plan pricing (max=179) and centralize constants to avoid drift.

This hardcodes max: 179, which appears to contradict the PR objective description (and the same constants are duplicated in apps/api/src/stripe.ts). At minimum, verify the intended price and move DEV_PLAN_PRICES/DevPlanTier/getDevPlanCreditsLimit to a shared module.

#!/bin/bash
set -euo pipefail

# Find all dev plan pricing definitions/usages to ensure no drift.
rg -n --hidden --glob '!.git/**' 'DEV_PLAN_PRICES|DevPlanTier|getDevPlanCreditsLimit|DEV_PLAN_CREDITS_MULTIPLIER' .

Also applies to: 61-64


143-165: Wrap personal-org + membership + default-project creation in a DB transaction.

These multi-step inserts aren’t atomic; a partial failure can leave an orphaned org (or org without a default project).

Also applies to: 241-266


299-315: Keep cancel_url query param spelling consistent (canceled vs cancelled).

cancel_url uses ?canceled=true, but the codebase uses devPlanCancelled elsewhere. Aligning spelling avoids brittle UI parsing / confusion.


391-404: Remove blocking sleep waiting for webhooks in cancel/resume.

Sleeping in the request handler is unreliable and ties up capacity; the dashboard already polls /status, so return immediately after Stripe update.

Proposed fix
 	await stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
 		cancel_at_period_end: true,
 	});

-	// Wait for webhook to process
-	await new Promise((resolve) => {
-		setTimeout(resolve, 3000);
-	});
-
 	return c.json({
 		success: true,
 	});
 	await stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
 		cancel_at_period_end: false,
 	});

-	// Wait for webhook to process
-	await new Promise((resolve) => {
-		setTimeout(resolve, 3000);
-	});
-
 	return c.json({
 		success: true,
 	});

Also applies to: 468-491


579-604: Guard subscription.items access + validate currentTier instead of casting.

subscription.items.data[0].id can throw if items are empty, and personalOrg.devPlan as DevPlanTier is fragile if it’s "none" or unexpected.

Proposed fix
 	const subscription = await stripe.subscriptions.retrieve(
 		personalOrg.devPlanStripeSubscriptionId,
 	);

+	const firstItem = subscription.items.data[0];
+	if (!firstItem) {
+		throw new HTTPException(500, { message: "Invalid subscription: no items found" });
+	}
+
+	const currentTier = personalOrg.devPlan;
+	if (currentTier === "none" || !(currentTier in DEV_PLAN_PRICES)) {
+		throw new HTTPException(400, { message: "Current plan is not a valid dev plan tier" });
+	}
+
 	// Update subscription with new tier
 	await stripe.subscriptions.update(personalOrg.devPlanStripeSubscriptionId, {
 		items: [
 			{
-				id: subscription.items.data[0].id,
+				id: firstItem.id,
 				price: newPriceId,
 			},
 		],
 		proration_behavior: "create_prorations",
 		metadata: {
 			...subscription.metadata,
 			devPlan: newTier,
 		},
 	});

 	// Update local database immediately
 	const newCreditsLimit = getDevPlanCreditsLimit(newTier);
 	const isUpgrade =
-		DEV_PLAN_PRICES[newTier] >
-		DEV_PLAN_PRICES[personalOrg.devPlan as DevPlanTier];
+		DEV_PLAN_PRICES[newTier] > DEV_PLAN_PRICES[currentTier];
apps/code/src/app/dashboard/page.tsx (1)

66-74: Consider reducing polling frequency.

A 5-second polling interval for dev plan status is aggressive given that status typically only changes during explicit user actions. Consider increasing to 30 seconds or removing automatic polling in favor of refetching after mutations complete.

🧹 Nitpick comments (7)
apps/worker/src/worker.ts (1)

669-672: Consider batching organization fetches to avoid N+1 queries.

The organization query runs inside the loop over orgCosts, which can have up to BATCH_SIZE (100) entries. Pre-fetching all relevant organizations in a single query would reduce database round-trips.

Proposed optimization
+			// Pre-fetch all organizations that have costs to deduct
+			const orgIds = Array.from(orgCosts.keys());
+			const orgsData = await tx.query.organization.findMany({
+				where: { id: { in: orgIds } },
+			});
+			const orgsMap = new Map(orgsData.map((o) => [o.id, o]));
+
 			for (const [orgId, totalCost] of orgCosts.entries()) {
 				if (totalCost.greaterThan(0)) {
 					let remainingCost = totalCost;

-					// Fetch the organization to check for dev plan
-					const org = await tx.query.organization.findFirst({
-						where: { id: { eq: orgId } },
-					});
+					const org = orgsMap.get(orgId);

 					// First, try to deduct from dev plan credits if available
apps/code/src/app/layout.tsx (1)

17-20: Consider adding display: "swap" for consistency.

The Inter font is configured with display: "swap" but Geist_Mono is not. Adding it would ensure consistent font-loading behavior and prevent invisible text during load.

Suggested change
 const geistMono = Geist_Mono({
 	variable: "--font-mono",
 	subsets: ["latin"],
+	display: "swap",
 });
.env.example (1)

59-59: Fix dotenv-linter UnorderedKey warnings (optional).

This is low-impact, but it’ll keep .env.example consistent with the linter expectations.

Also applies to: 201-208

apps/api/src/routes/dev-plans.ts (1)

24-59: Avoid returning inactive/deleted API keys from getOrCreatePersonalOrgApiKey.

Right now status != "deleted" can return inactive keys and hand them out as the “Dev Plan API Key”. Consider filtering to status == "active" and (if you want to scope) description == "Dev Plan API Key" to avoid exposing unrelated keys.

apps/gateway/src/chat/chat.ts (1)

1854-1871: Consider extracting totalAvailableCredits computation (and avoid float drift).

The same “regular + dev plan remaining” math is duplicated 3 times and uses parseFloat on decimal strings. A small helper (and/or Decimal) would reduce repetition and edge-case drift.

Also applies to: 1961-1981, 2015-2028

apps/code/src/app/dashboard/page.tsx (2)

97-98: Consider logging errors for debugging.

The caught errors are discarded, which makes debugging production issues difficult. Consider logging to console or capturing in PostHog for observability.

-		} catch {
+		} catch (error) {
+			console.error("Subscribe failed:", error);
 			toast.error("Failed to start subscription");

294-299: Add aria-label for accessibility.

The show/hide toggle button lacks an accessible label for screen readers.

 <Button
 	variant="outline"
 	onClick={() => setShowApiKey(!showApiKey)}
+	aria-label={showApiKey ? "Hide API key" : "Show API key"}
 >
 	{showApiKey ? "Hide" : "Show"}
 </Button>
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe71aa8 and 7ba9c36.

⛔ Files ignored due to path filters (4)
  • apps/admin/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/code/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/playground/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
  • apps/ui/src/lib/api/v1.d.ts is excluded by !**/v1.d.ts
📒 Files selected for processing (12)
  • .env.example
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/routes/subscriptions.ts
  • apps/api/src/stripe.ts
  • apps/code/src/app/dashboard/page.tsx
  • apps/code/src/app/layout.tsx
  • apps/code/src/app/page.tsx
  • apps/gateway/src/chat/chat.ts
  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/worker/src/worker.ts
  • packages/db/src/schema.ts
  • packages/db/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/db/src/schema.ts
  • packages/db/src/types.ts
  • apps/api/src/routes/subscriptions.ts
🧰 Additional context used
📓 Path-based instructions (8)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/worker/src/worker.ts
  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/stripe.ts
  • apps/code/src/app/layout.tsx
  • apps/code/src/app/dashboard/page.tsx
  • apps/code/src/app/page.tsx
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • apps/worker/src/worker.ts
  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/stripe.ts
  • apps/code/src/app/layout.tsx
  • apps/code/src/app/dashboard/page.tsx
  • apps/code/src/app/page.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/worker/src/worker.ts
  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/stripe.ts
  • apps/code/src/app/layout.tsx
  • apps/code/src/app/dashboard/page.tsx
  • apps/code/src/app/page.tsx
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/worker/src/worker.ts
  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/stripe.ts
  • apps/code/src/app/layout.tsx
  • apps/code/src/app/dashboard/page.tsx
  • apps/code/src/app/page.tsx
**/*.spec.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Unit tests should use *.spec.ts file naming convention

Files:

  • apps/gateway/src/lib/rate-limit.spec.ts
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/stripe.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/stripe.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/gateway/src/lib/rate-limit.spec.ts
  • apps/gateway/src/chat/chat.ts
  • apps/api/src/routes/dev-plans.ts
  • apps/api/src/stripe.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to apps/{ui,playground}/**/*.{ts,tsx} : Use Next.js App Router with React Server Components for frontend development
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to apps/{ui,playground}/**/*.{ts,tsx} : Use Next.js App Router with React Server Components for frontend development

Applied to files:

  • apps/code/src/app/layout.tsx
  • apps/code/src/app/dashboard/page.tsx
  • apps/code/src/app/page.tsx
📚 Learning: 2025-12-03T12:42:26.162Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-03T12:42:26.162Z
Learning: Applies to apps/{ui,playground,docs}/**/*.{ts,tsx} : Use `next/link` for links and `next/navigation`'s router for programmatic navigation

Applied to files:

  • apps/code/src/app/layout.tsx
  • apps/code/src/app/dashboard/page.tsx
  • apps/code/src/app/page.tsx
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Applies to apps/{ui,playground}/**/*.{tsx} : Use `next/link` for links and `next/navigation`'s router for programmatic navigation

Applied to files:

  • apps/code/src/app/layout.tsx
  • apps/code/src/app/page.tsx
🧬 Code graph analysis (6)
apps/worker/src/worker.ts (1)
packages/db/src/schema.ts (1)
  • organization (109-162)
apps/gateway/src/chat/chat.ts (2)
packages/db/src/schema.ts (1)
  • organization (109-162)
packages/models/src/models.ts (1)
  • ModelDefinition (181-241)
apps/api/src/routes/dev-plans.ts (6)
packages/db/src/db.ts (1)
  • db (21-25)
packages/db/src/schema.ts (4)
  • shortid (39-39)
  • user (41-53)
  • apiKey (291-318)
  • project (264-289)
packages/db/src/index.ts (1)
  • tables (14-16)
apps/api/src/stripe.ts (1)
  • ensureStripeCustomer (34-71)
apps/api/src/routes/payments.ts (1)
  • stripe (13-18)
packages/logger/src/index.ts (2)
  • error (153-160)
  • logger (181-181)
apps/code/src/app/layout.tsx (3)
apps/code/postcss.config.mjs (1)
  • config (1-3)
apps/code/src/lib/config-server.ts (1)
  • getConfig (12-24)
apps/code/src/components/providers.tsx (1)
  • Providers (21-69)
apps/code/src/app/dashboard/page.tsx (4)
apps/code/src/lib/auth-client.ts (1)
  • useAuth (18-31)
apps/code/src/lib/config.tsx (1)
  • useAppConfig (25-31)
apps/code/src/lib/fetch-client.ts (1)
  • useApi (20-26)
apps/code/src/hooks/useUser.ts (1)
  • useUser (14-85)
apps/code/src/app/page.tsx (1)
apps/code/src/components/ui/button.tsx (1)
  • Button (60-60)
🪛 dotenv-linter (4.0.0)
.env.example

[warning] 59-59: [UnorderedKey] The CODE_URL key should go before the PLAYGROUND_URL key

(UnorderedKey)


[warning] 204-204: [UnorderedKey] The STRIPE_DEV_PLAN_MAX_PRICE_ID key should go before the STRIPE_DEV_PLAN_PRO_PRICE_ID key

(UnorderedKey)

🔇 Additional comments (16)
apps/worker/src/worker.ts (1)

662-667: LGTM!

The introduction of remainingCost and the updated comments clearly document the two-step deduction flow (dev plan credits first, then regular credits). Referral earnings correctly remain based on total cost, ensuring referrers receive consistent earnings regardless of how the referred org's costs are split.

Also applies to: 722-723

apps/code/src/app/layout.tsx (4)

1-9: LGTM!

Imports are well-organized with type imports separated, using top-level imports and path aliases consistently.


22-22: LGTM!

Using force-dynamic is appropriate here since getConfig() reads environment variables that need to be evaluated at runtime.


24-47: LGTM!

Metadata is comprehensive with proper metadataBase configuration for OG image resolution. The versioned query params on assets are a good cache-busting strategy.


49-59: LGTM!

The layout follows Next.js App Router conventions correctly. The suppressHydrationWarning is appropriate given the ThemeProvider in Providers that may cause attribute mismatches during hydration.

apps/code/src/app/page.tsx (7)

1-5: LGTM!

Imports are properly structured using top-level imports, next/link for navigation, and lucide-react icons. This follows the coding guidelines and retrieved learnings for Next.js App Router patterns.


45-63: LGTM!

Header navigation properly uses next/link for client-side navigation. The CTA copy has been updated from "Free Trial" to "Get Started," addressing the earlier review feedback about potentially misleading messaging.


65-87: LGTM!

Hero section is well-structured with clear messaging. The anchor link to #pricing properly connects to the pricing section's id attribute on line 124.


89-122: LGTM!

Features section accurately reflects the "3x your subscription price" multiplier mentioned in the PR objectives (DEV_PLAN_CREDITS_MULTIPLIER default 3). Clean grid layout with good semantic structure.


134-177: LGTM!

Pricing cards are well-implemented with proper key usage (plan.tier), conditional styling for the popular plan, and query parameter passing to signup. The pattern of passing ?plan=${plan.tier} enables pre-selection of the chosen plan on the signup page.


182-209: LGTM!

CTA section maintains consistent messaging. The footer's dynamic year (new Date().getFullYear()) is appropriate since this is a React Server Component, avoiding client-side re-evaluation concerns.


31-42: No action required—prices are already consistent.

The Max plan is priced at $179 in both the frontend code (line 33) and backend configuration (stripe.ts and dev-plans.ts). There is no discrepancy to address.

Likely an incorrect or invalid review comment.

apps/gateway/src/lib/rate-limit.spec.ts (1)

81-116: Test fixture updates look consistent with the new org schema.

Good expansion of the mocked organization shape to include dev-plan fields.

Also applies to: 135-170, 186-221, 304-339

apps/code/src/app/dashboard/page.tsx (3)

42-47: Verify Max plan price: PR description says $149, code shows $179.

The PR objectives state DEV_PLAN_PRICES (lite: 29, pro: 79, max: 149 as described), but the Max plan here is priced at $179. Please confirm which price is correct.


179-198: LGTM!

The layout structure, header navigation with next/link, and responsive design are well-implemented. The loading state handling provides good user feedback.


359-420: LGTM!

The "No Active Plan" view with the subscription grid is well-structured. The "Most Popular" badge positioning and plan cards provide clear visual hierarchy for users to make subscription decisions.

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

Comment on lines +706 to +725
// Get API key if user has an active dev plan
let apiKey: string | null = null;
if (personalOrg.devPlan !== "none") {
// Find the default project for this org
const project = await db.query.project.findFirst({
where: {
organizationId: {
eq: personalOrg.id,
},
},
});

if (project) {
apiKey = await getOrCreatePersonalOrgApiKey(
personalOrg.id,
project.id,
user.id,
);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid side effects in GET /status and make project selection deterministic.

/status auto-creates an API key (DB write) and findFirst for the project has no ordering, so which project gets an API key is nondeterministic. Consider:

  • move API key creation to an explicit POST (or create on subscribe/webhook), and
  • fetch the intended “Default Project” deterministically (e.g., by name or createdAt order).

Also applies to: 727-741

🤖 Prompt for AI Agents
In `@apps/api/src/routes/dev-plans.ts` around lines 706 - 725, The GET /status
handler currently performs a DB write by calling getOrCreatePersonalOrgApiKey
when personalOrg.devPlan !== "none" and uses db.query.project.findFirst without
ordering, causing nondeterministic project selection; change the handler to
remove side effects (do not create API keys on GET) and instead only read
existing keys, moving creation to an explicit POST endpoint (or
subscription/webhook flow) that calls getOrCreatePersonalOrgApiKey, and make
project lookup deterministic by replacing db.query.project.findFirst with a
deterministic query (e.g., findFirst with an orderBy createdAt or lookup by a
“Default Project” name/flag) so the code that currently references
personalOrg.devPlan, db.query.project.findFirst, and
getOrCreatePersonalOrgApiKey no longer performs writes on GET and selects a
predictable project.

Comment thread apps/api/src/stripe.ts
Comment on lines +281 to +304
// Check if this is a dev plan subscription
const isDevPlan = metadata?.subscriptionType === "dev_plan";
const devPlanTier = metadata?.devPlan as DevPlanTier | undefined;

logger.info(
`Found organization: ${organization.name} (${organization.id}), current plan: ${organization.plan}`,
`Found organization: ${organization.name} (${organization.id}), current plan: ${organization.plan}, isDevPlan: ${isDevPlan}`,
);

// Update organization with subscription ID and upgrade to pro plan
try {
const subscriptionId =
typeof subscription === "string" ? subscription : subscription?.id;

const result = await db
.update(tables.organization)
.set({
plan: "pro",
stripeSubscriptionId: subscriptionId,
subscriptionCancelled: false,
})
.where(eq(tables.organization.id, organizationId))
.returning();
if (isDevPlan && devPlanTier) {
// Handle dev plan subscription
const creditsLimit = getDevPlanCreditsLimit(devPlanTier);

await db
.update(tables.organization)
.set({
devPlan: devPlanTier,
devPlanCreditsLimit: creditsLimit.toString(),
devPlanCreditsUsed: "0",
devPlanBillingCycleStart: new Date(),
devPlanStripeSubscriptionId: subscriptionId,
devPlanCancelled: false,
})
.where(eq(tables.organization.id, organizationId));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate metadata.devPlan before using it (don’t cast untrusted strings).

const devPlanTier = metadata?.devPlan as DevPlanTier can turn an unexpected value into NaN credits and persist bad state. Validate metadata.devPlan is one of the known tiers before calling getDevPlanCreditsLimit.

🤖 Prompt for AI Agents
In `@apps/api/src/stripe.ts` around lines 281 - 304, The code unsafely casts
metadata.devPlan to DevPlanTier; validate metadata.devPlan against the allowed
DevPlanTier values before using it. In the block that sets isDevPlan and
devPlanTier, replace the unchecked cast by checking metadata?.devPlan is one of
the known tiers (e.g., via a Set/map or a type-guard function
isValidDevPlanTier(value): value is DevPlanTier) and only assign devPlanTier
when valid; if invalid, log/warn and skip calling getDevPlanCreditsLimit and the
DB update that writes devPlanCreditsLimit/related fields. Ensure
getDevPlanCreditsLimit is called only with a validated devPlanTier and that you
do not persist NaN or unexpected values.

Comment thread apps/api/src/stripe.ts
Comment on lines +348 to +353
lineItems: [
{
description: `Dev Plan ${devPlanTier.toUpperCase()} ($${creditsLimit} credits included)`,
amount: (session.amount_total || 0) / 100,
},
],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Invoice line item wording mixes currency and credits.

Dev Plan ... ($${creditsLimit} credits included) looks like dollars but it’s a credit limit; consider removing $ to avoid confusing invoices.

🤖 Prompt for AI Agents
In `@apps/api/src/stripe.ts` around lines 348 - 353, The invoice line item
description in the lineItems array mixes currency and credit units; update the
description construction (where description is built using devPlanTier and
creditsLimit) to remove the dollar sign and clearly indicate credits (e.g., "Dev
Plan ${devPlanTier.toUpperCase()} (${creditsLimit} credits included)") so it
doesn't read like a dollar amount; ensure only the description string is changed
and leave amount calculation (amount: (session.amount_total || 0) / 100)
untouched.

Comment thread apps/api/src/stripe.ts
Comment on lines +1108 to +1113
if (isDevPlanRenewal) {
// Handle dev plan renewal - reset credits
const creditsLimit = getDevPlanCreditsLimit(
organization.devPlan as DevPlanTier,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Defensively validate organization.devPlan before casting to DevPlanTier.

Even if “should not happen”, a DB inconsistency would currently compute NaN credits on renewal. Validate organization.devPlan in DEV_PLAN_PRICES (and handle "none").

🤖 Prompt for AI Agents
In `@apps/api/src/stripe.ts` around lines 1108 - 1113, Validate
organization.devPlan before casting to DevPlanTier: inside the isDevPlanRenewal
block, check that organization.devPlan is a string and that organization.devPlan
in DEV_PLAN_PRICES (and explicitly exclude "none") before calling
getDevPlanCreditsLimit; if the check fails, handle defensively by logging a
warning/error and either set creditsLimit to a safe default (e.g., 0) or abort
the renewal flow to avoid computing NaN. Update the code paths that use
creditsLimit (the existing getDevPlanCreditsLimit call and subsequent logic) to
use the validated value or early-return on invalid devPlan.

Comment thread apps/api/src/stripe.ts
Comment on lines +1431 to +1437
// Send dev plan cancelled email
await sendTransactionalEmail({
to: organization.billingEmail,
subject: "Your LLMGateway Dev Plan Has Been Cancelled",
html: generateSubscriptionCancelledEmailHtml(organization.name),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Suppress email failures in webhook handlers to avoid repeated Stripe retries.

Unlike the dev-plan checkout invoice email, this cancellation email isn’t guarded; a transient email failure can cause Stripe to retry the webhook and re-run side effects.

🤖 Prompt for AI Agents
In `@apps/api/src/stripe.ts` around lines 1431 - 1437, The cancellation email send
isn't protected and can surface transient failures to Stripe; wrap the await
sendTransactionalEmail call (the call using sendTransactionalEmail with
generateSubscriptionCancelledEmailHtml and organization.billingEmail) in a
try/catch inside the webhook handler so any errors are caught, logged via
processLogger or similar, and not re-thrown — do not change the handler's
success flow or return value so Stripe sees the webhook as handled even if email
sending fails.

Comment on lines +157 to +162
const handleCopyApiKey = async () => {
if (devPlanStatus?.apiKey) {
await navigator.clipboard.writeText(devPlanStatus.apiKey);
toast.success("API key copied to clipboard");
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add error handling for clipboard API.

navigator.clipboard.writeText can throw if permissions are denied or the API is unavailable. Wrap in try-catch to prevent unhandled rejections.

Suggested fix
 const handleCopyApiKey = async () => {
 	if (devPlanStatus?.apiKey) {
-		await navigator.clipboard.writeText(devPlanStatus.apiKey);
-		toast.success("API key copied to clipboard");
+		try {
+			await navigator.clipboard.writeText(devPlanStatus.apiKey);
+			toast.success("API key copied to clipboard");
+		} catch {
+			toast.error("Failed to copy API key");
+		}
 	}
 };
📝 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 handleCopyApiKey = async () => {
if (devPlanStatus?.apiKey) {
await navigator.clipboard.writeText(devPlanStatus.apiKey);
toast.success("API key copied to clipboard");
}
};
const handleCopyApiKey = async () => {
if (devPlanStatus?.apiKey) {
try {
await navigator.clipboard.writeText(devPlanStatus.apiKey);
toast.success("API key copied to clipboard");
} catch {
toast.error("Failed to copy API key");
}
}
};
🤖 Prompt for AI Agents
In `@apps/code/src/app/dashboard/page.tsx` around lines 157 - 162, The
handleCopyApiKey handler calls navigator.clipboard.writeText without error
handling; wrap the await call in a try-catch inside handleCopyApiKey (keeping
the existing devPlanStatus?.apiKey guard), toast.success on success and on
failure catch the error, log it (console.error or process logger) and call
toast.error with a clear message like "Failed to copy API key" (optionally
include error.message); ensure the function remains async and does not allow
unhandled rejections.

Comment thread apps/code/src/app/dashboard/page.tsx
Personal orgs should always have plan='free' since they use the
separate devPlan field for tracking coding plan subscriptions.
This adds a safeguard in the checkout webhook handler.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
apps/api/src/stripe.ts (1)

1556-1565: handleSubscriptionCreated doesn't distinguish dev plan subscriptions.

This handler unconditionally sets plan: "pro" and stripeSubscriptionId for all subscriptions. For dev plan subscriptions, this will incorrectly overwrite the organization state set by handleCheckoutSessionCompleted, assigning plan: "pro" when it should remain "free" (dev plans use separate devPlan* fields).

🛠️ Suggested fix
 async function handleSubscriptionCreated(
 	event: Stripe.CustomerSubscriptionCreatedEvent,
 ) {
 	const subscription = event.data.object;
 	const { customer, metadata } = subscription;

 	logger.info(
 		`Processing subscription created for customer: ${customer}, subscription: ${subscription.id}`,
 	);

 	const result = await resolveOrganizationFromStripeEvent({
 		metadata: metadata as { organizationId?: string } | undefined,
 		customer: typeof customer === "string" ? customer : customer?.id,
 		subscription: subscription.id,
 	});

 	if (!result) {
 		logger.error(
 			`Organization not found for customer: ${customer}, subscription: ${subscription.id}`,
 		);
 		return;
 	}

 	const { organizationId, organization } = result;

 	logger.info(
 		`Found organization: ${organization.name} (${organization.id}) for subscription creation`,
 	);

+	// Skip for dev plan subscriptions - they are handled by handleCheckoutSessionCompleted
+	const isDevPlan = metadata?.subscriptionType === "dev_plan";
+	if (isDevPlan) {
+		logger.info(
+			`Skipping subscription.created for dev plan subscription ${subscription.id} - handled by checkout.session.completed`,
+		);
+		return;
+	}
+
+	// Skip for personal orgs - they should use devPlan, not pro plan
+	if (organization.isPersonal) {
+		logger.warn(
+			`Skipping subscription.created for personal org ${organizationId} - personal orgs should use devPlan`,
+		);
+		return;
+	}
+
 	try {
 		await db
 			.update(tables.organization)
 			.set({
 				plan: "pro",
 				stripeSubscriptionId: subscription.id,
 				subscriptionCancelled: false,
 			})
🤖 Fix all issues with AI agents
In `@apps/api/src/stripe.ts`:
- Around line 1490-1495: Wrap the call to sendTransactionalEmail (the one using
organization.billingEmail and
generateSubscriptionCancelledEmailHtml(organization.name)) in a try/catch so
email-send failures don’t bubble up and trigger Stripe webhook retries; on error
catch and log the failure (include the organization id/name and the error) but
do not rethrow—allow the webhook handler to complete successfully.
- Around line 1122-1133: The dev plan renewal branch currently always inserts a
new transaction record (tables.transaction) using stripeInvoiceId = invoice.id
and type "dev_plan_renewal", which can create duplicates on webhook retries;
mirror the pro plan branch by first querying for an existing transaction with
stripeInvoiceId === invoice.id (and optionally type === "dev_plan_renewal")
using the same db/query helper, and only perform the insert if no existing
record is found—if found, skip the insert or update status as appropriate to
maintain idempotency.
- Around line 1163-1178: The pro plan branch inserts a subscription_start
transaction unconditionally, causing duplicates on webhook retries; before
inserting in the else branch, query tables.transaction for an existing record
with stripeInvoiceId === invoice.id (and same organizationId) like
handleCheckoutSessionCompleted does, and if one exists skip the insert (or
return early), otherwise proceed to insert the transaction with the same fields
(organizationId, type: "subscription_start", amount, currency, status,
stripePaymentIntentId, stripeInvoiceId, description).
♻️ Duplicate comments (5)
apps/api/src/stripe.ts (5)

20-32: Extract shared dev plan pricing constants to avoid duplication.

This code duplicates DEV_PLAN_PRICES, DevPlanTier, and getDevPlanCreditsLimit which also exist in apps/api/src/routes/dev-plans.ts. Consider extracting to a shared module to prevent drift when prices change.


281-283: Validate metadata.devPlan before using it.

The cast metadata?.devPlan as DevPlanTier can produce an invalid tier if Stripe metadata contains an unexpected value. While line 290 checks devPlanTier is truthy, it doesn't validate it's a known tier, which would cause getDevPlanCreditsLimit to return NaN.

🛠️ Suggested validation
 	// Check if this is a dev plan subscription
 	const isDevPlan = metadata?.subscriptionType === "dev_plan";
-	const devPlanTier = metadata?.devPlan as DevPlanTier | undefined;
+	const rawDevPlan = metadata?.devPlan;
+	const devPlanTier: DevPlanTier | undefined =
+		rawDevPlan && rawDevPlan in DEV_PLAN_PRICES
+			? (rawDevPlan as DevPlanTier)
+			: undefined;

348-353: Invoice line item wording mixes currency and credits.

The description ($${creditsLimit} credits included) appears to show a dollar amount but creditsLimit is a credit count. Remove the $ to avoid confusing invoices.

🔧 Suggested fix
-							description: `Dev Plan ${devPlanTier.toUpperCase()} ($${creditsLimit} credits included)`,
+							description: `Dev Plan ${devPlanTier.toUpperCase()} (${creditsLimit} credits included)`,

1116-1121: Defensively validate organization.devPlan before casting to DevPlanTier.

If organization.devPlan is "none" or an unexpected value due to database inconsistency, getDevPlanCreditsLimit will compute NaN credits. Validate before calling.

🛠️ Suggested validation
 	if (isDevPlanRenewal) {
 		// Handle dev plan renewal - reset credits
+		const currentDevPlan = organization.devPlan;
+		if (!currentDevPlan || !(currentDevPlan in DEV_PLAN_PRICES)) {
+			logger.error(
+				`Invalid devPlan "${currentDevPlan}" for organization ${organizationId} during renewal`,
+			);
+			return;
+		}
 		const creditsLimit = getDevPlanCreditsLimit(
-			organization.devPlan as DevPlanTier,
+			currentDevPlan as DevPlanTier,
 		);

1439-1444: Wrap email send in try/catch to avoid webhook retry loops.

Unlike the invoice email in handleCheckoutSessionCompleted (lines 356-361), this sendTransactionalEmail call is unguarded. A transient email failure will cause the webhook to fail and Stripe to retry, potentially re-running database side effects.

🛠️ Suggested fix
 		// Send dev plan cancelled email
+		try {
 		await sendTransactionalEmail({
 			to: organization.billingEmail,
 			subject: "Your LLMGateway Dev Plan Has Been Cancelled",
 			html: generateSubscriptionCancelledEmailHtml(organization.name),
 		});
+		} catch (e) {
+			logger.error(
+				"Dev plan cancellation email failed; suppressing webhook failure",
+				e as Error,
+			);
+		}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ba9c36 and 99285bc.

📒 Files selected for processing (1)
  • apps/api/src/stripe.ts
🧰 Additional context used
📓 Path-based instructions (7)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/api/src/stripe.ts
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • apps/api/src/stripe.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/api/src/stripe.ts
apps/{gateway,api}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use Hono framework with Zod validation and OpenAPI documentation for backend APIs

Files:

  • apps/api/src/stripe.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/api/src/stripe.ts
{apps/api,apps/gateway,packages/db}/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

{apps/api,apps/gateway,packages/db}/**/*.ts: Use Drizzle ORM with latest object syntax for database operations
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/api/src/stripe.ts
apps/{gateway,api}/src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

apps/{gateway,api}/src/**/*.ts: Use Hono for backend framework in Gateway and API services
Use Zod schemas for validation in Hono services

Files:

  • apps/api/src/stripe.ts
🧬 Code graph analysis (1)
apps/api/src/stripe.ts (4)
packages/db/src/schema.ts (2)
  • organization (109-162)
  • transaction (191-235)
apps/api/src/utils/invoice.ts (1)
  • generateAndEmailInvoice (192-283)
apps/api/src/posthog.ts (1)
  • posthog (3-6)
apps/api/src/utils/email.ts (1)
  • sendTransactionalEmail (38-105)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (17)
  • GitHub Check: build-unified (linux/amd64)
  • GitHub Check: build-split (gateway, linux/amd64)
  • GitHub Check: build-split (ui, linux/amd64)
  • GitHub Check: build-split (api, linux/amd64)
  • GitHub Check: build-split (playground, linux/amd64)
  • GitHub Check: build-split (admin, linux/amd64)
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: autofix
  • GitHub Check: generate / run
  • GitHub Check: test / run
  • GitHub Check: lint / run
  • GitHub Check: build / run
  • GitHub Check: title
🔇 Additional comments (1)
apps/api/src/stripe.ts (1)

386-394: LGTM!

Good safeguard to prevent personal orgs from being assigned the pro plan—aligns with the PR objective that personal orgs should use the devPlan field instead.

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

Comment thread apps/api/src/stripe.ts
Comment on lines +1122 to +1133
// Create transaction record for dev plan renewal
await db.insert(tables.transaction).values({
organizationId,
type: "subscription_start",
type: "dev_plan_renewal",
amount: (invoice.amount_paid / 100).toString(),
creditAmount: creditsLimit.toString(),
currency: invoice.currency.toUpperCase(),
status: "completed",
stripePaymentIntentId: (invoice as any).payment_intent,
stripeInvoiceId: invoice.id,
description: "Pro subscription started",
})
.returning();
description: `Dev Plan ${organization.devPlan?.toUpperCase()} renewed`,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing duplicate transaction check for dev plan renewal.

Similar to the pro plan branch, the dev plan renewal inserts a transaction without checking if one already exists for this stripeInvoiceId. Webhook retries could create duplicate dev_plan_renewal transactions.

🛠️ Suggested fix
+		// Check for existing transaction to avoid duplicates on webhook retry
+		const existingTransaction = await db.query.transaction.findFirst({
+			where: {
+				stripeInvoiceId: {
+					eq: invoice.id,
+				},
+			},
+		});
+
+		if (existingTransaction) {
+			logger.info(
+				"Dev plan renewal transaction already exists for invoice; skipping",
+				{ stripeInvoiceId: invoice.id },
+			);
+			return;
+		}
+
 		// Create transaction record for dev plan renewal
 		await db.insert(tables.transaction).values({
📝 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 transaction record for dev plan renewal
await db.insert(tables.transaction).values({
organizationId,
type: "subscription_start",
type: "dev_plan_renewal",
amount: (invoice.amount_paid / 100).toString(),
creditAmount: creditsLimit.toString(),
currency: invoice.currency.toUpperCase(),
status: "completed",
stripePaymentIntentId: (invoice as any).payment_intent,
stripeInvoiceId: invoice.id,
description: "Pro subscription started",
})
.returning();
description: `Dev Plan ${organization.devPlan?.toUpperCase()} renewed`,
});
// Check for existing transaction to avoid duplicates on webhook retry
const existingTransaction = await db.query.transaction.findFirst({
where: {
stripeInvoiceId: {
eq: invoice.id,
},
},
});
if (existingTransaction) {
logger.info(
"Dev plan renewal transaction already exists for invoice; skipping",
{ stripeInvoiceId: invoice.id },
);
return;
}
// Create transaction record for dev plan renewal
await db.insert(tables.transaction).values({
organizationId,
type: "dev_plan_renewal",
amount: (invoice.amount_paid / 100).toString(),
creditAmount: creditsLimit.toString(),
currency: invoice.currency.toUpperCase(),
status: "completed",
stripePaymentIntentId: (invoice as any).payment_intent,
stripeInvoiceId: invoice.id,
description: `Dev Plan ${organization.devPlan?.toUpperCase()} renewed`,
});
🤖 Prompt for AI Agents
In `@apps/api/src/stripe.ts` around lines 1122 - 1133, The dev plan renewal branch
currently always inserts a new transaction record (tables.transaction) using
stripeInvoiceId = invoice.id and type "dev_plan_renewal", which can create
duplicates on webhook retries; mirror the pro plan branch by first querying for
an existing transaction with stripeInvoiceId === invoice.id (and optionally type
=== "dev_plan_renewal") using the same db/query helper, and only perform the
insert if no existing record is found—if found, skip the insert or update status
as appropriate to maintain idempotency.

Comment thread apps/api/src/stripe.ts
Comment on lines +1163 to +1178
} else {
// Handle regular pro plan subscription
// Create transaction record for subscription start
const [transaction] = await db
.insert(tables.transaction)
.values({
organizationId,
type: "subscription_start",
amount: (invoice.amount_paid / 100).toString(),
currency: invoice.currency.toUpperCase(),
status: "completed",
stripePaymentIntentId: (invoice as any).payment_intent,
stripeInvoiceId: invoice.id,
description: "Pro subscription started",
})
.returning();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing duplicate transaction check for invoice payment.

Unlike handleCheckoutSessionCompleted (lines 411-420) which checks for existing transactions by stripeInvoiceId before inserting, this pro plan branch inserts unconditionally. If Stripe retries the webhook, duplicate subscription_start transactions will be created.

🛠️ Suggested fix
+		// Check for existing transaction to avoid duplicates on webhook retry
+		const existingTransaction = await db.query.transaction.findFirst({
+			where: {
+				stripeInvoiceId: {
+					eq: invoice.id,
+				},
+			},
+		});
+
+		if (existingTransaction) {
+			logger.info(
+				"Subscription transaction already exists for invoice; skipping duplicate insert",
+				{ stripeInvoiceId: invoice.id },
+			);
+			return;
+		}
+
 		// Create transaction record for subscription start
 		const [transaction] = await db
 			.insert(tables.transaction)
📝 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
} else {
// Handle regular pro plan subscription
// Create transaction record for subscription start
const [transaction] = await db
.insert(tables.transaction)
.values({
organizationId,
type: "subscription_start",
amount: (invoice.amount_paid / 100).toString(),
currency: invoice.currency.toUpperCase(),
status: "completed",
stripePaymentIntentId: (invoice as any).payment_intent,
stripeInvoiceId: invoice.id,
description: "Pro subscription started",
})
.returning();
} else {
// Handle regular pro plan subscription
// Check for existing transaction to avoid duplicates on webhook retry
const existingTransaction = await db.query.transaction.findFirst({
where: {
stripeInvoiceId: {
eq: invoice.id,
},
},
});
if (existingTransaction) {
logger.info(
"Subscription transaction already exists for invoice; skipping duplicate insert",
{ stripeInvoiceId: invoice.id },
);
return;
}
// Create transaction record for subscription start
const [transaction] = await db
.insert(tables.transaction)
.values({
organizationId,
type: "subscription_start",
amount: (invoice.amount_paid / 100).toString(),
currency: invoice.currency.toUpperCase(),
status: "completed",
stripePaymentIntentId: (invoice as any).payment_intent,
stripeInvoiceId: invoice.id,
description: "Pro subscription started",
})
.returning();
🤖 Prompt for AI Agents
In `@apps/api/src/stripe.ts` around lines 1163 - 1178, The pro plan branch inserts
a subscription_start transaction unconditionally, causing duplicates on webhook
retries; before inserting in the else branch, query tables.transaction for an
existing record with stripeInvoiceId === invoice.id (and same organizationId)
like handleCheckoutSessionCompleted does, and if one exists skip the insert (or
return early), otherwise proceed to insert the transaction with the same fields
(organizationId, type: "subscription_start", amount, currency, status,
stripePaymentIntentId, stripeInvoiceId, description).

Comment thread apps/api/src/stripe.ts
Comment on lines +1490 to +1495
// Send subscription cancelled email
await sendTransactionalEmail({
to: organization.billingEmail,
subject: "Your LLMGateway Subscription Has Been Cancelled",
html: generateSubscriptionCancelledEmailHtml(organization.name),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Wrap email send in try/catch to avoid webhook retry loops.

Same issue as the dev plan cancellation email—transient email failures will cause Stripe webhook retries.

🛠️ Suggested fix
 		// Send subscription cancelled email
+		try {
 		await sendTransactionalEmail({
 			to: organization.billingEmail,
 			subject: "Your LLMGateway Subscription Has Been Cancelled",
 			html: generateSubscriptionCancelledEmailHtml(organization.name),
 		});
+		} catch (e) {
+			logger.error(
+				"Subscription cancellation email failed; suppressing webhook failure",
+				e as Error,
+			);
+		}
🤖 Prompt for AI Agents
In `@apps/api/src/stripe.ts` around lines 1490 - 1495, Wrap the call to
sendTransactionalEmail (the one using organization.billingEmail and
generateSubscriptionCancelledEmailHtml(organization.name)) in a try/catch so
email-send failures don’t bubble up and trigger Stripe webhook retries; on error
catch and log the failure (include the organization id/name and the error) but
do not rethrow—allow the webhook handler to complete successfully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/worker/src/worker.ts`:
- Around line 679-715: The current read-then-write on
organization.devPlanCreditsUsed (via tx.query.organization.findFirst and then
tx.update(organization).set(...)) has a TOCTOU race; replace it with a single
atomic update so deduction and bounds-check happen in the DB. Implement an
atomic UPDATE on organization that sets devPlanCreditsUsed = devPlanCreditsUsed
+ LEAST(:remainingCost, GREATEST(devPlanCreditsLimit - devPlanCreditsUsed, 0))
(or equivalent SQL using LEAST/GREATEST) and add a WHERE clause for id = :orgId
and devPlan != 'none' and devPlanCreditsUsed < devPlanCreditsLimit; use the
number of affected rows and the computed new/old values returned (or a SELECT
... RETURNING) to determine how much was actually deducted and update
remainingCost accordingly, removing the separate tx.query.organization.findFirst
and the two-step calculation.
♻️ Duplicate comments (2)
apps/worker/src/worker.ts (2)

700-706: Credit precision risk: avoid Decimal -> number for balance updates.

This concern was raised in a previous review but remains unaddressed. toNumber() can introduce floating-point rounding errors, especially with fractional costs. Prefer keeping arithmetic in string/Decimal form for Postgres numeric math.

♻️ Proposed fix (keep arithmetic in numeric/string form)
-						const deductNumber = deductFromDevPlan.toNumber();
+						const deductValue = deductFromDevPlan.toString();

 						await tx
 							.update(organization)
 							.set({
-								devPlanCreditsUsed: sql`${organization.devPlanCreditsUsed} + ${deductNumber}`,
+								devPlanCreditsUsed: sql`${organization.devPlanCreditsUsed} + ${deductValue}::numeric`,
 							})
 							.where(eq(organization.id, orgId));

717-730: Same precision concern for regular credits deduction.

Consistent with the dev plan deduction, use .toString() instead of .toNumber() for the regular credits update to maintain numeric precision.

♻️ Proposed fix
 				if (remainingCost.greaterThan(0)) {
-					const costNumber = remainingCost.toNumber();
+					const costValue = remainingCost.toString();
 					await tx
 						.update(organization)
 						.set({
-							credits: sql`${organization.credits} - ${costNumber}`,
+							credits: sql`${organization.credits} - ${costValue}::numeric`,
 						})
 						.where(eq(organization.id, orgId));

 					logger.debug(
-						`Deducted ${costNumber} regular credits from organization ${orgId}`,
+						`Deducted ${costValue} regular credits from organization ${orgId}`,
 					);
 				}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 775d7c3 and ef2d2e6.

📒 Files selected for processing (5)
  • apps/worker/src/worker.ts
  • packages/db/migrations/1768492522_chilly_makkari.sql
  • packages/db/migrations/meta/1768492522_snapshot.json
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/schema.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/db/src/schema.ts
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx,js,jsx,json,md}

📄 CodeRabbit inference engine (CLAUDE.md)

Always use tabs for indentation

Files:

  • packages/db/migrations/meta/_journal.json
  • apps/worker/src/worker.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Never use any or as any unless absolutely necessary in TypeScript code
For database reads: Use db().query.<table>.findMany() or db().query.<table>.findFirst()

Files:

  • apps/worker/src/worker.ts
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx,js,jsx}: Always use top-level import, never use require or dynamic imports
No unnecessary code comments

Files:

  • apps/worker/src/worker.ts
**/*.{js,ts,tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Always use top-level import, never use require or dynamic imports

Files:

  • apps/worker/src/worker.ts
🧬 Code graph analysis (1)
apps/worker/src/worker.ts (1)
packages/db/src/schema.ts (1)
  • organization (109-162)
🔇 Additional comments (3)
packages/db/migrations/meta/_journal.json (1)

571-578: LGTM!

The journal entry correctly references the new migration with the proper sequential index and matching tag.

packages/db/migrations/1768492522_chilly_makkari.sql (1)

1-9: LGTM! Migration is well-structured and backward compatible.

The schema additions are sensible:

  • Appropriate use of numeric for credit fields (decimal precision)
  • UNIQUE constraint on dev_plan_stripe_subscription_id correctly allows multiple NULLs
  • Reasonable defaults ensure backward compatibility for existing rows
  • Timestamp columns align with existing schema pattern (plain timestamp without timezone is used consistently across the codebase)
apps/worker/src/worker.ts (1)

732-733: LGTM!

The comment correctly documents that referral earnings are calculated from the total cost (sum of both dev plan and regular credit deductions), which aligns with the implementation using totalCost on line 741.

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

Comment thread apps/worker/src/worker.ts
Comment on lines +679 to +715
// Fetch the organization to check for dev plan
const org = await tx.query.organization.findFirst({
where: { id: { eq: orgId } },
});

// First, try to deduct from dev plan credits if available
if (org && org.devPlan !== "none") {
const devPlanCreditsLimit = new Decimal(
org.devPlanCreditsLimit || "0",
);
const devPlanCreditsUsed = new Decimal(
org.devPlanCreditsUsed || "0",
);
const devPlanRemaining =
devPlanCreditsLimit.minus(devPlanCreditsUsed);

if (devPlanRemaining.greaterThan(0)) {
const deductFromDevPlan = Decimal.min(
remainingCost,
devPlanRemaining,
);
const deductNumber = deductFromDevPlan.toNumber();

await tx
.update(organization)
.set({
devPlanCreditsUsed: sql`${organization.devPlanCreditsUsed} + ${deductNumber}`,
})
.where(eq(organization.id, orgId));

logger.debug(
`Deducted ${deductNumber} dev plan credits from organization ${orgId}`,
);

remainingCost = remainingCost.minus(deductFromDevPlan);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential race condition on dev plan credits.

The pattern of reading devPlanCreditsUsed, calculating available credits, then updating introduces a TOCTOU (time-of-check to time-of-use) race. If two workers process logs for the same organization concurrently, both may read the same devPlanCreditsUsed value, calculate the full remaining budget is available, and both attempt to deduct—potentially over-spending the dev plan credit limit.

Consider adding a FOR UPDATE lock when fetching the organization, or using a single atomic SQL statement that performs the min calculation and update together.

🔒 Option 1: Add row-level lock when fetching organization
 				// Fetch the organization to check for dev plan
-				const org = await tx.query.organization.findFirst({
-					where: { id: { eq: orgId } },
-				});
+				const [org] = await tx
+					.select()
+					.from(organization)
+					.where(eq(organization.id, orgId))
+					.for("update");
🔒 Option 2: Atomic update with LEAST/GREATEST in SQL
+				// Atomically deduct from dev plan credits, capping at available balance
+				if (org && org.devPlan !== "none") {
+					const costValue = remainingCost.toString();
+					const result = await tx
+						.update(organization)
+						.set({
+							devPlanCreditsUsed: sql`${organization.devPlanCreditsUsed} + LEAST(
+								${costValue}::numeric,
+								GREATEST(${organization.devPlanCreditsLimit} - ${organization.devPlanCreditsUsed}, 0)
+							)`,
+						})
+						.where(eq(organization.id, orgId))
+						.returning({
+							deducted: sql<string>`LEAST(
+								${costValue}::numeric,
+								GREATEST(${organization.devPlanCreditsLimit} - ${organization.devPlanCreditsUsed}, 0)
+							)`,
+						});
+					// Use result[0].deducted to update remainingCost
+				}
🤖 Prompt for AI Agents
In `@apps/worker/src/worker.ts` around lines 679 - 715, The current
read-then-write on organization.devPlanCreditsUsed (via
tx.query.organization.findFirst and then tx.update(organization).set(...)) has a
TOCTOU race; replace it with a single atomic update so deduction and
bounds-check happen in the DB. Implement an atomic UPDATE on organization that
sets devPlanCreditsUsed = devPlanCreditsUsed + LEAST(:remainingCost,
GREATEST(devPlanCreditsLimit - devPlanCreditsUsed, 0)) (or equivalent SQL using
LEAST/GREATEST) and add a WHERE clause for id = :orgId and devPlan != 'none' and
devPlanCreditsUsed < devPlanCreditsLimit; use the number of affected rows and
the computed new/old values returned (or a SELECT ... RETURNING) to determine
how much was actually deducted and update remainingCost accordingly, removing
the separate tx.query.organization.findFirst and the two-step calculation.

steebchen and others added 3 commits January 15, 2026 16:51
Prevent negative percentage display when usage exceeds 100%.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add code builder and runtime stages to split.dockerfile
- Add code port 3004 to unified.dockerfile
- Add code program to supervisord.conf
- Add code service to docker-compose files (split and unified)
- Add code to images.yml workflow matrix
- Add code endpoint to test-split-docker.sh and test-unified-docker.sh
- Add code app to .github/start.sh smoke test

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@infra/docker-compose.unified.local.yml`:
- Line 13: Update the ORIGIN_URLS default in docker-compose.unified.local.yml to
include the code service on port 3004 so CORS defaults match the hardcoded
fallbacks in apps/gateway/src/app.ts and apps/api/src/index.ts; specifically
modify the ORIGIN_URLS environment variable default (symbol: ORIGIN_URLS) to
include http://localhost:3004 alongside the existing entries so that overridden
ORIGIN_URLS values won’t omit the code origin.
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef2d2e6 and 4373e5c.

📒 Files selected for processing (11)
  • .github/start.sh
  • .github/test-split-docker.sh
  • .github/test-unified-docker.sh
  • .github/workflows/images.yml
  • apps/code/src/app/dashboard/page.tsx
  • infra/docker-compose.split.local.yml
  • infra/docker-compose.split.yml
  • infra/docker-compose.unified.local.yml
  • infra/split.dockerfile
  • infra/supervisord.conf
  • infra/unified.dockerfile
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/code/src/app/dashboard/page.tsx
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-03T12:42:26.162Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-03T12:42:26.162Z
Learning: Run `pnpm build` to ensure production builds work

Applied to files:

  • infra/split.dockerfile
📚 Learning: 2025-12-03T12:42:14.219Z
Learnt from: CR
Repo: theopenco/llmgateway PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-03T12:42:14.219Z
Learning: Run `pnpm build` to ensure production builds work after code changes

Applied to files:

  • infra/split.dockerfile
🪛 Hadolint (2.14.0)
infra/split.dockerfile

[error] 120-120: COPY with more than 2 arguments requires the last argument to end with /

(DL3021)


[error] 121-121: COPY with more than 2 arguments requires the last argument to end with /

(DL3021)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (19)
  • GitHub Check: build-split (admin, linux/amd64)
  • GitHub Check: build-split (ui, linux/amd64)
  • GitHub Check: build-split (code, linux/amd64)
  • GitHub Check: build-split (api, linux/amd64)
  • GitHub Check: build-split (gateway, linux/amd64)
  • GitHub Check: build-split (docs, linux/amd64)
  • GitHub Check: build-split (worker, linux/amd64)
  • GitHub Check: build-split (playground, linux/amd64)
  • GitHub Check: build-unified (linux/amd64)
  • GitHub Check: test / run
  • GitHub Check: build / run
  • GitHub Check: lint / run
  • GitHub Check: generate / run
  • GitHub Check: e2e-shards (5)
  • GitHub Check: e2e-shards (4)
  • GitHub Check: e2e-shards (3)
  • GitHub Check: e2e-shards (2)
  • GitHub Check: e2e-shards (1)
  • GitHub Check: autofix
🔇 Additional comments (11)
infra/unified.dockerfile (1)

123-123: LGTM!

The addition of port 3004 for the Code service is consistent with the other infrastructure changes in this PR and follows the existing port numbering scheme.

.github/start.sh (1)

11-11: LGTM!

The "code" app entry and its port mapping (3004) are correctly added and consistent with the infrastructure changes across this PR.

Also applies to: 19-19

.github/test-unified-docker.sh (1)

25-25: LGTM!

The Code service endpoint is correctly configured for health checks and integration testing, following the same pattern as other services.

.github/workflows/images.yml (1)

59-59: LGTM!

The "code" app is consistently added to all three CI jobs (build-split, merge-split, test-split), ensuring complete CI coverage for the new Code service image. The split.dockerfile contains the required code target stage at line 264, confirming the build will succeed.

Also applies to: 130-130, 221-221

infra/supervisord.conf (1)

76-86: LGTM!

The new [program:code] section follows the established pattern for other Next.js apps (ui, playground, docs, admin) with consistent configuration for autostart, autorestart, logging, and environment variables. Port 3004 aligns with the other docker-compose configurations in this PR.

infra/split.dockerfile (2)

117-124: LGTM! Code builder stage follows established patterns.

The code-builder stage is consistent with other builder stages (api-builder, ui-builder, etc.).

Regarding the Hadolint warnings (DL3021) on lines 120-121: these are false positives. The --parents flag changes COPY semantics to preserve directory structure, making the destination . valid. The same pattern is used throughout this Dockerfile for all other builder stages (e.g., lines 57-58, 66-67, 75-76).


263-278: LGTM!

The code runtime stage mirrors the structure of other Next.js app runtime stages (ui, playground, docs, admin) with proper standalone output copying, port exposure, and environment configuration.

infra/docker-compose.split.yml (1)

209-237: LGTM!

The new code service follows the established pattern for frontend services in this compose file. The configuration is consistent with similar services (ui, playground, docs) including healthcheck, networking, and environment variables.

.github/test-split-docker.sh (2)

21-21: LGTM!

The code endpoint addition is consistent with the port configuration (3004) used across all other infrastructure files in this PR.


169-171: LGTM!

The code service override follows the same pattern as other services in the heredoc.

infra/docker-compose.split.local.yml (1)

232-263: LGTM!

The code service configuration for local development is well-structured and consistent with other frontend services in this file. The build target code correctly references the runtime stage defined in split.dockerfile, and the environment variables align with the production compose file.

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

ports:
- "3002:3002" # UI
- "3003:3003" # Playground
- "3004:3004" # Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the docker-compose file to understand the context
cat -n infra/docker-compose.unified.local.yml | head -50

Repository: theopenco/llmgateway

Length of output: 2230


🏁 Script executed:

# Search for ORIGIN_URLS and CORS-related configurations
rg -n "ORIGIN_URLS|allowedOrigins|cors" --type yaml --type ts --type json -C 2 | head -80

Repository: theopenco/llmgateway

Length of output: 4424


🏁 Script executed:

# Check if port 3004 is mentioned anywhere else in the codebase
rg -n "3004" --type yaml --type ts --type json -g '!node_modules' | head -40

Repository: theopenco/llmgateway

Length of output: 989


Add http://localhost:3004 to ORIGIN_URLS for consistency.

The port mapping and CODE_URL follow the established patterns. However, the ORIGIN_URLS default on line 28 does not include http://localhost:3004, even though the code's CORS middleware (in apps/gateway/src/app.ts and apps/api/src/index.ts) includes it in hardcoded fallback defaults. This inconsistency could cause CORS failures if ORIGIN_URLS is explicitly overridden without including port 3004.

Update line 28 to: - ORIGIN_URLS=${ORIGIN_URLS:-http://localhost:3002,http://localhost:3003,http://localhost:3004,http://localhost:4002}

🤖 Prompt for AI Agents
In `@infra/docker-compose.unified.local.yml` at line 13, Update the ORIGIN_URLS
default in docker-compose.unified.local.yml to include the code service on port
3004 so CORS defaults match the hardcoded fallbacks in apps/gateway/src/app.ts
and apps/api/src/index.ts; specifically modify the ORIGIN_URLS environment
variable default (symbol: ORIGIN_URLS) to include http://localhost:3004
alongside the existing entries so that overridden ORIGIN_URLS values won’t omit
the code origin.

steebchen and others added 4 commits January 15, 2026 17:24
…ion-ime9td' into terragon/add-dev-plans-subscription-ime9td
- Move DEV_PLAN_PRICES, DevPlanTier, and getDevPlanCreditsLimit
  to @llmgateway/shared for reuse across api and stripe modules
- Add getOrCreatePersonalOrg helper with transaction atomicity
  to prevent partial state on org creation failures
- Simplify dev-plans.ts handlers by using shared helpers

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@steebchen
steebchen merged commit 583aa65 into main Jan 15, 2026
26 of 28 checks passed
@steebchen
steebchen deleted the terragon/add-dev-plans-subscription-ime9td branch January 15, 2026 18:58
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