The first ten minutes
The six setup steps, why they come in that order, and why the wizard cannot report a step done that is not.
This document is about what happens between installing Glion and having an agent do something useful. It is the part of the product a new operator judges it by, and for a while it was the worst part of it.
What it used to be
A fresh install put you on the Fleet screen, which was empty, above a checklist of four things you had not done. Each item was a link to a different screen. Nothing said which to do first, nothing carried you from one to the next, and nothing was remembered — navigate away and the checklist started again from the beginning, with no idea where you had got to.
Worse, the order it implied was wrong. The first item was “give a provider an API key”, which meant the first thing Glion asked of a stranger was money, before it had shown them anything at all. The one thing it can do with no key — look at your own network and tell you what is on it — was three screens away under a menu item called Discovery.
What it is now
One route, /setup, six steps, and the order is the product’s real dependency
graph rather than a list:
| step | what it asks for | needs a key | |
|---|---|---|---|
| 1 | Your network | confirm a subnet, then scan it | no |
| 2 | What is yours | keep or set aside what was found | no |
| 3 | Reach them | one token per service | no |
| 4 | A model | one provider key | — |
| 5 | Your team | four agents shaped by what was found | yes |
| 6 | First task | one task, grounded in the finds | yes |
The two steps that need no credentials come first. By the time Glion asks for an API key it has already told the operator that there are twenty-six services on their network, named them, and connected to several — which is the whole argument for the product, made with their own infrastructure rather than a screenshot.
Every step is skippable, every skip is recoverable, and you can walk backwards freely. A step is locked only when it genuinely cannot be attempted, and when it is locked it says why in words meant for a person: “Each agent needs a model to run on. Add a provider key first.”
The one idea it rests on
server/src/onboarding/journey.ts splits every piece of state into one of two
kinds, and the split is what stops the wizard from lying.
Derived — whether a step is done. Read from the database on every request and never stored. A provider key added on the Models screen counts. A team built from the Discovery screen counts. There is no flag anywhere that can report a step finished when the state says otherwise, which is the failure mode of every setup wizard that has ever told a user they had completed something they had since undone.
Declared — the cursor, and what was skipped. These are intentions. “I do not want to connect anything yet” appears in no table and cannot be inferred from one, so it is the operator’s to say and Glion’s to remember. They are the only two things written down.
There is exactly one step whose completion is an acknowledgement rather than a state change. Confirming which discovered services are actually yours changes nothing if they all are — there is no row to write. Pressing Continue is the action, so the cursor moving past that step is recorded. That is still a real gesture by a person, not a guess, and it is read by that step alone.
The precision matters more than it looks. An earlier version marked every step
before the new cursor, which meant clicking step four in the stepper marked
“what is yours” as done — a step the operator had never opened, whose entire
purpose is for them to look at a list. Only a cursor move of exactly one step
forward acknowledges anything, because that is the only move Continue makes;
jumping the stepper and going back acknowledge nothing. acknowledge() is a
pure function and server/test/journey.test.ts pins all four cases.
A consequence worth stating: an instance that has been running for six months
is never greeted as though it had just been installed, because status is
derived from the same real state. Nothing was migrated to make that true.
The scan says where it has got to
The other half of the problem was the scan itself. It was one POST that answered after up to a minute of silence — the longest unexplained wait in the product, and the very first thing anyone asks Glion to do. More than one operator concluded it had hung, reloaded the page, threw the result away and put the packets on the network a second time.
server/src/discovery/job.ts runs the same sweep in the background and
publishes progress on the console’s existing live stream as discovery
messages. The job outlives the request that started it, so a browser that
reloads mid-scan reattaches through GET /api/discovery/scan/current instead
of watching it vanish, and a console whose SSE is being buffered by a proxy
polls the same endpoint instead.
Two details in there are less obvious than they look.
Only one scan runs at a time. Two concurrent sweeps at concurrency 256 put twice the packets on someone’s network for no extra information, and the obvious way to cause that is to double-click Scan. A second request while one is running returns the running job.
The bar only ever moves right. A scan is three legs — a liveness pass over
the range, a fingerprint pass over what answered, then asking each open port
what it is — and the operator has one bar. Each leg’s size becomes knowable
only when the one before it ends, so the denominator is an estimate that gets
corrected twice. Left alone, each correction snapped the bar backwards at
exactly the moment the scan got interesting, which reads as a fault in the
network being measured rather than in the estimate. The numerator carries the
finished legs forward with it, mergeProgress refuses to lose ground, and
server/test/scan-job.test.ts pins it.
The first version of this fixed only the first correction, and an adversarial review found the bar still resetting at the second one — one phase past the guard written to prevent it. Worth remembering: a guard against a class of bug is not the same as a guard at every place the class occurs.
The counts say what they can actually know. live_hosts is machines that
answered; found is services identified. The sweep knows the first and cannot
know the second — what it has are open ports — so it leaves found alone.
Filling it from the port count made the figure climb through the sweep and then
fall to zero the moment identification began, and a count that falls reads as
something going wrong.
The audit row is still written before the first packet, by the route rather
than the job, because the route is the only thing that knows the requesting
address. Both scan endpoints go through the same announce helper; neither can
skip it.
The API
GET /api/onboarding/journey the six steps, derived + declared
POST /api/onboarding/journey { cursor | skip | unskip | complete | dismiss | restart }
POST /api/discovery/scan/start { cidr } -> { job }, returns immediately
GET /api/discovery/scan/current the job in flight, or the last one
POST /api/discovery/scan the same sweep, awaited (unchanged)
restart deliberately clears the intentions and nothing else. It does not
un-discover a network or un-key a provider — those are real state, and a wizard
that could delete them by being reopened would be a trap.
Where the code is
server/src/onboarding/journey.ts |
the six steps, and the derived/declared split |
server/src/discovery/job.ts |
the background sweep and its progress |
server/src/routes/discovery.ts |
the endpoints above, and announce |
web/src/screens/Setup.tsx |
the wizard, and the resume card the Fleet shows |
web/src/components/ConnectCard.tsx |
one service’s connection, shared with /connect |
web/src/styles/wizard.css |
the stepper, the progress bar, the finish panel |
server/test/journey.test.ts |
the ways the derived/declared split could break |
server/test/scan-job.test.ts |
the range refusal, and the monotonic bar |
The three security checks — TLS, a console password, an encryption key — live
in journey.ts and are shown in two places from that one definition. They are
deliberately not a step of the wizard: a console reachable over plain HTTP with
no password is not something an operator may skip past, it is a standing fact
about the deployment, and the Fleet screen keeps saying so until it is fixed.