actualbudget/actual · error

Failed to get server config.

Error message

Failed to get server config.

What it means

setSecret stores a named secret (e.g. bank-sync credentials) on the sync server. It reads the global server config via getServer(); when the app is running without a configured server URL, getServer() returns null and this error is thrown because there is no server to store the secret on.

Source

Thrown at packages/loot-core/src/server/accounts/app.ts:735

async function setSecret({
  name,
  value,
  fileId = null,
}: {
  name: string;
  value: string | null;
  fileId?: string | null;
}) {
  const userToken = await asyncStorage.getItem('user-token');

  if (!userToken) {
    return { error: 'unauthorized' };
  }

  const serverConfig = getServer();
  if (!serverConfig) {
    throw new Error('Failed to get server config.');
  }

  const headers = {
    'X-ACTUAL-TOKEN': userToken,
    ...(fileId ? { 'X-Actual-File-Id': fileId } : {}),
  };

  try {
    if (value === null) {
      return await del(
        serverConfig.BASE_SERVER + '/secret/' + name,
        {},
        headers,
      );
    }

    return await post(
      serverConfig.BASE_SERVER + '/secret',

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Configure a sync server URL: set it in the app's advanced settings or call actual.init({ serverURL: 'https://your-server' }) in the API.
  2. If self-hosting, start the sync server and point the client at it before using secrets/bank sync.
  3. Check that nothing called setServer(null) or reset the server URL after initialization.
  4. If you intentionally run local-only, avoid secret-set/bank-sync APIs — they require a server.

Example fix

// before
await actual.init({ dataDir: './data' });
await actual.q('messages').insert(...); // then later setSecret fails
// after
await actual.init({ dataDir: './data', serverURL: 'https://actual.example.com' });
await actual.login('user@example.com', 'password');
// now setSecret works
Defensive patterns

Strategy: validation

Validate before calling

// read the same config the app uses
import { getServer } from './server-config';
if (!getServer()) {
  throw new Error('No sync server configured; set a server URL before storing secrets.');
}

Type guard

function hasServerConfig(c: ReturnType<typeof getServer>): c is NonNullable<ReturnType<typeof getServer>> {
  return c != null && typeof c.BASE_SERVER === 'string';
}

Try / catch

try {
  await send('secret-set', { name, value });
} catch (e) {
  if (e.message === 'Failed to get server config.') {
    // local-only mode: surface a 'server required' state instead of crashing
    reportServerRequired(name);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Calling the 'secret-set' handler (setSecret) while the client is in local-only mode ('Don't use a server') or before setServer(url) has been called, so the module-level config in server-config.ts is still null.

Common situations: Using a local-only budget but attempting to configure bank sync (GoCardless/SimpleFin/Pluggy.ai/Akahu) which requires a server; forgetting to run actual.init with serverUrl in the API package; the server URL was reset via setServer(null); standalone/desktop app started without a server URL.

Related errors


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