juanfont/headscale · critical

creating oauth_access_tokens index

Error message

creating oauth_access_tokens index: %w

What it means

The oauth migration fails creating unique index idx_oauth_access_tokens_prefix on oauth_access_tokens(prefix). The table is freshly created in the same transaction so duplicates cannot exist; realistic causes are missing INDEX privilege, an aborted Postgres transaction from an earlier statement, or lock failure. Like its oauth_clients twin, this error is frequently the visible tail of an earlier failure in the same migration.

Solutions

  1. Find the first error in the same migration in the log and fix that; this index error usually disappears with it
  2. Grant the role CREATE INDEX (Postgres) or otherwise clear the wrapped permission error
  3. Restart headscale after the fix - the guards make the migration idempotent
  4. Verify: query pg_indexes (Postgres) or sqlite_master (SQLite) for idx_oauth_access_tokens_prefix
Defensive patterns

Strategy: try-catch

Validate before calling

// Postgres: verify index-creation privilege ahead of the upgrade
var can bool
db.QueryRow("SELECT has_schema_privilege(current_user, current_schema(), 'CREATE')").Scan(&can)
if !can {
	log.Fatal("role cannot create indexes in this schema")
}

Try / catch

// Treat as symptom: on Postgres this usually means the migration transaction was
// already aborted; catch, unwrap fully, and surface the first failure
if err := runMigrations(db); err != nil {
	for e := err; e != nil; e = errors.Unwrap(e) {
		log.Error().Err(e).Msg("migration error chain")
	}
	os.Exit(1)
}

Prevention

When it happens

Trigger: Executing CREATE UNIQUE INDEX after a prior statement in the migration transaction already failed (Postgres abort semantics), or with CREATE INDEX revoked / database locked.

Common situations: Diagnosing the last line of a failed migration instead of the first; constrained roles; concurrent SQLite writers.

Related errors


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

Appendix: source

Thrown at hscontrol/db/db.go:895

					if !tx.Migrator().HasTable(&types.OAuthAccessToken{}) {
						err := tx.Exec(`CREATE TABLE oauth_access_tokens(
  id integer PRIMARY KEY AUTOINCREMENT,
  prefix text,
  hash blob,
  client_id text,
  scopes text,
  tags text,
  expiration datetime,
  created_at datetime
)`).Error
						if err != nil {
							return fmt.Errorf("creating oauth_access_tokens table: %w", err)
						}

						err = tx.Exec(`CREATE UNIQUE INDEX idx_oauth_access_tokens_prefix ON oauth_access_tokens(prefix)`).Error
						if err != nil {
							return fmt.Errorf("creating oauth_access_tokens index: %w", err)
						}
					}

					return nil
				},
				Rollback: func(db *gorm.DB) error { return nil },
			},
			{
				// Clear stale key expiry on tagged nodes. A tagged node is
				// owned by its tags and never expires (KB 1068), but a buggy
				// handleLogout stamped a past expiry on it, leaving it
				// permanently Expired and unable to re-authenticate. The
				// buggy writer is fixed, so this only repairs rows written
				// before the upgrade; a fixed server cannot recreate them.
				// Match the tagged-node predicate the earlier
				// clear-tagged-node-user-id migration uses (a nil tags slice
				// marshals to 'null', so exclude it).
				// Fixes: https://github.com/juanfont/headscale/issues/3371

View on GitHub (pinned to 565fd254d0)