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,121 @@
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import Image from "next/image";
import { notFound } from "next/navigation";
import { getContentItemBySlug, getContentItems } from "@turbostarter/cms";
import { CollectionType } from "@turbostarter/cms";
import { getTranslation } from "@turbostarter/i18n/server";
import { badgeVariants } from "@turbostarter/ui-web/badge";
import { BLOG_PREFIX } from "~/config/paths";
import { getMetadata } from "~/lib/metadata";
import { Mdx } from "~/modules/common/mdx";
import { TurboLink } from "~/modules/common/turbo-link";
import {
Section,
SectionDescription,
SectionHeader,
SectionTitle,
} from "~/modules/marketing/layout/section";
dayjs.extend(duration);
export default async function Page({
params,
}: {
params: Promise<{ slug: string; locale: string }>;
}) {
const { t } = await getTranslation({ ns: "marketing" });
const item = getContentItemBySlug({
collection: CollectionType.BLOG,
slug: (await params).slug,
locale: (await params).locale,
});
if (!item) {
return notFound();
}
return (
<Section>
<SectionHeader className="max-w-3xl">
<div className="mr-auto flex flex-wrap gap-1 md:gap-1.5">
{item.tags.map((tag) => (
<TurboLink
key={tag}
href={`${BLOG_PREFIX}?tag=${tag}`}
className={badgeVariants({ variant: "outline" })}
>
{t(`blog.tag.${tag}`)}
</TurboLink>
))}
</div>
<SectionTitle as="h1" className="mt-2 text-left">
{item.title}
</SectionTitle>
<div className="text-muted-foreground mr-auto flex flex-wrap items-center gap-1.5">
<time
className="text-muted-foreground"
dateTime={item.publishedAt.toISOString()}
>
{dayjs(item.publishedAt).format("MMMM D, YYYY")}
</time>
{item.timeToRead && <span>·</span>}
{typeof item.timeToRead !== "undefined" && (
<span>
{t("blog.timeToRead", {
time: Math.ceil(dayjs.duration(item.timeToRead).asMinutes()),
})}
</span>
)}
</div>
<SectionDescription className="text-left">
{item.description}
</SectionDescription>
<div className="relative -mx-2 mt-4 aspect-[12/8] w-[calc(100%+1rem)]">
<Image
alt=""
fill
src={item.thumbnail}
className="rounded-lg object-cover"
/>
</div>
</SectionHeader>
<Mdx mdx={item.mdx} />
</Section>
);
}
export function generateStaticParams() {
return getContentItems({ collection: CollectionType.BLOG }).items.map(
(post) => ({
slug: post.slug,
}),
);
}
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string; locale: string }>;
}) {
const item = getContentItemBySlug({
collection: CollectionType.BLOG,
slug: (await params).slug,
locale: (await params).locale,
});
if (!item) {
return notFound();
}
return getMetadata({
title: item.title,
description: item.description,
})({ params });
}

View File

