From 138b5a24ae9dd546b0c503aefba1e077efbfbe27 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?=
<35082514+alezmad@users.noreply.github.com>
Date: Sat, 4 Apr 2026 23:47:52 +0100
Subject: [PATCH] feat(web): first-time user onboarding flow
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
New user signs in → /dashboard (user) → hits server-side getMyMeshes → 0
results → redirects to /dashboard/meshes/new?onboarding=1. Create-mesh
page renders a welcome banner explaining what a mesh is. After submit,
if ?onboarding=1 was set, the form bounces to
/dashboard/meshes/[id]/invite?onboarding=1 instead of the mesh detail
page. Invite page renders a "🎉 Mesh created" banner with the
`claudemesh join ` CLI snippet.
The onboarding flag is URL-driven — no persistence needed, dismissal
happens naturally when the user navigates away.
Also rewrites the /dashboard (user) home page from the placeholder
"Welcome to your Dashboard" TurboStarter card grid to a claudemesh-
native view: top 6 meshes with badges, All meshes / New mesh CTAs.
Removes the unused Card/Icons imports.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.../(user)/meshes/[id]/invite/page.tsx | 20 +++
.../dashboard/(user)/meshes/new/page.tsx | 24 +++-
.../app/[locale]/dashboard/(user)/page.tsx | 136 ++++++++++--------
.../web/src/modules/mesh/create-mesh-form.tsx | 10 +-
4 files changed, 127 insertions(+), 63 deletions(-)
diff --git a/apps/web/src/app/[locale]/dashboard/(user)/meshes/[id]/invite/page.tsx b/apps/web/src/app/[locale]/dashboard/(user)/meshes/[id]/invite/page.tsx
index 5b507c8..dbc1138 100644
--- a/apps/web/src/app/[locale]/dashboard/(user)/meshes/[id]/invite/page.tsx
+++ b/apps/web/src/app/[locale]/dashboard/(user)/meshes/[id]/invite/page.tsx
@@ -13,13 +13,33 @@ export const generateMetadata = getMetadata({
export default async function InvitePage({
params,
+ searchParams,
}: {
params: Promise<{ id: string }>;
+ searchParams: Promise<{ onboarding?: string }>;
}) {
const { id } = await params;
+ const { onboarding } = await searchParams;
+ const isOnboarding = onboarding === "1";
return (
<>
+ {isOnboarding && (
+
+
+ 🎉 Mesh created
+
+
+ Now generate your first invite link to share with a teammate — or
+ use it yourself to join this mesh from another laptop. Your
+ teammate runs{" "}
+
+ claudemesh join <link>
+ {" "}
+ in their terminal.
+
+ Create your first mesh in 10 seconds. A mesh is the space where
+ your Claude Code sessions talk to each other. You can invite
+ teammates, share context, and route messages — all end-to-end
+ encrypted.
+
+
+ )}
New mesh
@@ -23,7 +43,7 @@ export default function NewMeshPage() {
-
+
>
);
diff --git a/apps/web/src/app/[locale]/dashboard/(user)/page.tsx b/apps/web/src/app/[locale]/dashboard/(user)/page.tsx
index 46c15b6..bf095da 100644
--- a/apps/web/src/app/[locale]/dashboard/(user)/page.tsx
+++ b/apps/web/src/app/[locale]/dashboard/(user)/page.tsx
@@ -1,66 +1,84 @@
-"use client";
+import Link from "next/link";
+import { redirect } from "next/navigation";
-import { useTranslation } from "@turbostarter/i18n";
-import { Card, CardContent, CardHeader, CardTitle } from "@turbostarter/ui-web/card";
-import { Icons } from "@turbostarter/ui-web/icons";
+import { getMyMeshesResponseSchema } from "@turbostarter/api/schema";
+import { handle } from "@turbostarter/api/utils";
+import { Badge } from "@turbostarter/ui-web/badge";
+import { buttonVariants } from "@turbostarter/ui-web/button";
-/**
- * Dashboard Home Page
- *
- * Welcome page for authenticated users.
- */
-export default function DashboardPage() {
- const { t } = useTranslation("dashboard");
+import { pathsConfig } from "~/config/paths";
+import { api } from "~/lib/api/server";
+import { getMetadata } from "~/lib/metadata";
+
+export const generateMetadata = getMetadata({
+ title: "Dashboard",
+ description: "Your meshes.",
+});
+
+export default async function DashboardHomePage() {
+ const { data } = await handle(api.my.meshes.$get, {
+ schema: getMyMeshesResponseSchema,
+ })({
+ query: { page: "1", perPage: "6", sort: JSON.stringify([]) },
+ });
+
+ // First-time onboarding: 0-mesh user → bounce to create
+ if (data.length === 0) {
+ redirect(`${pathsConfig.dashboard.user.meshes.new}?onboarding=1`);
+ }
return (
-
-
-
-
- {t("welcome.title", { defaultValue: "Welcome to your Dashboard" })}
-
-
- {t("welcome.description", { defaultValue: "Get started by exploring the features below." })}
-