feat(db): mesh data model — meshes, members, invites, audit log

- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 21:19:32 +01:00
commit d3163a5bff
1384 changed files with 314925 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
import { useQuery } from "@tanstack/react-query";
import { billing } from "~/modules/billing/lib/api";
export const useCustomer = () => useQuery(billing.queries.customer.get);

View File

@@ -0,0 +1,38 @@
import { handle } from "@turbostarter/api/utils";
import { api } from "~/lib/api/client";
import type { InferRequestType } from "hono/client";
const KEY = "billing";
const queries = {
customer: {
get: {
queryKey: [KEY, "customer"],
queryFn: () => handle(api.billing.customer.$get)(),
},
},
};
const mutations = {
portal: {
get: {
mutationKey: [KEY, "portal"],
mutationFn: (data: InferRequestType<typeof api.billing.portal.$get>) =>
handle(api.billing.portal.$get)(data),
},
},
checkout: {
create: {
mutationKey: [KEY, "checkout"],
mutationFn: (data: InferRequestType<typeof api.billing.checkout.$post>) =>
handle(api.billing.checkout.$post)(data),
},
},
};
export const billing = {
queries,
mutations,
};

View File

@@ -0,0 +1,142 @@
import { PricingPlanType, FEATURES } from "@turbostarter/billing";
interface PlanFeature {
readonly id: string;
readonly available: boolean;
readonly title: string;
readonly addon?: React.ReactNode;
}
export const PLAN_FEATURES: Record<PricingPlanType, PlanFeature[]> = {
[PricingPlanType.FREE]: [
{
id: FEATURES[PricingPlanType.FREE].SYNC,
available: true,
title: "billing:plan.starter.features.sync",
},
{
id: FEATURES[PricingPlanType.FREE].BASIC_SUPPORT,
available: true,
title: "billing:plan.starter.features.basicSupport",
},
{
id: FEATURES[PricingPlanType.FREE].LIMITED_STORAGE,
available: true,
title: "billing:plan.starter.features.limitedStorage",
},
{
id: FEATURES[PricingPlanType.FREE].EMAIL_NOTIFICATIONS,
available: true,
title: "billing:plan.starter.features.emailNotifications",
},
{
id: FEATURES[PricingPlanType.FREE].BASIC_REPORTS,
available: true,
title: "billing:plan.starter.features.basicReports",
},
{
id: FEATURES[PricingPlanType.PREMIUM].ADVANCED_SYNC,
available: false,
title: "billing:plan.premium.features.advancedSync",
},
{
id: FEATURES[PricingPlanType.PREMIUM].PRIORITY_SUPPORT,
available: false,
title: "billing:plan.premium.features.prioritySupport",
},
{
id: FEATURES[PricingPlanType.PREMIUM].MORE_STORAGE,
available: false,
title: "billing:plan.premium.features.moreStorage",
},
],
[PricingPlanType.PREMIUM]: [
{
id: FEATURES[PricingPlanType.PREMIUM].ADVANCED_SYNC,
available: true,
title: "billing:plan.premium.features.advancedSync",
},
{
id: FEATURES[PricingPlanType.PREMIUM].PRIORITY_SUPPORT,
available: true,
title: "billing:plan.premium.features.prioritySupport",
},
{
id: FEATURES[PricingPlanType.PREMIUM].MORE_STORAGE,
available: true,
title: "billing:plan.premium.features.moreStorage",
},
{
id: FEATURES[PricingPlanType.PREMIUM].TEAM_COLLABORATION,
available: true,
title: "billing:plan.premium.features.teamCollaboration",
},
{
id: FEATURES[PricingPlanType.PREMIUM].SMS_NOTIFICATIONS,
available: true,
title: "billing:plan.premium.features.smsNotifications",
},
{
id: FEATURES[PricingPlanType.PREMIUM].ADVANCED_REPORTS,
available: true,
title: "billing:plan.premium.features.advancedReports",
},
{
id: FEATURES[PricingPlanType.ENTERPRISE].UNLIMITED_STORAGE,
available: false,
title: "billing:plan.enterprise.features.unlimitedStorage",
},
{
id: FEATURES[PricingPlanType.ENTERPRISE].CUSTOM_BRANDING,
available: false,
title: "billing:plan.enterprise.features.customBranding",
},
{
id: FEATURES[PricingPlanType.ENTERPRISE].DEDICATED_SUPPORT,
available: false,
title: "billing:plan.enterprise.features.dedicatedSupport",
},
],
[PricingPlanType.ENTERPRISE]: [
{
id: FEATURES[PricingPlanType.ENTERPRISE].UNLIMITED_STORAGE,
available: true,
title: "billing:plan.enterprise.features.unlimitedStorage",
},
{
id: FEATURES[PricingPlanType.ENTERPRISE].CUSTOM_BRANDING,
available: true,
title: "billing:plan.enterprise.features.customBranding",
},
{
id: FEATURES[PricingPlanType.ENTERPRISE].DEDICATED_SUPPORT,
available: true,
title: "billing:plan.enterprise.features.dedicatedSupport",
},
{
id: FEATURES[PricingPlanType.ENTERPRISE].API_ACCESS,
available: true,
title: "billing:plan.enterprise.features.apiAccess",
},
{
id: FEATURES[PricingPlanType.ENTERPRISE].USER_ROLES,
available: true,
title: "billing:plan.enterprise.features.userRoles",
},
{
id: FEATURES[PricingPlanType.ENTERPRISE].AUDIT_LOGS,
available: true,
title: "billing:plan.enterprise.features.auditLogs",
},
{
id: FEATURES[PricingPlanType.ENTERPRISE].SINGLE_SIGN_ON,
available: true,
title: "billing:plan.enterprise.features.singleSignOn",
},
{
id: FEATURES[PricingPlanType.ENTERPRISE].ADVANCED_ANALYTICS,
available: true,
title: "billing:plan.enterprise.features.advancedAnalytics",
},
],
};

