Contact Sales
diff --git a/website/src/app/(v2)/(marketing)/solutions/game-servers/page.tsx b/website/src/app/(v2)/(marketing)/solutions/game-servers/page.tsx
new file mode 100644
index 0000000000..935fbfed0a
--- /dev/null
+++ b/website/src/app/(v2)/(marketing)/solutions/game-servers/page.tsx
@@ -0,0 +1,816 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import {
+ Terminal,
+ Zap,
+ Globe,
+ ArrowRight,
+ Box,
+ Database,
+ Check,
+ Cpu,
+ RefreshCw,
+ Clock,
+ Cloud,
+ LayoutGrid,
+ Activity,
+ Wifi,
+ AlertCircle,
+ Gamepad2,
+ MessageSquare,
+ Bot,
+ Users,
+ FileText,
+ Workflow,
+ Gauge,
+ Eye,
+ Play,
+ Calendar,
+ GitBranch,
+ Timer,
+ Mail,
+ CreditCard,
+ Bell,
+ Server,
+ Sword,
+ Trophy,
+ Target,
+} from "lucide-react";
+import { motion } from "framer-motion";
+
+// --- Shared Design Components ---
+const Badge = ({ text, color = "red" }) => {
+ const colorClasses = {
+ orange: "text-orange-400 border-orange-500/20 bg-orange-500/10",
+ blue: "text-blue-400 border-blue-500/20 bg-blue-500/10",
+ purple: "text-purple-400 border-purple-500/20 bg-purple-500/10",
+ emerald: "text-emerald-400 border-emerald-500/20 bg-emerald-500/10",
+ red: "text-red-400 border-red-500/20 bg-red-500/10",
+ };
+
+ return (
+
+
+ {text}
+
+ );
+};
+
+const CodeBlock = ({ code, fileName = "match.ts" }) => {
+ return (
+
+
+
+
+
+
+ {code.split("\n").map((line, i) => (
+
+
+ {i + 1}
+
+
+ {(() => {
+ // Simple custom tokenizer for this snippet
+ const tokens = [];
+ let current = line;
+
+ // Handle comments first (consume rest of line)
+ const commentIndex = current.indexOf("//");
+ let comment = "";
+ if (commentIndex !== -1) {
+ comment = current.slice(commentIndex);
+ current = current.slice(0, commentIndex);
+ }
+
+ // Split remaining code by delimiters but keep them
+ const parts = current
+ .split(/([a-zA-Z0-9_$]+|"[^"]*"|'[^']*'|\s+|[(){},.;:[\]])/g)
+ .filter(Boolean);
+
+ parts.forEach((part, j) => {
+ const trimmed = part.trim();
+
+ // Keywords
+ if (
+ [
+ "import",
+ "from",
+ "export",
+ "const",
+ "return",
+ "async",
+ "await",
+ "function",
+ "if",
+ "else",
+ ].includes(trimmed)
+ ) {
+ tokens.push(
+
+ {part}
+ ,
+ );
+ }
+ // Functions & Special Rivet Terms
+ else if (
+ ["actor", "broadcast", "deathmatch", "isValidMove"].includes(trimmed)
+ ) {
+ tokens.push(
+
+ {part}
+ ,
+ );
+ }
+ // Object Keys / Properties / Methods
+ else if (
+ [
+ "state",
+ "actions",
+ "players",
+ "scores",
+ "map",
+ "join",
+ "move",
+ "connectionId",
+ "name",
+ "hp",
+ "pos",
+ "x",
+ "y",
+ "id",
+ ].includes(trimmed)
+ ) {
+ tokens.push(
+
+ {part}
+ ,
+ );
+ }
+ // Strings
+ else if (part.startsWith('"') || part.startsWith("'")) {
+ tokens.push(
+
+ {part}
+ ,
+ );
+ }
+ // Numbers
+ else if (!isNaN(Number(trimmed)) && trimmed !== "") {
+ tokens.push(
+
+ {part}
+ ,
+ );
+ }
+ // Default (punctuation, variables like 'c', etc)
+ else {
+ tokens.push(
+
+ {part}
+ ,
+ );
+ }
+ });
+
+ if (comment) {
+ tokens.push(
+
+ {comment}
+ ,
+ );
+ }
+
+ return tokens;
+ })()}
+
+
+ ))}
+
+
+
+
+ );
+};
+
+const SolutionCard = ({ title, description, icon: Icon, color = "red" }) => {
+ const getColorClasses = (col) => {
+ switch (col) {
+ case "orange":
+ return {
+ bg: "bg-orange-500/10",
+ text: "text-orange-400",
+ hoverBg: "group-hover:bg-orange-500/20",
+ border: "border-orange-500/80",
+ glow: "rgba(249,115,22,0.1)",
+ };
+ case "blue":
+ return {
+ bg: "bg-blue-500/10",
+ text: "text-blue-400",
+ hoverBg: "group-hover:bg-blue-500/20",
+ border: "border-blue-500/80",
+ glow: "rgba(59,130,246,0.1)",
+ };
+ case "purple":
+ return {
+ bg: "bg-purple-500/10",
+ text: "text-purple-400",
+ hoverBg: "group-hover:bg-purple-500/20",
+ border: "border-purple-500/80",
+ glow: "rgba(168,85,247,0.1)",
+ };
+ case "emerald":
+ return {
+ bg: "bg-emerald-500/10",
+ text: "text-emerald-400",
+ hoverBg: "group-hover:bg-emerald-500/20",
+ border: "border-emerald-500/80",
+ glow: "rgba(16,185,129,0.1)",
+ };
+ case "red":
+ return {
+ bg: "bg-red-500/10",
+ text: "text-red-400",
+ hoverBg: "group-hover:bg-red-500/20",
+ border: "border-red-500/80",
+ glow: "rgba(239,68,68,0.1)",
+ };
+ default:
+ return {
+ bg: "bg-red-500/10",
+ text: "text-red-400",
+ hoverBg: "group-hover:bg-red-500/20",
+ border: "border-red-500/80",
+ glow: "rgba(239,68,68,0.1)",
+ };
+ }
+ };
+ const c = getColorClasses(color);
+
+ return (
+
+ {/* Top Shine Highlight */}
+
+
+ {/* Soft Glow */}
+
+
+ {/* Sharp Edge Highlight (Masked & Shortened) */}
+
+
+
+
+ {description}
+
+
+ );
+};
+
+// --- Page Sections ---
+const Hero = () => (
+
+
+
+
+
+
+
+
+
+ Game Servers.
+ Serverless.
+
+
+
+ Launch an authoritative game server for every match instantly. Scale to millions of
+ concurrent players without managing fleets or Kubernetes.
+
+
+
+
+ Deploy Match
+
+
+
+
+ See Examples
+
+
+
+
+
+
+
+
{
+ c.state.players[c.connectionId] = { name, hp: 100, pos: {x:0, y:0} };
+ c.broadcast("player_join", c.state.players);
+ },
+
+ move: (c, { x, y }) => {
+ // Authoritative movement validation
+ if (isValidMove(c.state.map, x, y)) {
+ c.state.players[c.connectionId].pos = { x, y };
+ c.broadcast("update", { id: c.connectionId, x, y });
+ }
+ }
+ }
+});`}
+ />
+
+
+
+
+
+);
+
+const GameLoopArchitecture = () => {
+ const [tick, setTick] = useState(0);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setTick((t) => t + 1);
+ }, 500);
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+
+
+
+ The Game Loop
+
+
+ Traditional serverless functions die after a request. Rivet Actors stay alive,
+ maintaining the game state in memory and ticking the simulation as long as players are
+ connected.
+
+
+
+
+ {/* Visualization */}
+
+ {/* Central Actor (Server) */}
+
+
+
+ {/* Packets */}
+
+ {/* Outgoing Update (Broadcast) */}
+
+
+
+
+ {/* Clients */}
+
+ {/* Client 1 */}
+
+
+
+
+
Player 1
+ {/* Incoming Input */}
+
+
+
+ {/* Client 2 */}
+
+
+
+
+
Player 2
+ {/* Incoming Input P2 */}
+
+
+
+
+ {/* Server Console */}
+
+
server.tick({tick - 1})
+
+ server.tick({tick}) > Broadcasting State Snapshot
+
+
+ {tick % 2 !== 0 ? (
+
+ < Player 1 Input: Move(x: 12, y: 40)
+
+ ) : (
+
+ < Player 2 Input: Attack(target: P1)
+
+ )}
+
+
+
+
+ {/* Feature List */}
+
+
+
+
+ Authoritative Logic
+
+
+ Run your game logic (movement, hit detection, inventory) on the server to prevent
+ cheating. The Actor is the single source of truth.
+
+
+
+
+
+
+ Instant Connectivity
+
+
+ Clients connect directly to the specific Actor instance hosting their match via
+ WebSockets. No database polling latency.
+
+
+
+
+
+
+ Persistence on Exit
+
+
+ When the match ends, the final state (scores, loot, XP) is automatically saved to
+ disk.
+
+
+
+
+
+
+ );
+};
+
+const GameFeatures = () => {
+ const features = [
+ {
+ title: "Lobby Management",
+ description:
+ "Create persistent lobby actors that hold players before a match starts. Handle chat, loadouts, and ready states.",
+ icon: Users,
+ color: "red",
+ },
+ {
+ title: "Matchmaking",
+ description:
+ "Use a singleton 'Matchmaker' actor to queue players and spawn new Match actors when groups are formed.",
+ icon: Target,
+ color: "blue",
+ },
+ {
+ title: "Turn-Based Games",
+ description:
+ "Perfect for card games or board games. Actors can sleep for days between turns without incurring compute costs.",
+ icon: Clock,
+ color: "orange",
+ },
+ {
+ title: "Leaderboards",
+ description:
+ "High-throughput counters and sorting in memory. Update scores instantly without hammering a database.",
+ icon: Trophy,
+ color: "purple",
+ },
+ {
+ title: "Economy & Inventory",
+ description:
+ "Transactional state for trading items or currency. Ensure no item duplication glitches with serialized execution.",
+ icon: CreditCard,
+ color: "emerald",
+ },
+ {
+ title: "Spectator Mode",
+ description:
+ "Allow thousands of users to subscribe to a match actor to watch real-time updates without affecting player latency.",
+ icon: Eye,
+ color: "red",
+ },
+ ];
+
+ return (
+
+
+
+
+ Built for Multiplayer
+
+
+ Infrastructure primitives that understand the needs of modern games.
+
+
+
+
+ {features.map((feat, idx) => (
+
+
+
+ ))}
+
+
+
+ );
+};
+
+const UseCases = () => (
+
+
+
+
+
+
+ Real-Time Strategy (RTS)
+
+
+ A persistent world 4X strategy game where thousands of players move armies on a shared
+ map.
+
+
+ {[
+ "Sharding: Map divided into hex grids, each controlled by an Actor",
+ "Fog of War: Calculated on server, only visible units sent to client",
+ "Persistence: Game state survives server updates seamlessly",
+ ].map((item, i) => (
+
+
+
+
+ {item}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
Sector: Alpha-9
+
Units: 4,291 Active
+
+
+
+ Live
+
+
+
+
+ Tick Rate
+ 20Hz
+
+
+
+ Combat resolved in Grid[44,12]. 12 units lost. Updating clients...
+
+
+
+
+
+
+
+);
+
+const Ecosystem = () => (
+
+
+
+ Integrates with your engine
+
+
+ {["Unity", "Unreal Engine", "Godot", "Phaser", "Three.js", "PlayCanvas"].map(
+ (tech, i) => (
+
+ {tech}
+
+ ),
+ )}
+
+
+
+);
+
+export default function GameServersPage() {
+ return (
+
+
+
+
+
+
+
+
+ {/* CTA Section */}
+
+
+
+ Launch day ready.
+
+
+ Focus on the gameplay. Let Rivet handle the state, scaling, and persistence.
+
+
+
+ Start Building
+
+
+ Read the Docs
+
+
+
+
+
+
+ );
+}
+
diff --git a/website/src/app/(v2)/[section]/[[...page]]/layout.tsx b/website/src/app/(v2)/[section]/[[...page]]/layout.tsx
index d93586f18d..1a6212d06a 100644
--- a/website/src/app/(v2)/[section]/[[...page]]/layout.tsx
+++ b/website/src/app/(v2)/[section]/[[...page]]/layout.tsx
@@ -1,69 +1,3 @@
-import { Header } from "@/components/v2/Header";
-import { findActiveTab, findPageForHref, Sitemap } from "@/lib/sitemap";
-import { sitemap } from "@/sitemap/mod";
-import { Button } from "@rivet-gg/components";
-import Link from "next/link";
-import type { CSSProperties } from "react";
-import { buildFullPath, buildPathComponents } from "./util";
-import { NavigationStateProvider } from "@/providers/NavigationStateProvider";
-import { Tree } from "@/components/DocsNavigation";
-
-function Subnav({ path }: { path: string[] }) {
- const fullPath = buildFullPath(path);
- return (
-
- {sitemap.map((tab, i) => {
- const isActive = findPageForHref(fullPath, tab);
- return (
-
-
- {tab.title}
-
-
- );
- })}
-
- );
-}
-
-export default function Layout({ params: { section, page }, children }) {
- const path = buildPathComponents(section, page);
- const fullPath = buildFullPath(path);
- const foundTab = findActiveTab(fullPath, sitemap as Sitemap);
-
- return (
-
- }
- variant="full-width"
- mobileSidebar={
- foundTab?.tab.sidebar ? (
-
- ) : null
- }
- />
-
-
- );
+export default function Layout({ children }) {
+ return <>{children}>;
}
diff --git a/website/src/app/(v2)/[section]/doc-page.tsx b/website/src/app/(v2)/[section]/doc-page.tsx
new file mode 100644
index 0000000000..334662ec70
--- /dev/null
+++ b/website/src/app/(v2)/[section]/doc-page.tsx
@@ -0,0 +1,206 @@
+import fs from "node:fs/promises";
+import path from "node:path";
+import { Button } from "@rivet-gg/components";
+import { faPencil, Icon } from "@rivet-gg/icons";
+import clsx from "clsx";
+import type { Metadata } from "next";
+import { notFound } from "next/navigation";
+import { Comments } from "@/components/Comments";
+import { DocsNavigation } from "@/components/DocsNavigation";
+import { DocsPageDropdown } from "@/components/DocsPageDropdown";
+import { DocsTableOfContents } from "@/components/DocsTableOfContents";
+import { Prose } from "@/components/Prose";
+import { findActiveTab, type Sitemap } from "@/lib/sitemap";
+import { sitemap } from "@/sitemap/mod";
+import { buildFullPath, buildPathComponents, VALID_SECTIONS } from "./util";
+
+interface Param {
+ section: string;
+ page?: string[];
+}
+
+function createParamsForFile(section: string, file: string): Param {
+ const step1 = file.replace("index.mdx", "");
+ const step2 = step1.replace(".mdx", "");
+ const step3 = step2.split("/");
+ const step4 = step3.filter((x) => x.length > 0);
+
+ return {
+ section,
+ page: step4.length > 0 ? step4 : undefined,
+ };
+}
+
+async function loadContent(pathComponents: string[]) {
+ const module = pathComponents.join("/");
+ try {
+ return {
+ path: `${module}.mdx`,
+ component: await import(`@/content/${module}.mdx`),
+ };
+ } catch (error) {
+ if ((error as NodeJS.ErrnoException).code === "MODULE_NOT_FOUND") {
+ try {
+ const indexModule = `${module}/index`;
+ return {
+ path: `${indexModule}.mdx`,
+ component: await import(`@/content/${indexModule}.mdx`),
+ };
+ } catch (indexError) {
+ if ((indexError as NodeJS.ErrnoException).code === "MODULE_NOT_FOUND") {
+ return notFound();
+ }
+ throw indexError;
+ }
+ }
+ throw error;
+ }
+}
+
+export async function generateDocMetadata(
+ section: string,
+ page?: string[],
+): Promise
{
+ const pathComponents = buildPathComponents(section, page);
+ const {
+ component: { title, description },
+ } = await loadContent(pathComponents);
+
+ const fullPath = buildFullPath(pathComponents);
+ const canonicalUrl = `https://www.rivet.dev${fullPath}/`;
+
+ return {
+ title: `${title} - Rivet`,
+ description,
+ alternates: {
+ canonical: canonicalUrl,
+ },
+ };
+}
+
+export async function renderDocPage(section: string, page?: string[]) {
+ if (!VALID_SECTIONS.includes(section)) {
+ return notFound();
+ }
+
+ const pathComponents = buildPathComponents(section, page);
+ const {
+ path: componentSourcePath,
+ component: { default: Content, tableOfContents, title, description },
+ } = await loadContent(pathComponents);
+
+ const fullPath = buildFullPath(pathComponents);
+ const foundTab = findActiveTab(fullPath, sitemap as Sitemap);
+ const parentPage = foundTab?.page.parent;
+
+ const markdownPath = componentSourcePath
+ .replace(/\.mdx$/, "")
+ .replace(/\/index$/, "")
+ .replace(/\\/g, "/");
+
+ return (
+ <>
+
+ {foundTab?.tab.sidebar ? (
+
+ ) : null}
+
+
+ >
+ );
+}
+
+export async function getAllDocParams(): Promise> {
+ const staticParams: Array = [];
+ const seenParams = new Set();
+
+ for (const section of VALID_SECTIONS) {
+ const dir = path.join(process.cwd(), "src", "content", section);
+
+ try {
+ const baseKey = `${section}`;
+ if (!seenParams.has(baseKey)) {
+ seenParams.add(baseKey);
+ staticParams.push({ section });
+ }
+
+ const dirs = await fs.readdir(dir, { recursive: true });
+ const files = dirs.filter((file) => file.endsWith(".mdx"));
+
+ for (const file of files) {
+ const param = createParamsForFile(section, file);
+ const finalParam: Param =
+ param.page === undefined
+ ? { section: param.section }
+ : { section: param.section, page: param.page };
+
+ const key = finalParam.page
+ ? `${finalParam.section}/${finalParam.page.join("/")}`
+ : finalParam.section;
+
+ if (!seenParams.has(key)) {
+ seenParams.add(key);
+ staticParams.push(finalParam);
+ }
+ }
+ } catch (error) {
+ const baseKey = `${section}`;
+ if (!seenParams.has(baseKey)) {
+ seenParams.add(baseKey);
+ staticParams.push({ section });
+ }
+ }
+ }
+
+ return staticParams;
+}
+
diff --git a/website/src/app/(v2)/[section]/layout.tsx b/website/src/app/(v2)/[section]/layout.tsx
new file mode 100644
index 0000000000..acf6fbdb27
--- /dev/null
+++ b/website/src/app/(v2)/[section]/layout.tsx
@@ -0,0 +1,76 @@
+import { Header } from "@/components/v2/Header";
+import { findActiveTab, findPageForHref, Sitemap } from "@/lib/sitemap";
+import { sitemap } from "@/sitemap/mod";
+import { Button } from "@rivet-gg/components";
+import Link from "next/link";
+import type { CSSProperties, ReactNode } from "react";
+import { buildFullPath, buildPathComponents } from "./util";
+import { NavigationStateProvider } from "@/providers/NavigationStateProvider";
+import { Tree } from "@/components/DocsNavigation";
+
+function Subnav({ path }: { path: string[] }) {
+ const fullPath = buildFullPath(path);
+ return (
+
+ {sitemap.map((tab, i) => {
+ const isActive = findPageForHref(fullPath, tab);
+ return (
+
+
+ {tab.title}
+
+
+ );
+ })}
+
+ );
+}
+
+export default function Layout({
+ params: { section, page },
+ children,
+}: {
+ params: { section: string; page?: string[] };
+ children: ReactNode;
+}) {
+ const path = buildPathComponents(section, page);
+ const fullPath = buildFullPath(path);
+ const foundTab = findActiveTab(fullPath, sitemap as Sitemap);
+
+ return (
+
+ }
+ variant="full-width"
+ mobileSidebar={
+ foundTab?.tab.sidebar ? (
+
+ ) : null
+ }
+ />
+
+
+ );
+}
+
diff --git a/website/src/app/(v2)/[section]/util.ts b/website/src/app/(v2)/[section]/util.ts
new file mode 100644
index 0000000000..9015299740
--- /dev/null
+++ b/website/src/app/(v2)/[section]/util.ts
@@ -0,0 +1,19 @@
+export const VALID_SECTIONS = ["docs", "guides"];
+
+export function buildPathComponents(
+ section: string,
+ page?: string[],
+): string[] {
+ let defaultedPage = page ?? [];
+
+ if (defaultedPage[defaultedPage.length - 1] === "index") {
+ defaultedPage = defaultedPage.slice(0, -1);
+ }
+
+ return [section, ...defaultedPage];
+}
+
+export function buildFullPath(pathComponents: string[]): string {
+ return `/${pathComponents.join("/")}`;
+}
+
diff --git a/website/src/app/layout.tsx b/website/src/app/layout.tsx
index 8dc64bc5aa..6f528e183d 100644
--- a/website/src/app/layout.tsx
+++ b/website/src/app/layout.tsx
@@ -13,7 +13,7 @@ else if (process.env.CF_PAGES_URL)
export const metadata: Metadata = {
metadataBase,
- title: "Rivet - Build and scale stateful workloads",
+ title: "Rivet - Stateful Backends. Finally Solved.",
description:
"Rivet is a library for long-lived processes with durable state, realtime, and scalability. Easily self-hostable and works with your infrastructure.",
twitter: {
diff --git a/website/src/components/v2/Header.tsx b/website/src/components/v2/Header.tsx
index 8718ce3a14..cd93bb08c2 100644
--- a/website/src/components/v2/Header.tsx
+++ b/website/src/components/v2/Header.tsx
@@ -306,7 +306,7 @@ export function Header({
}
subnav={subnav}
- support={null}
+ support={<>>}
links={
{!learnMode && (
@@ -379,10 +379,11 @@ function DocsMobileNavigation({ tree }) {
];
const mainLinks = [
- { href: "/solutions", label: "Solutions", isDropdown: true },
+ { href: "/", label: "Home" },
{ href: "/docs", label: "Documentation" },
{ href: "/changelog", label: "Changelog" },
{ href: "/pricing", label: "Pricing" },
+ { href: "https://dashboard.rivet.dev/", label: "Dashboard" },
];
const solutions = [