juanfont/headscale · critical
adding user_id to api_keys: %w
Error message
adding user_id to api_keys: %w
What it means
Migration '202606191500-api-key-user-id' fails adding the optional user_id column to api_keys (GORM AddColumn guarded by HasColumn), which supports user-owned API keys in the v2 API. Failure causes mirror the other AddColumn migrations: missing ALTER privilege, SQLite lock, disk full, or schema drift where a conflicting user_id object already exists but HasColumn missed it.
Source
Thrown at hscontrol/db/db.go:793
`).Error
if err != nil {
return fmt.Errorf("recovering user_id on untagged nodes: %w", err)
}
return nil
},
Rollback: func(db *gorm.DB) error { return nil },
},
{
// Add an optional owning user to API keys so the v2 API can
// create user-owned (untagged) auth keys, mirroring Tailscale's
// "key owned by the creating identity".
ID: "202606191500-api-key-user-id",
Migrate: func(tx *gorm.DB) error {
if !tx.Migrator().HasColumn(&types.APIKey{}, "user_id") {
err := tx.Migrator().AddColumn(&types.APIKey{}, "user_id")
if err != nil {
return fmt.Errorf("adding user_id to api_keys: %w", err)
}
}
return nil
},
Rollback: func(db *gorm.DB) error { return nil },
},
{
// Add a free-text description to pre-auth keys, set via the
// v2 keys API.
ID: "202606191501-pre-auth-key-description",
Migrate: func(tx *gorm.DB) error {
if !tx.Migrator().HasColumn(&types.PreAuthKey{}, "description") {
err := tx.Migrator().AddColumn(&types.PreAuthKey{}, "description")
if err != nil {
return fmt.Errorf("adding description to pre_auth_keys: %w", err)
}
}View on GitHub (pinned to 565fd254d0)
Solutions
- Read the wrapped DB error and address it (grant ALTER, stop the second instance, free space)
- If drift makes AddColumn fail, pre-create the column manually (ALTER TABLE api_keys ADD COLUMN user_id integer REFERENCES users(id)) and restart - the HasColumn guard then skips it
- Re-verify after startup that api_keys.user_id exists and is nullable
Example fix
-- before: guard missed a drifted schema, AddColumn fails ALTER TABLE api_keys ADD COLUMN user_id bigint; -- after: restart headscale; HasColumn sees the column and skips AddColumn -- verify: \d api_keys (user_id present, nullable)
Defensive patterns
Strategy: validation
Validate before calling
// Pre-flight: confirm the DB accepts DDL before the upgrade window
if _, err := db.Exec("CREATE TABLE IF NOT EXISTS _probe(id integer)"); err != nil {
log.Fatalf("cannot execute DDL (locked/readonly/no privilege): %v", err)
}
db.Exec("DROP TABLE IF EXISTS _probe") Prevention
- Grant DDL privileges; single instance per DB
- Pre-create expected columns in staging to validate migration guards behave idempotently
- Test v2-API upgrades on a database copy first
When it happens
Trigger: Upgrading to a version with the v2 keys API on a database lacking api_keys.user_id while DDL is blocked by locks, privileges, or disk space.
Common situations: Least-privilege Postgres roles; multiple headscale instances on one SQLite file; Docker volumes at capacity.
Related errors
- adding description to pre_auth_keys: %w
- automigrating types.Route: %w
- automigrating types.PreAuthKey: %w
- automigrating types.Node: %w
- adding column types.Node: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/0694b98f5d3005a1.
Report an issue: GitHub.