Skip to content

feat(billing): add subscriptions and plans support - #303

Merged
steebchen merged 21 commits into
mainfrom
feat/subscriptions
Jun 6, 2025
Merged

steebchen merged 21 commits into
mainfrom
feat/subscriptions

Conversation

@steebchen

@steebchen steebchen commented Jun 6, 2025

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Introduced subscription management with Pro plan upgrades, cancellations, and resumptions via the dashboard.
    • Added a billing and plan management UI integrated with Stripe for seamless payments.
    • Organizations can view and manage their subscription plan, expiration, and status.
  • Improvements

    • Provider key creation and advanced project modes now require a Pro subscription, with UI badges and upgrade prompts.
    • Enhanced organization and project settings to enforce subscription-based feature access.
    • Payment methods list auto-refreshes after adding new methods.
  • Database & API

    • Extended organization schema with subscription plan, expiration, and cancellation status.
    • Added new API endpoints for subscription lifecycle management.

steebchen added 13 commits June 6, 2025 17:25
Extended `schema.ts` with `stripeSubscriptionId`, `plan`, and
`planExpiresAt` fields for enhanced subscription management. Added
`subscriptions` route in `api`. Updated API type definitions to reflect
these changes. Improves flexibility in handling user plans and statuses.
Introduced `resolveOrganizationFromStripeEvent` to streamline Stripe
event handling and ensure accurate organization identification. Updated
event handlers across various Stripe events to utilize this helper,
improving code reuse and maintainability.

Additionally, updated UI toast messages in plan management to clarify
users may need to refresh the page after
Added PostHog tracking for subscription creation and cancellation in
Stripe event handlers. Captures relevant details such as organization
ID, plan changes, and source. Improves analytics on subscription
activity.
Included `plan` and `planExpiresAt` in organization test data and
`mode` in project test data for better simulation of real-world
scenarios in tests.
Included `plan` field in organization test data to align with
real-world scenarios and improve test coverage.
Included `plan` in organization setup and `mode` in project setup within
e2e tests. Enhances test fidelity by aligning test data with real-world
configurations.
Implemented API support for resuming canceled Pro subscriptions. Updated
`/subscriptions/resume-pro-subscription` route and added related type
definitions. Integrated functionality into UI plan management with
confirmation dialog and status update. Enhances user experience for
re-activating subscriptions.
Replaced `any` with specific Stripe event type annotations in event
handlers. Updated functions to destructure `event.data.object` for
relevant fields. Improves type safety and readability in Stripe
webhook processing.
Ensured safe handling of optional fields like `metadata` and `subscription` in Stripe event data. Converted `payment_method` and `customer` to consistent string identifiers. Improves robustness and error handling in webhook processing.
Added checks for `current_period_end` to avoid accessing undefined
fields in subscription objects. Improved robustness in Stripe event
processing. Updated UI to use `toDateString` for date display in plan
management.
Added `subscriptionCancelled` field to the organization schema. Updated
Stripe event handlers to track subscription status changes, ensuring
accurate updates for cancellations and reactivations. Integrated
PostHog tracking for subscription reactivation events.
Replaced `cancelAtPeriodEnd` with `subscriptionCancelled` in the schema. Updated subscription cancellation logic, UI components, and database migration. Refactored API routes for accurate subscription status management.
@coderabbitai

coderabbitai Bot commented Jun 6, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

This update introduces full support for Pro plan subscriptions, including backend schema changes, Stripe integration, new API endpoints for subscription management, and UI components for plan upgrades and billing. It enforces Pro plan restrictions on provider keys and project modes, adds plan information to organization data, and updates tests and documentation accordingly.

Changes

