{"record":{"id":"4d1b0a2fd2db8b76","repo":"actualbudget/actual","slug":"failed-to-retrieve-owner-count","errorCode":null,"errorMessage":"Failed to retrieve owner count","messagePattern":"Failed to retrieve owner count","errorType":"http","errorClass":null,"httpStatus":500,"severity":"critical","filePath":"packages/sync-server/src/app-admin.js","lineNumber":25,"sourceCode":"  errorMiddleware,\n  requestLoggerMiddleware,\n  validateSessionMiddleware,\n} from './util/middlewares';\nimport { validateSession } from './util/validate-user';\n\nconst app = express();\napp.use(express.json());\napp.use(express.urlencoded({ extended: true }));\napp.use(requestLoggerMiddleware);\n\nexport { app as handlers };\n\napp.get('/owner-created/', (req, res) => {\n  try {\n    const ownerCount = UserService.getOwnerCount();\n    res.json(ownerCount > 0);\n  } catch {\n    res.status(500).json({ error: 'Failed to retrieve owner count' });\n  }\n});\n\n// NOTE: This endpoint intentionally has no isAdmin check, which allows user\n// enumeration by any authenticated user. This is a known, accepted trade-off\n// (wont-fix). Actual's multi-user/OpenID feature is intended for friends &\n// family setups, not SaaS, so the attack surface is low. The endpoint is also\n// used in the budget ownership transfer flow, where neither the current nor the\n// target user is necessarily an admin — adding isAdmin would break that flow\n// without a substantial refactor.\napp.get('/users/', validateSessionMiddleware, (req, res) => {\n  const users = UserService.getAllUsers();\n  res.json(\n    users.map(u => ({\n      ...u,\n      owner: u.owner === 1,\n      enabled: u.enabled === 1,\n    })),","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/sync-server/src/app-admin.js#L7-L43","documentation":"HTTP 500 from GET /owner-created/ with `{error:'Failed to retrieve owner count'}`. The endpoint queries `UserService.getOwnerCount()` inside a try/catch; any exception thrown while counting owner users in the account database is swallowed and converted into this generic 500 response.","triggerScenarios":"`UserService.getOwnerCount()` throws — typically because the users table does not exist (un-migrated or corrupted account.sqlite), the DB file is locked/unreadable, or the sqlite query fails for any reason.","commonSituations":"Fresh server where the account database was never bootstrapped/migrated; file-permission problems on account.db after moving the data directory (wrong ACTUAL_USER_ID/ownership in Docker); another process holding a write lock on the SQLite file.","solutions":["Check the sync-server logs — the catch clause hides the original exception, so reproduce locally to see the sqlite error.","Run the server bootstrap/migrations so the users table exists (complete initial setup or restart to trigger migrations).","Fix file permissions/ownership on the account database file and confirm no other process locks it, then retry GET /owner-created/."],"exampleFix":"// before: account.db replaced by an empty file without migrations\n// after: restore a valid database or re-bootstrap\nmv account.sqlite account.sqlite.bak\nyarn workspace @actual-app/sync-server bootstrap","handlingStrategy":"retry","validationCode":"// pre-flight: confirm the DB is reachable before probing the endpoint\nfs.accessSync(accountDbPath, fs.constants.R_OK | fs.constants.W_OK);","typeGuard":"function isOwnerCountFailure(err) {\n  return err?.response?.status === 500 && err.response.data?.error === 'Failed to retrieve owner count';\n}","tryCatchPattern":"try {\n  const res = await get('/owner-created/');\n  ownerExists = res.data === true;\n} catch (e) {\n  if (isOwnerCountFailure(e) && attempts < 3) return retryWithBackoff();\n  throw e;\n}","preventionTips":["Complete server bootstrap before clients call /owner-created/","Verify account.db permissions and ownership (especially in Docker with ACTUAL_USER_ID)","Avoid running a second process against the same SQLite file"],"tags":["http-500","database","sqlite","sync-server"],"backgroundTag":"database-unavailable","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}