actualbudget/actual · critical

Failed to retrieve owner count

Error message

Failed to retrieve owner count

What it means

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.

Source

Thrown at packages/sync-server/src/app-admin.js:25

  errorMiddleware,
  requestLoggerMiddleware,
  validateSessionMiddleware,
} from './util/middlewares';
import { validateSession } from './util/validate-user';

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(requestLoggerMiddleware);

export { app as handlers };

app.get('/owner-created/', (req, res) => {
  try {
    const ownerCount = UserService.getOwnerCount();
    res.json(ownerCount > 0);
  } catch {
    res.status(500).json({ error: 'Failed to retrieve owner count' });
  }
});

// NOTE: This endpoint intentionally has no isAdmin check, which allows user
// enumeration by any authenticated user. This is a known, accepted trade-off
// (wont-fix). Actual's multi-user/OpenID feature is intended for friends &
// family setups, not SaaS, so the attack surface is low. The endpoint is also
// used in the budget ownership transfer flow, where neither the current nor the
// target user is necessarily an admin — adding isAdmin would break that flow
// without a substantial refactor.
app.get('/users/', validateSessionMiddleware, (req, res) => {
  const users = UserService.getAllUsers();
  res.json(
    users.map(u => ({
      ...u,
      owner: u.owner === 1,
      enabled: u.enabled === 1,
    })),

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Check the sync-server logs — the catch clause hides the original exception, so reproduce locally to see the sqlite error.
  2. Run the server bootstrap/migrations so the users table exists (complete initial setup or restart to trigger migrations).
  3. Fix file permissions/ownership on the account database file and confirm no other process locks it, then retry GET /owner-created/.

Example fix

// before: account.db replaced by an empty file without migrations
// after: restore a valid database or re-bootstrap
mv account.sqlite account.sqlite.bak
yarn workspace @actual-app/sync-server bootstrap
Defensive patterns

Strategy: retry

Validate before calling

// pre-flight: confirm the DB is reachable before probing the endpoint
fs.accessSync(accountDbPath, fs.constants.R_OK | fs.constants.W_OK);

Type guard

function isOwnerCountFailure(err) {
  return err?.response?.status === 500 && err.response.data?.error === 'Failed to retrieve owner count';
}

Try / catch

try {
  const res = await get('/owner-created/');
  ownerExists = res.data === true;
} catch (e) {
  if (isOwnerCountFailure(e) && attempts < 3) return retryWithBackoff();
  throw e;
}

Prevention

When it happens

Trigger: `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.

Common situations: 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.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/4d1b0a2fd2db8b76. Report an issue: GitHub.