Skip to content

Feat/add new pricing#3758

Merged
Siumauricio merged 7 commits intocanaryfrom
feat/add-new-pricing
Feb 19, 2026
Merged

Feat/add new pricing#3758
Siumauricio merged 7 commits intocanaryfrom
feat/add-new-pricing

Conversation

@Siumauricio
Copy link
Copy Markdown
Contributor

@Siumauricio Siumauricio commented Feb 19, 2026

What is this PR about?

Please describe in a short paragraph what this PR is about.

Checklist

Before submitting this PR, please make sure that:

  • You created a dedicated branch based on the canary branch.
  • You have read the suggestions in the CONTRIBUTING.md file https://github.com/Dokploy/dokploy/blob/canary/CONTRIBUTING.md#pull-request
  • You have tested this PR in your local instance. If you have not tested it yet, please do so before submitting. This helps avoid wasting maintainers' time reviewing code that has not been verified by you.

Issues related (if applicable)

Screenshots (if applicable)

Greptile Summary

This PR introduces a new tiered pricing system (Hobby and Startup) alongside the existing legacy plan. It adds UI for upgrading/changing plans, updates Stripe webhook handling to calculate server quantities correctly for multi-item subscriptions, and implements an upgradeSubscription mutation for in-app plan changes.

Key changes:

  • Added Hobby tier: $4.50/mo per server (annual: $45.90/server/year)
  • Added Startup tier: $15/mo base (includes 3 servers) + $4.50/mo per additional server
  • Implemented upgrade flow for legacy users to switch to new plans
  • Updated webhook to handle Startup's multi-item subscriptions (base + extra servers)
  • Added upgradeSubscription endpoint to change plans via Stripe subscription update API

Critical issues found:

  • Annual pricing math doesn't match the advertised "20% off" discount
  • Subscription update logic uses index-based mapping that could fail with unexpected items
  • Missing validation for required Startup price IDs could create incomplete subscriptions
  • Webhook uses stale data when updating server quantities after payment success

Confidence Score: 2/5

  • This PR has multiple logical errors in critical billing code that could cause incorrect charges and subscription failures
  • The pricing calculation inconsistencies (20% off claimed but only ~15% given), subscription update logic that could fail with mismatched line items, missing validation for required price IDs, and use of stale data in the webhook all represent significant issues in payment-critical code. These bugs could lead to customer billing disputes, failed subscription updates, or incorrect server quota enforcement.
  • Pay close attention to apps/dokploy/components/dashboard/settings/billing/show-billing.tsx (pricing calculations), apps/dokploy/server/api/routers/stripe.ts (subscription update logic), apps/dokploy/server/utils/stripe.ts (price ID validation), and apps/dokploy/pages/api/stripe/webhook.ts (stale data usage)

Last reviewed commit: 6619043

Siumauricio and others added 5 commits February 19, 2026 02:04
…e options

- Added new pricing calculations for Hobby and Startup tiers, including server quantity handling.
- Implemented upgrade functionality for users on legacy plans to switch to Hobby or Startup plans.
- Updated Stripe webhook to correctly calculate server quantities based on subscription items.
- Refactored getStripeItems utility to accommodate new billing tiers and server quantity logic.
…ast notifications

- Integrated a confirmation dialog for upgrading plans, providing users with clear details about their current and new plans.
- Added toast notifications to inform users of successful upgrades or errors during the process.
- Updated UI elements for better styling and user experience in the billing section.
…ervers

- Modified the billing section to clarify that users can add more servers at the specified monthly rate.
- Added monthly pricing display for both Hobby and Startup tiers when the annual plan is selected.
- Enhanced user experience by providing clearer pricing information based on server quantity.
… Stripe router

- Updated the product ID array construction to directly include Hobby and Startup product IDs.
- Streamlined the refinement logic for server quantity validation, improving readability without altering functionality.
- Ensured consistent validation messages for the Startup plan requirements.
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

Comment thread apps/dokploy/components/dashboard/settings/billing/show-billing.tsx Outdated
Comment thread apps/dokploy/components/dashboard/settings/billing/show-billing.tsx Outdated
Comment on lines +251 to +261
const updateItems: Stripe.SubscriptionUpdateParams["items"] =
currentItems.map((item, i) => {
if (i < newItems.length) {
return {
id: item.id,
price: newItems[i]!.price,
quantity: newItems[i]!.quantity,
};
}
return { id: item.id, deleted: true };
});
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.

Subscription update logic can fail with mismatched item counts

The mapping assumes currentItems and newItems are aligned by index, but Startup tier creates 2 line items (base + extra servers) while Hobby creates 1. When upgrading from Startup (2 items) to Hobby (1 item), the second item will be marked deleted: true, but if downgrading from Hobby (1 item) to Startup (2 items), the loop at lines 263-267 adds the missing item. This works, but consider what happens if current subscription has unexpected items (e.g., manual adjustments in Stripe dashboard) - the index-based mapping could create incorrect results.

Suggested change
const updateItems: Stripe.SubscriptionUpdateParams["items"] =
currentItems.map((item, i) => {
if (i < newItems.length) {
return {
id: item.id,
price: newItems[i]!.price,
quantity: newItems[i]!.quantity,
};
}
return { id: item.id, deleted: true };
});
const updateItems: Stripe.SubscriptionUpdateParams["items"] = [
...currentItems.map((item) => ({ id: item.id, deleted: true })),
...newItems.map((newItem) => ({ price: newItem.price, quantity: newItem.quantity })),
];

const extraServerPrice = isAnnual
? HOBBY_PRICE_ANNUAL_ID || BASE_ANNUAL_MONTHLY_ID
: HOBBY_PRICE_MONTHLY_ID || BASE_PRICE_MONTHLY_ID;
if (basePrice) items.push({ price: basePrice, quantity: 1 });
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.

Empty basePrice creates incomplete subscription

If STARTUP_BASE_PRICE_MONTHLY_ID or STARTUP_BASE_PRICE_ANNUAL_ID is empty/undefined, the code only pushes base price if (basePrice). This means a Startup subscription could be created with only the extra servers line item, missing the base that includes the first 3 servers. This would result in incorrect billing.

Add validation to ensure required price IDs exist before creating startup subscriptions.

Comment on lines +517 to +520
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() => setUpgradeServerQty((q) => q + 1)}
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.

Hardcoded 3-second delay is a code smell

Using arbitrary delays to wait for data propagation is unreliable. If Stripe webhook processing takes longer than 3 seconds, the user will see stale data. If it's faster, the user waits unnecessarily. Consider using polling or webhook confirmation instead.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Feb 19, 2026

Additional Comments (1)

apps/dokploy/pages/api/stripe/webhook.ts
Using stale data after database update

admin.serversQuantity is retrieved before the database update on line 194. Line 205 should use serversQuantity (the newly calculated value) instead of admin.serversQuantity.

			await updateServersBasedOnQuantity(admin.id, serversQuantity);

- Eliminated outdated comments related to legacy plan detection and current pricing calculations.
- Improved code readability by removing unnecessary comments that no longer serve a purpose.
- Revised pricing calculations for Hobby and Startup tiers to reflect accurate annual rates with 20% discount.
- Updated comments for clarity and consistency in pricing logic.
- Adjusted billing display to show the correct annual pricing for additional servers.
@Siumauricio Siumauricio merged commit 7a62f47 into canary Feb 19, 2026
4 checks passed
@Siumauricio Siumauricio deleted the feat/add-new-pricing branch February 19, 2026 21:01
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