Bootstrap the first admin

When you flip --auth-mode=enabled for the first time, you need an admin user to log in with. This page walks you through the POST /v1/admin/bootstrap flow.

The route is intentionally pre-auth — it has to be reachable when no users exist yet. It’s protected by:

  1. An IsAdminBound probe — refuses if any admin role binding already exists at the default org scope.
  2. The unique email constraint on users.primary_email — a second call with the same email collides.
  3. Operator network policy — you SHOULD restrict this route to a local Unix socket or loopback interface.

Prerequisites

  1. --auth-mode=enabled set in config or on the command line.

    auth:
      mode: enabled
      master_key_file: /etc/nanosync/master.key
  2. Master key generated and readable by the server process.

    openssl rand -hex 32 > /etc/nanosync/master.key
    chmod 0600 /etc/nanosync/master.key
    chown nanosync:nanosync /etc/nanosync/master.key
  3. Database reachable and migrations applied.

    On first start, nanosync auto-runs goose up. Confirm with:

    nanosync status

Two ways to bootstrap

The ns admin bootstrap command POSTs to the local server. Run it from the same host as the daemon:

ns admin bootstrap \
  --email=admin@acme.com \
  --password='S3cur3ly-Pick-A-Long-One' \
  --display-name='Acme Admin'

On success:

✓ admin created: user_id=00000000-0000-0000-0000-…  email=admin@acme.com

You can omit --password to be prompted interactively (recommended — keeps the password out of shell history).

For ops automation or when you can’t run the CLI on the host:

curl -fsSL -X POST http://localhost:8080/v1/admin/bootstrap \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "admin@acme.com",
    "password": "S3cur3ly-Pick-A-Long-One",
    "display_name": "Acme Admin"
  }'

Successful response (201 Created):

{
  "user_id": "00000000-0000-0000-0000-…",
  "email":   "admin@acme.com"
}

Verify the bootstrap

Log in with the new account:

ns auth login --email=admin@acme.com
ns auth whoami

Expected output:

admin@acme.com
  user_id:  00000000-0000-0000-0000-…
  org_id:   00000000-0000-0000-0000-000000000001
  system:   false

Confirm the admin role binding exists:

ns role list --scope=org

You should see the admin built-in role bound to your user at the default org.


Common errors

StatusMessageCauseFix
503auth not configuredServer is running in --auth-mode=noneRestart with --auth-mode=enabled
409bootstrap already completedAn admin user already existsUse existing admin to manage further accounts
400email and password requiredEmpty fields in request bodyProvide both
400invalid request bodyMalformed JSONCheck Content-Type + JSON syntax
500create user failedDB-level error (often a stale connection)Check server logs; verify DB connectivity

Disaster recovery — admin credentials lost

If the only admin loses their password and you have direct DB access, you can recover by inserting a new password hash directly. Do this only when no other admin exists.

-- 1. Generate a new argon2id hash. Use any argon2 CLI / library:
--    argon2 'NewSecurePassword' -id -t 3 -m 16 -p 4 -e
--
-- 2. Update the existing admin user, or insert a new one + bind admin role.

-- Update existing:
UPDATE users
  SET password_hash = '$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>',
      password_algo = 'argon2id',
      status = 'active'
  WHERE primary_email = 'admin@acme.com';

-- Or insert a fresh admin (UUID generated by your client):
INSERT INTO users (id, kind, primary_email, display_name, status, password_hash, password_algo)
  VALUES ('<new-uuid>', 'user', 'recovery@acme.com', 'Recovery Admin', 'active',
          '$argon2id$v=19$m=…', 'argon2id');

INSERT INTO role_bindings (id, subject_type, subject_id, role_id, scope_type, scope_id)
  VALUES ('<new-uuid>', 'user', '<new-uuid>',
          '00000000-0000-0000-0000-0000000000a1',  -- built-in admin role
          'org',
          '00000000-0000-0000-0000-000000000001'); -- default org

Then log in with the recovery account, audit the change, and revoke the old admin if compromised.


Security notes


What’s next