Skip to content

User Provisioning & Session Acquisition

This page documents the complete flow from zero to an authenticated session with org access. It covers the operator-only provisioning steps that must happen before a user can log in and use the Minimum Viable Flow.

When to read this page

Read this if you are setting up users for the first time, onboarding a new tenant, or need to understand the security gates between user creation and first login.

Overview

Operator (IAM)                          User (API Gateway)
──────────────                          ──────────────────
1. Create user (uas_usercreate)
2. Issue email token (uas_emailissuetoken)
3. Confirm email token (uas_emailconfirmtoken)
4. Verify user status (uas_userstatusset)
5. Mint invitation (ofm_invitationcreate)
                                        6. Login (POST /usm/session/create)
                                        7. Create org (POST /ofm/org/create)
8. Verify org (ofm_orgstatusset)
                                        9. Resolve member (POST /ofm/member/resolve)
                                        → Continue to Minimum Viable Flow

Steps 1-5 and 8 are operator-only (direct Lambda invokes, IAM-gated). Steps 6-7 and 9 are user-facing (API Gateway, session-authenticated).


0) Set placeholders

sh
export AWS_PROFILE="g3nretailstack"
export AWS_REGION="us-east-1"
export API_BASE="https://api.g3nretailstack.com"
export EMAIL="user@example.com"
export PASSCODE='Abcd!234'

1) Create user (operator-only)

Direct Lambda invoke via uas_usercreate. Creates a user with unverified status.

sh
aws lambda invoke \
  --function-name uas_usercreate \
  --cli-binary-format raw-in-base64-out \
  --payload file:///dev/stdin \
  --profile "$AWS_PROFILE" \
  /dev/stdout <<'PAYLOAD'
{
  "email": "user@example.com",
  "passcode": "Abcd!234",
  "caption": "Jane Doe",
  "reason": "onboarding",
  "actor": "admin"
}
PAYLOAD

Required fields:

FieldTypeDescription
emailstringNormalized (trim + lowercase). Must match /.+@.+\..+/. Globally unique.
passcodestringMinimum 8 characters. Must contain uppercase, lowercase, digit, and special character.

Optional: caption (display name, defaults to "user"), reason, actor, orgcode, cccode.

Response:

json
{
  "success": true,
  "data": { "user_id": "U...", "account_ref": "..." },
  "revision": "REV_GUID"
}

Save user_id and revision for the next steps.

What happens:

  • User record created with status unverified.
  • Primary email record created with status unverified.
  • Passcode hashed with Argon2id and stored immediately.
  • Email uniqueness enforced (409 if email already exists).

Common errors:

HTTPCodeCause
400passcode-policy-failedPasscode does not meet complexity requirements
409duplicate-emailEmail already registered

2) Issue email verification token (operator-only)

Direct Lambda invoke via uas_emailissuetoken. Generates a 32-byte random token (base64url-encoded) with 48-hour TTL.

sh
aws lambda invoke \
  --function-name uas_emailissuetoken \
  --cli-binary-format raw-in-base64-out \
  --payload file:///dev/stdin \
  --profile "$AWS_PROFILE" \
  /dev/stdout <<'PAYLOAD'
{
  "user_id": "USER_ID",
  "email": "user@example.com",
  "expected_revision": "REV_FROM_STEP_1",
  "reason": "email-verification",
  "actor": "admin"
}
PAYLOAD

Required fields:

FieldTypeDescription
user_idstringFrom step 1
emailstringThe email to verify
expected_revisionstringCurrent revision from previous response

Response:

json
{
  "success": true,
  "data": { "token": "BASE64_TOKEN" },
  "revision": "NEW_REV"
}

Save token and the new revision.

Token is returned once

The raw token is returned only in this response. Only the SHA-256 hash is stored. If you lose it, issue a new token (last-issued wins).


3) Confirm email token (operator-only)

Direct Lambda invoke via uas_emailconfirmtoken. Verifies the token and transitions the email to verified.

sh
aws lambda invoke \
  --function-name uas_emailconfirmtoken \
  --cli-binary-format raw-in-base64-out \
  --payload file:///dev/stdin \
  --profile "$AWS_PROFILE" \
  /dev/stdout <<'PAYLOAD'
{
  "user_id": "USER_ID",
  "token": "BASE64_TOKEN_FROM_STEP_2",
  "expected_revision": "REV_FROM_STEP_2",
  "reason": "email-verification",
  "actor": "admin"
}
PAYLOAD

Required fields:

FieldTypeDescription
user_idstringFrom step 1
tokenstringRaw token from step 2
expected_revisionstringRevision from step 2

Response:

json
{
  "success": true,
  "data": { "email": "user@example.com", "status": "verified" },
  "revision": "NEW_REV"
}

Save the new revision.

Common errors:

HTTPCodeCause
400token-expiredToken older than 48 hours
400token-mismatchToken does not match stored hash

4) Verify user status (operator-only)

Direct Lambda invoke via uas_userstatusset. Transitions user from unverified to verified.