View File

@@ -0,0 +1,66 @@
"use client";
import { memo } from "react";
import {
calculatePriceDiscount,
formatPrice,
BillingDiscountType,
} from "@turbostarter/billing";
import { Trans } from "@turbostarter/i18n";
import { Icons } from "@turbostarter/ui-web/icons";
import type {
Discount as DiscountType,
PricingPlanPrice,
} from "@turbostarter/billing";
interface DiscountProps {
readonly currency: string;
readonly priceWithDiscount?: PricingPlanPrice & {
discount: DiscountType | undefined;
};
}
export const Discount = memo<DiscountProps>(
({ priceWithDiscount, currency }) => {
if (!priceWithDiscount?.discount) {
return null;
}
const discount = calculatePriceDiscount(
priceWithDiscount,
priceWithDiscount.discount,
);
if (!discount) {
return null;
}
return (
<p className="sm mt-2 text-center md:text-lg">
<Icons.Gift className="text-primary mr-1.5 mb-1.5 inline-block h-5 w-5" />
<span className="text-primary">
<Trans
i18nKey="billing:discount.specialOffer"
values={{
discount:
discount.type === BillingDiscountType.PERCENT
? discount.percentage + "%"
: formatPrice({
amount:
discount.original.amount - discount.discounted.amount,
currency,
}),
}}
components={{
bold: <span className="font-semibold" />,
}}
/>
</span>
</p>
);
},
);
Discount.displayName = "Discount";

View File

@@ -0,0 +1,88 @@
"use client";
import { memo } from "react";
import { BillingModel } from "@turbostarter/billing";
import { useTranslation } from "@turbostarter/i18n";
import { Tabs, TabsList, TabsTrigger } from "@turbostarter/ui-web/tabs";
import {
SectionBadge,
SectionDescription,
SectionHeader,
SectionTitle,
} from "~/modules/marketing/layout/section";
import { Discount } from "./discount";
import type {
Discount as DiscountType,
PricingPlanPrice,
RecurringInterval,
} from "@turbostarter/billing";
interface PricingHeaderProps {
readonly currency: string;
readonly model: BillingModel;
readonly intervals: RecurringInterval[];
readonly activeInterval: RecurringInterval;
readonly onIntervalChange: (billing: RecurringInterval) => void;
readonly priceWithDiscount?: PricingPlanPrice & {
discount: DiscountType | undefined;
};
}
export const PricingHeader = memo<PricingHeaderProps>(
({
model,
activeInterval,
intervals,
onIntervalChange,
priceWithDiscount,
currency,
}) => {
const { t } = useTranslation("billing");
return (
<SectionHeader>
<SectionBadge>{t("pricing.label")}</SectionBadge>
<SectionTitle>{t("pricing.title")}</SectionTitle>
<SectionDescription className="text-muted-foreground max-w-2xl text-center">
{t("pricing.description")}
</SectionDescription>
<Discount
{...(priceWithDiscount && {
priceWithDiscount,
})}
currency={currency}
/>
{model === BillingModel.RECURRING && intervals.length > 0 && (
<Tabs
className="mt-2 lg:mt-4"
value={activeInterval}
onValueChange={(value) =>
onIntervalChange(value as RecurringInterval)
}
>
<TabsList>
{intervals.map((interval) => (
<TabsTrigger
key={interval}
value={interval}
className="capitalize"
aria-controls={undefined}
>
{t(`interval.${interval}`)}
</TabsTrigger>
))}
</TabsList>
</Tabs>
)}
</SectionHeader>
);
},
);
PricingHeader.displayName = "PricingHeader";

