toeverything/AFFiNE · error · ActionForbidden

action_forbidden

action_forbidden

Error message

First user already created

What it means

ActionForbidden('First user already created') from the self-hosted POST /create-admin-user handler (packages/backend/server/src/core/selfhost/controller.ts:42). The endpoint bootstraps the very first admin; once server.initialized() reports an existing first user, every further call is refused.

Solutions

  1. Treat it as done: sign in with the admin you already created.
  2. To grant admin later, promote an existing user via the administrator feature or the server CLI.
  3. Verify you are connected to the intended instance/database before re-running setup.

Example fix

// before
await fetch('/api/selfhost/create-admin-user', { method: 'POST', body }); // second call -> action_forbidden

// after
const status = await api.getSetupStatus(); // mirrors server.initialized()
if (status.initialized) router.replace('/signin');
else await fetch('/api/selfhost/create-admin-user', { method: 'POST', body });
Defensive patterns

Strategy: try-catch

Validate before calling

const status = await api.getSetupStatus(); // mirrors server.initialized()
if (status.initialized) router.replace('/signin');

Type guard

const isActionForbidden = (e: unknown): e is ActionForbidden =>
  e instanceof ActionForbidden;

Try / catch

try {
  await api.createAdmin(input);
} catch (e) {
  if (e instanceof ActionForbidden) return router.replace('/signin'); // setup already done
  throw e;
}

Prevention

When it happens

Trigger: Calling /create-admin-user a second time, or hitting it on an instance (or shared database) whose initial setup already completed.

Common situations: Setup wizard re-run after a refresh, a redirect loop back to /create-admin-user, or pointing at the wrong database that is already initialized.

Related errors


AI-assisted analysis of toeverything/AFFiNE@2af30773ae (2026-08-18). Data as JSON: /api/errors/844eb44a7e664b0c. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/core/selfhost/controller.ts:43

export class CustomSetupController {
  constructor(
    private readonly config: Config,
    private readonly models: Models,
    private readonly auth: AuthService,
    private readonly sessionIssuer: SessionIssuer,
    private readonly mutex: Mutex,
    private readonly server: ServerService
  ) {}

  @Public()
  @Post('/create-admin-user')
  async createAdmin(
    @Req() req: Request,
    @Res() res: Response,
    @Body() input: CreateUserInput
  ) {
    if (await this.server.initialized()) {
      throw new ActionForbidden('First user already created');
    }

    validators.assertValidEmail(input.email);

    if (!input.password) {
      throw new PasswordRequired();
    }

    validators.assertValidPassword(
      input.password,
      this.config.auth.passwordRequirements
    );

    await using lock = await this.mutex.acquire('createFirstAdmin');

    if (!lock) {
      throw new InternalServerError();
    }

View on GitHub (pinned to 2af30773ae)