sh
aws lambda invoke \
  --function-name uas_userstatusset \
  --cli-binary-format raw-in-base64-out \
  --payload file:///dev/stdin \
  --profile "$AWS_PROFILE" \
  /dev/stdout <<'PAYLOAD'
{
  "user_id": "USER_ID",
  "status": "verified",
  "expected_revision": "REV_FROM_STEP_3",
  "reason": "onboarding",
  "actor": "admin"
}
PAYLOAD

Preconditions (both must be true):

  1. Primary email is verified (done in step 3).
  2. Passcode is set (done in step 1).

Response:

json
{
  "success": true,
  "data": { "status": "verified" },
  "revision": "NEW_REV"
}

User status FSM:

unverified  -> verified, doomed
verified    -> suspended, doomed
suspended   -> verified, doomed
doomed      -> (terminal)

Common errors:

HTTPCodeCause
400email-not-verifiedPrimary email not yet verified
400passcode-missingPasscode not yet set
400user-invalid-transitionFSM transition not allowed

User is now ready to log in

After this step, the user can create sessions via the public API. If they also need org access, continue to step 5.


5) Mint invitation (operator-only)

Direct Lambda invoke via ofm_invitationcreate. Creates an org-creation invitation code.

sh
aws lambda invoke \
  --function-name ofm_invitationcreate \
  --cli-binary-format raw-in-base64-out \
  --payload file:///dev/stdin \
  --profile "$AWS_PROFILE" \
  /dev/stdout <<'PAYLOAD'
{
  "caption": "Onboarding ACME Corp",
  "reason": "tenant-onboarding",
  "actor": "admin"
}
PAYLOAD

Optional fields: caption, expires_at_utc (ISO-8601; defaults to 30 days), referral_code, schedule, reason, actor.

Response:

json
{
  "success": true,
  "data": {
    "invitation_guid": "INV_GUID",
    "code": "ABC-DEF-1234",
    "status": "pending",
    "expires_at_utc": "2026-03-18T..."
  }
}

Give the invitation code to the user.


6) Login (user-facing)

The user creates a session via the public API.

sh
curl -sS -X POST "$API_BASE/usm/session/create" \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "user@example.com",
    "passcode": "Abcd!234",
    "caption": "web-session",
    "session_label": "browser"
  }'

Required fields:

FieldTypeDescription
emailstringRegistered email
passcodestringUser's passcode

Optional: caption, session_label, ttl_seconds (1-86400, default 3600), ttl_refresh_enabled (default true).

Response:

json
{
  "success": true,
  "data": {
    "session_guid": "SESSION_GUID",
    "user_id": "USER_ID",
    "status": "active",
    "expires_at_utc": "2026-02-16T12:00:00.000Z",
    "ttl_seconds": 3600,
    "ttl_refresh_enabled": true
  }
}

Save session_guid — this is the bearer token for all subsequent API calls.

Auth placement

USM uses body auth — credentials go in the JSON body. All downstream org-scoped services use header auth (x-session-guid). See Headers & Identity.

Validation performed by USM:

  1. Email lookup via UAS table (GSI).
  2. User status must be verified.
  3. Email status must be verified.
  4. Passcode verified with Argon2.
  5. Active session count checked against per-user cap (default 1024).

Common errors:

HTTPCodeCause
401invalid-passcodeWrong credentials or unknown email (intentionally ambiguous)
403user-not-verifiedUser status is not verified
403email-not-verifiedEmail status is not verified
429too-many-sessionsSession cap reached; close existing sessions first

Session lifecycle:

  • Default TTL: 1 hour (configurable via ttl_seconds).
  • Sliding TTL: each POST /usm/session/validate call extends expiry if ttl_refresh_enabled is true.
  • Close: POST /usm/session/close with session_guid in body.
  • Doom: sessions are doomed (terminal) on close, expiry, or user status change.

7) Create org (user-facing)

The user redeems the invitation code to create an organization.

sh
curl -sS -X POST "$API_BASE/ofm/org/create" \
  -H 'Content-Type: application/json' \
  -H "x-session-guid: $SESSION_GUID" \
  -d '{
    "orgcode": "ACME",
    "invitation_code": "ABC-DEF-1234",
    "user_guid": "USER_ID",
    "caption": "Acme Corp"
  }'

Required fields:

FieldTypeDescription
orgcodestringGlobal unique, immutable. Format: ^[A-Z][A-Z0-9_-]{0,9}$
invitation_codestringFrom step 5. Must be pending and not expired.
user_guidstringThe creating user's ID

Optional: caption, timezone (IANA), fiscal_calendar.

Response:

json
{
  "success": true,
  "data": {
    "org_guid": "ORG_GUID",
    "orgcode": "ACME",
    "status": "unverified",
    "owners": { "create_owner_user_guid": "...", "primary_owner_user_guid": "..." },
    "cost_centre": { "cc_guid": "...", "cccode": "XXXX-XXXX-XXXX" }
  },
  "revision": "ORG_REV"
}

Save org_guid and revision.

What happens atomically:

  1. Invitation marked accepted with used_by details.
  2. Org created in unverified status.
  3. Master cost centre auto-allocated.
  4. Creator registered as create_owner + primary_owner.
  5. Creator registered as active member.

