{"record":{"id":"0694b98f5d3005a1","repo":"juanfont/headscale","slug":"adding-user-id-to-api-keys-w","errorCode":null,"errorMessage":"adding user_id to api_keys: %w","messagePattern":"adding user_id to api_keys: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":793,"sourceCode":"\t\t\t\t\t\t`).Error\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn fmt.Errorf(\"recovering user_id on untagged nodes: %w\", err)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tRollback: func(db *gorm.DB) error { return nil },\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Add an optional owning user to API keys so the v2 API can\n\t\t\t\t// create user-owned (untagged) auth keys, mirroring Tailscale's\n\t\t\t\t// \"key owned by the creating identity\".\n\t\t\t\tID: \"202606191500-api-key-user-id\",\n\t\t\t\tMigrate: func(tx *gorm.DB) error {\n\t\t\t\t\tif !tx.Migrator().HasColumn(&types.APIKey{}, \"user_id\") {\n\t\t\t\t\t\terr := tx.Migrator().AddColumn(&types.APIKey{}, \"user_id\")\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"adding user_id to api_keys: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t},\n\t\t\t\tRollback: func(db *gorm.DB) error { return nil },\n\t\t\t},\n\t\t\t{\n\t\t\t\t// Add a free-text description to pre-auth keys, set via the\n\t\t\t\t// v2 keys API.\n\t\t\t\tID: \"202606191501-pre-auth-key-description\",\n\t\t\t\tMigrate: func(tx *gorm.DB) error {\n\t\t\t\t\tif !tx.Migrator().HasColumn(&types.PreAuthKey{}, \"description\") {\n\t\t\t\t\t\terr := tx.Migrator().AddColumn(&types.PreAuthKey{}, \"description\")\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"adding description to pre_auth_keys: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}","sourceCodeStart":775,"sourceCodeEnd":811,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L775-L811","documentation":"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.","triggerScenarios":"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.","commonSituations":"Least-privilege Postgres roles; multiple headscale instances on one SQLite file; Docker volumes at capacity.","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"],"exampleFix":"-- before: guard missed a drifted schema, AddColumn fails\nALTER TABLE api_keys ADD COLUMN user_id bigint;\n-- after: restart headscale; HasColumn sees the column and skips AddColumn\n-- verify: \\d api_keys  (user_id present, nullable)","handlingStrategy":"validation","validationCode":"// Pre-flight: confirm the DB accepts DDL before the upgrade window\nif _, err := db.Exec(\"CREATE TABLE IF NOT EXISTS _probe(id integer)\"); err != nil {\n\tlog.Fatalf(\"cannot execute DDL (locked/readonly/no privilege): %v\", err)\n}\ndb.Exec(\"DROP TABLE IF EXISTS _probe\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["database","migration","api-keys","gorm","ddl"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}