Connection, threading, authentication, screens and invite-link handling. See client content installation for shared libraries.
Everything runs on the UI thread. PlatformClient::update() advances the
socket, HTTP requests and timers, and runs every callback; nothing blocks and no
callback runs on another thread. Online::pump(), called by Application every
frame, updates the shared client once it exists, so sign-in, refreshes and
keepalives continue while the player moves between screens. Screens poll state
from onTimer or register listeners.
Online::Services (OnlineServices.h) holds the shared InstanceConfig,
PlatformClient, MapCache and quick-match search. Application owns one for the
run (Online::ServicesOwner, its first member, so the services outlive every screen):
Online::services() creates them on first use, and they are destroyed when the game
exits, which closes the connection. Tools and test harnesses that run without an
Application get process-lifetime services instead. Online objects take what they
need explicitly: OnlineMatch and PlatformRoom receive the map cache and skin storage, and
QuickMatch the client. Services::addHook returns an id for removeHook. The client
is not started until a screen calls client.start(origin).
| Call | Effect |
|---|---|
start(origin) / stop() |
Sign in and connect to an instance (normalized origin); stop closes everything and fails outstanding requests with cancelled. |
connection() |
Stopped, Connecting, Handshaking (session.hello sent), Online, Waiting (backoff; retryInMs(), retryNow()). |
auth(), account() |
SignedOut, SigningIn, SignedIn; the SelfAccount fields id, displayName, kind, role, plus raw JSON. |
simSupported(), simMismatch(), sessionId(), lastError() |
From session.hello, including whether this client or the server is behind when the sim version is not served; the last connection or sign-in problem for a status line. |
request(method, params, handler, timeoutMs) |
A realtime request. Queued until the socket is online; the handler receives {ok, result, error} exactly once, or never after cancelRequest(id). |
addListener(event, handler) |
Server events by name ("" for all), e.g. room.state, match.start. addStateListener fires on any connection, auth, account or handoff change. |
rest(method, path, body, handler) |
An authenticated REST call (Authorization: Bearer). A 401 refreshes the token once and retries. refreshAccount() and rename(name) wrap /api/v1/accounts/me. |
beginBrowserSignIn(mode, provider) |
auth.handoff.begin, then opens the system browser at signInUrl. handoff() holds the state, confirmationCode to show, browserOpened, failure and conflict. openSignInPage() retries opening; cancelBrowserSignIn() cancels. |
signInAsGuest(), signOut() |
Guest sign-in with this device's credential (creating a guest if needed); sign-out revokes the refresh token and stays signed out. |
accessToken() |
For other HTTP requests to the same instance, such as map downloads. |
Error codes in a failed response are the platform's ErrorCode values, or
local ones: timeout, disconnected (the socket closed while the request was
in flight), cancelled, network and http_<status>.
- The socket is
wss://<host>/realtime(text frames, at most 64 KiB, the platform's limit). The first request issession.hellowith protocol 1, the client platform and version, the sim version and, when signed in, the access token. Anunauthenticatedanswer drops the token, says hello anonymously and signs in again;update_requiredstops the client. - Lost or failed connections retry after
min(60 s, 1 s × 2^attempt), reduced at random by up to half so clients dropped together spread out. A successful hello resets the backoff. A connection that does not open within 20 s counts as failed. - The server pings every socket every 30 s; the transport answers. When nothing
has arrived for 25 s the client also sends
session.pingand reconnects if it is not answered within 10 s, which detects dead connections the operating system has not noticed. - Requests in flight when the socket closes fail with
disconnected; requests not yet sent wait for the next socket until their own timeout.
On start the client signs in with what it remembers for the instance: the
refresh token, else the guest device credential, else (by default) a new guest
from POST /api/v1/auth/guest, keeping the returned credential. It connects
after that attempt finishes, with or without an account.
- Refresh tokens rotate on every use and reuse revokes the whole sign-in, so the client never has two refreshes in flight. Every new refresh token is written to disk before it is used.
- The access token is refreshed after its lifetime (
exp − iatfrom the JWT, independent of the local clock) minus the larger of one minute and a fifth of the lifetime, but not before half of it: at 8 minutes for the default 10. The new token is attached to the socket withsession.authenticate. - A refused refresh token (expired, revoked or reused) is forgotten; the client falls back to the device credential when automatic sign-in is on. Network or server errors retry after 4–60 s while the current token lasts.
session.revokedsigns out and turns automatic sign-in off, so a sign-out elsewhere or a ban is not undone by the device credential.signOut()posts/api/v1/auth/sign-out, forgets the tokens, turns automatic sign-in off and reconnects anonymously. The device credential is kept: it is the only key to a guest account, and a latersignInAsGuest()returns to it.
beginBrowserSignIn asks for an attempt, keeps the resume token in memory only
and opens the instance's sign-in page (never a page on another origin) through
GAGCore::ApplicationHost::openUrl: SDL_OpenURL natively, window.open in
the browser. In the browser openUrl and copyText run on the page's thread
(MAIN_THREAD_EM_ASM): the threaded runtime's application worker has no
window, document or clipboard. They run synchronously, while the click
still grants transient activation; a browser that refuses the new tab anyway
(Safari counts only the DOM event itself) gets a real link at the bottom of
the page (glob2OpenUrl in browser/shell.html), which the player taps.
glob2Diagnostics.snapshot().opened lists what opened and how
(browser/tests/online-links.spec.js). The sign-in page's address only
arrives with the server's reply, after the click has run out, so the hub and
Settings call prepareUrlWindow() during the click. In the browser that opens
an empty tab (glob2PrepareWindow), and the next openUrl loads the page there;
an unused empty tab closes after 30 seconds. On native hosts it does nothing. While waiting, the hub
says the game will continue by itself and shows the code only for comparing, with a Copy
button. It keeps an "Open page again" button calling openSignInPage(), which
becomes the main button when the browser did not open. After a reconnect the client
sends auth.handoff.resume, so a phone that lost its socket while the browser
was in front still receives auth.handoff.completed. Without a result the
attempt fails locally as expired one minute after its expiry.
The quick-match, profile and map screens use the Glob2UI::Screen pattern. They are reached from
the online hub; the map chooser of the editor menu and the editor's own menu
also offer Share online….
Implementation: src/online/screens/QuickMatchScreen.cpp (SearchStrip).
A running search at the top of the hub, Maps and Profile: the queue, the timer, when an AI joins (or that AI opponents are off), Cancel search and, on the hub, Details. Find match starts the search and the hub stays in front; search notices and failures become hub toasts. Leaving Online (Back on the hub) ends the search.
Implementation: src/online/screens/QuickMatchScreen.cpp.
The search in detail, opened with the strip's Details: the timer, the opponent rating range, the relay region and round trip, the AI backfill countdown with the AI that would play and Allow an AI opponent (on by default), with Profile and Maps over the running search. It closes, back to the hub, when the search ends (the hub shows why) or becomes a match, so the match's results return to the hub. Back to online (and Escape) keeps the search running; only Cancel ends it. The account chip ends the search and opens sign-in or the account menu on the hub.
Implementation: src/online/screens/QuickMatchScreen.cpp.
Ranked queues: both players accept within the countdown, each player's answer shown live. AI-backfilled and casual matches: who you play, then a 3-second start countdown.
Implementation: src/online/screens/OnlineProfileScreen.cpp.
Rating of each queue with its trend and provisional flag, win rate, typical game length and best map over recent games, the match list (filters All, Ranked, Rooms, vs AI) with Replay and the match page.
Implementation: src/online/screens/OnlineMapsScreen.cpp.
Browse (search, size, colonies, sort, detail with the server preview, Use in a room, Like, map page, Report) and My maps (upload, checking, rejected with the reason, visibility, update, delete). Opened from the hub, Use in a room closes it and the hub opens a room with the map; opened from a room's map chooser, it gives the map to that room and closes. Back names where it returns to.
Implementation: src/online/screens/OnlineGeneratorsScreen.cpp.
The Generators tab in Maps, also linked from Settings → Map generators: search/tags/type filters, exact releases, manifest controls, server preview, explicit installation/replacement and Use in a room. Packages are hash-checked and persisted with rollback and release provenance.
Implementation: src/online/screens/OnlineMapsScreen.cpp (MapShareScreen).
Title, description and visibility (Unlisted by default), then the upload and the server's validation and preview.
Search state. Online::QuickMatch (src/online/QuickMatch.h) holds the one
search, in one queue or (on 'queue.multi' instances, with the hub's Also
search … toggles) in several, keeping each queue's ticket and status and showing
the queue whose match prompt opens: it probes the relays (RelayProbe), sends queue.join, follows
queue.status, answers queue.proposal with queue.respond, toggles backfill
with queue.update and leaves with queue.leave. It lives in the online
services and is pumped by Online::pump(), so a search continues while the
player opens Profile or Maps. QuickMatchPresenter shows the match-found
prompt over whichever screen is in front and flashes the window
(SDL_FlashWindow) when a match is found. There is no phone notification: the
platform layer has no local notifications for a backgrounded app.
Hand-offs (src/online/OnlineHandoff.h) connect screen groups built
separately. The connecting flow registers Online::setMatchHandler; the quick
match calls Online::beginMatch(assignment) with the match.start
MatchAssignment once the prompt closes. "Use in a room" downloads the map
into the map cache and calls Online::useMapInRoom({mapId, hash, title, ...}),
which the room screen registers with setRoomMapHandler. The results screen
offers a rematch through Online::requestRematch, registered by the room
screen: an unrated room with the same players. Its Find another match
calls Online::requestQueueAgain, registered by the hub, which searches the
same queue once the match's screens close. A hand-off made before its
handler exists is kept for it.
Data. The profile reads GET /api/v1/players/{id} (PlayerProfile: the
ratings with rank, the rating history for the trend lines and the aggregates)
and GET /api/v1/players/{id}/matches (a MatchList) for the list;
summarizeProfile falls back to the match list for anything the profile
leaves out. Replays come from GET /api/v1/matches/{id}/artifacts/replay and
open in the replay viewer; deep links go to <origin>/players/<id>,
<origin>/matches/<id> and <origin>/maps/<id>. Map previews are the server's
lossless WebP renditions (MapVersionInfo.previewUrl), fetched with
PlatformClient::restRaw and decoded through the shared asset loader. Uploads send the map's uncompressed bytes to
POST /api/v1/maps/{id}/versions?simVersion=<key> and poll the version until
validation is valid or invalid and the preview is no longer pending
(Online::MapShare, src/online/MapCatalog.h).
InstanceConfig keeps the selected instance (the official one,
OFFICIAL_INSTANCE_ORIGIN, by default) and, per
instance origin, the device credential, refresh token, automatic sign-in flag,
last display name and trust. It is stored in online/instances.json in the user
directory through FileManager (written atomically with mode 0600 for new files)
and, in the browser, in the site's IndexedDB-backed storage, synced after each
write with ApplicationHost::persistStorage(). Credentials are not in
preferences.txt, which players attach to bug reports.
The official origin is one build-time setting: DEFAULT_ORIGIN in
scons/official_instance.py (currently https://app.glob2online.com), overridable
with scons official_instance=https://example.org. Every build path passes it to
the client as GLOB2_OFFICIAL_INSTANCE_ORIGIN, and the Android and iOS packaging
scripts derive the App Link host and associated domain from it.
When the official instance moves, the origin it leaves goes into FORMER_ORIGINS
there (https://glob2online.com became the public website when the app moved to
app.glob2online.com). The client then reads a stored selection of a former
origin as the official one (Online::currentOrigin), treats it as trusted, maps
its invite links (https://glob2online.com/j/<code>, which the website also
redirects) and glob2://join?instance= links to the official origin, and hides
its record from the server list. The record itself, with its device credential
and refresh token, stays keyed by the origin that issued it and is never copied.
Builds with another official_instance carry no former origins.
The browser shell selects its serving origin for a fresh profile, so hosted
/play/ clients connect to their own platform even when the compiled default
predates a hostname cutover. On the official app host it also changes a saved
apex selection to https://app.glob2online.com. Custom instance selections stay
intact, and device credentials and refresh tokens remain keyed to their original
origin; the shell never copies them to the new origin. Initialization waits for
successful storage restoration before writing configuration.
{
"version": 1,
"selected": "https://app.glob2online.com",
"instances": {
"https://games.example.org": {
"trusted": true, "autoSignIn": true,
"deviceCredential": "<43 characters>", "refreshToken": "<opaque>",
"lastDisplayName": "Guest-0042"
}
}
}Origins are normalized (lowercase, no default port, no path); http:// is
accepted only for loopback development instances. Credentials of one instance
are only ever sent to that instance.
Trust. The official and the selected instance are trusted. An invite link
to any other instance needs the player's confirmation (isTrusted, then
trust(origin, remember)); the prompt's "remember" box starts ticked
(REMEMBER_TRUST_BY_DEFAULT). Unremembered trust lasts until the game exits.
Settings › Online chooses the instance (the official one, a remembered one,
or any address, checked with GET /api/v1/instance before switching), shows
the display name and the sign-in methods linked on that instance, and signs
out. "Forget" drops everything remembered about an instance.
The play path is built from these screens (Glob2UI::Screen pattern,
docs/development/ui-framework.md), with shared presentation behavior:
Implementation: src/online/screens/OnlineHubScreen.*.
The main menu's Play online entry starts the client, creating a guest on
first contact. The account chip presents sign-in and confirmation codes; banners
explain offline/update-required states. Invite links use takePendingJoin and
ask for trust when changing instance.
A sidebar, or phone tabs, selects the panes:
- Play: queue choice from
InstanceInfo.queues, its map pool, primary Find match action, players online/searching fromGET /api/v1/stats, Create room, Join by code and the last match. Rooms start invite-only unless Show in Open rooms is selected. - Rooms:
GET /api/v1/rooms, filters and available map previews, with Create room or Quick match actions when empty. - Leaderboard: the player's rank and the top 50 of the first rated queue, fetched while shown and highlighting the player's row.
Maps, Profile & history and Online settings open their own screens. Recent match
summaries combine the REST history with match.updated events.
Discord community in the Play pane and main menu (under More on phones)
opens a shared invitation panel. Join Discord opens the invitation through the
host's browser/app handoff; Copy link copies it, and the selectable link provides
a manual fallback. Back returns to the previous menu while any online search
continues. The invitation is defined in src/ui/DiscordCommunity.cpp.
Implementation: src/online/screens/RoomScreen.* over RoomBackend.
One screen for online rooms (Online::PlatformRoom) and LAN rooms (Lan::LanRoom): Map / Players & Teams / Game Rules tabs, seats with controller, team and remove, invite (or how to join on the network), chat and ready. Phones get a Seats / Map / Rules / Chat bar and Start or Ready in the thumb corner, mirrored by the thumb-side setting. The host edits map and rules with the custom-game screen in room mode (CustomGameScreen::useForRoom); the server generates a random map from the generator descriptor (src/online/RoomSetup.*), and a premade or own map is uploaded and played as {kind: "upload"}. Members without a seat are listed under the seats, and Ready says why it is unavailable.
Implementation: src/game/screens/MatchStartScreen.*, src/online/OnlineMatch.*.
From match.start to the first tick: seat confirmed, map download by hash, engine load, relay connection (Online::RelayTransport), waiting for the other players' presence. A relay that refuses the match as new (Reject 5) is reported with match.reconnect {relayUnavailable: true} and the new assignment restarts the flow.
Implementation: src/net/ConnectionOverlay.*, using the snapshot produced by
src/net/turn/TurnMatchPresenter.* from the read-only turn session.
Online and LAN games share a permanent per-seat connection panel, details view and notices for disconnects and returns. Centre cards present local reconnect with its grace countdown, catch-up progress and desync rejoin. The menu's Leave match action explains the consequence and asks for confirmation; turn matches do not expose Load or Save actions.
Connection quality owns names, thresholds, units and presentation. Turn timing and recovery owns stalled-link detection, connection retry and buffering resets, and engine integration describes session teardown.
Implementation: src/game/screens/EndGameScreen.*.
The results screen opens when the local colony wins or leaves a turn match.
Online matches add an outcome banner and rating card. match.updated refreshes
it live; while open, the screen also rereads GET /api/v1/matches/{id} every
10 seconds. It distinguishes a match still running, record collection,
verification, verified/unverifiable outcomes, unrated room games and draws.
After 45 seconds waiting or 60 seconds verifying, the card explains that the
final result will appear in history.
A player who leaves sees Defeat immediately, with the final result pending the
match's end. A link opens <origin>/matches/<id>. Room games return to the room.
Quick-match results offer Find another match for the same queue and Rematch
through Online::requestRematch/match.rematch, creating an unrated room with the
same players. A received match.rematchOffered names the inviter.
Implementation: src/ui/settings/SettingsScreenOnline.cpp.
See instances and stored credentials.
Quick-match cards start the shared search (Online::quickMatch()), shown as
the search strip; its Details opens QuickMatchScreen. Guests see rated queues closed, with the reason and Sign in,
and the first queue they can enter is the primary one; a handler registered with OnlineHubScreen::setQuickMatch
replaces that. The hub registers Online::setMatchHandler (an assigned quick
match opens the starting screen) and Online::setRematchHandler, and attaches
QuickMatchPresenter so the match-found prompt appears over any screen. The
Room screen registers Online::setRoomMapHandler for the map browser's "Use in a
room".
MatchAssignment carries mapTitle and, for rated queue matches,
ratingPreview {ladder, before, ifWon, ifLost, provisional} for the greyed
"1528 → 1543?" before verification.
Pausing. Rooms and LAN games pause freely; queue matches carry a
pause limit that the turn session enforces.
GameGUI::pauseState shows it: the menu (the touch sheet and, in network
matches, the desktop menu too) offers "Pause game (N left)", or a disabled "No
pauses left"; a pause the player has none left of is not sent. The Paused label
names who paused and, under a limit, when the game resumes by itself, and sits
above the phone HUD's action bar on a dark backing.
Leaving a match sends Quit to the relay; the connection stays open after the
game's session is gone until it is written (at most 3 s, and the shutdown screen
waits for it), so closing the window does not leave the seat in reconnect
grace.
During backgrounding or a long reload, the native WebSocket buffers at most 4096 unread messages or 1 MiB. At the cap it stops reading, allowing TCP flow control to hold traffic at the relay until the client drains its queue. The browser cannot pause WebSocket reads and instead queues up to 16384 messages or 4 MiB. These are message/byte limits, not a guaranteed duration: bundle size, presence traffic and the relay's outgoing backlog also affect how long a paused client can remain connected. Recovery follows the turn session's reconnect path.
End-to-end check. OnlinePlayHarness (scons release=1 server=0 online-play-test) drives the real screens against a live instance. In a room
run, the host signs in as a guest, creates a room with a one-minute sudden-death
timer and writes its code; the guest joins by the code, takes a seat and readies;
the host presses Start; both play. The guest leaves after GLOB2_E2E_GUEST_LEAVE
seconds (default 40; 0 stays to the end) by closing its window, or by the in-game
Quit with GLOB2_E2E_LEAVE_BY=menu. The host logs the other seat's presence as its
connection panel shows it, the game's end, and every change of the results card,
and waits until the platform has settled the result before returning to the room.
For colony appearance checks, GLOB2_E2E_OPENGL=1 enables live meshes and
GLOB2_E2E_EXPECT_SKINS=N requires N authorized textures to arrive within
30 seconds of play on each client. Equip the fixture accounts before starting
the match; the harness uses the real assignment and texture downloads.
The quick role plays a casual quick match (AI backfill) and leaves after
GLOB2_E2E_QUICK_LEAVE seconds. Every stage is captured:
build/darwin/client/release/src/OnlinePlayHarness host https://app.glob2online.com artifacts/e2e &
sleep 20
build/darwin/client/release/src/OnlinePlayHarness guest https://app.glob2online.com artifacts/e2e
build/darwin/client/release/src/OnlinePlayHarness quick https://app.glob2online.com artifacts/e2e-quick| Form | Source |
|---|---|
glob2://join?instance=<origin>&code=<code> |
any instance; custom scheme |
https://<instance>/j/<code> |
the instance's web link (landing page in apps/web) |
?join=<code> |
the web client's page; the instance is the page's origin |
online join <link or code> [--instance <origin>] |
command line; a bare code without --instance means the official instance |
"Play this map" opens a dialog with two illustrated choices. Play Locally in
Custom Game downloads the selected catalog version into the verified map cache
and opens Custom Game with it loaded; Play in Multiplayer connects to the
instance and creates an invite-only room with that catalog map in the initial
room.create request. Neither choice starts a match automatically.
Both use /play/?map=<mapId>&version=<sha256>&title=<title>&mode=<local|multiplayer>.
The browser shell passes online play-map or online host-map <mapId> --hash <sha256> --title <title> with
--instance <origin>. A missing mode on an older map link means multiplayer.
The dialog also offers Open in installed app for each choice, using
glob2://play?instance=<origin>&map=<mapId>&version=<sha256>&title=<title>&mode=<mode>.
Desktop URL handlers, Android intents and iOS URL events accept the same intent;
the official mobile apps also associate /play/* HTTPS links. A running main
menu, online hub or local Custom Game screen accepts a launch immediately; a
launch received during a match or room waits until the player leaves it. Multiplayer
launches retain the existing instance trust and authentication checks. Local
fetches send credentials only when the linked origin is the current client's
origin; inaccessible private maps report a download failure in Custom Game.
A link becomes the pending join (Online::pendingJoin(),
takePendingJoin() in InviteLink.h). The online hub takes it, asks for trust
when needed, connects to the instance and sends room.join. Links arrive:
- at launch, as a command-line argument (Windows, Linux, and the browser shell,
which passes
online join <code> --instance <origin>for?join=); - while running on macOS and iOS (URL events, which SDL delivers as
SDL_DROPFILE;Application::frametakes invite links out of the event stream); - on Android through
Glob2Activity.takeLaunchLink()(onCreateandonNewIntent; the activity issingleTask), polled byOnline::pump(); - for iOS universal links through
mobile/ios/LaunchLinks.mm, which addsapplication:continueUserActivity:restorationHandler:to SDL's app delegate.
A running desktop game is not handed links from a second launch in v1: the second launch joins directly. In-client "Join by code" covers the rest.
Registration.
| Platform | Where |
|---|---|
| Windows installer | windows/win32_installer.nsi: HKCR\glob2 URL protocol → glob2.exe "%1" |
| macOS | darwin/Info.plist CFBundleURLTypes |
| Linux (deb, rpm, Flatpak, Snap) | data/glob2.desktop: Exec=glob2 %u, MimeType=x-scheme-handler/glob2 |
| Android | intent filter for glob2://join and a verified App Link for https://<official host>/j/ (officialInstanceHost placeholder) |
| iOS | CFBundleURLTypes and applinks:<official host> (written by mobile/ios.py) |
App Links and universal links cover the official domain only; self-hosted
instances use glob2://. The Amazon and China editions have no online play and
declare none of these (mobile/android.py strips the invite intent filters,
mobile/ios.py the associated domain and URL scheme). They need the instance to serve
/.well-known/assetlinks.json (the release signing certificate's SHA-256) and
/.well-known/apple-app-site-association (team id plus
org.globulation2.glob2, path /j/*). For the official instance that is
app.glob2online.com; the public website at the apex serves neither file and
redirects /j/* to the app, so an apex invite opens in the browser first. What the
maintainer supplies for these files is in
hosting: mobile app links.
session.hello reports SimVersion::local(): VERSION_MINOR,
NET_PROTOCOL_VERSION and the simulation data hash (simDataHash() in
src/online/SimVersion.cpp, the same value info sim-version --format json prints). Only a
process that never initialized the Toolkit file system (some unit tests) reports
64 zeros, which the platform answers with simSupported: false.
-
glob2-unit-testssuitesQuickMatch,MapCatalogandOnlineResources(src/online/): relay probes beforequeue.join, search progress, ranked prompts with live answers, AI backfill and its start countdown, requeues, declines with a cooldown, cancelling at every step, the share flow, catalog queries, hand-offs, and parsing of every protocol fixture the screens read. -
The
UIPresentationsuite and the mobile gallery capture every online screen state from canned data (src/ui/OnlineUIFixtures.h). -
glob2-unit-tests(src/online/): envelope codec, timestamps, token lifetimes and refresh scheduling, backoff, SHA-256 vectors, origin normalization, configuration persistence and trust, link parsing and launch arguments, the map cache (verification, eviction, index recovery, downloads), andPlatformClientagainst a scripted socket, HTTP and clock (sign-in paths, hello, correlation, timeouts, events, queued requests, backoff, refresh and its serialisation, refusal fallback, keepalive, browser sign-in with resume, revocation, sign-out). -
test/online_service/test_platform_client.pyruns the real platform API (with a one-minute access token) behind a local TLS proxy and drivesplatform-client-probe(built bytransport-test): guest creation,session.hello, request correlation and errors, REST, a dropped connection, a scheduled refresh,auth.handoff.begin/cancel, a returning launch, refresh-token reuse revoking the sign-in with fallback to the device credential, and sign-out. It needsGLOB2_PLATFORM_DIR(aplatform/checkout with dependencies installed) andGLOB2_PLATFORM_DATABASE_URL(a Postgres role that may create databases); see the file's docstring.