feat: Marketing site
Server Deploy / deploy (push) Successful in 5m51s

This commit is contained in:
2026-07-22 16:50:13 +01:00
parent 7a3b8cb700
commit 693d59a3e2
51 changed files with 10656 additions and 12 deletions
+131
View File
@@ -0,0 +1,131 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "About",
description:
"Vantage began as a weekend fix for a lost laptop and grew into a fleet control plane. How it is built, and what it deliberately does not do.",
};
export default function AboutPage() {
return (
<>
<section className="rail band band--open">
<span className="tag">About</span>
<h1 style={{ fontSize: "var(--s-3)", margin: "0.8rem 0 1.1rem", maxWidth: "20ch" }}>
Built for the fleet nobody was given a budget to manage.
</h1>
<div className="split" style={{ marginTop: "2.4rem" }}>
<div className="prose">
<p>
Vantage started as a weekend fix for a bad afternoon. A laptop was lost, and finding every server that
trusted its key meant SSHing into each one with a text editor open. The list lived in someone&apos;s head.
Two of the boxes were not on it.
</p>
<p>
The obvious tools were all heavier than the problem. A configuration management stack to write one file. A
bastion host that becomes the thing you now have to keep alive. A certificate authority with a rotation
story nobody wanted to own.
</p>
<p>
So it began with one job done properly: hold <code>authorized_keys</code> to a known state. Then the same
agent turned out to be the right place to run a deploy script, check whether a service was answering, and
open a shell when something was on fire. Each addition had to earn its place by riding the connection that
already existed.
</p>
<p>
Today it runs across homelabs, small hosting providers, and agencies who inherit client servers and need
to prove who can reach them.
</p>
</div>
<div>
<span className="tag">How it is built</span>
<div className="specs">
<div className="spec">
<span className="spec__k">SERVER</span>
<div>
<h3>Go, MongoDB, Redis</h3>
<p>
One Go binary serving REST for the interface and gRPC for agents. MongoDB holds everything durable;
Redis holds sessions and nothing else.
</p>
</div>
</div>
<div className="spec">
<span className="spec__k">WEB</span>
<div>
<h3>Next.js</h3>
<p>
An operations interface, not a brochure: dense tables, live log streams, and state you can read at a
glance.
</p>
</div>
</div>
<div className="spec">
<span className="spec__k">AGENT</span>
<div>
<h3>Go, Linux and Windows</h3>
<p>
A single static binary under systemd or as a Windows service. No runtime, no dependencies, no
package manager involved.
</p>
</div>
</div>
<div className="spec">
<span className="spec__k">CONSOLE</span>
<div>
<h3>Guacamole</h3>
<p>
Protocol handling is a solved problem. We proxy the connection and manage the credentials around it.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<section className="rail band">
<span className="tag">Security posture</span>
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "24ch" }}>
The parts worth being specific about.
</h2>
<div className="caps">
<article className="cap">
<span className="cap__k">Tokens</span>
<h3>Hashed, never stored plain</h3>
<p>
Agent tokens and the secrets read token are held as SHA-256 hashes. The plaintext exists on the
agent&apos;s own disk at 0600 and nowhere else.
</p>
</article>
<article className="cap">
<span className="cap__k">At rest</span>
<h3>AES-256-GCM</h3>
<p>
Private keys, passphrases, vault secrets, identity provider secrets and console credentials are encrypted
with a key held only by your deployment.
</p>
</article>
<article className="cap">
<span className="cap__k">One-time</span>
<h3>Tokens that expire and spend</h3>
<p>
Pre-registration tokens last an hour and work once. Console session tokens are consumed the moment the
tunnel opens.
</p>
</article>
<article className="cap">
<span className="cap__k">Recorded</span>
<h3>Every mutation is audited</h3>
<p>
Assignments, revocations, runs, console sessions, secret reveals and settings changes are attributed and
kept.
</p>
</article>
</div>
</section>
</>
);
}
+62
View File
@@ -0,0 +1,62 @@
import type { Metadata } from "next";
import { ContactForm } from "@/components/ContactForm";
export const metadata: Metadata = {
title: "Contact",
description: "Sales questions, self-hosted licensing, security disclosures and bug reports.",
};
const CHANNELS = [
{
title: "Support",
body: "Everything else, including anything urgent.",
link: "support@hostxtra.co.uk",
href: "mailto:support@hostxtra.co.uk",
},
{
title: "Security disclosure",
body: "Encrypted reports, acknowledged within 72 hours.",
link: "support@hostxtra.co.uk",
href: "mailto:support@hostxtra.co.uk?subject=Security%20disclosure",
},
{
title: "Bugs and feature requests",
body: "Public tracker, read by the people who write the code.",
link: "git.vantage.sh/vantage",
href: "https://git.vantage.sh/vantage",
},
{
title: "Status",
body: "Control plane uptime and incident history.",
link: "status.vantage.sh",
href: "https://status.vantage.sh",
},
];
export default function ContactPage() {
return (
<section className="rail band band--open">
<span className="tag">Contact</span>
<h1 style={{ fontSize: "var(--s-3)", margin: "0.8rem 0 1rem", maxWidth: "15ch" }}>
Tell us what your fleet looks like.
</h1>
<div className="split" style={{ marginTop: "2.4rem" }}>
<div className="card">
<ContactForm />
</div>
<div>
<p className="prose">Pick the right door and you will get a faster answer.</p>
{CHANNELS.map((channel) => (
<div className="chan" key={channel.title}>
<h3>{channel.title}</h3>
<p>{channel.body}</p>
<a href={channel.href}>{channel.link}</a>
</div>
))}
</div>
</div>
</section>
);
}
+1184
View File
File diff suppressed because it is too large Load Diff
+41
View File
@@ -0,0 +1,41 @@
import type { Metadata } from "next";
import "./globals.css";
import { Footer } from "@/components/Footer";
import { Nav } from "@/components/Nav";
import { ThemeScript } from "@/components/ThemeScript";
export const metadata: Metadata = {
metadataBase: new URL("https://vantage.sh"),
title: {
default: "Vantage — one control plane for the whole fleet",
template: "%s — Vantage",
},
description:
"Self-hosted fleet control: SSH key assignment, workflow execution, service monitoring, a secrets vault and a browser console, across every server you manage.",
openGraph: {
type: "website",
siteName: "Vantage",
title: "Vantage — one control plane for the whole fleet",
description:
"Self-hosted fleet control: SSH keys, workflows, monitors, secrets and consoles, over one outbound agent connection.",
},
icons: { icon: "/images/vantage_logo.svg" },
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en-GB">
<head>
<ThemeScript />
</head>
<body>
<a className="skip" href="#main">
Skip to content
</a>
<Nav />
<main id="main">{children}</main>
<Footer />
</body>
</html>
);
}
+210
View File
@@ -0,0 +1,210 @@
import Link from "next/link";
import { InstrumentPanel } from "@/components/InstrumentPanel";
export default function OverviewPage() {
return (
<>
<div className="heroband">
<section className="rail hero">
<span className="tag">Self-hosted fleet control plane</span>
<h1>Your servers, under one pane of glass you actually own.</h1>
<p className="lede">
Vantage holds SSH keys, runs scripts, watches services, stores secrets and opens consoles across every
machine you manage. One agent per server, outbound connections only, all state in your own database.
</p>
<div className="hero__acts">
<Link className="btn btn--solid" href="/start">
Create your organisation
</Link>
<Link className="btn btn--line" href="/platform">
What it does
</Link>
</div>
<p className="hero__foot">Free for 3 servers · Linux and Windows agents · Self-host the whole stack</p>
</section>
</div>
<InstrumentPanel />
<section className="rail band band--open">
<span className="tag">The problem</span>
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "24ch" }}>
Six tools, six sources of truth, one afternoon lost.
</h2>
<div className="split" style={{ marginTop: "2rem" }}>
<p className="prose">
Most small fleets end up with keys in a spreadsheet, scripts in someone&apos;s home directory, uptime checks
in a separate service, secrets in a chat thread, and no record of who ran what. None of those systems know
about each other, so every question who can reach this box, what ran on it last, is it even up gets
answered by hand.
</p>
<div className="specs specs--flush">
<div className="spec">
<span className="spec__k">ONE AGENT</span>
<div>
<h3>Everything rides one connection</h3>
<p>
Keys, steps, checks and inventory all travel over the same outbound link. Installing a second thing is
not the answer.
</p>
</div>
</div>
<div className="spec">
<span className="spec__k">ONE RECORD</span>
<div>
<h3>Every change is an audit event</h3>
<p>
Assignments, revocations, runs, console sessions and settings changes all land in the same log,
attributed to a person.
</p>
</div>
</div>
</div>
</div>
</section>
<section className="rail band">
<span className="tag">Capabilities</span>
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "22ch" }}>One control plane, six jobs.</h2>
<div className="caps">
<article className="cap">
<span className="cap__k">Access</span>
<h3>SSH keys</h3>
<p>
Assign public keys per server and revoke them softly. The agent diffs desired state against the file and
rewrites <code>authorized_keys</code> atomically.
</p>
<ul>
<li>fingerprint deduplication</li>
<li>agent-side keypair generation</li>
<li>revocation history preserved</li>
</ul>
</article>
<article className="cap">
<span className="cap__k">Execution</span>
<h3>Workflows</h3>
<p>
A library of bash and PowerShell steps with declared inputs, outputs and secret references, composed into
workflows that target a set of servers.
</p>
<ul>
<li>live streamed step logs</li>
<li>stop, continue or retry on failure</li>
<li>values passed between steps</li>
</ul>
</article>
<article className="cap">
<span className="cap__k">Uptime</span>
<h3>Monitors</h3>
<p>
HTTP, TCP, ICMP and TLS checks, run either from the control plane or from an agent inside the target
network.
</p>
<ul>
<li>incidents and uptime history</li>
<li>certificate expiry warnings</li>
<li>alerts to five channel types</li>
</ul>
</article>
<article className="cap">
<span className="cap__k">Secrets</span>
<h3>Vault</h3>
<p>
Grouped key/value secrets encrypted with AES-256-GCM, injected into workflow steps at execution and never
written to logs.
</p>
<ul>
<li>read token for External Secrets Operator</li>
<li>rotatable, hashed at rest</li>
<li>reveal is an audited action</li>
</ul>
</article>
<article className="cap">
<span className="cap__k">Access</span>
<h3>Browser console</h3>
<p>
Open an SSH, RDP or VNC session in the browser. SSH authenticates with a stored key, and every session is
recorded in the audit log.
</p>
<ul>
<li>one-time session tokens</li>
<li>credentials consumed on connect</li>
<li>no client software</li>
</ul>
</article>
<article className="cap">
<span className="cap__k">Health</span>
<h3>Inventory and updates</h3>
<p>
CPU, memory, swap, disks and kernel reported continuously, alongside pending OS package updates you can
apply from the interface.
</p>
<ul>
<li>metrics every 30 seconds</li>
<li>one-click package updates</li>
<li>agents update themselves</li>
</ul>
</article>
</div>
</section>
<section className="rail band">
<span className="tag">Getting started</span>
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "20ch" }}>
Four steps, and the first three take a minute.
</h2>
<div className="flow">
<div className="flow__c">
<span className="flow__n">FIRST</span>
<h3>Create an organisation</h3>
<p>You become the owner. Everything inside is invisible to every other organisation.</p>
</div>
<div className="flow__c">
<span className="flow__n">THEN</span>
<h3>Add a server</h3>
<p>Copy the install one-liner. It expires in an hour and works exactly once.</p>
</div>
<div className="flow__c">
<span className="flow__n">THEN</span>
<h3>Assign a key</h3>
<p>Paste a public key and tick the servers. It lands inside 30 seconds.</p>
</div>
<div className="flow__c">
<span className="flow__n">AFTER</span>
<h3>Build from there</h3>
<p>Add checks, write a step, invite the team, connect your identity provider.</p>
</div>
</div>
<pre className="code" style={{ marginTop: "1.6rem" }}>
<i># Linux</i>
{"\n"}
<b>curl</b> -fsSL https://vantage.sh/install | bash -s -- --server-id=<b>$ID</b> --token=<b>$TOKEN</b>
{"\n\n"}
<i># Windows</i>
{"\n"}
<b>irm</b> https://vantage.sh/install.ps1 | <b>iex</b>
</pre>
</section>
<section className="rail band band--flush">
<div className="card card--cta">
<div style={{ maxWidth: "48ch" }}>
<h2 style={{ fontSize: "var(--s-1)" }}>Three servers, free, no card.</h2>
<p style={{ color: "var(--ink-2)", marginTop: "0.4rem", fontSize: "0.94rem" }}>
Create an organisation, install one agent, and watch a key land on a real box.
</p>
</div>
<Link className="btn btn--solid" href="/start">
Create organisation
</Link>
</div>
</section>
</>
);
}
+184
View File
@@ -0,0 +1,184 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Platform",
description:
"How Vantage fits together: a control plane you run, one agent per server, and a single outbound connection between them.",
};
export default function PlatformPage() {
return (
<>
<section className="rail band band--open">
<span className="tag">Platform</span>
<h1 style={{ fontSize: "var(--s-3)", margin: "0.8rem 0 1rem", maxWidth: "19ch" }}>
How the pieces fit together.
</h1>
<p className="lede">
Three moving parts: a control plane you run, an agent on each server, and one outbound connection between
them.
</p>
<div className="split" style={{ marginTop: "3rem" }}>
<div>
<h2 style={{ fontSize: "var(--s-2)", maxWidth: "18ch" }}>The agent never listens.</h2>
<div className="prose" style={{ marginTop: "1rem" }}>
<p>
Every agent dials out to the control plane over gRPC with TLS. Nothing needs an inbound port, nothing
needs a static address, and a machine behind NAT is no different from one with a public IP.
</p>
<p>
Key state is polled on a 30-second interval, because 30 seconds is fine for access control and polling
is simple to reason about. Everything that should not wait running a step, opening a console, applying
updates is pushed down a bidirectional command stream the agent holds open.
</p>
</div>
</div>
<div className="specs specs--flush">
<div className="spec">
<span className="spec__k">POLL</span>
<div>
<h3>SyncKeys, every 30s</h3>
<p>The desired key set for this server. Unchanged state means no disk write at all.</p>
</div>
</div>
<div className="spec">
<span className="spec__k">PUSH</span>
<div>
<h3>Command stream</h3>
<p>Generate a key, run a step, apply updates, update the agent, clean up a workspace.</p>
</div>
</div>
<div className="spec">
<span className="spec__k">REPORT</span>
<div>
<h3>Inventory and checks</h3>
<p>
Metrics every 30 seconds, a full hardware snapshot every 15 minutes, and monitor results as they
complete.
</p>
</div>
</div>
</div>
</div>
</section>
<section className="rail band">
<span className="tag">Write path</span>
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "24ch" }}>
The file is never half-written.
</h2>
<div className="split split--even" style={{ marginTop: "2rem" }}>
<p className="prose">
The agent computes the desired <code>authorized_keys</code> content, compares it to what is on disk, and
stops there if nothing changed. When it does need to write, it writes a temporary file in the same directory
and renames it over the real one. A machine that loses power mid-write keeps the file it had.
</p>
<pre className="code">
<i>// agent poll, simplified</i>
{"\n"}
desired := client.SyncKeys(serverID, token){"\n"}
current := keys.ReadAuthorizedKeys(){"\n\n"}
<b>if</b> !keys.StateChanged(current, desired) {"{"}
{"\n "}
<i>// nothing to do</i>
{"\n "}
<b>return</b> nil{"\n"}
{"}"}
{"\n\n"}
keys.WriteAuthorizedKeys(desired){"\n"}
<i>// write .tmp, os.Rename(), chmod 0600</i>
</pre>
</div>
</section>
<section className="rail band">
<span className="tag">Tenancy and identity</span>
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "22ch" }}>
Organisations are the boundary.
</h2>
<div className="caps">
<article className="cap">
<span className="cap__k">Isolation</span>
<h3>Scoped at the query</h3>
<p>
Every server, key, workflow, monitor and secret belongs to an organisation, and every lookup is filtered
by it. Uniqueness constraints are enforced by the database, not by application logic.
</p>
</article>
<article className="cap">
<span className="cap__k">Roles</span>
<h3>Owner, admin, member</h3>
<p>
Members operate the fleet. Admins and owners manage people, identity settings and the secrets read token.
</p>
</article>
<article className="cap">
<span className="cap__k">Identity</span>
<h3>Local or OIDC, per organisation</h3>
<p>
Sign in with email and password, or connect your own provider. Each organisation configures its own issuer
and client.
</p>
</article>
<article className="cap">
<span className="cap__k">Sessions</span>
<h3>Server-side, 24 hours</h3>
<p>
Cookies carry an opaque identifier and nothing else. Session bodies live in Redis, so losing it signs
everyone out and costs no durable data.
</p>
</article>
</div>
</section>
<section className="rail band">
<span className="tag">What we do not build</span>
<h2 style={{ fontSize: "var(--s-2)", marginTop: "0.7rem", maxWidth: "22ch" }}>The scope is the feature.</h2>
<div className="specs" style={{ maxWidth: "70ch" }}>
<div className="spec">
<span className="spec__k">NOT A PROXY</span>
<div>
<h3>We are never in the SSH path</h3>
<p>
Vantage assigns keys; your client connects straight to the box. If our control plane is down, your SSH
still works.
</p>
</div>
</div>
<div className="spec">
<span className="spec__k">NO CUSTODY</span>
<div>
<h3>Private keys stay put by default</h3>
<p>
Keys generated on a server stay on it unless you explicitly upload the private half, and anything stored
is encrypted with a key only your deployment holds.
</p>
</div>
</div>
<div className="spec">
<span className="spec__k">NO PER-USER</span>
<div>
<h3>Root, not every account</h3>
<p>
Vantage manages one file per server. Per-user key management is a different product with a different
failure mode.
</p>
</div>
</div>
<div className="spec">
<span className="spec__k">NO PLUGINS</span>
<div>
<h3>An agent you can read in an evening</h3>
<p>
A few thousand lines of Go with no extension system. Auditability beats extensibility on a binary that
runs as root.
</p>
</div>
</div>
</div>
</section>
</>
);
}
+154
View File
@@ -0,0 +1,154 @@
import Link from "next/link";
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Pricing",
description:
"Priced per managed server. People, keys, workflows and secrets are free. Free for 3 servers, £4 per server per month, or £290 a year self-hosted.",
};
const COMPARISON: [string, string, string, string][] = [
["Managed servers", "3", "Unlimited", "Unlimited"],
["Members", "1", "Unlimited", "Unlimited"],
["SSH key assignment", "Yes", "Yes", "Yes"],
["Workflows and step library", "Yes", "Yes", "Yes"],
["Monitors", "3", "Unlimited", "Unlimited"],
["Secrets vault", "—", "Yes", "Yes"],
["Browser console", "—", "Yes", "Yes"],
["OIDC single sign-on", "—", "Yes", "Yes"],
["Audit history", "30 days", "Forever", "Forever"],
["Runs on your hardware", "—", "—", "Yes"],
["Support", "Community", "Next business day", "Priority"],
];
export default function PricingPage() {
return (
<section className="rail band band--open">
<span className="tag">Pricing</span>
<h1 style={{ fontSize: "var(--s-3)", margin: "0.8rem 0 1rem", maxWidth: "17ch" }}>
Per managed server. Nothing else counts.
</h1>
<p className="lede">
People are free. Keys, workflows, monitors and secrets are free. You pay for servers running an agent, because
that is the only number that grows with you.
</p>
<div className="plans">
<div className="plan">
<div>
<div className="plan__n">Solo</div>
<p className="plan__d">A homelab, a couple of VPSes, and the keys on your own laptop.</p>
</div>
<div className="plan__p">
£0 <span>forever</span>
</div>
<ul>
<li>Up to 3 servers</li>
<li>Keys, workflows and monitors</li>
<li>One member, one organisation</li>
<li>Community support</li>
</ul>
<Link className="btn btn--line" href="/start">
Create organisation
</Link>
</div>
<div className="plan plan--pick">
<div>
<div className="plan__n">Fleet</div>
<p className="plan__d">Real infrastructure, and more than one person holding the keys.</p>
</div>
<div className="plan__p">
£4 <span>/ server / month</span>
</div>
<ul>
<li>Unlimited servers and members</li>
<li>Owner, admin and member roles</li>
<li>OIDC single sign-on</li>
<li>Browser console and secrets vault</li>
<li>Full audit history</li>
<li>Email support, next business day</li>
</ul>
<Link className="btn btn--solid" href="/start">
Start 14-day trial
</Link>
</div>
<div className="plan">
<div>
<div className="plan__n">Self-hosted</div>
<p className="plan__d">The whole stack on your metal, behind your own boundary.</p>
</div>
<div className="plan__p">
£290 <span>/ year, per install</span>
</div>
<ul>
<li>Everything in Fleet, no server cap</li>
<li>Your MongoDB, Redis and certificates</li>
<li>Mirror agent releases internally</li>
<li>Priority support and upgrade notes</li>
</ul>
<Link className="btn btn--line" href="/contact">
Talk to us
</Link>
</div>
</div>
<div className="scroll">
<table className="cmp">
<thead>
<tr>
<th>Capability</th>
<th>Solo</th>
<th>Fleet</th>
<th>Self-hosted</th>
</tr>
</thead>
<tbody>
{COMPARISON.map(([capability, solo, fleet, selfHosted]) => (
<tr key={capability}>
<td>{capability}</td>
<td>{solo}</td>
<td>{fleet}</td>
<td>{selfHosted}</td>
</tr>
))}
</tbody>
</table>
</div>
<div style={{ marginTop: "3.2rem", maxWidth: "66ch" }}>
<span className="tag">Fine print, in plain words</span>
<div className="specs">
<div className="spec">
<span className="spec__k">COUNTING</span>
<div>
<h3>What counts as a server</h3>
<p>One running agent, one server. Remove a box and it stops billing that day.</p>
</div>
</div>
<div className="spec">
<span className="spec__k">LIMITS</span>
<div>
<h3>Going over on Solo</h3>
<p>
Nothing is deleted. A fourth agent registers and heartbeats, but stops syncing keys until you upgrade or
remove a server.
</p>
</div>
</div>
<div className="spec">
<span className="spec__k">EXIT</span>
<div>
<h3>Leaving</h3>
<p>
Export every server, key, workflow and secret group as JSON whenever you like. Agents keep their last
synced state on disk, so nobody is locked out mid-migration.
</p>
</div>
</div>
</div>
</div>
</section>
);
}
+75
View File
@@ -0,0 +1,75 @@
import type { Metadata } from "next";
import { OrgForm } from "@/components/OrgForm";
export const metadata: Metadata = {
title: "Create organisation",
description:
"An organisation owns its servers, keys, workflows, monitors and secrets. Free for three servers, hosted or self-hosted.",
};
export default function StartPage() {
return (
<section className="rail band band--open">
<div className="split">
<div>
<span className="tag">Create organisation</span>
<h1 style={{ fontSize: "var(--s-3)", margin: "0.8rem 0 1rem", maxWidth: "15ch" }}>
Set up your organisation.
</h1>
<p className="lede" style={{ fontSize: "var(--s-0)" }}>
An organisation owns its servers, keys, workflows, monitors and secrets. Nothing inside it is visible to any
other organisation. Confirm your email and it is created with you as its owner.
</p>
<div className="card" style={{ marginTop: "1.9rem" }}>
<OrgForm />
</div>
</div>
<div>
<span className="tag">What happens next</span>
<div className="specs">
<div className="spec">
<span className="spec__k">FIRST</span>
<div>
<h3>Confirm your email</h3>
<p>
We send a link that works once. Your organisation is created when you open it, not before.
</p>
</div>
</div>
<div className="spec">
<span className="spec__k">THEN</span>
<div>
<h3>Add a key</h3>
<p>
Paste the contents of <code>~/.ssh/id_ed25519.pub</code>. Vantage fingerprints it and refuses
duplicates.
</p>
</div>
</div>
<div className="spec">
<span className="spec__k">THEN</span>
<div>
<h3>Add a server</h3>
<p>Run the install command as root. It expires in an hour and works once.</p>
</div>
</div>
<div className="spec">
<span className="spec__k">THEN</span>
<div>
<h3>Watch it register</h3>
<p>The server moves from pending to active on first sync, usually inside 30 seconds.</p>
</div>
</div>
</div>
<pre className="code" style={{ marginTop: "1.8rem" }}>
<b>curl</b> -fsSL https://vantage.sh/install | \{"\n"}
{" "}bash -s -- --server-id=<b>$ID</b> --token=<b>$TOKEN</b>
</pre>
</div>
</div>
</section>
);
}