@@ -0,0 +1,128 @@
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
import Image from "next/image";
import {
CollectionType,
ContentStatus,
getContentItems,
} from "@turbostarter/cms";
import { getTranslation } from "@turbostarter/i18n/server";
import { SortOrder } from "@turbostarter/shared/constants";
import { Badge } from "@turbostarter/ui-web/badge";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@turbostarter/ui-web/card";
import { pathsConfig } from "~/config/paths";
import { getMetadata } from "~/lib/metadata";
import { TurboLink } from "~/modules/common/turbo-link";
import { TagsPicker } from "~/modules/marketing/blog/tags-picker";
import {
Section,
SectionBadge,
SectionDescription,
SectionHeader,
SectionTitle,
} from "~/modules/marketing/layout/section";
import type { ContentTag } from "@turbostarter/cms";
dayjs.extend(duration);
export const generateMetadata = getMetadata({
title: "marketing:blog.label",
description: "marketing:blog.description",
canonical: pathsConfig.marketing.blog.index,
});
export default async function BlogPage({
searchParams,
params,
}: {
searchParams: Promise<{ tag?: ContentTag }>;
params: Promise<{ locale: string }>;
}) {
const tag = (await searchParams).tag;
const locale = (await params).locale;
const { t } = await getTranslation({ ns: "marketing" });
const { items } = getContentItems({
collection: CollectionType.BLOG,
tags: tag ? [tag] : [],
sortBy: "publishedAt",
sortOrder: SortOrder.DESCENDING,
status: ContentStatus.PUBLISHED,
locale,
});
return (
<Section>
<SectionHeader className="flex flex-col items-center justify-center gap-3">
<SectionBadge>{t("blog.label")}</SectionBadge>
<SectionTitle as="h1">{t("blog.title")}</SectionTitle>
<SectionDescription>{t("blog.description")}</SectionDescription>
</SectionHeader>
<div className="-mt-2 sm:-mt-4 md:-mt-6 lg:-mt-10">
<TagsPicker />
</div>
<div className="grid grid-cols-1 items-start justify-center gap-x-6 gap-y-8 md:grid-cols-2 md:gap-y-12 lg:grid-cols-3 lg:gap-y-16">
{items.map((post) => (
<TurboLink
key={post.slug}
href={pathsConfig.marketing.blog.post(post.slug)}
className="group h-full basis-[34rem]"
>
<Card className="group-hover:bg-muted/50 h-full border-none shadow-none">
<CardHeader className="space-y-2 p-3 pb-2">
<div className="bg-muted -mx-3 -mt-3 mb-3 aspect-[12/8] overflow-hidden rounded-lg">
<div className="relative h-full w-full transition-transform duration-300 group-hover:scale-105">
<Image
alt=""
fill
src={post.thumbnail}
className="object-cover"
/>
</div>
</div>
<div className="flex flex-wrap gap-1 pb-1">
{post.tags.map((tag) => (
<Badge key={tag} variant="outline">
{t(`blog.tag.${tag}`)}
</Badge>
))}
</div>
<CardTitle className="leading-tight">{post.title}</CardTitle>
<div className="text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm">
<time dateTime={post.publishedAt.toISOString()}>
{dayjs(post.publishedAt).format("MMMM D, YYYY")}
</time>
<span>·</span>
<span>
{t("blog.timeToRead", {
time: Math.ceil(
dayjs.duration(post.timeToRead).asMinutes(),
),
})}
</span>
</div>
</CardHeader>
<CardContent className="p-3 pt-0">
<p className="text-muted-foreground text-sm leading-relaxed">
{post.description}
</p>
</CardContent>
</Card>
</TurboLink>
))}
</div>
</Section>
);
}

View File

@@ -0,0 +1,34 @@
import { getTranslation } from "@turbostarter/i18n/server";
import { withI18n } from "@turbostarter/i18n/with-i18n";
import { getMetadata } from "~/lib/metadata";
import { ContactForm } from "~/modules/marketing/contact/contact-form";
import {
Section,
SectionBadge,
SectionDescription,
SectionHeader,
SectionTitle,
} from "~/modules/marketing/layout/section";
export const generateMetadata = getMetadata({
title: "marketing:contact.label",
description: "marketing:contact.description",
});
const ContactPage = async () => {
const { t } = await getTranslation({ ns: "marketing" });
return (
<Section>
<SectionHeader>
<SectionBadge>{t("contact.label")}</SectionBadge>
<SectionTitle>{t("contact.title")}</SectionTitle>
<SectionDescription>{t("contact.description")}</SectionDescription>
</SectionHeader>
<ContactForm />
</Section>
);
};
export default withI18n(ContactPage);

View File

@@ -0,0 +1,12 @@
import { Footer } from "~/modules/marketing/layout/footer";
import { Header } from "~/modules/marketing/layout/header/header";
export default function MainLayout(props: { children: React.ReactNode }) {
return (
<>
<Header />
<main className="w-full">{props.children}</main>
<Footer />
</>
);
}

