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,43 @@
import { Button as ReactEmailButton } from "@react-email/components";
import { cva } from "class-variance-authority";
import { cn } from "@turbostarter/ui";
import type { ButtonProps as ReactEmailButtonProps } from "@react-email/components";
import type { VariantProps } from "class-variance-authority";
export const buttonVariants = cva(
"block rounded-md text-center text-sm font-medium whitespace-nowrap",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground",
destructive: "bg-destructive text-destructive-foreground",
outline: "border-input bg-background border",
secondary: "bg-secondary text-secondary-foreground",
link: "text-primary underline-offset-4",
},
size: {
default: "px-4 py-2.5",
sm: "rounded-md px-3",
lg: "rounded-md px-8",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
},
);
export type ButtonProps = ReactEmailButtonProps &
VariantProps<typeof buttonVariants>;
export const Button = ({ variant, size, className, ...props }: ButtonProps) => {
return (
<ReactEmailButton
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
};

View File

@@ -0,0 +1,29 @@
import { Img, Link, Text } from "@react-email/components";
import { env } from "../../../env";
interface FooterProps {
readonly origin: string;
}
export const Footer = ({ origin }: FooterProps) => {
return (
<>
<Img
src={`${origin}/images/logo.png`}
alt="Turbostarter Logo"
height={45}
className="mt-12"
/>
<Text className="text-muted-foreground max-w-[250px] leading-normal">
<Link
href="https://turbostarter.dev"
className="text-muted-foreground"
style={{ textDecoration: "underline" }}
>
{env.PRODUCT_NAME}
</Link>
</Text>
</>
);
};

View File

@@ -0,0 +1,16 @@
import { Img } from "@react-email/components";
interface HeaderProps {
readonly origin: string;
}
export const Header = ({ origin }: HeaderProps) => {
return (
<Img
src={`${origin}/images/logo-text.png`}
alt="Turbostarter Logo"
className="mb-10"
height={45}
/>
);
};

View File

@@ -0,0 +1,91 @@
import {
Container,
Font,
Head,
Html,
Section,
Tailwind,
} from "@react-email/components";
import { Footer } from "./footer";
import { Header } from "./header";
import type { PropsWithChildren } from "react";
export const Layout = ({
children,
origin,
locale,
}: PropsWithChildren<{ origin?: string | null; locale?: string }>) => {
return (
<Html lang={locale}>
<Head>
<Font
fontFamily="Geist"
fallbackFontFamily="Arial"
fontWeight={400}
fontStyle="normal"
webFont={{
url: "https://fonts.gstatic.com/s/geist/v3/gyByhwUxId8gMEwYGFWNOITddY4.woff2",
format: "woff2",
}}
/>
</Head>
<Tailwind
config={{
theme: {
extend: {
colors: {
background: "#ffffff",
foreground: "#09090b",
card: {
DEFAULT: "#ffffff",
foreground: "#09090b",
},
popover: {
DEFAULT: "#ffffff",
foreground: "#09090b",
},
primary: {
DEFAULT: "#f14704",
foreground: "#fff7ed",
},
secondary: {
DEFAULT: "#f4f4f5",
foreground: "#18181b",
},
muted: {
DEFAULT: "#f4f4f5",
foreground: "#71717b",
},
accent: {
DEFAULT: "#f4f4f5",
foreground: "#18181b",
},
success: {
DEFAULT: "#4ade80",
foreground: "#09090b",
},
destructive: {
DEFAULT: "#e7000b",
foreground: "#fff7ed",
},
border: "#e4e4e7",
input: "#e4e4e7",
ring: "#f14704",
},
},
},
}}
>
<Section className="p-1">
<Container className="bg-card text-card-foreground rounded-lg p-6">
{origin && <Header origin={origin} />}
{children}
{origin && <Footer origin={origin} />}
</Container>
</Section>
</Tailwind>
</Html>
);
};

View File

@@ -0,0 +1,67 @@
import { Heading, Preview, Text } from "@react-email/components";
import * as React from "react";
import { Trans } from "@turbostarter/i18n";
import { getTranslation } from "@turbostarter/i18n/server";
import { getOrigin } from "@turbostarter/shared/utils";
import { Button } from "../_components/button";
import { Layout } from "../_components/layout/layout";
import type {
EmailVariables,
EmailTemplate,
CommonEmailProps,
} from "../../types";
type Props = EmailVariables[typeof EmailTemplate.CHANGE_EMAIL] &
CommonEmailProps;
export const ChangeEmail = async ({ url, locale, newEmail }: Props) => {
const { t } = await getTranslation({ locale, ns: "auth" });
const origin = getOrigin(url);
return (
<Layout origin={origin} locale={locale}>
<Preview>{t("account.email.change.email.preview", { newEmail })}</Preview>
<Heading className="leading-tight tracking-tight">
{t("account.email.change.email.subject")}
</Heading>
<Text>
<Trans
ns="auth"
i18nKey="account.email.change.email.body"
values={{ newEmail }}
components={{
bold: <strong />,
}}
/>
</Text>
<Button href={url}>{t("account.email.change.email.cta")}</Button>
<Text>{t("account.email.change.email.or")}</Text>
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
{url}
</code>
<Text className="text-muted-foreground">
{t("account.email.change.email.disclaimer")}
</Text>
</Layout>
);
};
ChangeEmail.subject = async ({ locale }: CommonEmailProps) => {
const { t } = await getTranslation({ locale, ns: "auth" });
return t("account.email.change.email.subject");
};
ChangeEmail.PreviewProps = {
url: "http://localhost:3000/api/auth/verify-email?token=123&callbackURL=/dashboard/settings",
locale: "en",
newEmail: "john@doe.com",
};
export default ChangeEmail;

View File

@@ -0,0 +1,57 @@
import { Heading, Preview, Text } from "@react-email/components";
import * as React from "react";
import { getTranslation } from "@turbostarter/i18n/server";
import { getOrigin } from "@turbostarter/shared/utils";
import { Button } from "../_components/button";
import { Layout } from "../_components/layout/layout";
import type {
EmailVariables,
EmailTemplate,
CommonEmailProps,
} from "../../types";
type Props = EmailVariables[typeof EmailTemplate.CONFIRM_EMAIL] &
CommonEmailProps;
export const ConfirmEmail = async ({ url, locale }: Props) => {
const { t } = await getTranslation({ locale, ns: "auth" });
const origin = getOrigin(url);
return (
<Layout origin={origin} locale={locale}>
<Preview>{t("account.email.confirm.email.preview")}</Preview>
<Heading className="leading-tight tracking-tight">
{t("account.email.confirm.email.subject")}
</Heading>
<Text>{t("account.email.confirm.email.body")}</Text>
<Button href={url}>{t("account.email.confirm.email.cta")}</Button>
<Text>{t("account.email.confirm.email.or")}</Text>
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
{url}
</code>
<Text className="text-muted-foreground">
{t("account.email.confirm.email.disclaimer")}
</Text>
</Layout>
);
};
ConfirmEmail.subject = async ({ locale }: CommonEmailProps) => {
const { t } = await getTranslation({ locale, ns: "auth" });
return t("account.email.confirm.email.subject");
};
ConfirmEmail.PreviewProps = {
url: "http://localhost:3000/api/auth/verify-email?token=123&callbackURL=/dashboard",
locale: "en",
};
export default ConfirmEmail;

View File

@@ -0,0 +1,57 @@
import { Heading, Preview, Text } from "@react-email/components";
import * as React from "react";
import { getTranslation } from "@turbostarter/i18n/server";
import { getOrigin } from "@turbostarter/shared/utils";
import { Button } from "../_components/button";
import { Layout } from "../_components/layout/layout";
import type {
EmailTemplate,
EmailVariables,
CommonEmailProps,
} from "../../types";
type Props = EmailVariables[typeof EmailTemplate.DELETE_ACCOUNT] &
CommonEmailProps;
export const DeleteAccount = async ({ url, locale }: Props) => {
const { t } = await getTranslation({ locale, ns: "auth" });
const origin = getOrigin(url);
return (
<Layout origin={origin} locale={locale}>
<Preview>{t("account.delete.email.preview")}</Preview>
<Heading className="leading-tight tracking-tight">
{t("account.delete.email.subject")}
</Heading>
<Text>{t("account.delete.email.body")}</Text>
<Button href={url}>{t("account.delete.email.cta")}</Button>
<Text>{t("account.delete.email.or")}</Text>
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
{url}
</code>
<Text className="text-muted-foreground">
{t("account.delete.email.disclaimer")}
</Text>
</Layout>
);
};
DeleteAccount.subject = async ({ locale }: CommonEmailProps) => {
const { t } = await getTranslation({ locale, ns: "auth" });
return t("account.delete.email.subject");
};
DeleteAccount.PreviewProps = {
url: "http://localhost:3000/api/auth/delete-user/callback?token=123&callbackURL=/",
locale: "en",
};
export default DeleteAccount;

View File

@@ -0,0 +1,56 @@
import { Heading, Preview, Text } from "@react-email/components";
import * as React from "react";
import { getTranslation } from "@turbostarter/i18n/server";
import { getOrigin } from "@turbostarter/shared/utils";
import { Button } from "../_components/button";
import { Layout } from "../_components/layout/layout";
import type {
EmailVariables,
EmailTemplate,
CommonEmailProps,
} from "../../types";
type Props = EmailVariables[typeof EmailTemplate.MAGIC_LINK] & CommonEmailProps;
export const MagicLink = async ({ url, locale }: Props) => {
const { t } = await getTranslation({ locale, ns: "auth" });
const origin = getOrigin(url);
return (
<Layout origin={origin} locale={locale}>
<Preview>{t("login.magicLink.email.preview")}</Preview>
<Heading className="leading-tight tracking-tight">
{t("login.magicLink.email.subject")}
</Heading>
<Text>{t("login.magicLink.email.body")}</Text>
<Button href={url}>{t("login.magicLink.email.cta")}</Button>
<Text>{t("login.magicLink.email.or")}</Text>
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
{url}
</code>
<Text className="text-muted-foreground">
{t("login.magicLink.email.disclaimer")}
</Text>
</Layout>
);
};
MagicLink.subject = async ({ locale }: CommonEmailProps) => {
const { t } = await getTranslation({ locale, ns: "auth" });
return t("login.magicLink.email.subject");
};
MagicLink.PreviewProps = {
url: "http://localhost:3000/api/auth/magic-link/verify?token=123&callbackURL=/dashboard",
locale: "en",
};
export default MagicLink;

View File

@@ -0,0 +1,76 @@
import { Heading, Preview, Text } from "@react-email/components";
import * as React from "react";
import { Trans } from "@turbostarter/i18n";
import { getTranslation } from "@turbostarter/i18n/server";
import { getOrigin } from "@turbostarter/shared/utils";
import { Button } from "../_components/button";
import { Layout } from "../_components/layout/layout";
import type {
EmailVariables,
EmailTemplate,
CommonEmailProps,
} from "../../types";
type Props = EmailVariables[typeof EmailTemplate.ORGANIZATION_INVITATION] &
CommonEmailProps;
export const OrganizationInvitation = async ({
url,
inviter,
organization,
locale,
}: Props) => {
const { t } = await getTranslation({ locale, ns: "organization" });
const origin = getOrigin(url);
return (
<Layout origin={origin} locale={locale}>
<Preview>{t("members.invite.email.preview", { inviter })}</Preview>
<Heading className="leading-tight tracking-tight">
{t("members.invite.email.subject")}
</Heading>
<Text>
<Trans
i18nKey="members.invite.email.body"
ns="organization"
values={{ inviter, organization }}
components={{
bold: <strong />,
}}
/>
</Text>
<Button href={url}>
{t("members.invite.email.cta", { organization })}
</Button>
<Text>{t("members.invite.email.or")}</Text>
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
{url}
</code>
<Text className="text-muted-foreground">
{t("members.invite.email.disclaimer")}
</Text>
</Layout>
);
};
OrganizationInvitation.subject = async ({ locale }: CommonEmailProps) => {
const { t } = await getTranslation({ locale, ns: "organization" });
return t("members.invite.email.subject");
};
OrganizationInvitation.PreviewProps = {
url: "http://localhost:3000/auth/join?invitationId=h4zI2pKJrQkP5NQljdA8W57wG0V8LHrv",
locale: "en",
inviter: "John Doe",
organization: "Acme Inc",
};
export default OrganizationInvitation;

View File

@@ -0,0 +1,57 @@
import { Heading, Preview, Text } from "@react-email/components";
import * as React from "react";
import { getTranslation } from "@turbostarter/i18n/server";
import { getOrigin } from "@turbostarter/shared/utils";
import { Button } from "../_components/button";
import { Layout } from "../_components/layout/layout";
import type {
EmailVariables,
EmailTemplate,
CommonEmailProps,
} from "../../types";
type Props = EmailVariables[typeof EmailTemplate.CONFIRM_EMAIL] &
CommonEmailProps;
export const ResetPassword = async ({ url, locale }: Props) => {
const { t } = await getTranslation({ locale, ns: "auth" });
const origin = getOrigin(url);
return (
<Layout origin={origin}>
<Preview>{t("account.password.update.email.preview")}</Preview>
<Heading className="leading-tight tracking-tight">
{t("account.password.update.email.subject")}
</Heading>
<Text>{t("account.password.update.email.body")}</Text>
<Button href={url}>{t("account.password.update.email.cta")}</Button>
<Text>{t("account.password.update.email.or")}</Text>
<code className="border-border bg-muted inline-block rounded-md border border-solid px-5 py-3.5 font-mono text-xs">
{url}
</code>
<Text className="text-muted-foreground">
{t("account.password.update.email.disclaimer")}
</Text>
</Layout>
);
};
ResetPassword.subject = async ({ locale }: CommonEmailProps) => {
const { t } = await getTranslation({ locale, ns: "auth" });
return t("account.password.update.email.subject");
};
ResetPassword.PreviewProps = {
url: "http://localhost:3000/api/auth/reset-password/KwiyWf9xsTrfndZY5a0stg4p?callbackURL=/auth/password/update",
locale: "en",
};
export default ResetPassword;

View File

@@ -0,0 +1,45 @@
import { Heading, Preview, Row, Column } from "@react-email/components";
import * as React from "react";
import { getTranslation } from "@turbostarter/i18n/server";
import { Layout } from "./_components/layout/layout";
import type { EmailTemplate } from "../types";
import type { EmailVariables } from "../types";
type Props = EmailVariables[typeof EmailTemplate.CONTACT_FORM];
export const ContactForm = async (props: Props) => {
const { t } = await getTranslation({ ns: "marketing" });
return (
<Layout>
<Preview>{t("contact.email.subject")}</Preview>
<Heading className="leading-tight tracking-tight">
{t("contact.email.body")}
</Heading>
{Object.entries(props).map(([key, value]) => (
<Row key={key}>
<Column>
<strong>{key}</strong>: {value}
</Column>
</Row>
))}
</Layout>
);
};
ContactForm.subject = async () => {
const { t } = await getTranslation({ ns: "marketing" });
return t("contact.email.subject");
};
ContactForm.PreviewProps = {
name: "John Doe",
email: "john.doe@example.com",
message: "Hello, I'm interested in your services.",
};
export default ContactForm;

View File

@@ -0,0 +1,54 @@
import { render } from "@react-email/render";
import { EmailTemplate } from "../types";
import ChangeEmail from "./auth/change-email";
import { ConfirmEmail } from "./auth/confirm-email";
import DeleteAccount from "./auth/delete-account";
import { MagicLink } from "./auth/magic-link";
import { OrganizationInvitation } from "./auth/organization-invitation";
import { ResetPassword } from "./auth/reset-password";
import ContactForm from "./contact-form";
import type { CommonEmailProps, EmailVariables } from "../types";
interface EmailTemplateComponent<T extends EmailTemplate> {
(
props: EmailVariables[T] & CommonEmailProps,
): Promise<React.ReactElement> | React.ReactElement;
subject: ((props: CommonEmailProps) => Promise<string> | string) | string;
}
export const templates: {
[K in EmailTemplate]: EmailTemplateComponent<K>;
} = {
[EmailTemplate.RESET_PASSWORD]: ResetPassword,
[EmailTemplate.MAGIC_LINK]: MagicLink,
[EmailTemplate.CONFIRM_EMAIL]: ConfirmEmail,
[EmailTemplate.DELETE_ACCOUNT]: DeleteAccount,
[EmailTemplate.CHANGE_EMAIL]: ChangeEmail,
[EmailTemplate.ORGANIZATION_INVITATION]: OrganizationInvitation,
[EmailTemplate.CONTACT_FORM]: ContactForm,
} as const;
export const getTemplate = async <T extends EmailTemplate>({
id,
locale,
variables,
}: {
id: T;
variables: EmailVariables[T];
locale?: string;
}) => {
const template = templates[id];
const subject =
typeof template.subject === "function"
? await template.subject({ locale })
: template.subject;
const email = await template({ ...variables, locale });
const html = await render(email);
const text = await render(email, { plainText: true });
return { html, text, subject };
};