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,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";