View File

@@ -0,0 +1,134 @@
import { useMutation } from "@tanstack/react-query";
import { usePathname, useRouter } from "next/navigation";
import { toast } from "sonner";
import {
BillingModel,
PricingPlanType,
calculatePriceDiscount,
calculateRecurringDiscount,
getPlanPrice,
getHighestDiscountForPrice,
} from "@turbostarter/billing";
import { useTranslation } from "@turbostarter/i18n";
import { appConfig } from "~/config/app";
import { pathsConfig } from "~/config/paths";
import { billing } from "~/modules/billing/lib/api";
import { PLAN_FEATURES } from "~/modules/billing/pricing/constants/features";
import type { User } from "@turbostarter/auth";
import type {
Discount,
PricingPlan,
RecurringInterval,
} from "@turbostarter/billing";
export const usePlan = (
plan: PricingPlan,
options: {
model: BillingModel;
interval: RecurringInterval;
discounts: Discount[];
currency?: string;
},
) => {
const { t } = useTranslation("billing");
const router = useRouter();
const checkout = useMutation({
...billing.mutations.checkout.create,
onSuccess: (data) => {
if (!data.url) {
return toast.error(t("error.checkout"));
}
return router.push(data.url);
},
});
const getPortal = useMutation({
...billing.mutations.portal.get,
onSuccess: (data) => {
if (!data.url) {
return toast.error(t("error.portal"));
}
return router.push(data.url);
},
});
const pathname = usePathname();
const price = getPlanPrice(plan, options);
const features = plan.id in PLAN_FEATURES ? PLAN_FEATURES[plan.id] : null;
const discountForPrice = price
? getHighestDiscountForPrice(price, options.discounts)
: null;
const discount =
price && discountForPrice
? calculatePriceDiscount(price, discountForPrice)
: options.model === BillingModel.RECURRING
? calculateRecurringDiscount(plan, options.interval)
: null;
const handleCheckout = (user: User | null) => {
if (!user) {
const url = new URL(pathsConfig.auth.login);
url.searchParams.set("redirectTo", pathsConfig.marketing.pricing);
return router.push(url.toString());
}
if (!price) {
return;
}
checkout.mutate({
json: {
price: {
id: price.id,
},
redirect: {
success: `${appConfig.url}${pathsConfig.dashboard.user.index}`,
cancel: `${appConfig.url}${pathname}`,
},
},
});
};
const handleOpenPortal = (user: User | null) => {
if (!user) {
const url = new URL(pathsConfig.auth.login);
url.searchParams.set("redirectTo", pathsConfig.marketing.pricing);
return router.push(url.toString());
}
getPortal.mutate({
query: {
redirectUrl: `${appConfig.url}${pathname}`,
},
});
};
const hasPlan = (customerPlan: string | null) => {
if (!customerPlan) {
return false;
}
const currentPlanIndex = Object.values(PricingPlanType).indexOf(plan.id);
const customerCurrentPlanIndex = customerPlan
? Object.values(PricingPlanType).indexOf(customerPlan)
: -1;
return currentPlanIndex <= customerCurrentPlanIndex;
};
return {
isPending: checkout.isPending || getPortal.isPending,
price,
features,
discount,
handleCheckout,
handleOpenPortal,
hasPlan,
};
};

View File

