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

  1. Read the wrapped DB error and address it (grant ALTER, stop the second instance, free space)
  2. 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
  3. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/0694b98f5d3005a1. Report an issue: GitHub.