Contextator
ENTR

Administration & security

Accounts and Permissions

How sign-in, roles, project membership, passwords and sessions work in the dashboard — and what to do when nobody can sign in.

Updated:

The dashboard and the admin API are behind a personal account. Everyone who uses this instance signs in as themselves, and what they can do follows from their role.


First run: the setup code

A brand-new instance has no accounts, so the first thing it asks for is a setup code. Its only job is to make sure the operator, and not whoever reaches the server first, claims the first account.

Start the server and watch the log:

docker compose logs -f

While no account exists, every start prints a box:

┌─ Contextator first-run setup ───────────────────────────────────────────┐
│ No user accounts exist yet; the dashboard is waiting for its first one. │
│                                                                         │
│   Open   http://localhost:3444/setup                                    │
│   Code   KRTW-9MHD-2PQF                                                 │
│                                                                         │
│ A new code is printed on every start until that first account exists.   │
└─────────────────────────────────────────────────────────────────────────┘

Open http://localhost:3444/setup — the dashboard redirects you there anyway — and fill in the form:

Field What to enter
Setup code The code from the box. Case, dashes and spacing are ignored, so type it however it reads
Username Lowercase letters, digits, ., - or _, 2–63 characters, starting with a letter or digit
Display name Optional; what the top bar shows instead of the username
Password At least 12 characters. Nothing else is required of it

Create the root account makes the account, signs you in, and closes setup. From then on /setup redirects to /login and the code means nothing — there is nothing here to rotate later.

Choosing the code yourself

Set it in .env before the first start and you never have to go looking for it:

SETUP_CODE=ekip-kurulum-2026

A code you chose is never echoed into the log; the box says to use the one from your .env. Clear the variable and restart to have a fresh one generated and printed instead.

Signing in

/login asks for a username and a password, and nothing else. A few things it does on purpose:

  • A wrong password and an unknown username give the same answer — Wrong username or password — after the same amount of work, so the form cannot be used to find out who has an account here.
  • A disabled account is told so (This account is disabled) once the password is right.
  • Too many attempts are slowed down twice over. Per IP address, AUTH_LOGIN_MAX_ATTEMPTS failures inside AUTH_LOGIN_WINDOW_MIN minutes get a 429 with a Retry-After. Per account, the same threshold starts a lockout that doubles each further attempt, capped at an hour.
  • A temporary password sends you straight to /change-password and nothing else works until you replace it.

Signing in sets one cookie, contextator_session. It is HttpOnly, SameSite=Lax, scoped to /, and Secure whenever the instance is served over HTTPS.

Roles

There are three instance roles, and on top of them a per-project role for members:

root admin editor viewer
See the project list all all its own its own
Create / delete a project ✓ ✓ – –
Re-index (incremental or full) ✓ ✓ ✓ –
See a project’s sources ✓ ✓ ✓ ✓
Add, edit, delete, sync or test a source ✓ ✓ ✓ –
Upload and delete files ✓ ✓ ✓ –
See and regenerate a webhook secret ✓ ✓ ✓ –
See a project’s MCP tokens ✓ ✓ ✓ ✓
Mint and revoke an MCP token ✓ ✓ ✓ –
Change MCP access — require a token or an account, or make the endpoint open again ✓ ✓ – –
See a project’s members ✓ ✓ ✓ ✓
Add, change or remove a member ✓ ✓ – –
Manage accounts ✓ ✓ (not root ones) – –

editor and viewer are memberships, not roles: the third instance role is member, and a member account gets viewer or editor on each project it is added to. root and admin reach every project without being listed on any of them.

Three rules hold no matter who asks:

  • The last active root account cannot be deleted, demoted or disabled. An instance can never lock itself out of its own user management.
  • An admin cannot touch a root account and cannot hand out the root role. Only another root can.
  • Nobody can disable, demote or delete themselves.

A project you cannot see does not exist

A member sees only the projects it is listed on — in the project list, and in every URL. Asking for one it has no access to answers 404, the same as a project that was never created, so project ids cannot be probed by trying them.

The buttons are not the rule

The dashboard hides what you may not do, but every rule is enforced again on the server, from one table that covers routes by their method and shape rather than one by one. A button brought back with the browser’s developer tools still answers 403.

The Users page

