Thalix VaultDocumentation
Browse documentation
Administrators
publishedimplementedEdit this page

Sign-in and passwords

Implementation status: shipped. Password sign-in is the default for both the web app and the CLI. The operator bootstrap script below exists because self-service account creation is still invite-only; see Invitations for that gap. For definitions of anti-enumeration, audit event, and correlation ID, see Glossary.

How members sign in

Sign-in is email-and-password everywhere:

  • Web: /login presents an email and password form (Thalix Vault is invite-only, so only an email your organization has already admitted can sign in). A Forgot password? control on the same page starts password recovery inline. The old magic-link/one-time-code request UI is gone from the public surface: email links are used only for invitation acceptance and password reset.
  • CLI: tv login prompts for the email and reads the password with echo disabled. Automation uses tv login --user <email> --password-stdin with exactly one line of password on standard input — a password is never accepted as a command-line argument, so it cannot leak into shell history or a process listing. tv login --browser falls back to the older device-approval flow when a terminal password prompt is not usable.

Both surfaces talk to the same identity and return one uniform INVALID_CREDENTIALS error for an unknown email, a wrong password, or an invited-but-passwordless account — deliberately identical so sign-in attempts cannot enumerate accounts. Token storage (tv_ opaque token in the private CLI credential file) and tv logout are unchanged by the switch to passwords.

Password policy

Every password is at least 12 characters (and at most 128), enforced server-side at every set-point — initial set, recovery reset, and any future change — never only in a browser. The web reset form checks the length before submitting, but the server does not rely on that.

Bootstrapping the first password

Until a production recovery flow exists for a brand-new account, an operator seeds the initial password directly against the target Postgres database:

DATABASE_URL=postgres://…/postgres \
  node scripts/ops/set-initial-password.mjs --email person@example.com \
  <<< 'a-strong-password-at-least-12-characters'

The script:

  • requires DATABASE_URL (or SUPABASE_DB_URL) pointing at the target instance — locally, this is the DB_URL that supabase status reports;
  • reads the password from standard input only (never argv, never printed);
  • refuses anything shorter than 12 characters;
  • writes the same bcrypt format Supabase Auth itself stores in auth.users.encrypted_password, via pgcrypto's crypt(..., gen_salt('bf')), handed to SQL through an environment variable rather than spliced into a query string;
  • is idempotent — rerunning for the same email simply overwrites the password again, which also makes it the manual rotation path until self-service recovery is live in production.

The named account must already exist in auth.users; the script sets or resets its password, it does not create identities. Afterwards the member signs in immediately through tv login or the web /login page.

Recovery flow

  1. On /login, the member selects Forgot password? and enters their email.
  2. The server responds identically whether or not the email belongs to an accessible account (anti-enumeration): the page confirms a reset link was sent if the identity can access the Vault.
  3. The emailed link lands on /reset-password, where the member sets a new password (minimum 12 characters, entered twice). Success signs the session in and redirects to the post-login page.

Email links are used only here and for invitation acceptance — routine sign-in never involves an inbox round-trip.

Audit events

Password activity lands in the two existing audit domains, retrievable through tv admin audit list (see Audit and administration):

EventDomainMeaning
identity.loginIdentitySuccessful password sign-in (web session); metadata records "method": "password".
identity.login_failedIdentityFailed attempt — recorded even for unknown emails, with no subject or organization attached.
password.tokenCLI authCLI token issuance via password; results are token_issued, ineligible, access_denied, collision, or invalid_request.

An issued-token event's details are reduced to the token prefix alone ({"tokenPrefix":"tv_…"}) — no token material, hash, or password ever reaches the audit record.

Rate limits

Login and recovery attempts are rate-limited per email and globally, each with its own budget: per-email counters use a 15-minute window and global counters a 1-hour window, tracked separately for login and recovery. An exhausted budget returns the stable RATE_LIMITED error code (with a correlation ID) instead of processing the attempt; legitimate users behind a burst should wait and retry. See Automation and JSON output for the full error-code table.