Skip to content

Database

PostgreSQL. nexus and authz use the same database (one DSN per environment: NEXUS_DATABASE_DSN and AUTHZ_DATABASE_DSN point at the same database on the environment's Scaleway RDB instance, provisioned by infra/terraform/modules/database). Each service owns its own tables and there are no foreign keys between them. nexus uses Bun (github.com/uptrace/bun).

Migrations

internal/store/pgstore/migrations/NNN_name.{up,down}.sql, embedded in the binary (//go:embed) and applied by Bun's migrator at startup (DB.Migrate). They run in numeric order, 001 to 032. Add a migration as the next number with both files; never edit one that has shipped.

authz migrates the same way from its own embedded files (timestamp-prefixed). Neither service sets a table name, so both record applied migrations in Bun's default bun_migrations table in the shared database; the two naming schemes do not collide.

Tables owned by nexus

The column lists below are the result of all migrations. NOT NULL and defaults are shown where they matter.

devices

Column Type Notes
id VARCHAR(64) PK gw-<8 hex>
hostname VARCHAR(255) NOT NULL
display_name VARCHAR(255) NOT NULL DEFAULT ''
model, board, architecture Reported or set by an admin
agent_version VARCHAR(64) Reported by the agent
desired_agent_version VARCHAR(64) OTA pin; wins over the channel
agent_channel VARCHAR(32) NOT NULL DEFAULT 'stable' Release channel tracked
config_version BIGINT NOT NULL DEFAULT 0 Last version the agent applied
desired_config_version BIGINT NOT NULL DEFAULT 0 Version the platform wants
config_hash VARCHAR(64)
api_token VARCHAR(256) SHA-256 of the device token
mac_address VARCHAR(17) Informational, not identity
device_type VARCHAR(32) NOT NULL DEFAULT 'gateway'
status VARCHAR(16) NOT NULL DEFAULT 'holding' holding, assigned, prepared, retired
org_id UUID NULL = holding zone
enrolled_via UUID enrollment_codes.id used to enrol
last_seen_at, enrolled_at, created_at, updated_at TIMESTAMPTZ

enrollment_codes

Enrollment codes (MAC-bound, single-use, Monsieur Wifi APs), transfer codes, and bootstrap tokens for gateways (type='bootstrap': named, multi-use, not MAC-locked, revocable). Lookups go by code_hash; bootstrap tokens keep no plaintext (code is NULL).

Column Notes
id UUID PK
code VARCHAR(64) Nullable
code_hash VARCHAR(64) NOT NULL UNIQUE SHA-256
type VARCHAR(16) enrollment | transfer | bootstrap
name Label (batch, installer)
mac_address, device_type, serial_number MAC-bound codes
max_uses INTEGER, uses INTEGER NOT NULL DEFAULT 0 NULL/0 = unlimited
revoked_at Set = refused
created_by, claimed_by, claimed_at, expires_at, created_at, updated_at

organizations

Column Notes
id UUID PK
name VARCHAR(255) NOT NULL
slug VARCHAR(255) NOT NULL UNIQUE Immutable
owner_id VARCHAR(256) NOT NULL Zitadel sub, or system
plan VARCHAR(64) NOT NULL DEFAULT 'free'
created_at, updated_at

Migration 003 seeds the Digilan org (00000000-0000-0000-0000-000000000001, owner_id='system'). Orgs made with CreateOrganization are also owned by system; access to any org comes from ACL grants in authz, not from owner_id.

Configuration and state

Table Shape Notes
gateway_configs id BIGSERIAL, device_id → devices (cascade), version BIGINT, config_data JSONB, config_hash, created_by, created_at, status, applied_at, comment TEXT NOT NULL DEFAULT '', error TEXT NOT NULL DEFAULT ''; UNIQUE(device_id, version) Status: draft, pending, awaiting_confirm, applied, failed, rolled_back. See Config Lifecycle.
gateway_states device_id PK → devices, state_data JSONB, reported_at, updated_at One row per device, replaced on each state report
gateway_logs id, device_id, source, line, logged_at, received_at Indexed by device/source/time, plus a pg_trgm GIN index on line for search. pf rows kept 48 h, others 7 days.
gateway_metrics id, device_id, cpu, memory, temperature, recorded_at From heartbeats; kept 24 h
gateway_block_samples id, device_id, logging_packets_raw, silent_packets_raw, logging_packets, silent_packets, was_reset, recorded_at Block counters per state report, with reload-corrected increments; feeds GetBlockTrend
template_overrides PK (org_id, template_id), defaults JSONB, updated_at, updated_by Per-org service-profile defaults

Agent updates and releases

Table Shape Notes
agent_releases id UUID, version, arch (amd64|arm64), channel (default stable), sha256, signature (ed25519), object_key, yanked, timestamps; UNIQUE(version, arch, channel) object_key is a private bucket key; nexus presigns a short-lived GET at read time
agent_update_status device_id PK, phase, target_version, from_version, error, updated_at Last agent_update phase; read by GetAgentUpdateStatus

Audit and sessions

Table Shape Notes
viewer_tickets id, token_hash UNIQUE, device_id → devices, user_id, org_id, expires_at, used_at, created_at 30 s, single-use, hash only
shell_tickets same shape as viewer_tickets Separate table so the two can never cross
shell_sessions id UUID, device_id, user_id, org_id, started_at, ended_at, end_reason Root-console audit; backs ListShellSessions
device_events id UUID, device_id, event_type, actor, old_value, new_value, created_at Identity/lifecycle changes; backs ListDeviceEvents
portal_sessions id UUID, user_id, org_id → organizations, ip, user_agent, started_at, last_seen_at Portal visits, coalesced from Me calls; backs ListPortalSessions
user_preferences PK (user_id, key), value JSONB, updated_at Per-user portal preferences
device_command_replies command_id PK, device_id, exit_code, stdout, stderr, created_at, expires_at Private command replies (access codes) awaiting collection; 90 s TTL

Relays

Table Shape Notes
relay_bootstrap_tokens token_hash PK, environment, created_at, spent_at, relay_id Registered via RegisterRelayToken, spent by Enroll
relays id PK (rl-<8 hex>), hostname, public_ipv4, public_ipv6, region, version, api_token_hash UNIQUE, max_rendezvous, max_forward, rendezvous_sessions, forward_sessions, healthy, draining, wireguard_public_key, wireguard_port, tunnel_url, last_seen_at, enrolled_at, updated_at See Relay fleet
relay_sessions gateway_id PK, relay_id → relays (cascade), role, attached_at, wireguard_public_key, tunnel_address (unique when non-empty) One assignment per gateway

site_links (two devices, networks, presented ranges, tunnel subnet, transport direct|relay, state draft→awaiting_keys→configuring→ established, plus drifted/failed) and site_link_ranges (allocated CIDRs per org). Used by PortalLinkService, which is not mounted.

Tables owned by authz

Same database; written only through AuthzService. See Authz → Model.

Table Contents
roles id, name, alias, description
role_actions (role_id, target, action)
acl (subject_id, role_id, target, target_id)
service_keys bcrypt-hashed API keys

Why JSONB for config

  • One row is one complete config snapshot, so apply, confirm and rollback are a version number plus a status.
  • Adding a field to the config model needs no migration; the agent and portal agree on the shape.
  • The agent consumes the document as stored.