@@ -0,0 +1,212 @@
import { memo } from "react";
import { BillingModel, formatPrice } from "@turbostarter/billing";
import { isKey, useTranslation } from "@turbostarter/i18n";
import { cn } from "@turbostarter/ui";
import { Badge } from "@turbostarter/ui-web/badge";
import { Button, buttonVariants } from "@turbostarter/ui-web/button";
import { Card } from "@turbostarter/ui-web/card";
import { Icons } from "@turbostarter/ui-web/icons";
import { pathsConfig } from "~/config/paths";
import { useCustomer } from "~/modules/billing/hooks/use-customer";
import { TurboLink } from "~/modules/common/turbo-link";
import { usePlan } from "./hooks/use-plan";
import type { User } from "@turbostarter/auth";
import type {
Discount,
PricingPlan,
RecurringInterval,
} from "@turbostarter/billing";
interface PlanProps {
readonly plan: PricingPlan;
readonly user: User | null;
readonly interval: RecurringInterval;
readonly model: BillingModel;
readonly currency: string;
readonly discounts: Discount[];
}
export const Plan = memo<PlanProps>(
({ plan, interval, user, model, currency, discounts }) => {
const { data: customer } = useCustomer();
const { t, i18n } = useTranslation(["common", "billing"]);
const {
features,
price,
discount,
isPending,
handleCheckout,
handleOpenPortal,
hasPlan,
} = usePlan(plan, { model, interval, discounts, currency });
if (!price) {
return null;
}
return (
<div className="grow basis-[350px] rounded-lg">
<Card
className={cn(
"relative flex h-full flex-col gap-6 px-7 py-6 md:p-8",
plan.badge && "border-primary",
)}
>
{plan.badge && (
<Badge className="hover:bg-primary absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 px-4 py-1.5 uppercase">
{isKey(plan.badge, i18n, "billing") ? t(plan.badge) : plan.badge}
</Badge>
)}
<div>
<span className="text-lg font-semibold">
{isKey(plan.name, i18n, "billing") ? t(plan.name) : plan.name}
</span>
<p className="relative flex items-end gap-1 py-2">
{discount?.original &&
"amount" in discount.original &&
typeof discount.original.amount === "number" &&
discount.percentage > 0 && (
<span className="text-muted-foreground mr-2 text-lg line-through md:text-xl">
{formatPrice(
{
amount: discount.original.amount,
currency,
},
i18n.language,
)}
</span>
)}
<span className="text-4xl font-bold tracking-tighter md:text-5xl">
{price.custom
? isKey(price.label, i18n, "billing")
? t(price.label)
: price.label
: formatPrice(
{
amount:
discount?.discounted &&
"amount" in discount.discounted
? discount.discounted.amount
: price.amount,
currency,
},
i18n.language,
)}
</span>
{!price.custom && (
<span className="text-muted-foreground shrink-0 text-lg">
/{" "}
{price.type === BillingModel.RECURRING
? t(`interval.${price.interval}`)
: t("interval.lifetime")}
</span>
)}
</p>
<span className="text-sm">
{isKey(plan.description, i18n, "billing")
? t(plan.description)
: plan.description}
</span>
</div>
<div className="flex flex-col gap-1">
{features?.map((feature) => (
<div
key={feature.title}
className={cn("flex items-center gap-3 py-1", {
"opacity-50": !feature.available,
})}
>
<div
className={cn(
"flex size-5 shrink-0 items-center justify-center rounded-full",
feature.available ? "bg-primary" : "border-primary border",
)}
>
{feature.available ? (
<Icons.CheckIcon className="text-primary-foreground w-3" />
) : (
<Icons.X className="text-primary w-3" />
)}
</div>
<span className="text-md">
{isKey(feature.title, i18n, "billing")
? t(feature.title)
: feature.title}
{"addon" in feature && (
<span className="ml-2 whitespace-nowrap">
&nbsp;{feature.addon}
</span>
)}
</span>
</div>
))}
</div>
<div className="mt-auto flex flex-col gap-2">
{"trialDays" in price &&
price.trialDays &&
!hasPlan(customer?.plan ?? null) && (
<Button
variant="outline"
onClick={() => handleCheckout(user)}
disabled={isPending}
>
{isPending ? (
<Icons.Loader2 className="animate-spin" />
) : (
t("trial.period", { period: price.trialDays })
)}
</Button>
)}
{price.custom ? (
<TurboLink href={price.href} className={buttonVariants()}>
{hasPlan(customer?.plan ?? null)
? t("manage.plan.title")
: t("getStarted")}
</TurboLink>
) : price.amount === 0 ? (
<TurboLink
href={
user
? pathsConfig.dashboard.user.index
: pathsConfig.auth.login
}
className={buttonVariants({ variant: "outline" })}
>
{user ? t("goToDashboard") : t("trial.cta")}
</TurboLink>
) : (
<Button
onClick={() =>
model === BillingModel.RECURRING &&
hasPlan(customer?.plan ?? null)
? handleOpenPortal(user)
: handleCheckout(user)
}
disabled={isPending}
>
{isPending ? (
<Icons.Loader2 className="animate-spin" />
) : model === BillingModel.RECURRING &&
hasPlan(customer?.plan ?? null) ? (
t("manage.plan.title")
) : model === BillingModel.RECURRING ? (
t("subscribe")
) : (
t("getLifetimeAccess")
)}
</Button>
)}
</div>
</Card>
</div>
);
},
);
Plan.displayName = "Plan";

