{"record":{"id":"16046bff970253b8","repo":"juanfont/headscale","slug":"creating-oauth-access-tokens-table-w","errorCode":null,"errorMessage":"creating oauth_access_tokens table: %w","messagePattern":"creating oauth_access_tokens table: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"hscontrol/db/db.go","lineNumber":890,"sourceCode":"\t\t\t\t\t\terr = tx.Exec(`CREATE UNIQUE INDEX idx_oauth_clients_client_id ON oauth_clients(client_id)`).Error\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"creating oauth_clients index: %w\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif !tx.Migrator().HasTable(&types.OAuthAccessToken{}) {\n\t\t\t\t\t\terr := tx.Exec(`CREATE TABLE oauth_access_tokens(\n  id integer PRIMARY KEY AUTOINCREMENT,\n  prefix text,\n  hash blob,\n  client_id text,\n  scopes text,\n  tags text,\n  expiration datetime,\n  created_at datetime\n)`).Error\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"creating oauth_access_tokens table: %w\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\terr = tx.Exec(`CREATE UNIQUE INDEX idx_oauth_access_tokens_prefix ON oauth_access_tokens(prefix)`).Error\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"creating oauth_access_tokens index: %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// Clear stale key expiry on tagged nodes. A tagged node is\n\t\t\t\t// owned by its tags and never expires (KB 1068), but a buggy\n\t\t\t\t// handleLogout stamped a past expiry on it, leaving it\n\t\t\t\t// permanently Expired and unable to re-authenticate. The\n\t\t\t\t// buggy writer is fixed, so this only repairs rows written","sourceCodeStart":872,"sourceCodeEnd":908,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/db.go#L872-L908","documentation":"The oauth migration fails creating the oauth_access_tokens table (id, prefix, hash, client_id, scopes, tags, expiration, created_at) backing bearer-token validation for the v2 OAuth flow. Same failure class as the oauth_clients CREATE TABLE: privileges, read-only database, disk full, catalog collision, or an aborted transaction masking an earlier error.","triggerScenarios":"HasTable(&types.OAuthAccessToken{}) is false and the raw CREATE TABLE is rejected by the database due to environment problems or leftover catalog objects.","commonSituations":"First upgrade to a v2-API-capable headscale on a constrained or drifted database; Postgres roles without CREATE; Docker containers with full volumes.","solutions":["Address the wrapped DB error (privileges, read-only mount, space, locks)","Check for and remove leftover oauth_access_tokens objects from earlier failed attempts, then restart","Confirm after startup that both oauth tables exist alongside their indexes"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Pre-flight: catalog clean and DDL permitted\nvar collision bool\ndb.QueryRow(`SELECT EXISTS (SELECT 1 FROM sqlite_master\n\tWHERE name = 'oauth_access_tokens' AND type <> 'table')`).Scan(&collision)\nif collision {\n\tlog.Fatal(\"non-table object named oauth_access_tokens exists\")\n}\nif _, err := db.Exec(\"CREATE TABLE IF NOT EXISTS _probe(id integer)\"); err != nil {\n\tlog.Fatalf(\"DDL blocked: %v\", err)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Remove leftover experimental v2-API tables before the official upgrade","Verify disk space and writability in the DB volume","Single headscale instance per database during DDL"],"tags":["database","migration","oauth","ddl","create-table"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}