diff --git a/apps/web/src/app/[locale]/(marketing)/page.tsx b/apps/web/src/app/[locale]/(marketing)/page.tsx
index 52b96e7..dad146e 100644
--- a/apps/web/src/app/[locale]/(marketing)/page.tsx
+++ b/apps/web/src/app/[locale]/(marketing)/page.tsx
@@ -9,8 +9,14 @@ import { DemoDashboard } from "~/modules/marketing/home/demo-dashboard";
import { WhatIsClaudemesh } from "~/modules/marketing/home/what-is-claudemesh";
import { FAQ } from "~/modules/marketing/home/faq";
import { CallToAction } from "~/modules/marketing/home/cta";
+import { MeshStats } from "~/modules/marketing/home/mesh-stats";
import { LatestNewsToaster } from "~/modules/marketing/home/toaster";
+// Revalidate the page every 60s so the mesh-stats counter stays fresh
+// without hammering the DB. The /api/public/stats endpoint has its own
+// 60s in-memory cache too.
+export const revalidate = 60;
+
const HomePage = () => {
return (
{
+
);
diff --git a/apps/web/src/modules/marketing/home/mesh-stats.tsx b/apps/web/src/modules/marketing/home/mesh-stats.tsx
new file mode 100644
index 0000000..0b59bd9
--- /dev/null
+++ b/apps/web/src/modules/marketing/home/mesh-stats.tsx
@@ -0,0 +1,72 @@
+import {
+ publicStatsResponseSchema,
+ type PublicStatsResponse,
+} from "@turbostarter/api/schema";
+import { handle } from "@turbostarter/api/utils";
+
+import { api } from "~/lib/api/server";
+
+const ZERO_STATS: PublicStatsResponse = {
+ messagesRouted: 0,
+ meshesCreated: 0,
+ peersActive: 0,
+ lastUpdated: new Date(0).toISOString(),
+};
+
+const fetchStats = async (): Promise => {
+ try {
+ return await handle(api.public.stats.$get, {
+ schema: publicStatsResponseSchema,
+ })();
+ } catch {
+ return ZERO_STATS;
+ }
+};
+
+const nf = new Intl.NumberFormat("en-US");
+
+export const MeshStats = async () => {
+ const stats = await fetchStats();
+ const empty = stats.messagesRouted === 0;
+
+ return (
+
+
+
+
+ ciphertext routed
+
+ →
+ {empty ? (
+
+ ready to route
+
+ ) : (
+ <>
+
+ {nf.format(stats.messagesRouted)} messages
+
+ ·
+
+ {nf.format(stats.meshesCreated)} meshes
+
+ ·
+
+ {nf.format(stats.peersActive)} peers online
+
+ >
+ )}
+
+
+ broker sees none of it
+
+
+
+ );
+};
diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts
index 15926da..99478e3 100644
--- a/packages/api/src/index.ts
+++ b/packages/api/src/index.ts
@@ -15,6 +15,7 @@ import { authRouter } from "./modules/auth/router";
import { billingRouter } from "./modules/billing/router";
import { myRouter } from "./modules/mesh/router";
import { organizationRouter } from "./modules/organization/router";
+import { publicRouter } from "./modules/public/router";
import { storageRouter } from "./modules/storage/router";
import { onError } from "./utils/on-error";
@@ -51,6 +52,7 @@ const appRouter = new Hono()
.route("/billing", billingRouter)
.route("/my", myRouter)
.route("/organizations", organizationRouter)
+ .route("/public", publicRouter)
.route("/storage", storageRouter)
.onError(onError);
diff --git a/packages/api/src/modules/public/router.ts b/packages/api/src/modules/public/router.ts
new file mode 100644
index 0000000..3bb1004
--- /dev/null
+++ b/packages/api/src/modules/public/router.ts
@@ -0,0 +1,57 @@
+import { Hono } from "hono";
+
+import { count, isNull } from "@turbostarter/db";
+import { mesh, messageQueue, presence } from "@turbostarter/db/schema";
+import { db } from "@turbostarter/db/server";
+
+/**
+ * Unauthed public stats for the landing page counter.
+ *
+ * In-memory 60s cache. Results are aggregate counts only — no ids,
+ * no names, no ciphertext, no routing metadata. Safe for public consumption.
+ */
+const CACHE_TTL_MS = 60_000;
+
+interface PublicStats {
+ messagesRouted: number;
+ meshesCreated: number;
+ peersActive: number;
+ lastUpdated: string;
+}
+
+let cachedStats: { value: PublicStats; expiresAt: number } | null = null;
+
+const fetchStats = async (): Promise => {
+ const [[messagesRouted], [meshesCreated], [peersActive]] = await Promise.all([
+ db.select({ c: count() }).from(messageQueue),
+ db
+ .select({ c: count() })
+ .from(mesh)
+ .where(isNull(mesh.archivedAt)),
+ db
+ .select({ c: count() })
+ .from(presence)
+ .where(isNull(presence.disconnectedAt)),
+ ]);
+
+ return {
+ messagesRouted: messagesRouted?.c ?? 0,
+ meshesCreated: meshesCreated?.c ?? 0,
+ peersActive: peersActive?.c ?? 0,
+ lastUpdated: new Date().toISOString(),
+ };
+};
+
+export const publicRouter = new Hono().get("/stats", async (c) => {
+ const now = Date.now();
+ if (cachedStats && cachedStats.expiresAt > now) {
+ c.header("x-cache", "HIT");
+ return c.json(cachedStats.value);
+ }
+
+ const value = await fetchStats();
+ cachedStats = { value, expiresAt: now + CACHE_TTL_MS };
+ c.header("x-cache", "MISS");
+ c.header("cache-control", "public, max-age=60, s-maxage=60");
+ return c.json(value);
+});
diff --git a/packages/api/src/schema/mesh-user.ts b/packages/api/src/schema/mesh-user.ts
index 3f5db72..ae05cd5 100644
--- a/packages/api/src/schema/mesh-user.ts
+++ b/packages/api/src/schema/mesh-user.ts
@@ -186,6 +186,18 @@ export type GetMyMeshStreamResponse = z.infer<
typeof getMyMeshStreamResponseSchema
>;
+// ---------------------------------------------------------------------
+// Public stats (unauthed landing counter)
+// ---------------------------------------------------------------------
+
+export const publicStatsResponseSchema = z.object({
+ messagesRouted: z.number(),
+ meshesCreated: z.number(),
+ peersActive: z.number(),
+ lastUpdated: z.string(),
+});
+export type PublicStatsResponse = z.infer;
+
export const getMyInvitesResponseSchema = z.object({
sent: z.array(
z.object({