View File

@@ -0,0 +1,76 @@
import { notFound } from "next/navigation";
import {
CollectionType,
getContentItemBySlug,
getContentItems,
} from "@turbostarter/cms";
import { getTranslation } from "@turbostarter/i18n/server";
import { getMetadata } from "~/lib/metadata";
import { Mdx } from "~/modules/common/mdx";
import {
Section,
SectionBadge,
SectionDescription,
SectionHeader,
SectionTitle,
} from "~/modules/marketing/layout/section";
interface PageParams {
params: Promise<{
slug: string;
locale: string;
}>;
}
export default async function Page({ params }: PageParams) {
const item = getContentItemBySlug({
collection: CollectionType.LEGAL,
slug: (await params).slug,
locale: (await params).locale,
});
if (!item) {
return notFound();
}
const { t } = await getTranslation({ ns: "common" });
return (
<Section>
<SectionHeader>
<SectionBadge>{t("legal.label")}</SectionBadge>
<SectionTitle as="h1">{item.title}</SectionTitle>
<SectionDescription>{item.description}</SectionDescription>
</SectionHeader>
<Mdx mdx={item.mdx} />
</Section>
);
}
export function generateStaticParams() {
return getContentItems({ collection: CollectionType.LEGAL }).items.map(
({ slug, locale }) => ({
slug,
locale,
}),
);
}
export async function generateMetadata({ params }: PageParams) {
const item = getContentItemBySlug({
collection: CollectionType.LEGAL,
slug: (await params).slug,
locale: (await params).locale,
});
if (!item) {
return notFound();
}
return getMetadata({
title: item.title,
description: item.description,
})({ params });
}

View File

@@ -0,0 +1,43 @@
"use client";
import { useTranslation } from "@turbostarter/i18n";
import { buttonVariants } from "@turbostarter/ui-web/button";
import { Icons } from "@turbostarter/ui-web/icons";
import { pathsConfig } from "~/config/paths";
import { TurboLink } from "~/modules/common/turbo-link";
const HomePage = () => {
const { t } = useTranslation("common");
return (
<main className="flex min-h-[calc(100vh-4rem)] flex-col items-center justify-center px-4">
<div className="mx-auto max-w-3xl text-center">
<h1 className="text-4xl font-bold tracking-tight sm:text-6xl">
{t("home.title", { defaultValue: "Welcome to TurboStarter" })}
</h1>
<p className="mt-6 text-lg leading-8 text-muted-foreground">
{t("home.description", { defaultValue: "The fastest way to build your next SaaS. Authentication, billing, database, and UI components — all pre-configured and ready to go." })}
</p>
<div className="mt-10 flex items-center justify-center gap-x-6">
<TurboLink
href={pathsConfig.auth.login}
className={buttonVariants({ size: "lg" })}
>
{t("home.getStarted", { defaultValue: "Get Started" })}
<Icons.ArrowRight className="ml-2 size-4" />
</TurboLink>
<TurboLink
href="https://turbostarter.dev/docs"
className={buttonVariants({ variant: "outline", size: "lg" })}
target="_blank"
>
{t("home.documentation", { defaultValue: "Documentation" })}
</TurboLink>
</div>
</div>
</main>
);
};
export default HomePage;

View File

@@ -0,0 +1,7 @@
import { PricingSectionSkeleton } from "~/modules/billing/pricing/section";
const PricingLoadingPage = () => {
return <PricingSectionSkeleton />;
};
export default PricingLoadingPage;

View File

@@ -0,0 +1,13 @@
import { getMetadata } from "~/lib/metadata";
import { Pricing } from "~/modules/billing/pricing/pricing";
export const generateMetadata = getMetadata({
title: "billing:pricing.label",
description: "billing:pricing.description",
});
const PricingPage = () => {
return <Pricing />;
};
export default PricingPage;