View File

@@ -0,0 +1,56 @@
import { memo } from "react";
import { Skeleton } from "@turbostarter/ui-web/skeleton";
import { Plan } from "./plan/plan";
import type { User } from "@turbostarter/auth";
import type {
BillingModel,
Discount,
PricingPlan,
RecurringInterval,
} from "@turbostarter/billing";
interface PlansProps {
readonly plans: PricingPlan[];
readonly discounts: Discount[];
readonly user: User | null;
readonly interval: RecurringInterval;
readonly model: BillingModel;
readonly currency: string;
}
export const Plans = memo<PlansProps>(
({ plans, discounts, interval, user, model, currency }) => {
return (
<div className="flex w-full flex-wrap items-stretch justify-center gap-8 md:gap-6 lg:gap-4">
{plans.map((plan) => (
<Plan
key={plan.id}
plan={plan}
interval={interval}
model={model}
currency={currency}
user={user}
discounts={discounts}
/>
))}
</div>
);
},
);
export const PlansSkeleton = () => {
return (
<div className="flex w-full flex-wrap items-center justify-center gap-12 md:gap-6 lg:gap-4">
{Array.from({ length: 2 }).map((_, i) => (
<div key={i} className="grow-0 basis-[25rem] md:shrink-0">
<Skeleton className="h-[32rem] w-full" />
</div>
))}
</div>
);
};
Plans.displayName = "Plans";

View File

@@ -0,0 +1,30 @@
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
import { handle } from "@turbostarter/api/utils";
import { env } from "@turbostarter/billing/env";
import { api } from "~/lib/api/server";
import { getSession } from "~/lib/auth/server";
import { getQueryClient } from "~/lib/query/server";
import { billing } from "~/modules/billing/lib/api";
import { PricingSection } from "./section";
export const Pricing = async () => {
const { user } = await getSession();
const queryClient = getQueryClient();
if (user) {
await queryClient.prefetchQuery({
...billing.queries.customer.get,
queryFn: handle(api.billing.customer.$get),
});
}
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<PricingSection user={user} model={env.BILLING_MODEL} />
</HydrationBoundary>
);
};

View File

@@ -0,0 +1,84 @@
"use client";
import { memo, useState } from "react";
import {
RecurringInterval,
RecurringIntervalDuration,
config,
getPriceWithHighestDiscount,
} from "@turbostarter/billing";
import { useTranslation } from "@turbostarter/i18n";
import { Skeleton } from "@turbostarter/ui-web/skeleton";
import { Section, SectionHeader } from "~/modules/marketing/layout/section";
import { PricingHeader } from "./layout/header";
import { Plans, PlansSkeleton } from "./plans/plans";
import type { User } from "@turbostarter/auth";
import type { BillingModel } from "@turbostarter/billing";
interface PricingSectionProps {
readonly user: User | null;
readonly model: BillingModel;
}
export const PricingSection = memo<PricingSectionProps>(({ user, model }) => {
const { t } = useTranslation("billing");
const intervals = [
...new Set(
config.plans.flatMap((plan) =>
plan.prices
.flatMap((price) => ("interval" in price ? price.interval : null))
.filter((x): x is RecurringInterval => !!x),
),
),
].sort((a, b) => RecurringIntervalDuration[a] - RecurringIntervalDuration[b]);
const [activeInterval, setActiveInterval] = useState<RecurringInterval>(
intervals[0] ?? RecurringInterval.MONTH,
);
const priceWithDiscount = getPriceWithHighestDiscount(
config.plans,
config.discounts,
);
return (
<Section id="pricing" className="gap-10 sm:gap-12 md:gap-16 lg:gap-20">
<PricingHeader
currency={t("currency")}
model={model}
intervals={intervals}
activeInterval={activeInterval}
onIntervalChange={setActiveInterval}
{...(priceWithDiscount && { priceWithDiscount })}
/>
<Plans
plans={config.plans}
interval={activeInterval}
model={model}
currency={t("currency")}
discounts={config.discounts}
user={user}
/>
</Section>
);
});
export const PricingSectionSkeleton = () => {
return (
<Section id="pricing" className="gap-10 sm:gap-12 md:gap-16 lg:gap-20">
<SectionHeader className="flex flex-col items-center justify-center gap-3">
<Skeleton className="h-8 w-32" />
<Skeleton className="mt-4 h-12 w-72" />
<Skeleton className="h-8 w-96" />
</SectionHeader>
<PlansSkeleton />
</Section>
);
};
PricingSection.displayName = "PricingSection";