Identify accounts by a unique username, with the primary email as an alternative sign-in #1

Closed
opened 2026-09-05 11:47:10 +00:00 by tiagoagueda · 0 comments
Owner

Observation

every user is identified by a unique username (obligatory, and used to login) […] and by a primary e-mail address (that is obligatory, unique across all platform and can be used to login alternatively to username)

What happens today

Accounts have no username at all. The custom user model removes the column and identifies people by email:

  • src/postulo/accounts/models.py — username = None, USERNAME_FIELD = "email", REQUIRED_FIELDS = [].
  • src/postulo/config/settings/base.py — ACCOUNT_LOGIN_METHODS = {"email"}, ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"], ACCOUNT_USER_MODEL_USERNAME_FIELD = None.

The email half of the requirement already holds: email is unique=True on the model and ACCOUNT_UNIQUE_EMAIL = True, so a primary address is obligatory and unique across the instance.

Everything downstream assumes email-only identity: createsuperuser asks for email alone; User.display_name falls back to the email local part; the invite adapter (accounts/adapter.py) binds invitations to an email; the export (core/export.py, account section) writes email, first_name, last_name; the capture API's /api/v1/me (api/api.py) reports the owner by email; seed_demo keys on email.

What changes

  • Restore a username column on User: obligatory, unique, validated (allowed characters, minimum length, reserved names), with a decision on case-sensitivity.
  • allauth: ACCOUNT_USER_MODEL_USERNAME_FIELD = "username", ACCOUNT_LOGIN_METHODS = {"username", "email"}, add username* to ACCOUNT_SIGNUP_FIELDS. Sign-in form then accepts either.
  • Touch every place that assumes email identity: signup and invite acceptance, the profile page, the admin, display_name (precedence to decide: full name › username › email), createsuperuser (REQUIRED_FIELDS), the export/import format (bump FORMAT_VERSION and carry username), /api/v1/me, seed_demo, and the wiki pages Accounts and invitations and Getting started, which state that accounts are identified by email.

Why this is a breaking change

  • A required, unique column added to a model that already has rows: every existing account needs a username, so the migration must generate one (the email local part, de-duplicated, is the obvious candidate) and operators need telling that it happened.
  • The sign-in form and createsuperuser prompts change.
  • The export format changes shape.

Open questions

  1. Username rules: length, allowed characters, case-insensitive uniqueness, reserved names (admin, postulo, …), and whether it can be changed later.
  2. Does USERNAME_FIELD move to username, or stay email with username as an alternative login only? Moving it changes get_username() everywhere (tests assert it equals the email today) and what createsuperuser asks first.
  3. Where the username is displayed, given that most instances are single-user.

Related: the full-name requirement and mandatory email verification are filed separately so each can land on its own.

## Observation > every user is identified by a unique username (obligatory, and used to login) […] and by a primary e-mail address (that is obligatory, unique across all platform and can be used to login alternatively to username) ## What happens today Accounts have **no username at all**. The custom user model removes the column and identifies people by email: - `src/postulo/accounts/models.py` — `username = None`, `USERNAME_FIELD = "email"`, `REQUIRED_FIELDS = []`. - `src/postulo/config/settings/base.py` — `ACCOUNT_LOGIN_METHODS = {"email"}`, `ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"]`, `ACCOUNT_USER_MODEL_USERNAME_FIELD = None`. The email half of the requirement already holds: `email` is `unique=True` on the model and `ACCOUNT_UNIQUE_EMAIL = True`, so a primary address is obligatory and unique across the instance. Everything downstream assumes email-only identity: `createsuperuser` asks for email alone; `User.display_name` falls back to the email local part; the invite adapter (`accounts/adapter.py`) binds invitations to an email; the export (`core/export.py`, `account` section) writes `email`, `first_name`, `last_name`; the capture API's `/api/v1/me` (`api/api.py`) reports the owner by email; `seed_demo` keys on email. ## What changes - Restore a `username` column on `User`: obligatory, unique, validated (allowed characters, minimum length, reserved names), with a decision on case-sensitivity. - allauth: `ACCOUNT_USER_MODEL_USERNAME_FIELD = "username"`, `ACCOUNT_LOGIN_METHODS = {"username", "email"}`, add `username*` to `ACCOUNT_SIGNUP_FIELDS`. Sign-in form then accepts either. - Touch every place that assumes email identity: signup and invite acceptance, the profile page, the admin, `display_name` (precedence to decide: full name › username › email), `createsuperuser` (`REQUIRED_FIELDS`), the export/import format (bump `FORMAT_VERSION` and carry `username`), `/api/v1/me`, `seed_demo`, and the wiki pages *Accounts and invitations* and *Getting started*, which state that accounts are identified by email. ## Why this is a breaking change - A required, unique column added to a model that already has rows: every existing account needs a username, so the migration must generate one (the email local part, de-duplicated, is the obvious candidate) and operators need telling that it happened. - The sign-in form and `createsuperuser` prompts change. - The export format changes shape. ## Open questions 1. Username rules: length, allowed characters, case-insensitive uniqueness, reserved names (`admin`, `postulo`, …), and whether it can be changed later. 2. Does `USERNAME_FIELD` move to `username`, or stay `email` with username as an alternative login only? Moving it changes `get_username()` everywhere (tests assert it equals the email today) and what `createsuperuser` asks first. 3. Where the username is displayed, given that most instances are single-user. Related: the full-name requirement and mandatory email verification are filed separately so each can land on its own.
tiagoagueda added this to the 0.2.0 milestone 2026-09-05 11:47:10 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Postulo/postulo#1
No description provided.