Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/openstatusHQ/openstatus. ๐ซ Status page with uptime monitoring & API monitoring as code ๐ซ openstatus.dev
bun drizzle-orm monitoring monitoring-as-code nextjs observability on-call open-source shadcn-ui status-page statuspage synthetic-monitoring tinybird turso uptime uptime-checker uptime-monitor
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343import { Events, setupAnalytics } from "@openstatus/analytics";import { and, eq } from "@openstatus/db";import { user, usersToWorkspaces } from "@openstatus/db/src/schema";import { SsoDisabledEmail, sendEmail } from "@openstatus/emails";import type { ServiceContext } from "@openstatus/services";import { downgradeWorkspaceToFree, getWorkspaceByStripeId, updateWorkspacePlan,} from "@openstatus/services/workspace";import { TRPCError } from "@trpc/server";import type Stripe from "stripe";import { z } from "zod";
import { removeDomainFromVercelIfUnused } from "../../lib/vercel";import { createTRPCRouter, publicProcedure } from "../../trpc";import { buildFromSubscriptionOrThrow, cancelSupersededSubscriptions, getCurrentSubscription, isNewerSubscription, listLiveSubscriptions, stripe,} from "./shared";
const webhookProcedure = publicProcedure.input( z.object({ // From type Stripe.Event event: z.object({ id: z.string(), account: z.string().nullish(), created: z.number(), data: z.object({ object: z.record(z.string(), z.any()), }), type: z.string(), }), }),);
export const webhookRouter = createTRPCRouter({ customerSubscriptionUpdated: webhookProcedure.mutation(async (opts) => { const eventSubscription = opts.input.event.data .object as Stripe.Subscription;
const customerId = typeof eventSubscription.customer === "string" ? eventSubscription.customer : eventSubscription.customer.id;
// Deliberately built from Stripe's live state rather than from // `event.data.object`: Stripe guarantees neither delivery order nor // exactly-once delivery, so a late or duplicated event would otherwise // replay an outdated item set โ re-enabling an addon the customer just // removed, or dropping one they just bought. Re-reading makes every // delivery converge on the same result. It also keeps // `current_period_end` trustworthy, which the raw payload is not: it is // serialised with the API version pinned on the Stripe *endpoint*, and // newer versions moved that field onto the subscription items. const { live, current } = await getCurrentSubscription(customerId);
// Nothing live left โ `customer.subscription.deleted` owns the downgrade. if (!current) { return; }
const ws = await getWorkspaceByStripeId({ input: { stripeId: customerId }, db: opts.ctx.db, }); if (!ws) { throw new TRPCError({ code: "BAD_REQUEST", message: "Workspace not found", }); }
const oldPlan = ws.plan;
const built = buildFromSubscriptionOrThrow(current);
// Subscription has no recognized plan item (e.g. a standalone addon sub); // nothing to sync here, unlike sessionCompleted which always has a plan. // Bail before cancelling anything: if the newest subscription is not one // we can classify, the plan may well be carried by an older one, and // retiring that would leave the workspace paying for nothing. if (!built) { return; }
// The workspace follows the newest active subscription; anything older is // a leftover from a plan change that went through checkout. Cancelling by // age rather than by "whichever subscription this event named" is what // stops a stale event from retiring the subscription the customer is // actually on. await cancelSupersededSubscriptions(live, current);
// No `reason` metadata: `customer.subscription.updated` fires on trivial // changes too, so let the audit no-op-skip drop rows where nothing // tracked changed. The `stripe-subscription-updated` actor id still // identifies the source on the rows that do land. await updateWorkspacePlan({ ctx: { workspace: ws, actor: { type: "system", job: "stripe-subscription-updated" }, db: opts.ctx.db, }, input: { plan: built.plan, subscriptionId: current.id, endsAt: new Date(current.current_period_end * 1000), paidUntil: new Date(current.current_period_end * 1000), limits: built.limits, }, });
const newPlan = built.plan; if (newPlan !== oldPlan) { const customer = await stripe.customers.retrieve(customerId); if (!customer.deleted && customer.email) { const userResult = await opts.ctx.db .select() .from(user) .where(eq(user.email, customer.email)) .get(); if (!userResult) return;
const planOrder = ["free", "starter", "team", "scale"] as const; const oldIndex = planOrder.indexOf(oldPlan ?? "free"); const newIndex = planOrder.indexOf(newPlan ?? "free");
const event = newIndex > oldIndex ? Events.UpgradeWorkspace : Events.DowngradeWorkspace;
const analytics = await setupAnalytics({ userId: `usr_${userResult.id}`, email: userResult.email || undefined, workspaceId: String(ws.id), plan: newPlan, }); await analytics.track(event); } } }), sessionCompleted: webhookProcedure.mutation(async (opts) => { const session = opts.input.event.data.object as Stripe.Checkout.Session; if (typeof session.subscription !== "string") { throw new TRPCError({ code: "BAD_REQUEST", message: "Missing or invalid subscription id", }); } const subscription = await stripe.subscriptions.retrieve( session.subscription, ); const customerId = typeof subscription.customer === "string" ? subscription.customer : subscription.customer.id;
const ws = await getWorkspaceByStripeId({ input: { stripeId: customerId }, db: opts.ctx.db, }); if (!ws) { throw new TRPCError({ code: "BAD_REQUEST", message: "Workspace not found", }); }
// A replayed or late `checkout.session.completed` can name a subscription // that a newer one has already superseded. Writing it would move the // workspace back to the older plan while the newer subscription keeps // billing, so leave the workspace to that subscription's own events. const { live, current } = await getCurrentSubscription(customerId);
if (current && isNewerSubscription(current, subscription)) { return; }
const built = buildFromSubscriptionOrThrow(subscription); if (!built) { console.error("Invalid plan"); throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid plan", }); }
// Checkout always opens a new subscription, so anything else still active // predates it and would keep billing. Retire it here instead of waiting // for an unrelated `customer.subscription.updated` to come along. await cancelSupersededSubscriptions(live, subscription);
await updateWorkspacePlan({ ctx: { workspace: ws, actor: { type: "system", job: "stripe-session-completed" }, db: opts.ctx.db, }, input: { plan: built.plan, subscriptionId: subscription.id, endsAt: new Date(subscription.current_period_end * 1000), paidUntil: new Date(subscription.current_period_end * 1000), limits: built.limits, reason: "checkout_session_completed", }, });
const customer = await stripe.customers.retrieve(customerId); if (!customer.deleted && customer.email) { const userResult = await opts.ctx.db .select() .from(user) .where(eq(user.email, customer.email)) .get(); if (!userResult) return;
const analytics = await setupAnalytics({ userId: `usr_${userResult.id}`, email: userResult.email || undefined, workspaceId: String(ws.id), plan: built.plan, }); await analytics.track(Events.UpgradeWorkspace); } }), customerSubscriptionDeleted: webhookProcedure.mutation(async (opts) => { const subscription = opts.input.event.data.object as Stripe.Subscription; const customerId = typeof subscription.customer === "string" ? subscription.customer : subscription.customer.id;
// Only the customer's *last* subscription going away is a downgrade. This // event also fires for a subscription we retired ourselves as superseded, // and for one Stripe cancelled after dunning while another still stands โ // in both cases the customer is still subscribed and the cascade below // would be destructive. const live = await listLiveSubscriptions(customerId);
if (live.length > 0) { return; }
const ws = await getWorkspaceByStripeId({ input: { stripeId: customerId }, db: opts.ctx.db, });
if (!ws) { throw new TRPCError({ code: "BAD_REQUEST", message: "Workspace not found", }); }
// System actor โ no user is attributable to an involuntary Stripe // cancellation. The service verb runs the whole trim in one audited // transaction; a failed audit insert rolls the downgrade back and the // webhook returns non-2xx so Stripe retries. const ctx: ServiceContext = { workspace: ws, actor: { type: "system", job: "stripe-subscription-deleted" }, db: opts.ctx.db, };
const { customDomains, ssoDisabled } = await downgradeWorkspaceToFree({ ctx, });
// Best-effort after commit: owners must know SSO stopped working, but a // mail failure must not fail the webhook into Stripe retries. if (ssoDisabled) { try { const owners = await opts.ctx.db .select({ email: user.email }) .from(usersToWorkspaces) .innerJoin(user, eq(user.id, usersToWorkspaces.userId)) .where( and( eq(usersToWorkspaces.workspaceId, ws.id), eq(usersToWorkspaces.role, "owner"), ), ) .all();
const to = owners .map((owner) => owner.email) .filter((email): email is string => Boolean(email));
if (to.length > 0) { await sendEmail({ from: "Thibault from OpenStatus <thibault@openstatus.dev>", subject: "SSO has been disabled for your workspace", to, react: SsoDisabledEmail(), }); } } catch (err) { console.error("Failed to notify owners about SSO being disabled:", err); } }
// Free plan has no custom-domain feature โ release each domain on Vercel // unless another workspace's page still holds it. Best-effort after // commit: a Vercel error must not fail the webhook into Stripe retries. for (const domain of customDomains) { try { await removeDomainFromVercelIfUnused(opts.ctx.db, domain); } catch (err) { console.error("Failed to release domain from Vercel:", { domain, error: err, }); } }
const customer = await stripe.customers.retrieve(customerId);
if (!customer.deleted && customer.email) { const userResult = await opts.ctx.db .select() .from(user) .where(eq(user.email, customer.email)) .get(); if (!userResult) return;
const analytics = await setupAnalytics({ userId: `usr_${userResult.id}`, email: customer.email || undefined, workspaceId: String(ws.id), plan: "free", }); await analytics.track(Events.DowngradeWorkspace); } }),});