actualbudget/actual · error

login: User token not set

Error message

login: User token not set

What it means

After a sign-in exchange with the sync server, signIn expects the server response to contain a session token; if res.token is missing the response shape is unexpected and it throws rather than storing an undefined token.

Source

Thrown at packages/loot-core/src/server/auth/app.ts:278

      throw new Error('No sync server configured.');
    }
    res = await post(serverConfig.SIGNUP_SERVER + '/login', loginInfo);
  } catch (err) {
    if (err instanceof PostError) {
      return {
        error: err.reason || 'network-failure',
      };
    }

    throw err;
  }

  if (res.returnUrl) {
    return { redirectUrl: res.returnUrl };
  }

  if (!res.token) {
    throw new Error('login: User token not set');
  }

  await asyncStorage.setItem('user-token', res.token);
  return {};
}

async function signOut() {
  encryption.unloadAllKeys();
  await asyncStorage.multiRemove([
    'user-token',
    'encrypt-keys',
    'lastBudget',
    'readOnly',
  ]);
  return 'ok';
}

async function setToken({ token }: { token: string }) {

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Check that the sync server and client versions are compatible; upgrade the sync server.
  2. Verify the endpoint is a real Actual sync server (SIGNUP_SERVER) and not a proxy masking errors.
  3. Inspect the sign-in response for an error field (PostError reasons like 'invalid-password') and surface it before assuming success.

Example fix

// before
const res = await send(signupServer + '/login', { username, password });
if (res.token) ... // server version mismatch: no token, throws later
// after
if (res.token == null) {
  throw new Error('Sign-in failed: ' + (res.reason || 'no token returned'));
}
Defensive patterns

Strategy: try-catch

Validate before calling

function expectsToken(res) { return res && typeof res.token === 'string' && res.token.length > 0; }
if (!expectsToken(signInResponse)) throw new Error('Sign-in response missing token; check server version/compatibility');

Type guard

function hasToken(res: unknown): res is { token: string } {
  return typeof res === 'object' && res !== null && 'token' in res && typeof (res as { token?: unknown }).token === 'string';
}

Try / catch

try {
  await app.signIn({ password, useOpenId });
} catch (e) {
  if (e.message.includes('User token not set')) {
    console.error('Server returned no token — verify sync-server version matches the client and the endpoint is a real Actual server.');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling app.signIn with credentials when the server responds 200 without a token field — e.g. incompatible server version, an OpenID flow that did not finalize, or a proxy returning a non-standard body.

Common situations: Server/client version mismatch after an upgrade; reverse proxy stripping or altering the JSON body; pointing the client at a non-Actual endpoint that returns 200 OK with HTML/other JSON.

Related errors


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