Files/Group Change Summary
.env.example, apps/api/.env.example Added STRIPE_PRO_PRICE_ID for Pro plan pricing.
apps/api/src/routes/index.ts Registered new /subscriptions route.
apps/api/src/routes/subscriptions.ts Added new subscription management API endpoints (create, cancel, resume, status) with Stripe integration.
apps/api/src/routes/organization.ts Extended organization schema with plan and planExpiresAt fields.
apps/api/src/routes/keys-provider.ts Added Pro plan check to provider key creation route; restricts to Pro organizations.
apps/api/src/routes/keys-provider.e2e.ts, apps/api/src/routes/keys-provider.spec.ts,
apps/gateway/src/api.e2e.ts, apps/gateway/src/api.spec.ts
Updated test fixtures to include plan: "pro" in organizations and mode: "api-keys" in projects.
apps/api/src/stripe.ts Added helper for resolving organization from Stripe events; implemented handlers for subscription lifecycle events; updated payment and setup intent handlers; integrated organization plan/expiration logic.
apps/api/src/routes/payments.ts, apps/gateway/src/worker.ts Removed redundant timestamp field updates during payment method and log inserts.
apps/ui/src/components/billing/plan-management.tsx New React component for managing plans and billing, including upgrade, cancel, and resume actions.
apps/ui/src/components/credits/payment-methods-management.tsx Ensured payment methods list is refreshed after adding a new method.
apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx Gated provider key creation behind Pro plan; added UI feedback and validation for non-Pro users.
apps/ui/src/components/settings/project-mode-settings.tsx Restricted selection of "api-keys" and "hybrid" project modes to Pro plan users; updated UI accordingly.
apps/ui/src/hooks/useOrganization.ts, apps/ui/src/lib/api/v1.d.ts Added plan and planExpiresAt fields to organization types and API schemas; documented new subscription endpoints.
apps/ui/src/routes/dashboard/_layout/settings.tsx Added PlanManagement to billing tab; updated payment methods section title/description.
packages/db/src/schema.ts, packages/db/migrations/1749245675_cuddly_krista_starr.sql,
packages/db/migrations/meta/1749245675_snapshot.json, packages/db/migrations/meta/_journal.json, packages/db/migrations/1749246512_little_adam_destine.sql, packages/db/migrations/meta/1749246512_snapshot.json
Added subscription fields to organization table; changed defaults for project and log tables; added unique constraints; updated migration journal and snapshots.
apps/api/src/routes/activity.spec.ts, apps/api/src/routes/logs.spec.ts, packages/db/logs.ts Added mode and usedMode fields with value "api-keys" to logs in test data and static logs array.

Sequence Diagram(s)

Pro Subscription Lifecycle (Create, Cancel, Resume, Status)

sequenceDiagram
    participant User
    participant UI (PlanManagement)
    participant API (/subscriptions)
    participant DB
    participant Stripe

    User->>UI (PlanManagement): Click "Upgrade to Pro"
    UI (PlanManagement)->>API (/subscriptions): POST /subscriptions/create-pro-subscription
    API (/subscriptions)->>DB: Fetch user & organization, check plan/payment method
    API (/subscriptions)->>Stripe: Create subscription
    Stripe-->>API (/subscriptions): Subscription created, client secret (if needed)
    API (/subscriptions)->>DB: Update organization with subscription details
    API (/subscriptions)-->>UI (PlanManagement): Return client secret, subscriptionId
    UI (PlanManagement)->>Stripe: (If needed) Confirm payment with client secret
    Stripe-->>UI (PlanManagement): Payment confirmation result
    UI (PlanManagement)-->>User: Show success/error

    User->>UI (PlanManagement): Click "Cancel Pro"
    UI (PlanManagement)->>API (/subscriptions): POST /subscriptions/cancel-pro-subscription
    API (/subscriptions)->>Stripe: Set subscription to cancel at period end
    API (/subscriptions)->>DB: Update org cancellation status
    API (/subscriptions)-->>UI (PlanManagement): { success: true }
    UI (PlanManagement)-->>User: Show cancellation confirmation

    User->>UI (PlanManagement): Click "Resume Pro"
    UI (PlanManagement)->>API (/subscriptions): POST /subscriptions/resume-pro-subscription
    API (/subscriptions)->>Stripe: Resume subscription
    API (/subscriptions)->>DB: Update org cancellation status
    API (/subscriptions)-->>UI (PlanManagement): { success: true }
    UI (PlanManagement)-->>User: Show resume confirmation

    UI (PlanManagement)->>API (/subscriptions): GET /subscriptions/status
    API (/subscriptions)->>DB: Fetch org subscription details
    API (/subscriptions)-->>UI (PlanManagement): Return plan, subscriptionId, planExpiresAt, cancelled
Loading

Provider Key Creation with Pro Plan Enforcement

sequenceDiagram
    participant User
    participant UI (ProviderKeyDialog)
    participant API (/keys-provider)
    participant DB

    User->>UI (ProviderKeyDialog): Attempt to create provider key
    UI (ProviderKeyDialog)->>API (/keys-provider): POST /keys-provider
    API (/keys-provider)->>DB: Fetch organization, check plan
    alt Plan is "pro"
        API (/keys-provider)->>DB: Proceed with provider key creation
        API (/keys-provider)-->>UI (ProviderKeyDialog): Success
    else Plan is "free"
        API (/keys-provider)-->>UI (ProviderKeyDialog): 403 error, "Pro required"
        UI (ProviderKeyDialog)-->>User: Show upgrade prompt
    end
