Auth, Accounts & Control Plane API
1. Summary
Flatland3 uses three API surfaces, not one protocol for everything:
| Surface | Protocol | Who uses it |
|---|---|---|
| Game transport | TCP → QUIC; postcard intents + tick stream | TUI, bots, agents while playing |
| Control plane HTTP | REST/JSON | CLIs, agents, LLM automations, account admin |
| Internal cluster | gRPC (tonic) | gateway ↔ region worker ↔ control plane |
Human login is email + password. Automation uses scoped API tokens tied to the same account. Item instances use UUID v7. World interaction uses a 1.5 m server-side radius.
2. Locked decisions
| # | Decision | Value |
|---|---|---|
| A1 | Account auth | Email + password (argon2id hashed) |
| A2 | Automation auth | Per-account API tokens — create, revoke, scope, label |
| A3 | Item instance IDs | UUID v7 (time-ordered, globally unique, Postgres uuid type) |
| A4 | Interaction radius | 1.5 m — harvest, use, talk, loot (server validates distance) |
| A5 | Control plane public API | HTTP/JSON REST on separate port from game transport |
| A6 | Sim ↔ persistence hot path | gRPC (internal only, not exposed to clients) |
3. Auth model
3.1 Email + password (humans)
POST /v1/auth/register { email, password, display_name }
POST /v1/auth/login { email, password }
POST /v1/auth/logout Authorization: Bearer <session>
POST /v1/auth/refresh { refresh_token } # optional v2
- Password hashed with argon2id (never stored plain).
- Login returns a short-lived session token (opaque, stored in Redis, ~24 h TTL, sliding).
- TUI stores session in local config (
~/.config/flatland/session); gameHellocarries session token.
3.2 API tokens (CLI, agents, LLM automations)
Users manage tokens from account settings or CLI:
flatland auth login # email/password → session
flatland auth token create --name "ci-bot" --scope play,read
flatland auth token list
flatland auth token revoke <token_id>
| Property | Rule |
|---|---|
| Storage | Hash only in Postgres (like passwords); show plaintext once at creation |
| Prefix | flat_live_ / flat_test_ for log-safe identification |
| Scopes | play, read, write, world:read, world:write (fine-grained later) |
| TTL | Optional expiry; default no expiry, user revokes manually |
| Binding | Token belongs to account; play scope requires character_id at connect time |
Environment convention (agent-first):
export FLATLAND_API_TOKEN=flat_live_...
export FLATLAND_CHARACTER_ID=<uuid>
flatland status --json
flatland play --json
LLM agents and CI use API tokens — never email/password in scripts.
3.3 Game connect auth
ClientMessage::Hello extended:
Hello {
client_name: String,
protocol_version: u16,
auth: AuthCredential,
}
enum AuthCredential {
Session { token: String },
ApiToken { token: String, character_id: Uuid },
DevLocal, // embedded --local only, no auth
}
Gateway validates with control plane (or Redis cache) before spawning entity. Invalid auth → disconnect with error frame.
4. Control plane HTTP API
Base URL: http://127.0.0.1:7380/v1 (dev) — separate from game TCP :7373.
4.1 Why HTTP/JSON for control plane
| Concern | HTTP/JSON | gRPC to agents | Game binary protocol |
|---|---|---|---|
| LLM / curl / CI | ✓ native | awkward | wrong tool |
| OpenAPI / docs | ✓ | harder | N/A |
| Latency for play | too slow | — | ✓ |
| Type safety internal | — | ✓ | — |
Rule: Anything that is not 30 Hz realtime sim uses HTTP. Playing uses game transport.
4.2 Public endpoints (v1)
Auth & account
| Method | Path | Auth |
|---|---|---|
| POST | /auth/register |
none |
| POST | /auth/login |
none |
| POST | /auth/logout |
session |
| GET | /me |
session or token (read) |
| POST | /me/tokens |
session |
| GET | /me/tokens |
session |
| DELETE | /me/tokens/{id} |
session |
Characters
| Method | Path | Auth |
|---|---|---|
| GET | /characters |
session / token read |
| POST | /characters |
session |
| GET | /characters/{id} |
session / token read |
| GET | /characters/{id}/inventory |
session / token read |
| GET | /characters/{id}/state |
session / token read |
Agent / JSON play bridge (Tier 2b)
Read-only and meta over HTTP; mutating gameplay still goes through game transport with play scope:
| Method | Path | Purpose |
|---|---|---|
| GET | /characters/{id}/nearby |
Snapshot of AOI entities (cached ~1 tick) |
| POST | /characters/{id}/intents |
Optional later — queue intent if HTTP-only agent; prefer game TCP |
Recommendation for v1 agents: connect game TCP with API token + use --json on flatland for intent UX. HTTP is for account, inventory queries, and automation setup — not tick stream.
4.3 Response shape (agent-first)
All JSON responses:
{
"ok": true,
"data": { ... },
"error": null
}
CLI: flatland ... --json uses same envelope. Errors include code, message, details.
4.4 Internal gRPC (cluster only)
flatland-control-plane exposes gRPC on :7381 (localhost / private network only):
| Service | Called by | Purpose |
|---|---|---|
AuthService.ValidateCredential |
gateway | Hello auth |
CharacterService.Checkpoint |
region worker | position + vitals on tick boundary |
InventoryService.Commit |
region worker | harvest mint, trade (ACID) |
ItemService.Mint |
region worker | UUID v7 unique instance creation |
Region workers never talk to Postgres directly — all durable writes through control plane gRPC.
5. UUID v7 for item instances
Per 18-unique-items-and-procedural-economy.html:
CREATE TABLE item_instances (
id UUID PRIMARY KEY, -- v7 generated server-side
template_id TEXT NOT NULL,
owner_id UUID, -- character or container
props JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
- Mint authority: control plane only (region worker requests mint over gRPC).
- No client-supplied IDs — prevents collision and cheating.
- v7 over v4: better Postgres index locality for time-ordered loot/history queries.
6. Interaction radius (1.5 m)
Server validates for Intent::Use, Intent::Harvest, Intent::Talk, loot:
const INTERACTION_RADIUS_M: f32 = 1.5;
fn can_interact(actor: &Transform, target: &WorldCoord) -> bool {
let dx = actor.position.x - target.x;
let dy = actor.position.y - target.y;
let dz = actor.position.z - target.z;
dx * dx + dy * dy + dz * dz <= INTERACTION_RADIUS_M * INTERACTION_RADIUS_M
}
- Cylindrical or spherical — use spherical 1.5 m for consistency with AOI docs.
- Client may show hint in TUI; server is authoritative.
- Harvest nodes in segment YAML have anchor
(x, y, z); must be within radius.
7. Crate layout (updated)
crates/
control-plane/ # HTTP :7380 + gRPC :7381, sqlx, migrations
protocol/ # extend Hello auth, Intent::Harvest, Intent::Use
sim/ # interaction radius, resource nodes
...
tools/
flatland/ # auth login, --json, play
flatland-server/ # game TCP :7373 (gateway only)
flatland-world/ # world ops (later)
Two binaries in prod:
flatland-server— game gateway + region worker(s)flatland-control-plane— HTTP + gRPC + Postgres
Dev: docker compose up for Postgres + Redis; both binaries locally.
8. Implementation order (Tier 1 + 2)
| Step | Deliverable |
|---|---|
| 1 | control-plane crate, migrations (accounts, api_tokens, characters) |
| 2 | HTTP auth: register, login, token CRUD |
| 3 | gRPC ValidateCredential + gateway Hello auth |
| 4 | Character create/list, position checkpoint on disconnect |
| 5 | Intent::Harvest + 1.5 m check + resource nodes in segment |
| 6 | gRPC ItemService.Mint (UUID v7) + inventory persist |
| 7 | TUI: login flow, inventory panel, harvest key |
| 8 | flatland auth * + flatland --json |
9. Security notes
- Rate-limit
/auth/loginand token validation (Redis). - API token scopes enforced on every HTTP route and at game Hello.
- Audit log table for token use, mints, trades (Postgres append-only).
DevLocalauth disabled in production builds / flag.
Locks Tier 2 decisions from development planning session 2026-07-06.