Console advisories a user has dismissed — per-user, keyed by a stable slug
agent_tokens
magt- tokens for agent principals (SHA-256 hashed, shown once)
cli_logins
A CLI waiting for someone to approve it in a browser (device flow): the user code both sides show, the device code’s hash, its state
cli_tokens
mcli- tokens a CLI holds after an approved login, acting as the person who approved it (SHA-256 hashed, shown once)
console_sessions
One row per sign-in to the console: browser, address, last seen, expiry. The console’s token names its row (sid) and is good only while the row is live, so a sign-in can be ended; a password change or two-factor turned off ends all but the current one
oauth_clients
MCP clients that registered themselves (OAuth dynamic registration): name, redirect URIs, and a client secret’s hash for one that asked for a secret
oauth_grants
Approved connections: who approved, the principal it acts as (them, or an agent), the organisation, the client’s name as approved, last use, revoked_at. One live per principal, client and organisation (idx_oauth_grant_live)
oauth_codes
Authorization codes: single use, five minutes, with the redirect URI and PKCE challenge they were issued for
oauth_tokens
moat- access and mort- refresh tokens under a grant, SHA-256 hashed; a refresh token is marked when traded
installed_licenses
Enterprise licence tokens activated on this install
organizations
Tenancy root. network_isolation closes each of its projects to the rest of the cluster
organization_members
User ↔ Org join (roles: owner / admin / member)
mesh_reach
Whether a member’s machines may reach a granted service, stack or project on the mesh, once someone chose; without a row, databases are off and everything else on
mesh_rules
Network rules: a machine, and ports on it, that a person’s machines, one machine or every machine of the organisation may reach; for what is not a Meshploy resource. Added to the generated mesh policy, no console permission
mesh_policy_state
One row: whether the mesh obeys its generated access policy, who switched it, and when Headscale was last given a policy (or why that failed)
resource_permissions
Per-resource ACL grants (service, stack, job, project, route). source is empty for a grant given directly, else <kind>:<id> for one an edition derives and writes out per person; every check reads both, and the console gives and removes only direct ones. Unique per source (idx_resource_permission_grant_source)
K8s namespace: slug becomes the namespace name. A row with parent_project_id is an environment level of that project (env_name, env_level: 0 is production, each level below counts up), with a namespace of its own, <project slug>-<level>. network_open exempts it from its organization’s network isolation; sandboxed runs its pods on gVisor
promotion_groups
Services that move up a project’s levels together, along their own path of level IDs, from the one that builds to production. single marks one made by copying one service down
promotion_group_members
A service lineage in a group; at most one group per lineage per project
nodes
Mesh worker nodes + K3s + Headscale metadata. os is linux, darwin or windows; anything but Linux is always mesh only
node_registration_tokens
mreg-<hex> tokens for legacy worker self-registration
node_provisioning_tokens
mprov-<hex> single-use provisioning tokens (hashed, with expiry)
domains
Base domains an org routes on. Each carries its own dns_mode (delegation or ondemand); is_primary marks the one whose platform subdomains serve and that new routes default to; retiring_at stops new routes attaching while everything already on it keeps serving; dns_provider_id, on an on-demand domain only, names the DNS provider that writes its records; project_id reserves it for one project and its levels, whose routes alone may use it (a project with one cannot be deleted until it goes)
Docker Compose stacks — parsed spec + services, and the images their built services keep for rollback (rollback_enabled, image_retention)
stack_runs
One Sync or Apply of a stack and its rollout: the services in depends_on layers, each with its deployment and state, and what the apply said. The request answers with the first layer; the rest are followed here
services
Polymorphic workload: application or database. slug is the Kubernetes object name, fixed at creation and suffixed when the plain name is taken in the project; empty on pre-slug rows, which fall back to the display name. lineage_id ties the copies of one service across environment levels (a service’s lineage is COALESCE(lineage_id, id))
service_ports
Exposed ports per service
build_configs
Git source or an uploaded folder (upload_*: its digest, name and size, kept as a blob in the service’s registry; built while git_repo is empty), builder type, registry target, and how many images it keeps for rollback: rollback_enabled on keeps the last image_retention (3 for a service made on its own), off keeps every one (1:1 with service)
Hostname → service routing rule. In an environment level the hostname carries the level’s name (app-staging) while subdomain stays the service’s own; awaiting_deploy marks a route copied into a level with its service, published by that service’s first deploy there
ignored_endpoints
Discovered endpoints recorded as known and correct (org-scoped: node + address + port). Written by the API, not yet by the console
route_targets
Target per route: a service, a node port, an address (with target_tls when it speaks HTTPS), or a redirect
tcp_routes
A port the gateway publishes and forwards over the mesh
edge_versions
One counter per thing the edge proxy keeps in memory (routes here; an edition adds its own), moved by every GORM write to the tables watched for it, in the write’s transaction. The proxy reads it each second and reloads only when it moved
edge_fallbacks
During a migration whose edge was taken first: the old platform’s edge on its side port, and the hostnames the proxy hands it while it has no route of its own. One row for the gateway, removed at Finish
Deployment history + K8s artefacts + build log, and where the image came from: source (build, promotion, bring_down, rollback, image), branch, commit and its subject, and for an image moved between levels the level and deployment it came from; image_removed_at once its image was removed from the registry, when it can no longer be rolled back to
Connected DNS hosts (Cloudflare): the API token encrypted, and the zones it may write records in as of its last check (org-scoped). A domain still using one keeps it from being removed
git_integrations
Git provider connections (GitHub App; GitLab, Gitea/Forgejo and Bitbucket by token or OAuth)
db.Migrate() runs GORM AutoMigrate for all models, then applyConstraints(), which creates the unique indexes GORM cannot express as struct tags and runs a few idempotent data migrations (column cleanups and backfills):
Index
Constraint
idx_one_owner_per_org
Exactly one owner per organisation (partial: WHERE role = 'owner')
idx_users_email_unique
Email unique among humans only (partial: WHERE email <> ''); agents carry an empty email
idx_variable_group_service
At most one system-managed variable group per service (partial)
idx_variable_group_item_key
Item keys unique within a variable group
idx_service_variable_group
A service attaches a given group at most once
idx_jobs_project_name
Job names unique within a project
idx_route_target_path
One path rule per route
idx_resource_permission_grant
No duplicate permission grants
Domain names are unique across all organisations through the uniqueIndex tag on domains.base_domain, so one org cannot claim another’s domain.
idx_one_primary_domain_per_org is a partial unique index on domains(organization_id) WHERE is_primary: every base domain routes, and primary only decides whose platform subdomains serve, so two of them would leave that undecided.
Migrations run automatically on API startup; no migration CLI is needed.
EncryptedString is a custom GORM type that transparently encrypts on write and decrypts on read using AES-256-GCM. Call db.SetEncryptionKey(key) before any DB operation — the key must be exactly 32 characters.
Fields using this type (registry credentials, storage keys, git tokens) are stored as base64-encoded ciphertext and are never readable as plaintext in the database.
db.RegisterMigration(fn) registers additional schema migrations that run after AutoMigrate and applyConstraints. Call it from any package’s init() to extend the schema without modifying packages/db directly.