Loading

Project Mode Selection with Pro Plan Restriction

sequenceDiagram
    participant User
    participant UI (ProjectModeSettings)
    participant API (/projects)
    participant DB

    User->>UI (ProjectModeSettings): Select project mode
    alt Mode requires Pro & user is not Pro
        UI (ProjectModeSettings)-->>User: Show "Pro Only" badge, disable selection
    else
        UI (ProjectModeSettings)->>API (/projects): PATCH /projects/:id
        API (/projects)->>DB: Update project mode
        API (/projects)-->>UI (ProjectModeSettings): Success
    end
Loading

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 637a7ab and b54c3c6.

📒 Files selected for processing (7)
  • apps/api/src/routes/activity.spec.ts (4 hunks)
  • apps/api/src/routes/logs.spec.ts (3 hunks)
  • packages/db/logs.ts (29 hunks)
  • packages/db/migrations/1749246512_little_adam_destine.sql (1 hunks)
  • packages/db/migrations/meta/1749246512_snapshot.json (1 hunks)
  • packages/db/migrations/meta/_journal.json (1 hunks)
  • packages/db/src/schema.ts (3 hunks)
✅ Files skipped from review due to trivial changes (5)
  • apps/api/src/routes/activity.spec.ts
  • packages/db/logs.ts
  • packages/db/migrations/1749246512_little_adam_destine.sql
  • apps/api/src/routes/logs.spec.ts
  • packages/db/migrations/meta/1749246512_snapshot.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/schema.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: e2e / run
  • GitHub Check: build / run
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@steebchen steebchen changed the title feat(billing): add subscriptions and plans support feat(billing): add subscriptions and plans support Jun 6, 2025

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

🔭 Outside diff range comments (2)
apps/api/src/routes/keys-provider.e2e.ts (1)

92-132: ⚠️ Potential issue

Remove duplicate skipped test for custom baseUrl.

The test.skip("POST /keys/provider with custom baseUrl", ...) block appears twice; remove the redundant copy to avoid confusion.

Also applies to: 134-174

🧰 Tools
🪛 GitHub Check: lint / run

[warning] 94-94:
Unexpected console statement

🪛 GitHub Check: generate / run

[warning] 94-94:
Unexpected console statement

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

229-238: 🛠️ Refactor suggestion

Use amount_received & currency for accurate credit calculation

paymentIntent.amount is the intended amount, not the captured one.
amount_received (in the succeeded event) guarantees correct value and protects against partial captures or currency adjustments.
Also, credits should be adjusted based on the actual currency.

-const { metadata, amount } = paymentIntent;
-// Convert amount from cents to dollars
-const amountInDollars = amount / 100;
+const { metadata, amount_received: amountReceived, currency } = paymentIntent;
+
+// TODO: handle non-USD currencies – for USD cents ➜ dollars:
+const amountInDollars = currency.toLowerCase() === "usd"
+  ? amountReceived / 100
+  : amountReceived; // fallback 1:1 for now
🧹 Nitpick comments (6)
apps/api/.env.example (1)

3-3: Include STRIPE_PRO_PRICE_ID for Pro plan pricing.

Consider updating documentation (e.g., README or deployment guides) to include guidance on obtaining and configuring this price ID.

.env.example (1)

97-97: Add Pro plan Stripe price ID placeholder.

The placeholder STRIPE_PRO_PRICE_ID is correctly added under payment configuration.

Consider clarifying in comments that this value must be obtained from Stripe and kept in sync with apps/api/.env.example.

apps/ui/src/components/settings/project-mode-settings.tsx (1)

118-142: Comprehensive UI restrictions for Pro-only features.

The implementation provides excellent user experience with:

  • Disabled radio buttons for restricted modes
  • Visual indicators (muted text, "Pro Only" badges)
  • Clear labeling of plan requirements

However, there's a minor styling redundancy in the description text className.

-								<p
-									className={`text-sm ${requiresPro && !isProPlan ? "text-muted-foreground" : "text-muted-foreground"}`}
-								>
+								<p className="text-sm text-muted-foreground">
apps/ui/src/components/billing/plan-management.tsx (1)

316-359: <Elements> wrapper is unused – remove it or render a PaymentElement

<Elements> is only needed when you embed Stripe Elements (e.g. <PaymentElement/>, <CardElement/>).
In this dialog you just call stripe.confirmCardPayment, so the wrapper adds overhead and a dev-warning (“You must pass either options or stripe”). Either:

