juanfont/headscale · critical

creating oauth_clients table: %w

Error message

creating oauth_clients table: %w

What it means

The migration creating the oauth_clients table (raw CREATE TABLE guarded by HasTable) for the v2 API's OAuth client-credentials flow fails. Because it is guarded by HasTable, an existing table is skipped, so failure means genuine DDL rejection: missing CREATE privilege, read-only or full database, dialect problems with the raw SQL, or a leftover catalog object with the same name but a different kind (sequence, view).

Source

Thrown at hscontrol/db/db.go:869

					if tx.Name() != "sqlite" {
						return tx.AutoMigrate(&types.OAuthClient{}, &types.OAuthAccessToken{})
					}

					if !tx.Migrator().HasTable(&types.OAuthClient{}) {
						err := tx.Exec(`CREATE TABLE oauth_clients(
  id integer PRIMARY KEY AUTOINCREMENT,
  client_id text,
  secret_hash blob,
  scopes text,
  tags text,
  description text,
  user_id integer,
  created_at datetime,
  revoked datetime
)`).Error
						if err != nil {
							return fmt.Errorf("creating oauth_clients table: %w", err)
						}

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

					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

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Read the wrapped error: 'permission denied for schema' means GRANT CREATE ON SCHEMA; 'attempt to write a readonly database' means fix the mount; 'database is locked' means serialize instances
  2. If a non-table object named oauth_clients exists, drop it after review, then restart - HasTable will then attempt creation again
  3. Free disk space; large SQLite files may need VACUUM after failed DDL attempts
  4. Verify post-startup: SELECT * FROM oauth_clients LIMIT 1 should succeed (empty result)
Defensive patterns

Strategy: validation

Validate before calling

// Pre-flight: no object name collisions, DDL permitted (SQLite)
var kind string
err := db.QueryRow(`SELECT type FROM sqlite_master WHERE name = 'oauth_clients'`).Scan(&kind)
if err == nil && kind != "table" {
	log.Fatal("non-table object named oauth_clients exists; remove it")
}
if _, err := db.Exec("CREATE TABLE IF NOT EXISTS _probe(id integer)"); err != nil {
	log.Fatalf("DDL blocked: %v", err)
}
db.Exec("DROP TABLE IF EXISTS _probe")

Prevention

When it happens

Trigger: Creating oauth_clients when the DB user cannot CREATE tables, the database is read-only or out of space, or a leftover object named oauth_clients (from aborted experiments with the v2 API tables) confuses the catalog.

Common situations: Upgrading on locked SQLite databases; Postgres schemas where CREATE was revoked; leftover objects from earlier failed attempts at the v2 API tables.

Related errors


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