import { redirect } from "next/navigation"; import { createTenant } from "@/lib/tenant-registry"; // Public self-serve signup. Apex-level; no slug. Creates a trial tenant // via tenant-registry, which also provisions a Keycloak organization + // invites the user as IT_ADMIN. The portal middleware rewrites // signup. here, but the bare path also works for dev. // // After success the user lands at //dashboard. In prod they'd // follow the KC invite email to set a password; in dev (no Stalwart yet) // the invite_url is logged for the operator to share manually. export default async function StartPage({ searchParams, }: { searchParams: Promise<{ err?: string }>; }) { const flash = await searchParams; async function submit(formData: FormData) { "use server"; const slug = String(formData.get("slug") ?? "").trim().toLowerCase(); const name = String(formData.get("name") ?? "").trim(); const email = String(formData.get("email") ?? "").trim(); const plan = (String(formData.get("plan") ?? "starter") as "starter" | "professional" | "enterprise") || "starter"; if (!slug || !name || !email) { redirect("/start?err=missing_fields"); } const res = await createTenant({ slug, name, plan, admin_email: email, }); if (!res.ok) { redirect(`/start?err=${res.error}`); } redirect(`/${res.tenant.slug}/dashboard?ok=created`); } return (

Start a 14-day trial

Spin up your tenant. You'll get an email invite from Keycloak with a link to set your password.

{flash.err && (
{flash.err === "slug_taken" && "That slug is already in use. Pick another."} {flash.err === "invalid_input" && "Slug must be 3+ chars, lowercase letters / digits / hyphens."} {flash.err === "missing_fields" && "All fields are required."} {!["slug_taken", "invalid_input", "missing_fields"].includes(flash.err) && `Something broke: ${flash.err}`}
)}
); } const inputStyle: React.CSSProperties = { padding: "8px 10px", border: "1px solid #ddd", borderRadius: 6, fontSize: 14, };