diff --git a/Dockerfile b/Dockerfile
index 9056dcd..b4258bf 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,17 +1,19 @@
-# Multi-stage, one image, both planes -- which one runs is picked at container
-# start via APP (core|cloud), not at build time. This keeps a single build
-# cacheable across both instead of rebuilding the whole workspace twice.
+
FROM node:20-alpine AS deps
WORKDIR /w
COPY package.json package-lock.json ./
COPY packages/shared/package.json packages/shared/package.json
COPY apps/core/package.json apps/core/package.json
COPY apps/cloud/package.json apps/cloud/package.json
+COPY apps/community/package.json apps/community/package.json
+COPY apps/community-provider/package.json apps/community-provider/package.json
+COPY apps/community-admin/package.json apps/community-admin/package.json
+COPY apps/marketing/package.json apps/marketing/package.json
RUN npm install
FROM deps AS runtime
WORKDIR /w
COPY . .
COPY --from=deps /w/node_modules /w/node_modules
-EXPOSE 8080 8083
-CMD sh -c 'npm run build -w @xcloudify/${APP:?set APP=core|cloud} && npm run preview -w @xcloudify/${APP} -- --host 0.0.0.0 --port ${PORT:-8080}'
+EXPOSE 8080 8081 8082 8083 8084 8086
+CMD sh -c 'npm run build -w @xcloudify/${APP:?set APP=core|cloud|community|community-provider|community-admin|marketing} && npm run preview -w @xcloudify/${APP} -- --host 0.0.0.0 --port ${PORT:-8080}'
diff --git a/apps/marketing/.env.example b/apps/marketing/.env.example
new file mode 100644
index 0000000..431825b
--- /dev/null
+++ b/apps/marketing/.env.example
@@ -0,0 +1,9 @@
+CLOUD_URL=http://localhost:8080
+COMMUNITY_URL=http://localhost:8081
+CORE_URL=http://localhost:8083
+COMMUNITY_API_BASE_URL=http://localhost:5002/api
+# true|false to force the floating dev portal switcher on/off; unset follows
+# Vite's DEV flag (on for `vite dev`, off for a production build/preview).
+SHOW_DEV_NAV=
+
+ALLOWED_HOST=localhost
diff --git a/apps/marketing/docker-compose.yml b/apps/marketing/docker-compose.yml
new file mode 100644
index 0000000..53cf92b
--- /dev/null
+++ b/apps/marketing/docker-compose.yml
@@ -0,0 +1,20 @@
+
+services:
+ marketing:
+ build:
+ context: ../..
+ dockerfile: Dockerfile
+ container_name: xcloudify-marketing
+ environment:
+ APP: marketing
+ PORT: "8082"
+ ALLOWED_HOST: ${ALLOWED_HOST:-localhost}
+ VITE_CLOUD_URL: ${CLOUD_URL:-}
+ VITE_COMMUNITY_URL: ${COMMUNITY_URL:-}
+ VITE_CORE_URL: ${CORE_URL:-}
+ VITE_COMMUNITY_API_BASE_URL: ${COMMUNITY_API_BASE_URL:-http://localhost:5002/api}
+ # Shows the floating dev portal switcher regardless of build mode --
+ # unset falls back to Vite's DEV flag (see DevPortalSwitcher.tsx).
+ VITE_SHOW_DEV_NAV: ${SHOW_DEV_NAV:-}
+ ports:
+ - "8082:8082"
diff --git a/apps/marketing/index.html b/apps/marketing/index.html
new file mode 100644
index 0000000..64bcaa7
--- /dev/null
+++ b/apps/marketing/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ xcloudify
+
+
+
+
+
+
+
diff --git a/apps/marketing/package.json b/apps/marketing/package.json
new file mode 100644
index 0000000..2c600b0
--- /dev/null
+++ b/apps/marketing/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@xcloudify/marketing",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": { "@xcloudify/shared": "*" }
+}
diff --git a/apps/marketing/postcss.config.js b/apps/marketing/postcss.config.js
new file mode 100644
index 0000000..99417eb
--- /dev/null
+++ b/apps/marketing/postcss.config.js
@@ -0,0 +1 @@
+export default { plugins: { tailwindcss: {}, autoprefixer: {} } };
diff --git a/apps/marketing/src/App.tsx b/apps/marketing/src/App.tsx
new file mode 100644
index 0000000..f81f108
--- /dev/null
+++ b/apps/marketing/src/App.tsx
@@ -0,0 +1,29 @@
+/**
+ * Public marketing site. No auth, no session, no sidebar -- it is a separate
+ * deployable from the portals precisely so it can be served publicly without
+ * carrying any authenticated surface.
+ *
+ * The one live thing it does is the latency demo, which calls the community
+ * plane's anonymous endpoints directly (nodes/available + ping-session) and
+ * then measures browser-to-worker RTT over a real WebSocket. That is real
+ * infrastructure, not a mock.
+ */
+import { BrowserRouter, Routes, Route } from "react-router-dom";
+import { TooltipProvider } from "@/components/ui/tooltip";
+import { HomePage } from "@/components/HomePage";
+import { DevPortalSwitcher } from "./DevPortalSwitcher";
+import NotFound from "@/pages/NotFound";
+
+export default function App() {
+ return (
+
+
+
+
+ } />
+ } />
+
+
+
+ );
+}
diff --git a/apps/marketing/src/DevPortalSwitcher.tsx b/apps/marketing/src/DevPortalSwitcher.tsx
new file mode 100644
index 0000000..773c4d9
--- /dev/null
+++ b/apps/marketing/src/DevPortalSwitcher.tsx
@@ -0,0 +1,75 @@
+/**
+ * Dev-only quick nav. The public page has no portal-chooser section --
+ * every "sign in" CTA on HomePage links straight to cloud (see goToCloud
+ * there), and core has no public login at all. During local/staging work
+ * this is the only on-page way to reach core or community without typing
+ * the URL by hand.
+ *
+ * Visibility is VITE_SHOW_DEV_NAV (set via SHOW_DEV_NAV in .env / compose),
+ * not just Vite's built-in DEV flag -- the Docker path always runs `vite
+ * build` + `vite preview`, a production build, where DEV is false even on a
+ * box you're using for local testing. Leave the env var unset and it still
+ * falls back to DEV, so a plain `vite dev` shows it with zero config; set it
+ * explicitly to force the button on (or off) regardless of build mode.
+ */
+import { useState } from "react";
+import {
+ DropdownMenu, DropdownMenuContent, DropdownMenuItem,
+ DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { Boxes, Cloud as CloudIcon, Users, Wrench } from "lucide-react";
+
+type DevTarget = { label: string; href?: string; icon: React.ComponentType };
+
+const DEV_TARGETS: DevTarget[] = [
+ { label: "Core — operator console", href: import.meta.env.VITE_CORE_URL, icon: Boxes },
+ { label: "Cloud portal", href: import.meta.env.VITE_CLOUD_URL, icon: CloudIcon },
+ { label: "Community portal", href: import.meta.env.VITE_COMMUNITY_URL, icon: Users },
+];
+
+const explicitShowDevNav = import.meta.env.VITE_SHOW_DEV_NAV;
+const SHOW_DEV_NAV =
+ explicitShowDevNav != null ? explicitShowDevNav === "true" : import.meta.env.DEV;
+
+export function DevPortalSwitcher() {
+ const [open, setOpen] = useState(false);
+
+ if (!SHOW_DEV_NAV) return null;
+
+ return (
+
+ );
+}
diff --git a/apps/marketing/src/main.tsx b/apps/marketing/src/main.tsx
new file mode 100644
index 0000000..b569bfb
--- /dev/null
+++ b/apps/marketing/src/main.tsx
@@ -0,0 +1,5 @@
+import { createRoot } from "react-dom/client";
+import App from "./App";
+import "@/index.css";
+
+createRoot(document.getElementById("root")!).render();
diff --git a/apps/marketing/tailwind.config.ts b/apps/marketing/tailwind.config.ts
new file mode 100644
index 0000000..db5435b
--- /dev/null
+++ b/apps/marketing/tailwind.config.ts
@@ -0,0 +1,5 @@
+// Re-exports the shared theme so all four apps look identical. Content globs
+// are relative to this file, so each app scans its own src plus the shared
+// component library.
+import shared from "../../packages/shared/tailwind.config";
+export default shared;
diff --git a/apps/marketing/tsconfig.json b/apps/marketing/tsconfig.json
new file mode 100644
index 0000000..60472a7
--- /dev/null
+++ b/apps/marketing/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "compilerOptions": {
+ "target": "ES2020", "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"], "module": "ESNext",
+ "skipLibCheck": true, "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true, "isolatedModules": true,
+ "moduleDetection": "force", "noEmit": true, "jsx": "react-jsx",
+ "strict": false, "noUnusedLocals": false, "noUnusedParameters": false,
+ "noImplicitAny": false, "strictNullChecks": false, "allowJs": true,
+ "baseUrl": ".", "paths": { "@/*": ["../../packages/shared/src/*"] }
+ },
+ "include": ["src", "../../packages/shared/src"]
+}
diff --git a/apps/marketing/vite.config.ts b/apps/marketing/vite.config.ts
new file mode 100644
index 0000000..cb6410a
--- /dev/null
+++ b/apps/marketing/vite.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react-swc";
+import path from "path";
+
+// Every app resolves "@/" to the SHARED package, not its own src. That is
+// what lets ~110 shared components keep their existing imports untouched
+// while each app stays a thin entry point that only picks which portals to
+// mount. App-local files import relatively.
+const PORT = parseInt(process.env.PORT ?? "8082", 10);
+
+export default defineConfig({
+ server: { host: "::", port: PORT, allowedHosts: (process.env.ALLOWED_HOST ?? "localhost").split(",") },
+ preview: { host: "::", port: PORT, allowedHosts: (process.env.ALLOWED_HOST ?? "localhost").split(",") },
+ plugins: [react()],
+ resolve: { alias: { "@": path.resolve(__dirname, "../../packages/shared/src") } },
+});
diff --git a/packages/shared/src/components/HomePage.tsx b/packages/shared/src/components/HomePage.tsx
new file mode 100644
index 0000000..8c84adf
--- /dev/null
+++ b/packages/shared/src/components/HomePage.tsx
@@ -0,0 +1,484 @@
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
+import {
+ DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { useNavigate } from "react-router-dom";
+import {
+ Shield, Zap, Globe, Users, ArrowRight, Check, Boxes,
+ Cloud as CloudIcon, Server, Network, Database, ChevronDown,
+} from "lucide-react";
+import { CoreArchitectureDiagram } from "@/components/marketing/CoreArchitectureDiagram";
+import { LivePingDemo } from "@/components/marketing/LivePingDemo";
+
+type PlatformTab = "core" | "cloud" | "community";
+
+const PLATFORM_MODES: { value: PlatformTab; icon: typeof Boxes; label: string }[] = [
+ { value: "core", icon: Boxes, label: "Core" },
+ { value: "cloud", icon: CloudIcon, label: "Cloud" },
+ { value: "community", icon: Users, label: "Community" },
+];
+
+const HERO_STACK_ROWS = [
+ { icon: Server, label: "Compute" },
+ { icon: Network, label: "Network" },
+ { icon: Database, label: "Storage" },
+ { icon: Boxes, label: "Nodes" },
+];
+
+const HERO_MODE_BARS = [
+ { label: "Core", pct: 70 },
+ { label: "Cloud", pct: 45 },
+ { label: "Community", pct: 85 },
+];
+
+const CORE_POINTS = [
+ "One primitive for compute, network, and storage",
+ "Runs underneath any orchestrator you already use",
+ "Fewer nodes for the same headroom",
+];
+
+const CLOUD_FEATURES = [
+ { icon: Shield, title: "OIDC login", desc: "Single sign-on, no shared credentials to leak." },
+ { icon: Globe, title: "Projects & VDCs", desc: "Organize resources the way your org is structured." },
+ { icon: Users, title: "Role-based access", desc: "Invite teammates as readers or writers, per VDC." },
+];
+
+const COMMUNITY_POINTS = [
+ "Real machines, not a mockup",
+ "Latency measured directly browser → worker",
+ "No signup required to try it",
+];
+
+const CAPABILITIES = [
+ {
+ icon: Shield,
+ title: "Enterprise Security",
+ description: "Military-grade security with end-to-end encryption and compliance"
+ },
+ {
+ icon: Zap,
+ title: "Lightning Fast",
+ description: "Deploy and scale workloads instantly across global infrastructure"
+ },
+ {
+ icon: Globe,
+ title: "Global Reach",
+ description: "Multi-region deployment with edge computing capabilities"
+ },
+ {
+ icon: Users,
+ title: "Team Collaboration",
+ description: "Built for teams with role-based access and real-time collaboration"
+ }
+];
+
+const BENEFITS = [
+ "99.99% uptime SLA",
+ "Auto-scaling infrastructure",
+ "24/7 expert support",
+ "Enterprise-grade security",
+ "Global CDN network",
+ "One-click deployments"
+];
+
+function SectionHeading({ title, description }: { title: string; description: string }) {
+ return (
+
+
{title}
+
{description}
+
+ );
+}
+
+export const HomePage = () => {
+ const navigate = useNavigate();
+ // The marketing site hosts no authenticated surface; every "sign in" CTA
+ // is a real link out to wherever the cloud portal is actually deployed --
+ // that's the plane customers sign up on day to day (see the Cloud tab
+ // below). A no-op with no configured URL is fine here: on any real
+ // deploy this is set, and locally the dev switcher covers the gap.
+ const cloudUrl = import.meta.env.VITE_CLOUD_URL as string | undefined;
+ const goToCloud = () => {
+ if (cloudUrl) window.location.href = cloudUrl;
+ };
+ const [platformTab, setPlatformTab] = useState("core");
+
+ const goToPlatform = (tab: PlatformTab) => {
+ setPlatformTab(tab);
+ document.getElementById("platform")?.scrollIntoView({ behavior: "smooth", block: "start" });
+ };
+
+ return (
+
+ One infrastructure platform.
+ Run it your way.
+
+
+
+ The same compute, network, and storage primitives — self-hosted as
+ core, consumed managed as cloud, or leased from community hardware.
+ Pick the model that fits, switch anytime.
+
+ Core is identity-free infrastructure — compute, network, and
+ storage as one set of primitives underneath whatever orchestrator
+ you already run.
+
+
+ {CORE_POINTS.map((point) => (
+
+
+ {point}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
The managed control plane for teams
+
+ Cloud is what most customers use day to day: sign in, get a
+ project provisioned automatically, organize work into virtual
+ data centers, and invite teammates with read or write roles
+ scoped per VDC.
+
+
+
+
+ {CLOUD_FEATURES.map((f) => (
+
+
+
+
+
+
{f.title}
+
{f.desc}
+
+
+ ))}
+
+
+
+
+
+
+
+
Leased hardware, real machines
+
+ Community is spare capacity from third-party operators: register
+ your own machine to earn from it, or lease someone else's after
+ checking the latency yourself.
+
+
+ {COMMUNITY_POINTS.map((point) => (
+
+
+ {point}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+ {/* Capabilities Grid */}
+
+
+
+
+ {CAPABILITIES.map((feature) => (
+
+
+
+
+
+
{feature.title}
+
+ {feature.description}
+
+
+
+ ))}
+
+
+
+ {/* Benefits Section */}
+
+
+
+
+
+ Built for Modern Teams
+
+
+ From startups to Fortune 500 companies, XCloudify provides the tools
+ and infrastructure you need to succeed in the cloud-first world.
+
+
+
+ {BENEFITS.map((benefit) => (
+
+
+
+
+ {benefit}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
99.99%
+
Uptime SLA
+
+
+
< 100ms
+
Global Latency
+
+
+
24/7
+
Expert Support
+
+
+
+
+
+
+
+
+ {/* CTA Section */}
+
+
+
+
+ Ready to Get Started?
+
+
+ Join thousands of developers already building on XCloudify
+
+
+
+
+
+
+ {/* Footer */}
+
+
+
+ );
+};
diff --git a/packages/shared/src/components/marketing/CoreArchitectureDiagram.tsx b/packages/shared/src/components/marketing/CoreArchitectureDiagram.tsx
new file mode 100644
index 0000000..851df5c
--- /dev/null
+++ b/packages/shared/src/components/marketing/CoreArchitectureDiagram.tsx
@@ -0,0 +1,85 @@
+import { Boxes, Database, Network, Server } from "lucide-react";
+
+const SELF_MANAGED_LAYERS = [
+ "Your applications",
+ "Load balancer",
+ "Orchestrator (Kubernetes)",
+ "Storage layer — self-managed",
+ "Network / SDN — self-managed",
+];
+
+const CORE_LAYERS = ["Your applications", "Orchestrator (Kubernetes, or bring your own)"];
+
+/**
+ * Visualizes why xCloudify core exists: it collapses the storage/network
+ * layers teams otherwise hand-roll into one primitive underneath whatever
+ * orchestrator they already run, so the same headroom needs fewer nodes.
+ */
+export function CoreArchitectureDiagram() {
+ return (
+
+ );
+}
diff --git a/packages/shared/src/components/marketing/LivePingDemo.tsx b/packages/shared/src/components/marketing/LivePingDemo.tsx
new file mode 100644
index 0000000..d670110
--- /dev/null
+++ b/packages/shared/src/components/marketing/LivePingDemo.tsx
@@ -0,0 +1,204 @@
+import { useCallback, useEffect, useState } from "react";
+import { Cpu, Gauge, MapPin, MemoryStick, ShieldCheck, Wifi, WifiOff } from "lucide-react";
+
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import { communityApi, CommunityNode, measurePing } from "@/services/communityApi";
+
+interface PingResult {
+ medianMs: number | null;
+ path: string;
+ error?: string;
+}
+
+/**
+ * Decorative browser<->worker link. Dashed and static when there is no
+ * community plane to reach; pulsing when there is, so the demo's own visual
+ * matches what it is honestly able to claim in the copy above it.
+ */
+function ConnectionIllustration({ reachable }: { reachable: boolean }) {
+ return (
+
+ );
+}
+
+export function LivePingDemo() {
+ const [nodes, setNodes] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [offline, setOffline] = useState(false);
+ const [pings, setPings] = useState>({});
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ try {
+ setNodes(await communityApi.listAvailableNodes());
+ setOffline(false);
+ } catch (err) {
+ // A network-level failure just means this install isn't running a
+ // community plane -- expected on plenty of deploys. Fold it into the
+ // offline illustration below instead of surfacing the raw fetch
+ // error, but keep it in the console for anyone actually debugging.
+ console.error("LivePingDemo: could not reach the community plane", err);
+ setOffline(true);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ const runPing = async (node: CommunityNode) => {
+ setPings((p) => ({ ...p, [node.id]: "pending" }));
+ try {
+ const session = await communityApi.createPingSession(node.id);
+ const result = await measurePing(session);
+ setPings((p) => ({
+ ...p,
+ [node.id]: { medianMs: result.medianMs, path: result.path },
+ }));
+ } catch (err) {
+ setPings((p) => ({
+ ...p,
+ [node.id]: {
+ medianMs: null,
+ path: "unknown",
+ error: err instanceof Error ? err.message : String(err),
+ },
+ }));
+ }
+ };
+
+ return (
+
+
+
+
+ {offline
+ ? "This demo pings real community-hosted machines directly from your browser, not through our servers — it just needs a community plane to talk to, and this install isn't running one."
+ : "These are real, community-hosted machines on the marketplace right now. Ping one straight from your browser — no signup required. We only introduce the two ends; the measurement runs over a direct peer-to-peer connection to the machine, not through our servers."}
+
+
+
+ {loading &&
Loading live nodes…
}
+
+ {!loading && offline && (
+
+
+
+
+
+ Demo unavailable on this install
+
+
+ No community plane is configured here, so there's nothing to ping yet.
+