- <Elements stripe={stripe}>
+ <>
...
- </Elements>
+ </>

or actually collect card details with a PaymentElement when no default PM exists.
Cleaning this up simplifies the DOM and avoids Stripe console noise.

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

99-108: Organization plan field is never updated on successful purchase

After creating the subscription you set stripeSubscriptionId & subscriptionCancelled, but leave plan and planExpiresAt unchanged.
Until the webhook fires, the UI thinks the org is still on the Free plan.

Consider an immediate optimistic update:

- stripeSubscriptionId: subscription.id,
- subscriptionCancelled: false,
+ stripeSubscriptionId: subscription.id,
+ plan: "pro",
+ subscriptionCancelled: false,

(or document that the webhook will back-fill and make the UI poll).

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

180-187: Handle the payment_intent.payment_failed branch or explicitly ignore

The case "payment_intent.payment_failed": is empty – Stripe will still retry, but logging the failure (and maybe notifying the user) helps troubleshooting.

case "payment_intent.payment_failed":
  console.warn("Payment failed for PI:", event.data.object.id);
  break;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 022d66d and 73c915f.

📒 Files selected for processing (24)
  • .env.example (1 hunks)
  • apps/api/.env.example (1 hunks)
  • apps/api/src/routes/index.ts (2 hunks)
  • apps/api/src/routes/keys-provider.e2e.ts (2 hunks)
  • apps/api/src/routes/keys-provider.spec.ts (1 hunks)
  • apps/api/src/routes/keys-provider.ts (1 hunks)
  • apps/api/src/routes/organization.ts (1 hunks)
  • apps/api/src/routes/subscriptions.ts (1 hunks)
  • apps/api/src/stripe.ts (7 hunks)
  • apps/gateway/src/api.e2e.ts (2 hunks)
  • apps/gateway/src/api.spec.ts (2 hunks)
  • apps/ui/src/components/billing/plan-management.tsx (1 hunks)
  • apps/ui/src/components/credits/payment-methods-management.tsx (2 hunks)
  • apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx (5 hunks)
  • apps/ui/src/components/settings/project-mode-settings.tsx (5 hunks)
  • apps/ui/src/hooks/useOrganization.ts (1 hunks)
  • apps/ui/src/lib/api/v1.d.ts (4 hunks)
  • apps/ui/src/routes/dashboard/_layout/settings.tsx (2 hunks)
  • packages/db/migrations/1749230483_spooky_ultimatum.sql (1 hunks)
  • packages/db/migrations/1749243276_parallel_ultron.sql (1 hunks)
  • packages/db/migrations/meta/1749230483_snapshot.json (1 hunks)
  • packages/db/migrations/meta/1749243276_snapshot.json (1 hunks)
  • packages/db/migrations/meta/_journal.json (1 hunks)
  • packages/db/src/schema.ts (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
apps/api/src/routes/index.ts (1)
apps/api/src/routes/subscriptions.ts (1)
  • subscriptions (11-11)
apps/ui/src/components/credits/payment-methods-management.tsx (1)
apps/ui/src/lib/fetch-client.ts (1)
  • $api (13-13)
apps/ui/src/components/billing/plan-management.tsx (9)
packages/db/src/schema.ts (1)
  • organization (94-115)
apps/ui/src/hooks/useOrganization.ts (1)
  • useDefaultOrganization (17-29)
apps/ui/src/lib/components/use-toast.ts (2)
  • useToast (192-192)
  • toast (192-192)
apps/ui/src/lib/fetch-client.ts (1)
  • $api (13-13)
apps/ui/src/lib/components/card.tsx (6)
  • Card (85-85)
  • CardHeader (86-86)
  • CardTitle (88-88)
  • CardDescription (90-90)
  • CardContent (91-91)
  • CardFooter (87-87)
apps/ui/src/lib/components/badge.tsx (1)
  • Badge (46-46)
apps/ui/src/lib/components/dialog.tsx (7)
  • Dialog (123-123)
  • DialogTrigger (132-132)
  • DialogContent (125-125)
  • DialogHeader (128-128)
  • DialogTitle (131-131)
  • DialogDescription (126-126)
  • DialogFooter (127-127)
apps/ui/src/lib/components/button.tsx (1)
  • Button (59-59)
apps/ui/src/lib/stripe.ts (1)
  • useStripe (19-37)
apps/ui/src/components/settings/project-mode-settings.tsx (6)
packages/db/src/schema.ts (1)
  • organization (94-115)
apps/ui/src/hooks/useOrganization.ts (1)
  • useDefaultOrganization (17-29)
apps/ui/src/lib/components/use-toast.ts (1)
  • toast (192-192)
apps/ui/src/lib/components/radio-group.tsx (1)
  • RadioGroupItem (41-41)
apps/ui/src/lib/components/label.tsx (1)
  • Label (24-24)
apps/ui/src/lib/components/badge.tsx (1)
  • Badge (46-46)
🪛 Biome (1.9.4)
apps/api/src/stripe.ts

[error] 340-341: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build / run
🔇 Additional comments (33)
packages/db/migrations/1749243276_parallel_ultron.sql (1)

1-1: Add subscription_cancelled column to organization table.

Looks correct; this column will track cancellation state.

Verify that the ORM schema definition (e.g., packages/db/src/schema.ts) is updated to include this new column to prevent schema mismatch.

apps/api/src/routes/keys-provider.e2e.ts (1)

23-23: Align e2e test setup with subscription and project mode.

The test fixtures correctly include plan: "pro" and mode: "api-keys" to match the new gating logic.

Also applies to: 37-37

apps/api/src/routes/keys-provider.spec.ts (1)

21-21: Ensure spec test organization uses Pro plan.

Adding plan: "pro" aligns with the new subscription requirement for provider keys.

apps/gateway/src/api.e2e.ts (2)

82-82: Ensure default values for new organization fields
The test now inserts plan: "pro", but migrations also introduce plan_expires_at, stripe_subscription_id, and subscription_cancelled. Confirm that those columns are nullable or have defaults so this insert won’t fail.


95-95: Override updated project mode default
Explicitly setting mode: "api-keys" preserves the previous behavior despite the default changing to credits. This is correct to maintain test isolation.

packages/db/migrations/meta/_journal.json (2)

96-102: Validate journal entry for migration 1749230483_spooky_ultimatum
The new entry is correctly appended with the matching version and breakpoint flag.


103-109: Validate journal entry for migration 1749243276_parallel_ultron
This entry appears properly ordered and formatted in the migration journal.

apps/ui/src/hooks/useOrganization.ts (1)

7-8: Confirm API response includes new plan fields
The interface now requires plan and planExpiresAt. Ensure the /orgs endpoint returns these properties (or they are defaulted) so the hook’s data remains type-safe at runtime.

apps/gateway/src/api.spec.ts (2)

66-66: Ensure defaults for new organization columns
Adding plan: "pro" aligns with the schema change, but verify that plan_expires_at, stripe_subscription_id, and subscription_cancelled are nullable or have defaults to avoid insertion errors.


79-79: Override updated project mode default
Explicitly setting mode: "api-keys" maintains the legacy behavior now that the default has shifted to credits.

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

11-11: Register new subscriptions route
The subscriptions handler is correctly imported and mounted on /subscriptions.

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

17-18: LGTM: Clean schema extension for subscription plans.

The addition of plan and planExpiresAt fields follows proper Zod validation patterns with appropriate type constraints. The enum restriction to "free" and "pro" plans and nullable expiration date are well-designed for the subscription model.

apps/ui/src/routes/dashboard/_layout/settings.tsx (3)

4-4: LGTM: Proper import of PlanManagement component.

Clean import statement following existing patterns.


305-305: LGTM: Logical placement of plan management component.

The PlanManagement component is appropriately placed in the billing tab, above the payment methods card.


308-312: LGTM: Clear payment methods card description.

The updated description better clarifies that payment methods are used for both credits and subscriptions, improving user understanding.

apps/api/src/routes/keys-provider.ts (1)

119-127: LGTM: Proper plan-based authorization for provider keys.

The pro plan check is well-implemented:

  • Correctly placed after organization access verification
  • Clear error message with actionable guidance
  • Prevents unnecessary downstream processing for unauthorized requests

The authorization logic aligns with the subscription model introduced in this PR.

apps/ui/src/components/credits/payment-methods-management.tsx (2)

192-192: LGTM: Appropriate hook import for cache management.

Adding useQueryClient hook import to support query invalidation functionality.


227-230: LGTM: Consistent cache invalidation pattern.

The query invalidation after successful card setup follows the same pattern used elsewhere in the component (lines 43-48, 82-87). This ensures the payment methods list refreshes immediately after adding a new payment method, providing a better user experience.

packages/db/migrations/1749230483_spooky_ultimatum.sql (1)

1-4: Database migration looks good with minor observations.

The migration correctly adds subscription-related fields to the organization table and updates the project mode default. The changes align well with the subscription feature implementation.

Consider adding an index on stripe_subscription_id for performance if subscription lookups will be frequent, though this can be done in a future migration if needed.

apps/ui/src/components/provider-keys/create-provider-key-dialog.tsx (5)

15-16: Good addition of required UI components.

The Alert and Badge imports are appropriately added to support the Pro plan gating UI elements.


72-72: Correct implementation of plan detection.

The isProPlan boolean correctly checks the organization's plan status for Pro plan detection.


94-103: Excellent user feedback for plan restrictions.

The form submission guard provides clear feedback to users about the upgrade requirement. The toast message is informative and actionable.


202-209: Good visual indication of feature restrictions.

The alert component effectively communicates the Pro plan requirement with a clear message and "Pro Only" badge. This provides immediate visual feedback to users.


280-284: Proper submit button state management.

The submit button is correctly disabled for non-Pro users, preventing interaction while maintaining visual consistency with other disabled states.

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

103-103: Appropriate field addition for Stripe integration.

The stripeSubscriptionId text field correctly supports Stripe subscription tracking without unnecessary constraints.


105-111: Well-designed subscription plan schema.

The plan enum with "free" and "pro" values, NOT NULL constraint, and "free" default value provides a solid foundation for subscription management. The planExpiresAt and subscriptionCancelled fields complete the subscription lifecycle tracking.

apps/ui/src/components/settings/project-mode-settings.tsx (5)

6-7: Appropriate imports for subscription plan features.

The addition of useDefaultOrganization hook and Badge component correctly supports the Pro plan restrictions functionality.


18-18: Correct organization data fetching.

The organization data is properly fetched to access the subscription plan information.


34-34: Accurate plan detection logic.

The isProPlan boolean correctly determines Pro plan status from the organization's plan field.


48-57: Excellent validation and user feedback.

The save handler properly validates Pro plan requirements and provides clear, actionable feedback through the toast message. This prevents users from inadvertently losing functionality.


102-115: Well-structured mode configuration with plan requirements.

The addition of requiresPro flags clearly identifies which modes require Pro plan access. The configuration is clean and maintainable.

apps/ui/src/lib/api/v1.d.ts (1)

1-5: Auto-generated file – safe to omit from manual review

openapi-typescript will overwrite any edits here on the next generation cycle, so I’m intentionally skipping detailed comments.

packages/db/migrations/meta/1749243276_snapshot.json (1)

528-533: Store timestamps with timezone

plan_expires_at is declared as timestamp (no time zone). Webhook code converts Unix seconds via new Date(...), producing UTC times, but the DB will silently assume the session’s timezone.
Use timestamptz to avoid subtle off-by-hours issues during DST changes or in multi-region setups.

-"plan_expires_at": { "type": "timestamp", … }
+"plan_expires_at": { "type": "timestamptz", … }

Comment thread packages/db/src/schema.ts
Comment thread apps/ui/src/components/billing/plan-management.tsx
Comment thread apps/ui/src/components/billing/plan-management.tsx Outdated
Comment thread packages/db/migrations/meta/1749230483_snapshot.json
Comment thread apps/api/src/routes/subscriptions.ts Outdated
Comment thread apps/api/src/routes/subscriptions.ts
Comment thread packages/db/migrations/meta/1749243276_snapshot.json
Comment thread apps/api/src/stripe.ts
steebchen added 6 commits June 6, 2025 22:25
Added validation to ensure the `STRIPE_PRO_PRICE_ID` environment variable is set. Throws an HTTPException if not defined, improving error handling for subscription creation.
Ensures `subscriptionCancelled` is set to `false` when a subscription is reactivated. Updates database to reflect accurate subscription status.
Eliminated unnecessary `createdAt` and `updatedAt` fields from database update operations to streamline queries and improve consistency. No changes to functional behavior.
Added detailed status handling for payments, including additional toast messages for `requires_action`,
`processing`, and `requires_capture` statuses. Improved user feedback for various payment scenarios.
Removed unnecessary default values for mode and usedMode fields in the schema. Ensures cleaner schema definitions without changing functionality.

@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

♻️ Duplicate comments (1)
apps/api/src/stripe.ts (1)

324-340: Guard optional properties with optional chaining to prevent crashes

firstLineItem.parent.subscription_item_details may be undefined. A single malformed invoice would currently throw and fail the entire webhook delivery.

🧰 Tools
🪛 Biome (1.9.4)

[error] 334-335: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

🧹 Nitpick comments (1)
apps/api/src/stripe.ts (1)

271-282: Consider using the centralized helper for consistency.

While the direct metadata extraction works, using resolveOrganizationFromStripeEvent would provide better consistency and validation across all handlers.

-	const { metadata, payment_method } = setupIntent;
-	const organizationId = metadata?.organizationId;
-
-	if (!organizationId || !payment_method) {
-		console.error("Missing organizationId or payment_method in setupIntent");
-		return;
-	}
+	const { metadata, payment_method } = setupIntent;
+	
+	if (!payment_method) {
+		console.error("Missing payment_method in setupIntent");
+		return;
+	}
+	
+	const result = await resolveOrganizationFromStripeEvent({ metadata });
+	if (!result) {
+		console.error("Could not resolve organization from setup intent");
+		return;
+	}
+	
+	const { organizationId } = result;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73c915f and 3f59434.

📒 Files selected for processing (6)
  • apps/api/src/routes/payments.ts (0 hunks)
  • apps/api/src/routes/subscriptions.ts (1 hunks)
  • apps/api/src/stripe.ts (7 hunks)
  • apps/gateway/src/api.e2e.ts (2 hunks)
  • apps/gateway/src/worker.ts (0 hunks)
  • apps/ui/src/components/billing/plan-management.tsx (1 hunks)
💤 Files with no reviewable changes (2)
  • apps/api/src/routes/payments.ts
  • apps/gateway/src/worker.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/gateway/src/api.e2e.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/ui/src/components/billing/plan-management.tsx
  • apps/api/src/routes/subscriptions.ts
🧰 Additional context used
🪛 Biome (1.9.4)
apps/api/src/stripe.ts

[error] 334-335: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)

⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build / run
  • GitHub Check: e2e / run
🔇 Additional comments (8)
apps/api/src/stripe.ts (8)

10-10: Good addition of proper typing.

Adding the Stripe type import improves type safety for the webhook handlers.


45-140: Excellent centralization of organization resolution logic.

The resolveOrganizationFromStripeEvent helper function is well-designed with multiple fallback strategies and comprehensive error handling. This improves maintainability and reduces code duplication across webhook handlers.


181-196: Proper subscription event handling added.

The webhook handler now correctly processes all necessary Stripe subscription lifecycle events. The integration looks complete and follows the established pattern.


210-225: Good refactoring to use centralized helper.

The updated function properly leverages the new organization resolution helper, improving consistency across the codebase.


291-292: Good handling of payment method type variation.

Properly handles both string and object forms of the payment method, improving robustness.


316-426: Solid subscription invoice processing logic.

The function correctly handles subscription invoice payments, upgrades organizations to pro plan, and includes proper error handling and analytics tracking. The verification query provides good debugging information.

🧰 Tools
🪛 Biome (1.9.4)

[error] 334-335: Change to an optional chain.

Unsafe fix: Change to an optional chain.

(lint/complexity/useOptionalChain)


428-496: Comprehensive subscription update handling.

The function properly manages subscription expiration dates, cancellation status, and tracks reactivation events. The logic correctly identifies when a previously cancelled subscription becomes active again.