Open the account menu in the top right and choose Users (#/~users). It is there for root and admin accounts only.

One row per account: its username and display name, its role, whether it is active, disabled or has a password change pending, how many projects it reaches, and when it last signed in.

Button What it does
New user Creates an account — see below
Edit Display name, e-mail, role, and the active switch
Reset password Hands out a new temporary password and signs that account out everywhere
Disable / Enable A disabled account cannot sign in, and its open sessions end at once
Delete Removes the account, its sessions and its memberships. Click twice to confirm

A button you are not allowed to press is greyed out and says why when you hover it: the last active root account cannot be removed, demoted or disabled, you cannot change your own role or disable yourself, only a root account can change another root account.

Creating an account

New user asks for:

Field Notes
Username Lowercase, 2–63 characters. It cannot be changed afterwards
Display name, E-mail Optional
Role member or admin. root appears only if you are root yourself
Password Leave it empty and the server generates one; Generate fills one in for you
Must change password at first sign-in On by default

The password is shown once, in a dialog with a copy button, and is never recoverable afterwards. Send it to the person however you send such things; they replace it the first time they sign in.

For a member, creating the account is only half the job — it reaches nothing until you add it to a project.

Members of one project

Select a project and scroll to Members. This panel is about member accounts: root and admin are not listed there because they already reach every project.

  • Add member offers the active member accounts that are not on this project yet. If the list is empty, create one on the Users page first.
  • The role dropdown on each row switches between viewer and editor:
    • viewer — reads the project: its sources, its documents, its index history, its MCP tokens.
    • editor — everything a viewer does, plus adding, editing, syncing and deleting sources, uploading and deleting files, re-indexing, and minting or revoking MCP tokens.
  • Remove takes the account off the project. It stops seeing the project at all.

Only root and admin can add, change or remove a member; anyone else sees the list read-only.

Passwords

PASSWORD_MIN_LENGTH (12 by default) is the only rule, along with two obvious refusals: a password cannot be the username, and a new one cannot be the old one. No composition rules, no blocklist, 128 characters at most.

Passwords are stored as salted scrypt hashes. They are never written to the log, never returned by the API, and cannot be turned back into the password — not by you, not by anyone with a copy of the database. That is why a forgotten password is replaced rather than looked up.

Changing your own

Account menu → Change password. It asks for the current one, then the new one twice. Saving it signs out every other session of your account and keeps the one you are using.

Temporary passwords

An account created with a temporary password, or reset with one, carries must change password until it is replaced. While that stands, the person can sign in and change the password and nothing else — every other page and endpoint answers 403 password_change_required (Choose a new password before using the dashboard).

Sessions

A sign-in is a row in the database, not just a cookie, which is what makes signing someone out actually take effect. Only a hash of the session token is stored.

Default Setting
Idle before you must sign in again 12 hours AUTH_SESSION_IDLE_MS
Longest a session can live, however actively used 30 days AUTH_SESSION_TTL_DAYS

Sessions end early when:

What happened Which sessions end
You changed your own password Every other session of your account
An admin reset an account’s password All of that account’s
An account was disabled or deleted All of that account’s
An account was demoted to a lower role All of that account’s (a promotion does not)
Sign out in the account menu The one you are using

A role change applies immediately in any case — the role is read from the database on every request, not frozen into the session.

The dashboard has no page listing your own open sessions; the API does, at GET /api/auth/sessions, along with DELETE /api/auth/sessions?scope=others|all to end them. See Admin API.

ADMIN_TOKEN: access for scripts

ADMIN_TOKEN is still there and still works, but it is not the dashboard’s lock any more. It is machine access to /api/* — Authorization: Bearer <token> — acting with root permissions, for scripts, CI and cron jobs that have no person behind them.

curl -s http://localhost:3444/api/projects -H "Authorization: Bearer $ADMIN_TOKEN" | jq
  • Do not paste it into a browser. The dashboard does not accept it and has nowhere to put it; people sign in with an account. Treat the token like a root password.
  • It has no account behind it, so the endpoints that are about your account — changing a password, listing your own sessions — refuse it with 403 token_has_no_account.
  • It is exempt from the same-site check that guards cookie requests, because a bearer token is not something another site can make your browser send.

Leave it unset if nothing scripted needs the API.

When nobody can sign in

In order of how much you have to reach for:

  1. Another root or admin account. Users page → Reset password on the locked-out account.

  2. ADMIN_TOKEN. Set it in .env, docker compose up -d, and hand out a new password over the API — this is the escape hatch that needs no account at all:

    export API=http://localhost:3444/api
    ID=$(curl -s $API/users -H "Authorization: Bearer $ADMIN_TOKEN" | jq -r '.[] | select(.username=="alice") | .id')
    curl -s -X POST $API/users/$ID/password -H "Authorization: Bearer $ADMIN_TOKEN" \
      -H 'content-type: application/json' -d '{}' | jq -r .temporaryPassword

    The same token can create a fresh root account with POST /api/users if every account is gone. See Admin API.

  3. The reset script, on an installation run from source:

    npm run reset-password -- alice

    It takes the username as its only argument and asks nothing else. It talks to the database directly, so it needs neither a session nor ADMIN_TOKEN — only the .env that points at the database. It prints the account and a new password once, marks it as needing a change, re-enables the account if it was disabled, clears any lockout, and revokes every session it had. Given a name that does not exist, it lists the accounts that do.

      Account:   alice (admin)
      Password:  hT4mQpbWnK9rXdzLvCuA
    
      Shown once. Sign in with it; the dashboard will ask for a new one straight away.

    The script is not part of the Docker image — it needs the repository and its development dependencies — so on a container install use ADMIN_TOKEN above.

If an instance has no accounts at all, it goes back to asking for a setup code: restart it and open /setup.

What accounts do not cover

Accounts govern the dashboard and the admin API, and they reach /mcp/* too — but only for a caller that names one. An anonymous request to an open project, and a ctxm_… token on a closed one, name nobody: neither knows anything about who you are here, so a viewer membership grants nothing there and a token grants everything in its project to whoever holds it. A credential that does name an account is a different thing, and it is checked against that account’s membership on every request in every mode — being a member is what admits it, not the mode. account required is the mode that leaves no other way in. ADMIN_TOKEN is not an account and reaches none of it.

Both doors, side by side: Security.

Arrow keys to move, Enter to open.