The entry journeys
Table of Contents
Signing in and signing up are two conversations with one door, and almost all of the work behind them is the server's. This note traces both: which layer owns what, the order in which the server refuses a sign-in, the branch a registration takes, the code each refusal carries, and where every piece of state lives. It states the design the implementation is built to; the policy those interactions carry out is in registration and onboarding defaults, and the survey both answer to is analysing the sign-up process.
1. Detail
1.1. The layers, and what each one owns
| Layer | What it is | What it owns |
|---|---|---|
ores.web |
The browser application | The screens. It holds an opaque cookie and never a token, so it cannot decide by itself whether it is signed in: it asks. |
| The BFF | The browser's only HTTP surface, and one NATS connection per session | The token, the party the session is scoped to, and the translation of a cookie into an authenticated NATS call. It forwards the address the request arrived at, so the service can resolve the tenant. |
| NATS subjects | iam.v1.auth.*, iam.v1.ops.select_party, and the entity writes the defaults use |
The contract. A step reaches the server through one subject, and the browser names none of them directly: the BFF has one route per step. |
| The IAM auth handler | iam.v1.ops.login, .signup, .logout, .refresh and the policy read |
The door: resolving the tenant, reading the flags and the defaults, taking the branch, issuing the token, and stating the refusal. |
| The account service | The credential check and the security state | The password comparison, the failed-attempt counter, the lock at five, the last sign-in, and the session row. |
| The authorization service | Roles and assignments | Granting the nominated role at registration, and the role assignments the access journeys read. |
| PostgreSQL, with RLS | Tenant and party isolation, and the settings | The account, login record, session and account-party rows; the settings; and the flags the defaults live on. |
1.2. Where the state lives
| State | Home | Survives a reload |
|---|---|---|
| Whether the browser is signed in | An opaque cookie, held by the browser | Yes, until it expires |
The token, the tenant, the party, the build, passwordResetRequired |
The BFF's session map, keyed by a hash of the cookie | Yes, while the BFF runs; a BFF restart ends every session |
| The session itself: account, tenant, start and end time, client address | The IAM session row | Yes |
| Failed attempts and the lock | The login record | Yes |
| The switch, the default tenant, the default party, the default role | The settings and the flags on the tenant, party and role rows | Yes |
| A provisioning run, and any journey that waits on a person | A workflow instance in ores.workflow |
Yes; see Journey execution |
The browser holds no truth about the session. That is why a screen that needs to know asks the BFF, and why the door can be re-entered by a person who has not signed in at all.
1.3. The door asks the policy before it asks for anything
The one unauthenticated read the door makes. It answers the switch, the destination and whether a registration can be used at once, so the screen knows whether to offer a form before anybody types.
Figure 1: What the deployment offers the door: the policy read that answers the switch, the destination, the defaults and whether a registration can be used at once.
Source: entry_registration_policy_sequence.puml. Regenerate with
plantuml -tpng doc/knowledge/journeys/entry/entry_registration_policy_sequence.puml.
The read is separate from iam.v1.ops.bootstrap_status. The bootstrap read answers
whether the deployment has been set up; this one answers what it offers a person
who is not in it yet, and they change for different reasons.
1.4. Sign in
Figure 2: Sign in: the credentials form, the tenant resolved from the address, the credential check, the branch on the account's state and its parties, the session, and the party picker.
Source: entry_sign_in_sequence.puml. Regenerate with
plantuml -tpng doc/knowledge/journeys/entry/entry_sign_in_sequence.puml.
The order the server refuses in is the screen's order, and it is deliberate. A locked account is tested before the password, so a correct password does not open it and the person is told what is actually wrong. A wrong password is refused after the counter is incremented, and the counter locks the account at five; the reply does not say how many attempts remain, so the screen cannot leak the counter to somebody guessing. A pending account is refused next: that branch is new, and it is the state a registration leaves when its tenant nominates no default party. An account with no association at all is refused after that, and the refusal asks the person to contact an administrator.
One party is bound without asking, and the token carries it together with the visible party set that party-level RLS reads. Several parties return a short-lived token that can do nothing but choose one, and the reply names each party and marks the stored default.
Nothing here holds server state beyond the session row and the login record, so the journey keeps no state of its own. A person who closes the page and comes back starts a new session rather than resuming the old one.
1.5. Sign up
Figure 3: Sign up: the gate read before anything is created, the tenant resolved from the address or the flagged default, the validation, the role, and the two endings, an active account with a party or a pending one without.
Source: entry_sign_up_sequence.puml. Regenerate with
plantuml -tpng doc/knowledge/journeys/entry/entry_sign_up_sequence.puml.
Registration is the one path where an unauthenticated caller creates a row, so it reads its own gate first and refuses before it writes anything. Three properties are worth stating plainly, because each is the opposite of what the code does today:
- The gate is read before anything is created. The switch is the deployment's,
read at system scope, and it is seeded
false; a deployment nobody has asked refuses registration. Today the live path reads neither flag, so the switch is advisory and the door is open. - The tenant is resolved, never assumed. The address names it; failing that the
flagged default tenant does; failing both, the registration is refused with
no_registration_destinationrather than landing in the system tenant by omission. - The account's state is written, not inferred. A tenant that nominates a party
gets an active account with the association. One that does not gets a
pendingaccount, which the roster can find and the door refuses withaccount_pending.
The person is never signed in by the registration. A pending account cannot open a session, and an active one should arrive at the door deliberately.
1.6. The refusal codes
A refusal is a value the screen branches on, not a sentence it matches. Both
Entry responses carry error_code, drawn from the existing error-code
vocabulary where it already fits.
| Subject | Code | What happened |
|---|---|---|
login_response |
invalid_credentials |
No such account, a wrong password, or a tenant that cannot be resolved. The one sentence is deliberate: it says nothing about which account exists. |
login_response |
account_locked |
Five failed attempts have locked the account. Only an administrator clears it. |
login_response |
account_pending |
The account exists and an administrator has not finished setting it up. |
login_response |
no_party_assignment |
The account works in no party. |
login_response |
tenant_inactive |
The account's tenant is suspended or terminated. |
login_response |
bootstrap_mode |
The deployment has no account to sign in with yet. |
signup_response |
signup_disabled |
The deployment does not accept registrations. |
signup_response |
signup_requires_authorization |
The deployment asks for an approval nothing performs yet, so the door closes rather than pretending. |
signup_response |
no_registration_destination |
No tenant resolves for this address and none is flagged as the default. |
signup_response |
no_default_role |
The tenant nominates no role, so the account would hold nothing. |
signup_response |
username_taken, email_taken |
The name or the address is already in use. |
signup_response |
weak_password |
The password does not meet the policy the server enforces. |
signup_response |
invalid_request |
A field is missing or malformed. |
The message stays for a human and may change; the code is the contract. A screen that branches on the message breaks the first time a sentence is improved.
1.7. What this note commits the implementation to
The change table is registration and onboarding defaults. Read against this page, the parts that touch the interaction are these, and every one of them has landed:
iam.v1.ops.registration_policyis a new read, and the door asks it first.iam.v1.ops.signupgains the flag reads, the tenant resolution, the status write and the codes, and stops creating an account in the system tenant.iam.v1.ops.logingainserror_code, the pending refusal, and the tenant resolved from the address rather than typed into the username.- The BFF gains two routes,
GET /api/registration-policyandPOST /api/signup, the second under the same limiter family as the first, and forwards the hostname on both Entry paths. - The wire protocol exports the sign-up and policy schemas and carries the two calls, so the browser reaches them.
- Every Entry dialog carries the same splash header, and the registration door
is drawn on
JourneyPagerather than on a rail of its own. The sign-in dialog carries the banner above its work and no rail, because it has one step rather than a sequence.
2. See also
- Identity and Access — the structure note that orders this cluster, and where to read this page in it.
- Entry journeys — the group this page belongs to.
- Sign in and Sign up — the journeys themselves.
- registration and onboarding defaults — the policy these interactions carry out.
- Analysing the sign-up process — the survey, and the two defects these flows correct.
- Multi-Tenancy Architecture — how a pre-authentication request resolves its tenant.
- Multi-Party Login Flow — the party branch, and the visible party set the token carries.
- Journey execution — where a journey that does wait on somebody keeps its state.
- ores.iam.login_messages and ores.iam.signup_messages — the subjects whose replies gain a code.
- PlantUML sequence diagrams — the notation the three diagrams use.