Files
Cursor Agentandz c3af929c56 site: one API, one account, better cheaper faster
The bar is Models, Dev, Platform, Pricing, Docs, Enterprise, Log in, and Get API key. The homepage is seven sections. Launch pages stay in the tree and drop out of the index through lib/launch.ts. /platform forwards to /cloud. Model and repository floors come from the snapshots the build already keeps.

Co-authored-by: z <[email protected]>
2026-10-26 21:53:40 +00:00

1234 lines
45 KiB
TypeScript

'use client'
import React, { useEffect, useRef, useState } from "react"
import { useReducedMotion } from "framer-motion"
import { ArrowRight } from "lucide-react"
import { Github } from "@/components/brand"
import { CopyButton } from "@hanzo/ui/product"
import { Surface } from '@/components/product/Surface'
import Workloads from '@/components/cloud/Workloads'
import { KEYS } from "@/components/home/nav-data"
import { MODEL_COUNT } from '@/lib/counts'
import { ready, useSummary } from '@/lib/api/use'
import { cloudCategories, getPrimitive, spell, tour } from '@/lib/data/cloud-primitives'
import { MOVED } from '@/lib/publish'
import { Box, Caret, Text, View, XStack, YStack } from '@hanzo/ui'
import { Grid, tracks } from '@hanzo/ui/grid'
import { tap } from '@hanzo/app/lib/tap'
import { Action, Display, Lede, More, Title } from '@hanzo/ui/marketing'
const DOCS = "https://docs.hanzo.ai/docs/quickstart"
const GH = "https://github.com/hanzoai"
const STATUS = "https://status.hanzo.ai"
/**
* The image we operate for you, and the one you can pull — same artifact.
* Verified against GHCR: the tag resolves and the manifest answers.
*/
const DEPLOY = 'docker run -p 8080:8080 ghcr.io/hanzoai/cloud'
/** The installer, verified live: hanzo.sh answers with a POSIX shell script. */
const INSTALL = 'curl -fsSL https://hanzo.sh | sh'
/**
* The whole API, from your own machine. This is the CLI's OWN spelling —
* `Serve` is a registered subcommand in `hanzoai/cli` (src/main.rs) and takes
* `cloud` for the entire API or one service by name.
*
* NOT `hanzo up`, which several pages on this site print and which the shipped
* CLI does not have: an unrecognised first word is read as a task for the
* coding agent, so the command does not fail — it does something else entirely.
*/
const SERVE = 'hanzo serve cloud'
/**
* cloud.hanzo.ai's landing page — the body of `app/(marketing)/cloud/page.tsx`,
* promoted to this host's web root by the Dockerfile's `SURFACE=cloud`.
*
* It renders the PAGE only. The header and footer come from the shared
* `(marketing)` layout, which is the same chrome hanzo.ai wears — so the two
* hosts are one product with one nav, and this file cannot grow a second,
* divergent one.
*
* WHAT THIS PAGE SELLS, and what it does not. The apex sells Hanzo OS and the
* thesis behind it. This sells the ABSTRACTION: you can put a model, an agent,
* a database and a production application somewhere without first assembling a
* hyperscaler out of parts. From code to production without building the cloud
* first.
*
* That is a different argument from the one this page used to run, which was
* an inventory — the categories, then every product in them, twice. Counting
* the catalog explains what we own; it never explains why a reader should want
* one of these instead of ten. So the page now runs the workloads and the
* planes underneath them, and the catalog is a single explorer at the bottom
* for the reader who came to browse. `/products` is still the whole index.
*
* THE ARC, one claim to a screen: the cloud, the shape of an application on it,
* compute, where it can run, models, data, network, what you can see, what
* holds it, the one API under all of it, deploying, taking it with you, paying
* for it, then the catalog.
*
* NOTHING HERE ASSERTS A NUMBER IT CANNOT COUNT. The model count is asked of
* the gateway at read time. The API paths are the catalog's, which is itself
* gated on the served OpenAPI document. The tour's operations are checked
* against that document on every build. A claim this page cannot check is a
* claim this page does not make.
*/
/* ------------------------------------------------------------------ tour --- */
/** The products whose API this beat's operation belongs to, from the catalogue. */
function productsFor(path: string) {
return cloudCategories
.flatMap((c) => c.items)
.filter((p) => {
const api = p.api?.replace(/\/$/, '')
if (!api) return false
return path === api || path.startsWith(api + '/') || api.startsWith(path.replace(/\/$/, ''))
})
}
const TYPE_MS = 26
const HOLD_MS = 2100
/**
* The tour — one story, told as the operations that would actually run it.
*
* WHY IT CAN BE TRUSTED. Every beat is resolved at BUILD time against the served
* `/v1/openapi.json` (see `scripts/sync-catalog.mjs`): a beat survives iff the
* document carries its path AND its verb, so an invented path and a real path
* with the wrong verb are both dropped before they can reach a reader. The line
* is ours; the verb, the path and the description under it are the API's own
* words. The product chips come from the same catalogue the menu reads — and
* where an operation has no product row yet, none are drawn rather than a
* stand-in being found for it.
*
* IT IS THE PROOF, NOT THE DECORATION. It sits under the one-API claim because
* it is what settles it: a dozen operations across six planes, one key, and not
* one line of integration between them. A reader who does not believe the claim
* above can read the paths.
*
* MOTION IS OPTIONAL, not decorated with an opt-out. Under
* `prefers-reduced-motion` there is no typing and no auto-advance: the first
* beat is simply shown, whole, and the dots still work as buttons. Someone who
* wants to read is never a hostage — the panel pauses on the button, and on
* hover or keyboard focus, and a dot both selects a beat and stops the clock.
*
* NOTHING RESIZES. The line reserves two lines, the description two more and the
* chip row one, so a short beat and a long one occupy the same box and the fold
* never moves under the reader. One `setTimeout` chain drives it — no interval
* per beat, no timer left running when the tab is hidden.
*/
function Tour() {
const reduced = useReducedMotion()
const beats = tour.beats
const [at, setAt] = useState(0)
const [typed, setTyped] = useState(reduced ? Infinity : 0)
const [held, setHeld] = useState(false)
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
const beat = beats[at]
const line = beat?.line ?? ''
const paused = held || !!reduced
useEffect(() => {
if (paused || !beats.length) return
if (typed < line.length) {
timer.current = setTimeout(() => setTyped((n) => n + 1), TYPE_MS)
} else {
timer.current = setTimeout(() => {
setAt((i) => (i + 1) % beats.length)
setTyped(0)
}, HOLD_MS)
}
return () => clearTimeout(timer.current)
}, [typed, line.length, paused, beats.length])
// A hidden tab should not be typing to nobody.
useEffect(() => {
const onVis = () => setHeld(document.hidden ? true : false)
document.addEventListener('visibilitychange', onVis)
return () => document.removeEventListener('visibilitychange', onVis)
}, [])
const pick = (i: number) => {
setAt(i)
setTyped(Infinity)
setHeld(true)
}
if (!beats.length) return null
const shown = reduced ? line : line.slice(0, typed)
const lit = productsFor(beat.path)
return (
/* The three hold-handlers ride a plain element: gui's frame types no focus
or pointer handler, so on a Box they would reach nothing and the panel
would never pause for a reader. */
<div
onMouseEnter={() => setHeld(true)}
onMouseLeave={() => setHeld(false)}
onFocusCapture={() => setHeld(true)}
>
<Box
rounded="var(--radius-2xl)"
borderWidth={1}
borderColor="var(--white-10)"
bg="var(--white-03)"
p={20}
$sm={{ p: 28 }}
>
<Text render="p" display="block" fontSize="$1" lineHeight="$1" textTransform="uppercase" letterSpacing={0.55} color="var(--text-tertiary)">
{tour.story}
</Text>
{/* Two lines reserved, so a short beat and a long one are the same box. */}
<Text render="p" display="block" mt={12} minH="3.5rem" fontSize="$4" lineHeight="1.4rem" fontWeight="500" color="$ink">
{shown}
{/* The bar trailing the line as it types; the blink is @hanzo/ui's Caret. */}
{!reduced && typed < line.length ? <Caret render="span" display="inline-flex" ml={2} height={20} width={2} y={2} bg="var(--white-80)" /> : null}
</Text>
{/* The operation that beat would actually run, in the API's own words. */}
<Box mt="$1" minH="3.25rem">
<Text render="code" fontFamily="$mono" fontSize="$1" lineHeight="$1" color="var(--text-secondary)">
<Text color="var(--muted-foreground)">{beat.method}</Text> {beat.path}
</Text>
<Text render="p" display="block" mt={4} fontSize="$1" lineHeight="1.1172rem" color="var(--muted-foreground)" numberOfLines={2}>
{beat.summary}
</Text>
</Box>
{/* Real products, where the catalogue carries one for that operation. */}
<XStack mt="$3" minH="1.75rem" flexWrap="wrap" items="center" gap={6}>
{lit.map((p) => {
const Icon = p.icon
return (
<Text key={p.title} {...PILL} gap={6} px={10}>
<Text display="inline-flex" color="var(--muted-foreground)">
<Icon size={14} />
</Text>{' '}
{p.title}
</Text>
)
})}
</XStack>
{/* The button sits BESIDE the dots where there is room and ABOVE them
where there is not: at 390px the twelve targets wrap to three rows and
a vertically centred button reads as floating in the middle of them. */}
{/* A `YStack`, never a `Box`: a Box is `display: block`, so a gap and an
alignment on one reach nothing. The column IS the layout here. */}
<YStack
mt="$4"
items="flex-start"
gap="$3"
$sm={{ flexDirection: 'row', items: 'center' }}
>
<Text render={<button type="button" onClick={() => setHeld((v) => !v)} aria-pressed={held} />} {...PILL} px={12} bg="transparent" cursor="pointer">
{paused ? 'Play' : 'Pause'}
</Text>
<XStack mx={-4} flexWrap="wrap" items="center">
{beats.map((b, i) => (
// The DOT is drawn inside the target rather than being it. This site
// floors every button at 24px (44px where a finger points), which is
// WCAG 2.5.8 / 2.5.5 and correct — a 6px control is not clickable by
// anyone. Sized as a dot it obeyed the floor by stretching into a
// 6x24 bar; sized as a target with a dot in it, both are right. The
// width follows the same pointer question the height already asks,
// so a finger gets 44x44 and a mouse keeps a 24px dot row.
<View
key={b.path + b.method}
render={<button type="button" onClick={() => pick(i)} aria-label={b.line} aria-current={i === at ? 'true' : undefined} />}
group
width={24}
height={24}
items="center"
justify="center"
cursor="pointer"
$touchable={{ width: 44, height: 44 }}
>
<View
width={6}
height={6}
rounded="var(--radius-full)"
bg={i === at ? '$ink' : 'var(--text-tertiary)'}
transition="quickest"
$group-hover={i === at ? undefined : { bg: 'var(--muted-foreground)' }}
/>
</View>
))}
</XStack>
</YStack>
</Box>
</div>
)
}
/** A word in an outline: the tour's products and its play control. */
const PILL = {
display: 'inline-flex',
items: 'center',
gap: '$2',
py: 4,
borderWidth: 1,
borderColor: '$edge',
rounded: 'var(--radius-full)',
bg: 'var(--glass)',
fontSize: '$1',
lineHeight: '$1',
color: 'var(--text-secondary)',
} as const
/* ----------------------------------------------------------------- parts --- */
/** The section shell every band below shares, so none can drift from the rest. */
function Band({ children }: { children: React.ReactNode }) {
return (
<Box
render="section"
borderTopWidth={1}
borderColor="var(--neutral-900)"
px="$4"
py="$12"
$sm={{ px: '$5', py: '$14' }}
$lg={{ px: '$6' }}
>
<Box width="100%" maxW={1024} mx="auto">
{children}
</Box>
</Box>
)
}
/**
* One band's headline. One size, one weight, everywhere — and the size is the
* `title` ROLE rather than a scale written here, so this page cannot state a
* rung the rest of the site does not have.
*/
function Head({ children }: { children: React.ReactNode }) {
return <Title>{children}</Title>
}
/** The line under a headline. */
function Lead({ children }: { children: React.ReactNode }) {
return (
<Lede mt={24} maxW={672}>
{children}
</Lede>
)
}
/**
* The paragraph that follows a lead — the same measure and leading, one step
* further from the heading. Seven bands wrote it out identically.
*/
function Note({ top = 32, children }: { top?: number; children: React.ReactNode }) {
return (
<Text render="p" display="block" mt={top} maxW={672} fontSize="$4" lineHeight="1.5234rem" color="var(--muted-foreground)">
{children}
</Text>
)
}
/**
* A link out of a band — one shape, so twelve of them read as one page.
*
* `tap`, not a stated 44px: the floor is pointer-conditional site policy
* (24px always, 44px under a coarse pointer), and a `min-h-11` per link is the
* per-call-site copy of it that globals.css records as the defect.
*/
function Further({ href, children }: { href: string; children: React.ReactNode }) {
return (
<XStack mt={40}>
<More href={href} {...tap}>
{children} <ArrowRight size={16} />
</More>
</XStack>
)
}
/** A quieter way out beside a `Further`: the same shape, at book weight and in the muted ink. */
function Aside({ href, children }: { href: string; children: React.ReactNode }) {
return (
<More
href={href}
target="_blank"
rel="noreferrer noopener"
{...tap}
mt={40}
fontWeight="400"
color="var(--muted-foreground)"
hoverStyle={{ color: '$ink' }}>
{children}
</More>
)
}
/** A run of two links out, each carrying its own top margin. */
function Outs({ children }: { children: React.ReactNode }) {
return (
<XStack flexWrap="wrap" items="center" columnGap={32}>
{children}
</XStack>
)
}
/**
* A command, shown with a prompt glyph that is never copied.
*
* The row WRAPS rather than scrolling: a command that runs off the right edge
* of a phone is a command nobody can read before they run it. `overflow-wrap`
* at word boundaries, never `anywhere` — the latter broke the image name
* mid-token at 390px, which is a command nobody can trust.
*/
function Command({ value, id }: { value: string; id: string }) {
return (
<XStack
display="inline-flex"
maxW="100%"
flexWrap="wrap"
items="center"
gap="$3"
rounded="var(--radius-lg)"
borderWidth={1}
borderColor="var(--neutral-800)"
bg="$hover"
px="$4"
py={10}
>
<Text fontFamily="$mono" fontSize="$2" lineHeight="1.15rem" text="left" color="var(--text-secondary)" $platform-web={{ overflowWrap: 'break-word' }}>
$ {value}
</Text>
<CopyButton value={value} label={`Copy: ${value}`} size={20} id={id} />
</XStack>
)
}
/* ----------------------------------------------------------------- type --- */
/** A cell of a hairline grid: the gap is the rule, so each cell paints the ground. */
const CELL = { bg: 'var(--surface-card-quiet)', p: '$5' } as const
/** A cell's two lines: its name, and what it does. */
function CellTitle({ render = 'h3', children }: { render?: 'h3' | 'p'; children: React.ReactNode }) {
return (
<Text render={render} display="block" fontSize="$4" lineHeight="1.4rem" fontWeight="500" color="$ink">
{children}
</Text>
)
}
function CellLine({ top = 12, children }: { top?: number; children: React.ReactNode }) {
return (
<Text render="p" display="block" mt={top} fontSize="$2" lineHeight="1.3203rem" color="var(--muted-foreground)">
{children}
</Text>
)
}
/**
* A grid whose GAP is the rule between its cells: one hairline border round the
* whole block, a 1px gap filled by the block's own ground, and each cell
* painting the page black over it. Four bands draw it.
*/
function Cells({ columns, children }: { columns: 2 | 3 | 4; children: React.ReactNode }) {
return (
<Box
mt="$9"
overflow="hidden"
rounded="var(--radius-2xl)"
borderWidth={1}
borderColor="var(--neutral-800)"
bg="var(--neutral-800)"
>
<Grid columns={{ min: 240, max: columns }} gap={1}>
{children}
</Grid>
</Box>
)
}
/* ------------------------------------------------------------------ hero --- */
/**
* The claim, the reach, and one thing a developer can run.
*
* THE COPY ARRIVES. Each element rises on @hanzo/ui's `rise`, one 70ms beat
* after the one before it (`data-beat`) — quick enough that a reader who scrolls
* immediately is not waiting on it. It is a keyframe keyed on `data-motion`
* rather than a prop on a motion component, so it starts at FIRST PAINT instead
* of at hydration: an entrance declared in JavaScript ships `opacity: 0` in the
* HTML, and the largest element on the page has no business being invisible
* until a bundle lands. Under reduced motion there is no animation and no
* delay, so nothing waits hidden.
*/
function Hero({ models }: { models: string | null }) {
/** The nth thing to arrive: @hanzo/ui's `rise`, 70ms after the one before.
* The motion sheet turns the whole entrance off under reduced motion, so a
* reader who asked for stillness gets the page at once, delays and all. */
const rise = (i: number) => ({ 'data-motion': 'rise', '$platform-web': { animationDelay: `${i * 70}ms` } }) as const
// The model count leads the strip when the gateway has answered and is simply
// absent when it has not. A row that says "models" with no number is a row
// that spends a reader's attention on nothing.
const strip = [
models,
'GPUs',
'Functions',
'Containers',
'Databases',
'Observability',
'One API',
].filter(Boolean) as string[]
return (
// `svh`, not `vh`: mobile browser chrome makes `100vh` taller than the
// screen and the fold overflows by exactly the address bar.
<Box
render="section"
display="flex"
flexDirection="row"
minH="100svh"
width="100%"
items="center"
px="$4"
py="$6"
$sm={{ px: '$5', py: '$10' }}
/* From lg the copy starts under the header's wordmark: the header sets
its items a page gutter and 8px in, and so does this. At $6 the
heading sat 32px from the window while the logo above it sat 66. */
$lg={{ px: 'calc(var(--page-gutter) + 8px)' }}
>
{/* Two columns from lg, and the tracks change SHAPE at xl rather than
appearing there. Reading runs left to right, so the picture belongs
beside the copy at every width that can hold both.
At lg they share the width in proportion — 1fr to 1.1fr, which at 1024
is 434 against 478. Naming the fixed 34rem measure this early is what
used to force a stack: the first track takes its 544 whatever is left,
so the picture got 360 and a 360x203 still beside a full column of
type is worse than stacking.
From xl the measure is capped instead. Past ~34rem a line is wider
than anyone finishes reading, so every pixel beyond it belongs to the
picture — 544 against 776 at 1440. Keeping the proportion there would
grow both and leave the copy unreadable. */}
<Box
display="grid"
width="100%"
maxW={1600}
mx="auto"
items="center"
gap="$6"
gridTemplateColumns="minmax(0,1fr)"
$sm={{ gap: '$8' }}
$lg={{ gridTemplateColumns: 'minmax(0,1fr) minmax(0,1.1fr)' }}
$xl={{ gridTemplateColumns: 'minmax(0,34rem) minmax(0,1fr)', gap: '$9' }}
$xxl={{ maxW: 1800 }}
>
<YStack>
<Text render="p" display="block" {...rise(0)} fontSize="$2" lineHeight="1.15rem" fontWeight="500" textTransform="uppercase" letterSpacing={1.82} color="var(--muted-foreground)">
Hanzo Cloud
</Text>
<Display {...rise(1)} mt={20}>
{/* SOLID INK. A white -> neutral gradient clipped to the glyphs
makes the END of the line its dimmest point, so the last word
reads as a render that never finished. Hanzo's ink is
paper-white.
Four words, and they are the category rather than a feeling.
What this is IS the surprise: a cloud whose primitives assume
there is a model in the application.
The hyphen is U+2011, which does not offer a break. An ordinary
one does, and at 434px of measure the line broke after it: "The
AI-" over "native cloud.", which reads as a word cut in half. */}
The AI&#8209;native cloud.
</Display>
<Lede {...rise(2)} mt={28} maxW={672}>
Run models, agents, applications, data and infrastructure through one
operating plane.
</Lede>
<Text render="p" display="block" {...rise(3)} mt={20} maxW={672} fontSize="$3" lineHeight="1.4219rem" color="var(--muted-foreground)">
Kubernetes-native. Fully observable. Open source. Run it on Hanzo
infrastructure or infrastructure you control.
</Text>
<XStack {...rise(4)} mt={36} flexWrap="wrap" items="center" gap={12}>
<Action href={KEYS} fill>
Start building <ArrowRight size={16} />
</Action>
<Action href="/products">
Explore the cloud
</Action>
</XStack>
{/* The reach, as a strip rather than a paragraph. The one number in it
is asked of the gateway at read time rather than baked. */}
<Text render="p" display="block" {...rise(5)} mt={32} fontSize="$2" color="var(--muted-foreground)" $platform-web={{ lineHeight: '1.75' }}>
{strip.map((item, i) => (
<Text key={item} color="inherit">
{i > 0 && <Text color="var(--neutral-700)"> · </Text>}
{item}
</Text>
))}
</Text>
{/* ONE developer artifact on the fold, and it is the whole platform
running on a laptop. The tag resolves on GHCR and the console is
at :8080 with the API under /v1 on the same origin. */}
<YStack {...rise(6)} mt={28}>
<Command value={DEPLOY} id="deploy-cloud" />
</YStack>
</YStack>
{/* THE SECOND COLUMN. Below lg there is one track and it simply follows
the copy at full width, which is the only honest answer on a phone —
two tracks there give each about 170px.
The console, running. A reader deciding whether to put their work
here wants to see the thing they will be looking at every day, not a
diagram of how it fits together — and drawn rather than filmed, so
it reflows here, answers a screen reader, and can be opened. */}
<Box {...rise(7)} minW={0} mt="$9" $lg={{ mt: 0 }}>
<Surface name="Console" rail="apps" here="console" />
</Box>
</Box>
</Box>
)
}
/* ----------------------------------------------------------------- whole --- */
/**
* The shape of an AI application, and the fact that one system holds all of it.
*
* This is where the page used to state its inventory — the categories, then
* every product in them. An inventory answers "what exists"; a reader standing
* here is asking "will my application fit". So the stack is the WORKLOAD, not
* the catalog: the six things a modern application is made of, in the order
* they sit on each other.
*/
const WHOLE: [string, string][] = [
['Model', 'Inference, embeddings, routing, fallbacks.'],
['Agent', 'Tools, memory, sandboxes, orchestration.'],
['Application', 'Functions, containers, machines, endpoints.'],
['Data', 'Relational, vector, objects, cache.'],
['Infrastructure', 'Clusters, networking, regions, capacity.'],
['Operations', 'Identity, secrets, traces, billing.'],
]
function Whole() {
return (
<Band>
<Head>One cloud for the whole AI application.</Head>
<Lead>
Not a model API beside a container host beside a database beside a
tracing vendor. One system, with the parts already introduced to each
other.
</Lead>
<Box mt="$9" overflow="hidden" rounded="var(--radius-2xl)" borderWidth={1} borderColor="var(--neutral-800)">
<YStack render="ol" m={0} p={0}>
{WHOLE.map(([label, parts], i) => (
<Box
key={label}
render="li"
display="grid"
/* One track on a phone with the label's own lead above the line;
a 208px label column beside it from `$sm`, where the two are one
row and the row gap no longer applies. */
gridTemplateColumns="minmax(0,1fr)"
columnGap={32}
rowGap={8}
items="baseline"
borderTopWidth={i === 0 ? 0 : 1}
borderTopColor="var(--neutral-900)"
px={20}
py={20}
$sm={{
gridTemplateColumns: '13rem minmax(0,1fr)',
px: 28,
py: '$5',
}}
>
<Text fontFamily="$mono" fontSize="$1" lineHeight="$1" textTransform="uppercase" letterSpacing={1.54} color="var(--muted-foreground)">
{label}
</Text>
<Text display="block" fontSize="$4" lineHeight="1.4rem" color="var(--text-secondary)">
{parts}
</Text>
</Box>
))}
</YStack>
</Box>
<Note>
Everything shares the same organization, project, identity, policy,
usage and operational context.
</Note>
</Band>
)
}
/* ---------------------------------------------------------------- reach --- */
/**
* One control plane, more than one place to run.
*
* WHAT IS CLAIMED, and what is carefully not. Hanzo's own fleet runs on one
* provider; the other clouds are reached with credentials an org supplies for
* ITSELF, and a cluster is attached rather than provisioned. So the claim is
* "attach your own account or your own cluster" — which is what the code does —
* and never "we run your workloads across eight clouds", which nothing here
* would back.
*
* VISOR IS NAMED as the runtime and not as the product. The customer-facing
* names are Machines, GPUs, Clusters and Kubernetes — each of which passes this
* site's own reachability gate into the catalog — and Visor is what serves
* them, which is how the apex site already refers to it. The link goes to
* `/visor` here, never to a docs page: that path does not resolve.
*/
const REACH: [string, string][] = [
['Hanzo Cloud', 'Capacity we operate and meter, provisioned by the call.'],
['Your own cloud account', 'Your credentials, your machines, the same control plane over them.'],
['Your own cluster', 'Attach Kubernetes you already run and address it like any other.'],
]
function Reach() {
return (
<Band>
<Head>The cloud is bigger than one provider.</Head>
<Lead>
One control plane, different infrastructure underneath it. The API, the
console, the identities and the bill do not change when the machines do.
</Lead>
<Cells columns={3}>
{REACH.map(([title, line]) => (
<Box key={title} {...CELL} $sm={{ p: 28 }}>
<CellTitle>{title}</CellTitle>
<CellLine>{line}</CellLine>
</Box>
))}
</Cells>
<Note>
Machines, GPUs, clusters and Kubernetes are one surface whichever of
those it lands on. Visor is the runtime underneath them, and it meters
every provider against the org that asked.
</Note>
<Further href="/visor">Compute across your own accounts</Further>
</Band>
)
}
/* --------------------------------------------------------------- models --- */
function Models() {
return (
<Band>
<Head>Every model. One cloud interface.</Head>
<Lead>
Let Enso choose when you want the best answer for the job. Name a model
when you want exactly that one, every time. Run open weights when you
want to hold them yourself.
</Lead>
<Note>
Whichever you pick, auth, budgets, routing, fallbacks, usage and
observability stay in one system — so changing your mind about a model
is a string, not a migration.
</Note>
<XStack mt="$7" flexWrap="wrap" items="center" columnGap="$3" rowGap="$2">
{['OpenAI-compatible', '·', 'Anthropic-compatible'].map((w, i) => (
<Text key={i} fontFamily="$mono" fontSize="$2" lineHeight="1.15rem" color={w === '·' ? 'var(--neutral-700)' : 'var(--text-secondary)'}>
{w}
</Text>
))}
</XStack>
<Outs>
<Further href="/models">Every model we serve</Further>
<Further href="/agents">Build and run agents</Further>
</Outs>
</Band>
)
}
/* ----------------------------------------------------------------- data --- */
/**
* The data plane, one line per primitive.
*
* The LIST is the catalog's, not this file's: `cloudCategories` is read from
* the commerce snapshot and filtered to what publishes, so a primitive renamed
* upstream is renamed here and one withdrawn disappears. This file owns only
* the sentence under each name, keyed by slug — a primitive with no sentence
* still renders, with the catalog's own descriptor.
*/
const DATA: Record<string, string> = {
sql: 'Relational, per project and per tenant.',
vector: 'Embeddings stored and searched where they are used.',
kv: 'Cache and queues at request latency.',
s3: 'Objects, signed URLs, lifecycle rules.',
docdb: 'Documents on the MongoDB wire protocol.',
datastore: 'Columnar analytics over everything you collected.',
base: 'An application backend — records, rules, realtime.',
memory: 'What an agent is allowed to remember between runs.',
}
function Data() {
const plane = cloudCategories.find((c) => c.id === 'data')
if (!plane) return null
return (
<Band>
<Head>The data primitives AI applications actually use.</Head>
<Lead>
Not one database with an extension bolted on. Each primitive is the
right shape for the job, and they share a project, a key and a bill.
</Lead>
<View display="grid" gridTemplateColumns={tracks({ min: 280, max: 2 }, '40px')} columnGap={40} rowGap={32} mt={56}>
{plane.items.map((item) => (
<YStack key={item.slug} minW={0}>
<Text render="h3" display="block" fontSize="$4" lineHeight="1.4rem" fontWeight="500">
<More href={item.href} fontSize="$4" color="$ink">
{item.title}
</More>
</Text>
<CellLine top={8}>{DATA[item.slug] ?? item.desc}</CellLine>
</YStack>
))}
</View>
</Band>
)
}
/* -------------------------------------------------------------- network --- */
function Network() {
return (
<Band>
<Head>Everything connects without becoming your problem.</Head>
<Lead>
Services find each other. Traffic reaches them over TLS on a name you
chose. The edges are closed until you open one.
</Lead>
<Note>
The gateway fronts every API, DNS answers for your names, and routing
between services is something the cloud does rather than something you
write.
</Note>
<Further href="/network">The networking plane</Further>
</Band>
)
}
/* -------------------------------------------------------------- observe --- */
/**
* The differentiator, given a whole band rather than a card in a grid.
*
* A model call, an agent step, a query and a function invocation are four
* different products almost everywhere else, which means four different traces
* and no way to add them up. Here they are one request, so the sentence that
* matters is short enough to be a headline of its own.
*/
function Observe() {
return (
<Band>
<Head>See the entire system, not separate products.</Head>
<Lead>
One request, from the edge through an agent, a model, a tool, a database
and a function — and what every hop of it cost.
</Lead>
<Title render="p" quiet mt={56}>
One trace. End to end.
</Title>
<Note top={24}>
Logs, metrics, traces, evaluations and spend are read from the same
request, so &ldquo;why was that answer slow&rdquo; and &ldquo;why was
that answer expensive&rdquo; are the same question with the same answer.
</Note>
<Outs>
<Further href="/o11y">Tracing, metrics and evaluation</Further>
<Aside href={STATUS}>
Live status, in public <ArrowRight size={16} />
</Aside>
</Outs>
</Band>
)
}
/* ------------------------------------------------------------- security --- */
function Security() {
return (
<Band>
<Head>Security is part of the cloud.</Head>
<Lead>
Every person, agent and workload has an identity. Permissions follow the
resource rather than the caller. Secrets stay scoped to the environment
that needs them. Actions stay attributable to whoever took them.
</Lead>
<Note>
None of that is a product you add afterwards — an agent gets an identity
because it runs here, and the trail is written because the call went
through the same entry point as everything else.
</Note>
<Further href="/security">How identity, keys and audit work</Further>
</Band>
)
}
/* ------------------------------------------------------------------ api --- */
/**
* The one API, said as the addresses themselves.
*
* EVERY PATH IS THE CATALOG'S. `getPrimitive` reads the commerce snapshot,
* which `scripts/sync-catalog.mjs` gates on the served OpenAPI document — so a
* path printed here is one the platform answers, and a product whose path
* stops answering drops out of this list rather than becoming a 404 in a
* display font.
*/
const SURFACE = ['models', 'agents', 'functions', 'sql', 'vector', 's3']
function Api() {
const paths = SURFACE.map((slug) => getPrimitive(slug)?.api).filter(Boolean) as string[]
if (!paths.length) return null
return (
<Band>
<Head>One API. Every capability.</Head>
<Box
mt="$8"
overflow="hidden"
rounded="var(--radius-2xl)"
borderWidth={1}
borderColor="var(--neutral-800)"
bg="$sunken"
>
<Box
overflowX="auto"
px={20}
py="$5"
$sm={{ px: '$6', py: '$6' }}
>
{/* The API surface, as the addresses themselves, on a 2 leading. */}
<Text render="ul" display="block" m={0} p={0} fontFamily="$mono" fontSize="$2" color="var(--text-secondary)" $platform-web={{ lineHeight: '2' }}>
{paths.map((path) => (
<Text key={path} render="li" display="block" whiteSpace="nowrap" color="inherit">
<Text color="var(--text-tertiary)">api.hanzo.ai</Text>
{path}
</Text>
))}
<Text render="li" display="block" whiteSpace="nowrap" color="var(--text-tertiary)">
api.hanzo.ai/v1/&hellip;
</Text>
</Text>
</Box>
</Box>
<Note top={40}>
Same key. Same organization. Same project. Same policy model. Same usage
ledger.
</Note>
<Title render="p" quiet mt={24}>
The interface changes. The system doesn&rsquo;t.
</Title>
{tour.beats.length > 0 && (
<>
<Text render="p" display="block" mt={48} maxW={672} fontSize="$3" lineHeight="1.4219rem" color="var(--muted-foreground)">
{spell(tour.beats.length)} operations, one key, no glue. Each is
checked against the published OpenAPI document on every build, and a
beat the document does not carry is dropped rather than drawn.
</Text>
<Box mt="$6">
<Tour />
</Box>
</>
)}
</Band>
)
}
/* --------------------------------------------------------------- deploy --- */
function Deploy() {
return (
<Band>
<Head>Push code. Get a production system.</Head>
<Lead>
No separate build service to wire up. No container registry to stand up.
No secret product to buy. No logging to set up afterwards.
</Lead>
<Note>
What comes back is a running system with an address, a certificate, its
secrets, its metrics, its logs and its share of the bill — because those
were never separate purchases.
</Note>
<Outs>
<Further href="/registry">Builds, releases and rollback</Further>
<Further href="/cli">The CLI</Further>
</Outs>
</Band>
)
}
/* ------------------------------------------------------------- portable --- */
/**
* The exit, offered rather than hidden.
*
* Both commands are verified. `hanzo.sh` answers with a POSIX installer, and
* `serve` is a registered subcommand of the shipped CLI that takes `cloud` for
* the whole API. `hanzo up` — which several other pages on this site print — is
* NOT a command the CLI has; an unrecognised first word is read as a task for
* the coding agent, so it fails by doing something else.
*/
function Portable() {
return (
<Band>
<Head>Your cloud shouldn&rsquo;t lock you in.</Head>
<Lead>
The platform is open source, and the image we operate is the image you
can pull. Same API, same console, same data — on your cluster, in your
network, or on a laptop on a plane.
</Lead>
<YStack mt="$7" items="flex-start" gap="$3">
<Command value={INSTALL} id="install-cli" />
<Command value={SERVE} id="serve-cloud" />
</YStack>
{/* No margin on the row: `Further` and the source link each carry their own
40px, and a container that adds a second one spaces this band twice
as far off the commands above as every other band is spaced. */}
<Outs>
<Further href="/open-source">What is open, and under which license</Further>
<Aside href={GH}>
<Github size={16} /> View source
</Aside>
</Outs>
</Band>
)
}
/* ---------------------------------------------------------------- price --- */
/**
* The SHAPE of the bill, and one link to the rates.
*
* Plan mechanics are `/pricing`'s job. This page used to render the whole
* ladder — four plan cards, per-seat team pricing, minimum seats — which is a
* procurement question asked of a reader who is still deciding whether the
* thing is for them. What belongs here is what you are actually billed for.
*/
const PRICE: [string, string][] = [
['Models', 'by usage'],
['Compute', 'by execution'],
['Storage', 'by consumption'],
['Managed services', 'by usage'],
]
function Price() {
return (
<Band>
<Head>Pay for what runs.</Head>
<Lead>
Nothing is billed for existing. A function that was not called and a
machine that was not running cost what they did — nothing.
</Lead>
<Cells columns={4}>
{PRICE.map(([what, how]) => (
<Box key={what} {...CELL}>
<CellTitle render="p">{what}</CellTitle>
<Text render="p" display="block" mt={8} fontSize="$2" lineHeight="1.15rem" color="var(--muted-foreground)">
{how}
</Text>
</Box>
))}
</Cells>
<Note>
One organization balance and one usage ledger behind all of it, so every
line traces back to a request somebody made.
</Note>
<Further href="/pricing">Every plan and every rate</Further>
</Band>
)
}
/* -------------------------------------------------------------- explore --- */
/**
* The catalog, at the bottom, for the reader who came to browse.
*
* SEVEN, and they are the planes a reader builds ON. Apps and Web3 are not
* peers of Compute and Data — apps are built on this cloud, so listing them
* beside the primitives they consume tells a reader the wrong thing about what
* the cloud is. Both are still one click away in the full catalog, which is
* what `/products` is for.
*
* The names are the CATALOG'S names, not this file's. A card headed one thing
* that opens a page headed another is exactly the drift the catalog snapshot
* exists to prevent — and each card's href is a route the sync resolved
* against the filesystem.
*/
const EXPLORE = ['ai', 'compute', 'data', 'dev', 'observe', 'security', 'network']
function Explore() {
const shown = EXPLORE.map((id) => cloudCategories.find((c) => c.id === id)).filter(
Boolean,
) as typeof cloudCategories
if (!shown.length) return null
return (
<Band>
<Head>Explore the cloud.</Head>
<Lead>Every plane, and what is in it.</Lead>
<Cells columns={3}>
{shown.map((cat) => {
const Icon = cat.icon
return (
<Box key={cat.id} {...CELL}>
<Text render="h3" display="block" fontSize="$4" lineHeight="1.4rem" fontWeight="500">
<More href={MOVED[`/products/${cat.id}`]} gap={10} fontSize="$4" color="$ink">
<Text display="inline-flex" shrink={0} color="var(--muted-foreground)">
<Icon size={16} />
</Text>
{cat.title}
</More>
</Text>
<Text render="p" display="block" mt={16} fontSize="$2" $platform-web={{ lineHeight: '1.75' }}>
{/* Five, because a card is a taste of a plane rather than an
index of it — Observe carries seventeen products and a card
listing all of them is the wall this page just stopped
being. */}
{cat.items.slice(0, 5).map((item, i) => (
<Text key={item.slug} color="inherit">
{i > 0 && <Text color="var(--neutral-700)"> · </Text>}
<More href={item.href} {...tap} fontWeight="400" color="var(--muted-foreground)">
{item.title}
</More>
</Text>
))}
</Text>
</Box>
)
})}
</Cells>
<Further href="/products">View the complete catalog</Further>
</Band>
)
}
/* ----------------------------------------------------------------- cta --- */
function Close() {
return (
<Box
render="section"
position="relative"
overflow="hidden"
borderTopWidth={1}
borderColor="var(--neutral-900)"
px="$4"
py={112}
$sm={{ px: '$5', py: '$14' }}
$lg={{ px: '$6' }}
>
<Box position="relative" z="var(--z-raised)" width="100%" maxW={768} mx="auto" $platform-web={{ textAlign: 'center' }}>
<Title>Build on a cloud designed for AI.</Title>
<Lede mt={20} maxW={576} mx="auto">
Models. Agents. Compute. Data. Security. Observability.
</Lede>
<Lede mt={12} maxW={576} mx="auto">
One API from prototype to production.
</Lede>
<XStack mt={36} flexWrap="wrap" items="center" justify="center" gap="$3">
<Action href={KEYS} fill>
Start building <ArrowRight size={16} />
</Action>
<Action href={DOCS} target="_blank" rel="noreferrer noopener">
Read the docs
</Action>
</XStack>
<Text render="p" display="block" mt={28} fontSize="$2" lineHeight="1.15rem" color="var(--muted-foreground)">
Or run Hanzo Cloud on infrastructure you control.
</Text>
</Box>
</Box>
)
}
/* --------------------------------------------------------------- page ---- */
/**
* ProofStrip's rule, applied to a count asked at read time: floor to the
* hundred so the phrase stays true between one read and the next, and state a
* count below a hundred exactly rather than rounding it away to zero.
*/
function callable(n: number): string {
return n >= 100
? `${Math.floor(n / 100) * 100}+ callable models`
: `${n} callable models`
}
export default function CloudLanding() {
// How many models there are, asked of the price list when the page opens
// (lib/api). Until it answers — and if it never does — the phrase that would
// quote a number is not drawn at all.
const n = ready(useSummary())?.totalModels ?? MODEL_COUNT
return (
<>
<Hero models={n ? callable(n) : null} />
<Whole />
{/* Compute. `Workloads` is the band — one runtime, six kinds of work,
with the three isolations shown in the API's own words. */}
<Workloads />
<Reach />
<Models />
<Data />
<Network />
<Observe />
<Security />
<Api />
<Deploy />
<Portable />
<Price />
<Explore />
<Close />
</>
)
}