Org is unverified

The org starts in unverified status. All org-scoped writes are blocked until an operator verifies it (step 8). Reads work, but writes return 403 org-write-blocked.


8) Verify org (operator-only)

Direct Lambda invoke via ofm_orgstatusset. Transitions org from unverified to verified.

sh
aws lambda invoke \
  --function-name ofm_orgstatusset \
  --cli-binary-format raw-in-base64-out \
  --payload file:///dev/stdin \
  --profile "$AWS_PROFILE" \
  /dev/stdout <<'PAYLOAD'
{
  "org_guid": "ORG_GUID",
  "status": "verified",
  "expected_revision": "ORG_REV_FROM_STEP_7",
  "reason": "onboarding",
  "actor": "admin"
}
PAYLOAD

Org status FSM:

unverified  -> verified, frozen, doomed
verified   <-> parked, suspended
parked      -> frozen, doomed
suspended   -> frozen, doomed
frozen      -> doomed
doomed      -> (terminal)

After verification, all org-scoped services (PVM, PMC, ICS, PPM, SCM, PCM, CRM, etc.) accept writes for this org.


9) Resolve member (user-facing)

The user confirms their org membership and roles.

sh
curl -sS -X POST "$API_BASE/ofm/member/resolve" \
  -H 'Content-Type: application/json' \
  -H "x-session-guid: $SESSION_GUID" \
  -d '{ "orgcode": "ACME" }'

Response:

json
{
  "success": true,
  "data": {
    "org_guid": "ORG_GUID",
    "orgcode": "ACME",
    "org_status": "verified",
    "user_guid": "USER_ID",
    "is_owner": true,
    "member_state": "active",
    "roles": ["owner"]
  }
}

The user is now fully provisioned with an authenticated session and org access. Continue to the Minimum Viable Flow for product creation, publishing, and beyond.


Optimistic concurrency

All state-changing UAS operations require expected_revision:

  • Missing revision returns 428 expected-revision-required with current_revision in the error details.
  • Stale revision returns 409 conflict with both provided_revision and current_revision.
  • Each successful operation returns a new revision — use it for the next call.

Passcode management (operator-only)

Reset a user's passcode via uas_passcodeset (initial) or uas_passcodereset (reset):

sh
aws lambda invoke \
  --function-name uas_passcodereset \
  --cli-binary-format raw-in-base64-out \
  --payload file:///dev/stdin \
  --profile "$AWS_PROFILE" \
  /dev/stdout <<'PAYLOAD'
{
  "user_id": "USER_ID",
  "passcode": "NewPass!456",
  "expected_revision": "CURRENT_REV",
  "reason": "password-reset",
  "actor": "admin"
}
PAYLOAD

Passcode policy:

  • Minimum 8 characters.
  • Must contain: uppercase letter, lowercase letter, digit, special character (!@#$%^&*()-_=+?/><,."':; ~).
  • Cannot reuse any passcode from the last 90 days.

Passcode reset revokes all sessions

Changing a passcode emits a passcodeReset event. USM listens to this event and dooms all active sessions for the user. The user must log in again.


User status check (public API)

If you need to verify credentials or check user status without creating a session, use the UAS stat endpoint:

sh
curl -sS -X POST "$API_BASE/uas/stat" \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "user@example.com",
    "passcode": "Abcd!234"
  }'

This returns the user's status, emails, and payment methods. It does not create a session.


Adding team members

After the org is verified, the owner can invite other users:

Invite a member (owner)

sh
curl -sS -X POST "$API_BASE/ofm/member/invite/create" \
  -H 'Content-Type: application/json' \
  -H "x-session-guid: $SESSION_GUID" \
  -d '{
    "org_guid": "ORG_GUID",
    "invitee_user_guid": "OTHER_USER_ID",
    "role_profile_id": "inventory_clerk",
    "grants": ["facility:zones_write"],
    "reason": "team-expansion"
  }'

Accept invitation (invitee)

sh
curl -sS -X POST "$API_BASE/ofm/member/invite/accept" \
  -H 'Content-Type: application/json' \
  -H "x-session-guid: $INVITEE_SESSION_GUID" \
  -d '{ "code": "INVITE_CODE" }'

The invitee must already be a verified user with an active session. The invitee_user_guid on the invitation must match the session's user.


Quick reference: all Lambda functions

FunctionPurposeRequired fields
uas_usercreateCreate useremail, passcode
uas_emailissuetokenIssue email verification tokenuser_id, email, expected_revision
uas_emailconfirmtokenConfirm email tokenuser_id, token, expected_revision
uas_userstatussetSet user statususer_id, status, expected_revision
uas_passcodesetSet passcode (initial)user_id, passcode, expected_revision
uas_passcoderesetReset passcodeuser_id, passcode, expected_revision
ofm_invitationcreateMint org invitation(all optional)
ofm_orgstatussetSet org statusorg_guid, status, expected_revision

All operator Lambdas require AWS IAM credentials (--profile g3nretailstack). They are not exposed via API Gateway.