Skip to content

Database schema

Shared GORM models and database utilities. Imported by apps/api and apps/proxy via the Go workspace replace directive.


FilePurpose
db.goOpen(), Migrate(), FromEnv(), RegisterMigration()
models.goAll 44 CE table definitions
types.goCustom JSONB types: EnvVarsMap, JSONObject, StringArray
crypto.goEncryptedString — AES-256-GCM GORM type

TablePurpose
usersIdentity
trusted_devicesRemembered devices — skips 2FA prompt on re-login
recovery_codesOne-time 2FA recovery codes (hashed)
dismissed_noticesConsole advisories a user has dismissed — per-user, keyed by a stable slug
agent_tokensmagt- tokens for agent principals (SHA-256 hashed, shown once)
installed_licensesEnterprise licence tokens activated on this install
organizationsTenancy root
organization_membersUser ↔ Org join (roles: owner / admin / member)
resource_permissionsPer-resource ACL grants (service, stack, job, project)
org_invitationsEmail invitations to join an org
TablePurpose
projectsK8s namespace — slug becomes the namespace name
nodesMesh worker nodes + K3s + Headscale metadata
node_registration_tokensmreg-<hex> tokens for legacy worker self-registration
node_provisioning_tokensmprov-<hex> single-use provisioning tokens (hashed, with expiry)
domainsBase 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
TablePurpose
stacksDocker Compose stacks — parsed spec + services
servicesPolymorphic 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
service_portsExposed ports per service
build_configsGit source, builder type, registry target (1:1 with service)
database_configsEngine, version, storage size (1:1 with service)
volumesPersistent volumes
volume_mountsVolume ↔ Service mount (path + read-only flag)
volume_backup_configsBackup schedule for individual volumes
TablePurpose
variable_groupsNamed collections of key/value variables (project-scoped)
variable_group_itemsIndividual variable items within a group
service_variable_groupsService ↔ VariableGroup join
job_variable_groupsJob ↔ VariableGroup join
config_filesFiles projected into a workload at an absolute path; body is EncryptedString and never read back out (project-scoped)
service_config_filesService ↔ ConfigFile join
TablePurpose
routesHostname → service routing rule
ignored_endpointsDiscovered endpoints recorded as known and correct (org-scoped: node + address + port). Written by the API, not yet by the console
route_targetsTarget per route: a service, a node port, an address (with target_tls when it speaks HTTPS), or a redirect
tcp_routesA port the gateway publishes and forwards over the mesh
TablePurpose
deploymentsDeployment history + K8s artefacts + build log
TablePurpose
jobsJob definition (image, command, schedule, concurrency)
job_runsIndividual run records (status, logs, started/finished at)
TablePurpose
storage_integrationsS3-compatible storage credentials (org-scoped)
registry_integrationsContainer registry credentials (org-scoped)
git_integrationsGit provider connections (GitHub App; GitLab, Gitea/Forgejo and Bitbucket by token or OAuth)
TablePurpose
backup_configsScheduled DB backup config (service-scoped)
system_backup_configsOrg-wide system backup config
notification_channelsSlack / Discord / webhook / email event routing
notification_deliveriesOne row per attempt to send an event to a channel (success, error, test, retry); pruned after 30 days
org_email_configsSMTP credentials per org
TablePurpose
templates1-click deployment blueprints (official + user-created)

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):

IndexConstraint
idx_one_owner_per_orgExactly one owner per organisation (partial: WHERE role = 'owner')
idx_users_email_uniqueEmail unique among humans only (partial: WHERE email <> ''); agents carry an empty email
idx_variable_group_serviceAt most one system-managed variable group per service (partial)
idx_variable_group_item_keyItem keys unique within a variable group
idx_service_variable_groupA service attaches a given group at most once
idx_jobs_project_nameJob names unique within a project
idx_route_target_pathOne path rule per route
idx_resource_permission_grantNo 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.


import dbpkg "github.com/meshploy/packages/db"
// Open from DATABASE_URL env var
db, err := dbpkg.FromEnv()
// Or explicit DSN
db, err := dbpkg.Open(dsn)
// Run migrations
dbpkg.Migrate(db)
// Set encryption key before any encrypted field access
dbpkg.SetEncryptionKey(os.Getenv("ENCRYPTION_KEY"))