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 datetimeView on GitHub (pinned to 565fd254d0)
Solutions
- 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
- If a non-table object named oauth_clients exists, drop it after review, then restart - HasTable will then attempt creation again
- Free disk space; large SQLite files may need VACUUM after failed DDL attempts
- 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
- Don't hand-create experimental tables with the same names as upcoming migrations
- Keep disk headroom (DDL + WAL growth) before upgrades
- Grant CREATE on schema to the headscale role
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
- creating oauth_access_tokens table: %w
- adding column types.Node: %w
- dropping routes table: %w
- renaming table %s to %s_old: %w
- creating new table: %w
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/dfbf3456b58a6fd5.
Report an issue: GitHub.