feat(site): /start creates an account, not an instance

The form posts to admin's signup and the slug preview goes: there is no
instance at this point, and previewing one promises something the
submission does not create. Creating the instance is now a step in the
portal, which the page's What happens next panel spells out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
mrhid6
2026-07-26 13:37:30 +01:00
co-authored by Claude Opus 5
parent cb90ed12fb
commit 9cf04a6940
3 changed files with 36 additions and 39 deletions
+10 -13
View File
@@ -1,9 +1,9 @@
import type { Metadata } from "next";
import { InstanceForm } from "@/components/InstanceForm";
import { AccountForm } from "@/components/AccountForm";
export const metadata: Metadata = {
title: "Vantage Cloud",
description: "An instance owns its servers, keys, workflows, monitors and secrets. Free for three servers, hosted or self-hosted.",
description: "Your account is where instances, licences and billing live. Free for three servers, hosted or self-hosted.",
};
export default function StartPage() {
@@ -12,14 +12,13 @@ export default function StartPage() {
<div className="split">
<div>
<span className="tag">Vantage Cloud</span>
<h1 style={{ fontSize: "var(--s-3)", margin: "0.8rem 0 1rem", maxWidth: "15ch" }}>Set up your instance.</h1>
<h1 style={{ fontSize: "var(--s-3)", margin: "0.8rem 0 1rem", maxWidth: "15ch" }}>Create your account.</h1>
<p className="lede" style={{ fontSize: "var(--s-0)" }}>
An instance owns its servers, keys, workflows, monitors and secrets. Nothing inside it is visible to any other instance. Confirm your email and it is created with you
as its owner.
Your account is where instances, licences and billing live. Confirm your email and you can create a free instance straight away three servers, hosted by us.
</p>
<div className="card" style={{ marginTop: "1.9rem" }}>
<InstanceForm />
<AccountForm />
</div>
</div>
@@ -30,23 +29,21 @@ export default function StartPage() {
<span className="spec__k">FIRST</span>
<div>
<h3>Confirm your email</h3>
<p>We send a link that works once. Your instance is created when you open it, not before.</p>
<p>We send a link that works once. Your account 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>
<h3>Create your instance</h3>
<p>One click in the portal. It gets its own subdomain and a free licence, and you are its owner.</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>
<h3>Add a key and a server</h3>
<p>Paste your public key, then run the install command as root. It expires in an hour and works once.</p>
</div>
</div>
<div className="spec">
@@ -2,20 +2,11 @@
import { useState } from "react";
import { Honeypot } from "@/components/Honeypot";
import { submitSignup, type SubmitResult } from "@/lib/submit";
import { submitAccountSignup, type SubmitResult } from "@/lib/submit";
const MIN_PASSWORD = 12;
function slugify(value: string) {
return value
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-|-$/g, "");
}
export function InstanceForm() {
const [slug, setSlug] = useState("");
export function AccountForm() {
const [result, setResult] = useState<SubmitResult>({ state: "idle" });
const sending = result.state === "sending";
@@ -24,8 +15,8 @@ export function InstanceForm() {
const data = new FormData(event.currentTarget);
setResult({ state: "sending" });
setResult(
await submitSignup({
instance_name: String(data.get("instance_name") ?? ""),
await submitAccountSignup({
name: String(data.get("name") ?? ""),
email: String(data.get("email") ?? ""),
password: String(data.get("password") ?? ""),
website: String(data.get("website") ?? ""),
@@ -38,7 +29,7 @@ export function InstanceForm() {
<div role="status">
<h2 style={{ fontSize: "var(--s-1)" }}>Check your email.</h2>
<p style={{ color: "var(--ink-2)", marginTop: "0.5rem" }}>
We sent a confirmation link. Open it and <b>{slug || "your instance"}</b> is created with you as its owner. The link works once and expires in 24 hours.
We sent a confirmation link. Open it and your Vantage account is ready then you can create your first instance from the portal. The link works once and expires in 24 hours.
</p>
<p style={{ color: "var(--ink-3)", marginTop: "0.75rem", fontSize: "0.88rem" }}>
Nothing exists until you confirm if the email does not arrive, start again or contact support@hostxtra.co.uk.
@@ -54,14 +45,11 @@ export function InstanceForm() {
<Honeypot />
<div className="field">
<label htmlFor="o-instance">Instance name</label>
<input id="o-instance" name="instance_name" type="text" placeholder="Northgate Systems" required onChange={(e) => setSlug(slugify(e.target.value))} aria-describedby="o-instance-err" />
<span className="hostline">
<b>{slug || "your-instance"}</b>.vantage.hostxtra.co.uk
</span>
{fieldError("instance_name") && (
<small id="o-instance-err" className="field__err">
{fieldError("instance_name")}
<label htmlFor="o-name">Your organisation</label>
<input id="o-name" name="name" type="text" placeholder="Northgate Systems" required aria-describedby="o-name-err" />
{fieldError("name") && (
<small id="o-name-err" className="field__err">
{fieldError("name")}
</small>
)}
</div>
+16 -4
View File
@@ -9,6 +9,7 @@
*/
const SITE_API = (process.env.NEXT_PUBLIC_SITE_API ?? "").replace(/\/$/, "");
const ADMIN_API = (process.env.NEXT_PUBLIC_ADMIN_API_URL ?? "").replace(/\/$/, "");
const FALLBACK_ADDRESS = process.env.NEXT_PUBLIC_CONTACT_EMAIL ?? "support@hostxtra.co.uk";
export type SubmitState = "idle" | "sending" | "sent" | "error";
@@ -56,12 +57,23 @@ export async function submitContact(fields: { name: string; email: string; serve
return post(`${SITE_API}/api/contact`, fields);
}
export async function submitSignup(fields: { instance_name: string; email: string; password: string; website: string }): Promise<SubmitResult> {
if (!SITE_API) {
/*
* Account signup posts to the admin service, not sitesvc. The two form targets
* are deliberately separate variables rather than one base URL: contact and
* signup are owned by different services, and an implied shared host is how they
* silently end up pointing at the wrong one.
*/
export async function submitAccountSignup(fields: {
name: string;
email: string;
password: string;
website: string;
}): Promise<SubmitResult> {
if (!ADMIN_API) {
return {
state: "error",
message: `Signup is not available from here yet. Email ${FALLBACK_ADDRESS} and we will set you up.`,
message: `Signup is not connected yet. Email ${FALLBACK_ADDRESS} and we will set you up.`,
};
}
return post(`${SITE_API}/api/signup`, fields);
return post(`${ADMIN_API}/auth/signup`, fields);
}