diff --git a/apps/dashboard/src/components/dialogs/upgrade.tsx b/apps/dashboard/src/components/dialogs/upgrade.tsx index 219b0569..85726ab1 100644 --- a/apps/dashboard/src/components/dialogs/upgrade.tsx +++ b/apps/dashboard/src/components/dialogs/upgrade.tsx @@ -20,9 +20,10 @@ import { useQuery } from "@tanstack/react-query"; import { CalendarClock } from "lucide-react"; const PLANS = { - free: ["starter", "team"], - starter: ["team"], - team: [], + free: ["starter", "team", "scale"], + starter: ["team", "scale"], + team: ["scale"], + scale: [], } satisfies Record; export function UpgradeDialog( diff --git a/apps/dashboard/src/lib/rate-limit/chat.ts b/apps/dashboard/src/lib/rate-limit/chat.ts index c78c2631..07f56d38 100644 --- a/apps/dashboard/src/lib/rate-limit/chat.ts +++ b/apps/dashboard/src/lib/rate-limit/chat.ts @@ -10,6 +10,7 @@ const DAILY_LIMIT_BY_PLAN: Record = { free: 20, starter: 50, team: 50, + scale: 50, }; export type ChatRateLimitResult = { diff --git a/apps/status-page/src/data/plans.ts b/apps/status-page/src/data/plans.ts index 693bea14..f1fb2648 100644 --- a/apps/status-page/src/data/plans.ts +++ b/apps/status-page/src/data/plans.ts @@ -23,7 +23,7 @@ export const plans = [ description: "Perfect for uptime monitoring", price: 30, limits: { - monitors: 10, + monitors: 20, regions: 35, periodicity: "1m", "status-pages": 1, @@ -41,7 +41,7 @@ export const plans = [ description: "Perfect for global synthetic monitoring", price: 100, limits: { - monitors: 100, + monitors: 50, regions: 35, periodicity: "30s", "status-pages": 5, @@ -53,4 +53,22 @@ export const plans = [ "audit-log": true, }, }, + { + title: "Scale", + id: "scale", + description: "For teams running serious infrastructure at scale", + price: 500, + limits: { + monitors: 50, + regions: 35, + periodicity: "30s", + "status-pages": 10, + members: Number.POSITIVE_INFINITY, + "notification-channels": 20, + "custom-domain": true, + "password-protection": true, + "status-subscribers": true, + "audit-log": true, + }, + }, ]; diff --git a/apps/web/src/content/llms-context.ts b/apps/web/src/content/llms-context.ts index 42248d27..88dd7e87 100644 --- a/apps/web/src/content/llms-context.ts +++ b/apps/web/src/content/llms-context.ts @@ -18,8 +18,9 @@ export const PRODUCT_CONTEXT_MARKDOWN = `## Who it's for - **Hobby** — $0/month: 1 monitor, 6 regions, 10m check interval, 1 status page, 3 page components, 14-day data retention - **Starter** — $30/month: 20 monitors, 28 regions, 1m check interval, 1 status page, 20 components, 3-month retention, subscribers, custom domain, WhatsApp/SMS/PagerDuty alerts - **Pro** — $100/month: 50 monitors, 28 regions, 30s check interval, 5 status pages, 50 components, 12-month retention, private locations, OTel exporter, 20 notification channels +- **Scale** — $500/month: everything in Pro plus 10 status pages, 500 page components, and White Label, Magic Link auth, and IP Restriction included at no extra cost -Annual billing gives 2 months free (Starter $300/yr, Pro $1,000/yr). Pricing is available in USD, EUR, and INR. +Annual billing gives 2 months free (Starter $300/yr, Pro $1,000/yr, Scale $5,000/yr). Pricing is available in USD and EUR. ## Key Features diff --git a/apps/web/src/content/mdx-components/pricing-tabs.tsx b/apps/web/src/content/mdx-components/pricing-tabs.tsx index 6e763b77..5fbb525a 100644 --- a/apps/web/src/content/mdx-components/pricing-tabs.tsx +++ b/apps/web/src/content/mdx-components/pricing-tabs.tsx @@ -9,9 +9,9 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { useCallback } from "react"; const planColumns = [ - { plan: "free" as const, header: "Hobby" }, { plan: "starter" as const, header: "Starter" }, { plan: "team" as const, header: "Pro" }, + { plan: "scale" as const, header: "Scale" }, ] as const; export function PricingTabs() { @@ -67,7 +67,6 @@ function PricingUpdater({ interval }: { interval: BillingInterval }) { const formatPrice = useCallback( (plan: (typeof planColumns)[number]["plan"]) => { const config = allPlans[plan]; - if (plan === "free") return "Free - Hobby"; const price = getPriceConfig(plan, "USD", interval); const formatted = new Intl.NumberFormat(price.locale, { style: "currency", @@ -87,7 +86,7 @@ function PricingUpdater({ interval }: { interval: BillingInterval }) { const tables = document.querySelectorAll(".table-wrapper table"); for (const table of tables) { const headers = table.querySelectorAll("thead th"); - // The pricing table has 4 columns: Features | Hobby | Starter | Pro + // The pricing table has 4 columns: Features | Starter | Pro | Scale if (headers.length !== 4) continue; for (let i = 0; i < planColumns.length; i++) { diff --git a/apps/web/src/content/pages/unrelated/pricing.mdx b/apps/web/src/content/pages/unrelated/pricing.mdx index 1b3494bc..01725645 100644 --- a/apps/web/src/content/pages/unrelated/pricing.mdx +++ b/apps/web/src/content/pages/unrelated/pricing.mdx @@ -2,21 +2,21 @@ title: "Pricing" publishedAt: "2025-11-10" author: "Maximilian Kaske" -description: "Start free with uptime monitoring and a status page. Upgrade to Starter ($30/mo) or Pro ($100/mo) for more monitors, team collaboration, and advanced features. No credit card required." +description: "Start free with uptime monitoring and a status page. Upgrade to Starter ($30/mo), Pro ($100/mo), or Scale ($500/mo) for more monitors, team collaboration, and advanced features. No credit card required." category: "company" faq: - question: "What billing options are available?" - answer: "All paid plans are available with monthly or annual billing. Choose annual billing to get 2 months free — Starter at $300/year ($25/mo) and Pro at $1,000/year (~$83/mo)." + answer: "All paid plans are available with monthly or annual billing. Choose annual billing to get 2 months free — Starter at $300/year ($25/mo), Pro at $1,000/year (~$83/mo), and Scale at $5,000/year (~$416/mo)." - question: "Can I switch between monthly and annual billing?" answer: "Yes. You can switch between monthly and annual billing at any time from your dashboard." - - question: "What does the free plan include?" - answer: "The free plan includes one monitor, one status page with three components, and a minimum check interval of 10 minutes. No credit card required." + - question: "Do you offer a free plan?" + answer: "Yes. Our free Hobby plan includes one monitor, one status page with three components, and a minimum check interval of 10 minutes. No credit card required." - question: "What are add-ons and how are they billed?" answer: "Add-ons are workspace-level settings like White Label, Email Authentication, IP Restriction, and extra status pages. Once enabled, they apply to your entire workspace. Add-ons are billed monthly regardless of your plan's billing cycle." - question: "Can I upgrade or downgrade my plan?" answer: "Yes. You can upgrade or downgrade at any time from your dashboard. Changes take effect immediately, and billing is prorated." - question: "What currencies do you support?" - answer: "We support EUR, USD, and INR. Contact us at ping@openstatus.dev or book a call if you have questions about pricing in your currency." + answer: "We support EUR and USD. Contact us at ping@openstatus.dev or book a call if you have questions about pricing in your currency." - question: "Do you offer refunds?" answer: "Yes. If you are not happy with openstatus, reach out at ping@openstatus.dev within 30 days of your payment and we will issue a full refund — no questions asked." - question: "Can I cancel my subscription at any time?" @@ -33,50 +33,63 @@ faq: -| Features comparison | Free - Hobby |$30/month - Starter | $100/month - Pro | +| Features comparison | $30/month - Starter | $100/month - Pro | $500/month - Scale | | --- | --- | --- | --- | | [Status Pages](/status-page) | | | | -| Number of status pages | 1 | 1 +$20/mo./each | 5 +$20/mo./each | -| Number of components | 3 | 20 | 50 | +| Number of status pages | 1 +$20/mo./each | 5 +$20/mo./each | 10 +$20/mo./each | +| Number of components | 20 | 50 | 500 | | Maintenance status | + | + | + | | Toggle numbers visibility | + | + | + | -| Subscribers | | + | + | -| Custom domain | | + | + | -| Locale switcher | | + | + | -| Slack Agent | | + | + | -| White Label | | $300/mo. | $300/mo. | +| Subscribers | + | + | + | +| Custom domain | + | + | + | +| Locale switcher | + | + | + | +| Slack Agent | + | + | + | +| White Label | $300/mo. | $300/mo. | + | | **Audience** | | | | -| Password Protection | | + | + | -| Email Authentification | | $100/mo. | $100/mo. | -| IP Restriction | | $100/mo. | $100/mo. | +| Password Protection | + | + | + | +| Email Authentification | $100/mo. | $100/mo. | + | +| IP Restriction | $100/mo. | $100/mo. | + | | [Monitors](/uptime-monitoring) | | | | -| Check Interval | 10m | 1m | 30s | -| Number of monitors | 1 | 20 | 50 | +| Check Interval | 1m | 30s | 30s | +| Number of monitors | 20 | 50 | 50 | | Multi-region monitoring | + | + | + | -| Total regions | 6 | 28 | 28 | -| Regions per monitor | 6 | 6 | 28 | -| Data retention | 14 days | 3 months | 12 months | -| Response logs | | + | + | -| Private locations | | | + | -| OTel Exporter | | | + | -| Number of on-demand checks | 30/mo. | 100/mo. | 300/mo. | +| Total regions | 28 | 28 | 28 | +| Regions per monitor | 6 | 28 | 28 | +| Data retention | 3 months | 12 months | 24 months | +| Response logs | + | + | + | +| Private locations | | + | + | +| OTel Exporter | | + | + | | **Alerts** | | | | | Slack, Discord, Email, Webhook | + | + | + | -| WhatsApp | | + | + | -| SMS | | + | + | -| PagerDuty | | + | + | -| OpsGenie | | + | + | -| Grafana OnCall | | + | + | -| Number of notification channels | 1 | 10 | 20 | +| WhatsApp | + | + | + | +| SMS | + | + | + | +| PagerDuty | + | + | + | +| OpsGenie | + | + | + | +| Grafana OnCall | + | + | + | +| Number of notification channels | 10 | 20 | 20 | | **Collaboration** | | | | -| Team members | 1 | Unlimited | Unlimited | -| Audit log | | | + | +| Team members | Unlimited | Unlimited | Unlimited | +| Audit log | | + | + | --- +## Enterprise + +For teams that need more than what Scale offers — **SOC2 reports**, **SAML/SSO**, **custom regions**, **higher monitor and status page limits**, a **dedicated SLA**, custom contracts, and invoicing. We will tailor a plan to your requirements. + +Contact us + +--- + ## Frequently asked questions +
+ +Yes. Our free **Hobby** plan includes **one monitor**, **one status page with three components**, and a minimum check interval of **10 minutes**. No credit card required. + +
+
Yes. If you are not happy with openstatus, reach out at [ping@openstatus.dev](mailto:ping@openstatus.dev) within **30 days** of your payment and we will issue a full refund — no questions asked. @@ -85,7 +98,7 @@ Yes. If you are not happy with openstatus, reach out at [ping@openstatus.dev](ma
-All paid plans are available with **monthly** or **annual** billing. Choose annual billing to get **2 months free** — Starter at **$300/year** ($25/mo) and Pro at **$1,000/year** ($83/mo). +All paid plans are available with **monthly** or **annual** billing. Choose annual billing to get **2 months free** — Starter at **$300/year** ($25/mo), Pro at **$1,000/year** ($83/mo), and Scale at **$5,000/year** ($416/mo).
@@ -95,11 +108,6 @@ Yes. You can switch between monthly and annual billing at any time from your das
-
- -The free plan includes **one monitor**, **one status page with three components**, and a minimum check interval of **10 minutes**. No credit card required. - -
@@ -115,7 +123,7 @@ Yes. You can upgrade or downgrade at any time from your dashboard. Changes take
-We support **EUR**, **USD**, and **INR**. Contact us at [ping@openstatus.dev](mailto:ping@openstatus.dev) or [book a call](https://openstatus.dev/cal) if you have questions about pricing in your currency. +We support **EUR** and **USD**. Contact us at [ping@openstatus.dev](mailto:ping@openstatus.dev) or [book a call](https://openstatus.dev/cal) if you have questions about pricing in your currency.
diff --git a/packages/analytics/src/server.ts b/packages/analytics/src/server.ts index c4cede4b..2073052e 100644 --- a/packages/analytics/src/server.ts +++ b/packages/analytics/src/server.ts @@ -17,7 +17,7 @@ export type IdentifyProps = { fullName?: string | null; email?: string; workspaceId?: string; - plan?: "free" | "starter" | "team"; + plan?: "free" | "starter" | "team" | "scale"; // headers from the request location?: string; userAgent?: string; diff --git a/packages/api/src/router/stripe/utils.ts b/packages/api/src/router/stripe/utils.ts index 6880ec6a..f90f78e5 100644 --- a/packages/api/src/router/stripe/utils.ts +++ b/packages/api/src/router/stripe/utils.ts @@ -76,6 +76,23 @@ export const PLANS = [ }, }, }, + { + plan: "scale", + price: { + monthly: { + priceIds: { + test: "XXXX", + production: "price_1Te9BLBXJcTfzsyJlXLsuyFP", + }, + }, + yearly: { + priceIds: { + test: "XXXX", + production: "price_1Te9BLBXJcTfzsyJliabl2ou", + }, + }, + }, + }, ] satisfies Array<{ plan: WorkspacePlan; price: { diff --git a/packages/api/src/router/stripe/webhook.ts b/packages/api/src/router/stripe/webhook.ts index 84ed2fca..77cfbdbb 100644 --- a/packages/api/src/router/stripe/webhook.ts +++ b/packages/api/src/router/stripe/webhook.ts @@ -117,7 +117,7 @@ export const webhookRouter = createTRPCRouter({ .get(); if (!userResult) return; - const planOrder = ["free", "starter", "team"] as const; + const planOrder = ["free", "starter", "team", "scale"] as const; const oldIndex = planOrder.indexOf(oldPlan ?? "free"); const newIndex = planOrder.indexOf(newPlan ?? "free"); diff --git a/packages/db/src/schema/plan/config.ts b/packages/db/src/schema/plan/config.ts index a103fce4..7b906cea 100644 --- a/packages/db/src/schema/plan/config.ts +++ b/packages/db/src/schema/plan/config.ts @@ -3,7 +3,7 @@ import type { WorkspacePlan } from "../workspaces/validation"; import type { Addons, IntervalPrice, PlanLimits, Price } from "./schema"; type PlanConfig = { - title: "Hobby" | "Starter" | "Pro"; + title: "Hobby" | "Starter" | "Pro" | "Scale"; id: WorkspacePlan; description: string; price: IntervalPrice; @@ -24,8 +24,8 @@ export const allPlans: Record = { id: "free", description: "Perfect for personal projects", price: { - monthly: { USD: 0, EUR: 0, INR: 0 }, - yearly: { USD: 0, EUR: 0, INR: 0 }, + monthly: { USD: 0, EUR: 0 }, + yearly: { USD: 0, EUR: 0 }, }, addons: {}, limits: { @@ -71,8 +71,8 @@ export const allPlans: Record = { id: "starter", description: "Perfect for uptime monitoring", price: { - monthly: { USD: 30, EUR: 30, INR: 3_000 }, - yearly: { USD: 300, EUR: 300, INR: 30_000 }, + monthly: { USD: 30, EUR: 30 }, + yearly: { USD: 300, EUR: 300 }, }, addons: { "email-domain-protection": { @@ -82,7 +82,6 @@ export const allPlans: Record = { price: { USD: 100, EUR: 100, - INR: 10_000, }, }, "ip-restriction": { @@ -92,7 +91,6 @@ export const allPlans: Record = { price: { USD: 100, EUR: 100, - INR: 10_000, }, }, "white-label": { @@ -102,7 +100,6 @@ export const allPlans: Record = { price: { USD: 300, EUR: 300, - INR: 30_000, }, }, "status-pages": { @@ -111,7 +108,6 @@ export const allPlans: Record = { price: { USD: 20, EUR: 20, - INR: 2_000, }, }, }, @@ -158,8 +154,8 @@ export const allPlans: Record = { id: "team", description: "Perfect for global synthetic monitoring", price: { - monthly: { USD: 100, EUR: 100, INR: 10_000 }, - yearly: { USD: 1_000, EUR: 1_000, INR: 100_000 }, + monthly: { USD: 100, EUR: 100 }, + yearly: { USD: 1_000, EUR: 1_000 }, }, addons: { "email-domain-protection": { @@ -169,7 +165,6 @@ export const allPlans: Record = { price: { USD: 100, EUR: 100, - INR: 10_000, }, }, "ip-restriction": { @@ -179,7 +174,6 @@ export const allPlans: Record = { price: { USD: 100, EUR: 100, - INR: 10_000, }, }, "white-label": { @@ -189,7 +183,6 @@ export const allPlans: Record = { price: { USD: 300, EUR: 300, - INR: 30_000, }, }, "status-pages": { @@ -198,7 +191,6 @@ export const allPlans: Record = { price: { USD: 20, EUR: 20, - INR: 2_000, }, }, }, @@ -240,4 +232,60 @@ export const allPlans: Record = { "slack-agent": true, }, }, + scale: { + title: "Scale", + id: "scale", + description: "For teams running serious infrastructure at scale", + price: { + monthly: { USD: 500, EUR: 500 }, + yearly: { USD: 5_000, EUR: 5_000 }, + }, + addons: { + "status-pages": { + title: "Status Pages", + description: "Create and manage status pages for your workspace.", + price: { + USD: 20, + EUR: 20, + }, + }, + }, + limits: { + version: undefined, + monitors: 50, + "synthetic-checks": 300, + periodicity: ["30s", "1m", "5m", "10m", "30m", "1h"], + "multi-region": true, + "max-regions": AVAILABLE_REGIONS.length, + "data-retention": "12 months", + "status-pages": 10, + "page-components": 500, + maintenance: true, + "monitor-values-visibility": true, + "response-logs": true, + screenshots: true, + otel: true, + "status-subscribers": true, + "custom-domain": true, + i18n: true, + "password-protection": true, + "email-domain-protection": true, + "ip-restriction": true, + "white-label": true, + "no-index": true, + notifications: true, + sms: true, + "sms-limit": 100, + pagerduty: true, + opsgenie: true, + "grafana-oncall": true, + whatsapp: true, + "notification-channels": 20, + members: "Unlimited", + "audit-log": true, + regions: [...AVAILABLE_REGIONS], + "private-locations": true, + "slack-agent": true, + }, + }, }; diff --git a/packages/db/src/schema/plan/schema.ts b/packages/db/src/schema/plan/schema.ts index 143ef95b..a84a1f35 100644 --- a/packages/db/src/schema/plan/schema.ts +++ b/packages/db/src/schema/plan/schema.ts @@ -70,7 +70,6 @@ export type Limits = z.infer; const priceSchema = z.object({ USD: z.number(), EUR: z.number(), - INR: z.number(), }); export type Price = z.infer; @@ -101,14 +100,7 @@ export const addonsSchema = z.partialRecord( export type Addons = z.infer; -/** - * Enforces that addon keys in Limits must be set to false in plan configs - * (since addons can only be enabled by purchasing them) - */ -export type PlanLimits = { - [K in keyof Limits]: K extends keyof Addons - ? Limits[K] extends boolean - ? false // Force addon boolean fields to false - : Limits[K] // Non-boolean fields stay as-is - : Limits[K]; // Non-addon fields stay as-is -}; +// Addon flags can be true at the plan level when the plan bundles the addon +// by default (e.g. Scale includes white-label / magic-link / IP restriction). +// Otherwise they default to false and are flipped on via the addon purchase flow. +export type PlanLimits = Limits; diff --git a/packages/db/src/schema/plan/utils.ts b/packages/db/src/schema/plan/utils.ts index 6609dddd..610209c4 100644 --- a/packages/db/src/schema/plan/utils.ts +++ b/packages/db/src/schema/plan/utils.ts @@ -21,14 +21,10 @@ export function getPlanConfig(plan: WorkspacePlan | null) { export function getCurrency({ continent, - country, }: { continent: string; country: string; }) { - if (country === "IN") { - return "INR"; - } if (continent === "EU") { return "EUR"; } @@ -38,7 +34,6 @@ export function getCurrency({ type PriceObject = { USD: number; EUR: number; - INR: number; }; type PriceConfig = { @@ -88,7 +83,7 @@ export function getPlansForLimit( limit: keyof Limits, ): WorkspacePlan[] { const currentLimitValue = allPlans[currentPlan].limits[limit]; - const planOrder: WorkspacePlan[] = ["free", "starter", "team"]; + const planOrder: WorkspacePlan[] = ["free", "starter", "team", "scale"]; // Get plans that come after the current plan const availablePlans = planOrder.filter((plan) => { diff --git a/packages/db/src/schema/workspaces/constants.ts b/packages/db/src/schema/workspaces/constants.ts index a5e9c62f..8656ff8e 100644 --- a/packages/db/src/schema/workspaces/constants.ts +++ b/packages/db/src/schema/workspaces/constants.ts @@ -1,4 +1,4 @@ -export const workspacePlans = ["free", "starter", "team"] as const; +export const workspacePlans = ["free", "starter", "team", "scale"] as const; export const workspaceRole = ["owner", "admin", "member"] as const; export const workspacePlanHierarchy: Record< @@ -8,4 +8,5 @@ export const workspacePlanHierarchy: Record< free: 0, starter: 1, team: 2, + scale: 3, };