toeverything/AFFiNE · error · ActionForbidden
action_forbidden
action_forbidden
Error message
First user already created
What it means
Thrown by CustomSetupController.createAdmin when ServerService.initialized() returns true, i.e. the self-host instance already has its first admin user. The /api/setup/create-admin-user endpoint is meant to run exactly once; all subsequent calls are forbidden regardless of authentication.
Source
Thrown at packages/backend/server/src/core/selfhost/controller.ts:42
@Controller('/api/setup')
export class CustomSetupController {
constructor(
private readonly config: Config,
private readonly models: Models,
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 26c515e050)
Solutions
- Skip the call once the server reports initialized — query ServerService or GET the setup status endpoint first.
- If a fresh setup is genuinely required, wipe the database/user table so initialized() returns false.
- Guard the provisioning script with an idempotency check that polls initialized() before POSTing.
Example fix
// before
await fetch('/api/setup/create-admin-user', { method: 'POST', body });
// after
const { initialized } = await fetch('/api/setup/status').then(r => r.json());
if (initialized) {
throw new Error('Instance already initialized');
}
await fetch('/api/setup/create-admin-user', { method: 'POST', body }); Defensive patterns
Strategy: validation
Validate before calling
const { initialized } = await fetch('/api/setup/status').then(r => r.json());
if (initialized) {
// route to sign-in instead of setup
return navigate('/sign-in');
} Type guard
// n/a
Try / catch
try {
await createAdmin(input);
} catch (e) {
if (e?.code === 'action_forbidden' && /already created/.test(e.message)) {
// instance already set up — go to login
return navigate('/sign-in');
}
throw e;
} Prevention
- Poll the setup-status endpoint once on first launch before offering the setup form.
- Make provisioning scripts idempotent by gating on initialized().
- Document the one-shot nature of the setup endpoint for operators.
When it happens
Trigger: POST /api/setup/create-admin-user after the first successful setup; an automation script re-running setup; the setup UI being replayed in a browser tab.
Common situations: Operator forgot the instance was already initialized; restoring a DB snapshot into a fresh container and re-running setup; misconfigured init container hitting the endpoint twice.
Related errors
AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12).
Data as JSON: /api/errors/844eb44a7e664b0c.
Report an issue: GitHub.