498-550: Proper subscription cancellation and cleanup.

The function correctly downgrades organizations to the free plan, clears all subscription-related fields, and tracks the cancellation event. This ensures a clean state after subscription deletion.

Comment thread apps/api/src/stripe.ts
) {
const invoice = event.data.object;
const { customer, metadata } = invoice;
const subscription = (invoice as any).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.

🛠️ Refactor suggestion

Improve type safety for subscription access.

The type assertion (invoice as any).subscription bypasses TypeScript's type checking and could lead to runtime errors.

-	const subscription = (invoice as any).subscription;
+	const subscription = invoice.subscription;

If this causes type errors, consider updating the Stripe type definitions or using proper type guards.

📝 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 subscription = (invoice as any).subscription;
const subscription = invoice.subscription;
🤖 Prompt for AI Agents
In apps/api/src/stripe.ts at line 321, replace the unsafe type assertion
(invoice as any).subscription with a safer approach by either updating the
Stripe type definitions to include the subscription property or implementing a
type guard to check for the subscription property before accessing it. This will
improve type safety and prevent potential runtime errors.

@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

🧹 Nitpick comments (2)
packages/db/migrations/1749245675_cuddly_krista_starr.sql (2)

4-4: Add index and uniqueness constraint on stripe_subscription_id
Since you’ll frequently look up organizations by their Stripe subscription, consider adding a unique index to enforce one-to-one mapping and speed up queries.

Example diff:

 ALTER TABLE "organization" ADD COLUMN "stripe_subscription_id" text;
+CREATE UNIQUE INDEX idx_organization_stripe_sub_id ON "organization" ("stripe_subscription_id");

6-6: Prefer timezone-aware expiry timestamps
Storing expiry dates without time zone can cause ambiguity across regions. Consider using TIMESTAMPTZ:

-ALTER TABLE "organization" ADD COLUMN "plan_expires_at" timestamp;
+ALTER TABLE "organization" ADD COLUMN "plan_expires_at" timestamptz;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f59434 and 637a7ab.

📒 Files selected for processing (4)
  • packages/db/migrations/1749245675_cuddly_krista_starr.sql (1 hunks)
  • packages/db/migrations/meta/1749245675_snapshot.json (1 hunks)
  • packages/db/migrations/meta/_journal.json (1 hunks)
  • packages/db/src/schema.ts (3 hunks)
✅ Files skipped from review due to trivial changes (1)
  • packages/db/migrations/meta/1749245675_snapshot.json
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/db/migrations/meta/_journal.json
  • packages/db/src/schema.ts
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: build / run
🔇 Additional comments (3)
packages/db/migrations/1749245675_cuddly_krista_starr.sql (3)

1-2: Confirm downstream impact of dropping defaults on log
Removing the default values from log.mode and log.used_mode will require all INSERTs to explicitly set these columns. Verify that no existing queries or ORM mappings rely on the old defaults, or you may introduce failures.


3-3: Approve setting default for project.mode
Defaulting project.mode to 'credits' aligns with the new billing model and should work as intended.


7-7: Approve adding subscription_cancelled flag
A boolean defaulting to false is a clear, safe initial state for cancellations.

ALTER TABLE "log" ALTER COLUMN "used_mode" DROP DEFAULT;--> statement-breakpoint
ALTER TABLE "project" ALTER COLUMN "mode" SET DEFAULT 'credits';--> statement-breakpoint
ALTER TABLE "organization" ADD COLUMN "stripe_subscription_id" text;--> statement-breakpoint
ALTER TABLE "organization" ADD COLUMN "plan" text DEFAULT 'free' NOT NULL;--> statement-breakpoint

@coderabbitai coderabbitai Bot Jun 6, 2025

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

Split migration and enforce valid plan values
Adding a non-nullable column with a default rewrites the entire table. For large datasets, break it into steps and enforce allowed values via an ENUM or CHECK constraint:

-- 1. Add nullable column
-ALTER TABLE "organization" ADD COLUMN "plan" text DEFAULT 'free' NOT NULL;
+ALTER TABLE "organization" ADD COLUMN "plan" text;

-- 2. Backfill existing rows
+UPDATE "organization" SET "plan" = 'free';

-- 3. Lock in constraints
+ALTER TABLE "organization" ALTER COLUMN "plan" SET NOT NULL;
+ALTER TABLE "organization" ALTER COLUMN "plan" SET DEFAULT 'free';

-- 4. (Optional) Enforce allowed plans
+CREATE TYPE org_plan AS ENUM ('free', 'pro', 'enterprise');
+ALTER TABLE "organization" ALTER COLUMN "plan" TYPE org_plan USING plan::org_plan;
🤖 Prompt for AI Agents
In packages/db/migrations/1749245675_cuddly_krista_starr.sql at line 5, the
migration adds a non-nullable column with a default value in one step, which
rewrites the entire table and can be costly for large datasets. To fix this,
split the migration into multiple steps: first add the column as nullable
without a default, then backfill existing rows with the default value, and
finally alter the column to be non-nullable with the default. Additionally,
enforce valid plan values by defining an ENUM type or adding a CHECK constraint
to restrict the allowed values.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

ok but this still works right? this table is very small

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.

@steebchen Yes, absolutely! The migration will work perfectly fine for a small organization table. My suggestion was more of a scalability best practice for when tables have millions of rows. For small tables (which most organization tables are), the single-step approach you have is completely appropriate and will execute quickly without any issues.

The migration will add the column, set the default value, and make it non-nullable all in one go - no problems at all for your use case! 👍

🐰
♪ ♫ ♪
(◕‿◕)
_/

steebchen added 2 commits June 6, 2025 22:42
Added `mode` and `usedMode` fields to test data in logs and activity specs. Ensures comprehensive testing of mode-related functionalities.
Added unique constraints for `stripeCustomerId` and `stripeSubscriptionId` in the `organization` schema. Ensures constraints are reflected in migrations for data integrity.
@steebchen
steebchen requested a review from smakosh June 6, 2025 22:05
@steebchen
steebchen merged commit 4f84ea7 into main Jun 6, 2025
@steebchen
steebchen deleted the feat/subscriptions branch June 6, 2025 22:09
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.

1 participant