Login and access

Single sign-on through auth.minimalist-app.com · the _mst_session cookie flow · four roles, nine personas · no app in this estate has its own login

Cutover action required. The auth portal app registry still lists inventory → inv.minimalist-app.com, which is INV1. Adding i3 without retiring that entry puts two inventory tiles in the switcher and half the company will click the dormant one. Cutover checklist →

The sign-in surface

Two steps in one card. i3 renders neither of them: the portal owns both. This is the reference so nobody rebuilds it.

step 1 sign-instep 2 TOTPportal-owned
i3
Inventory OS

Sign in to continue

The single source of truth for every unit of physical stock. Requires a @minimalist.ae account.

Your session ended after 30 days. Sign back in to return to /drift/DRF-0142.
Sign in with SSO

Only @minimalist.ae accounts · TOTP 2FA required


orpasskey ready
This device is trusted until 28 Aug 2026. 2FA will be skipped. Untrust
Back

Two-factor code

Enter the 6-digit code from your authenticator app.

Signing in as ahmad@minimalist.ae
Verification codeFrom Authenticator
48219
Resend in 11s
Incorrect code. Check your authenticator and try again. 2 attempts left before a 5 minute lockout.

Can't access your codes? Request a reset from IT.

Enter verifyEsc back⌘. theme

What happens on every request

The round trip once, then the per-request work forever after

middleware v1.0.7.75
The operator's browser Browser auth.minimalist-app.com Auth portal i3.minimalist-app.com i3 1. GET /drift/DRF-0142 with no cookie 1 · GET /drift/DRF-0142 · no cookie 2. 302 to the portal with the original path url-encoded in redirect 2 · 302 /login?redirect=…%2Fdrift%2FDRF-0142 3. Google OAuth restricted to @minimalist.ae, or password, or passkey 3 · Google / password / passkey 4. TOTP 2FA, mandatory for every method 4 · TOTP 2FA · always required Portal mints an HS256 JWT with sub, email, name, iat, exp, jti and the apps dict 5 · mint HS256 JWT sub email jti exp apps{} 6. Redirect to i3 /sso-callback carrying the token 6 · /sso-callback?token= i3 sets its own Flask session and reads the domain-wide cookie 7 · set Flask session _mst_session on .minimalist-app.com httponly secure samesite=Lax 8. 302 back to the originally requested deep link 8 · 302 → /drift/DRF-0142 · the deep link survives Per-request middleware work every request after that verify HS256 · check exp check jti vs revocation list resolve apps.i3 → role, features no entry or disabled → Permission state Revocation poll: GET /api/sso-revocations, 15s cache, fails open poll /api/sso-revocations · 15s
Step 8 is the one v2 got wrong: its login hard-coded a redirect to inv.minimalist-app.com/, so a Slack deep link always landed on the home page. i3 round-trips the original path.

The cookie and the token

One cookie, every app on the domain. Never a per-app login.

no secrets on this page
PropertyValueWhy it matters
Cookie name_mst_sessionRead by every app's middleware, no per-app cookie exists
Domain.minimalist-app.comDeep links between siblings work without a second sign-in
Flagshttponly secure samesite=LaxNot readable from JavaScript, not sent cross-site
AlgorithmHS256Verified locally, so the portal being slow does not slow every request
Shared secretSSO_JWT_SECRET[see ~/.env.master] identical across every app
Lifetime30 daysSession, JWT and cookie expiry are all aligned. Any mismatch produces a redirect loop
Claimssub · email · name · iat · exp · jti · apps{}sub is the only stable identity, email can change
Per-app grantapps[i3] = {role, enabled, features[]}Missing or enabled:false is a hard deny, not a downgrade
Size budgetunder 3,500 bytesCurrently 2,140 B. Never add per-app metadata to the apps dict
Revocationjti vs /api/sso-revocations15 s consumer cache, so a rotation propagates in seconds not days
Revocation failurefail openPortal outage does not lock everyone out, but known-revoked jti stay rejected
Thin cookie mode/api/sso-validateWhen the apps claim is dropped, the payload is fetched with a 60 s cache
Thin cookie failurefail closedNo payload means logged out. A silent downgrade would be worse
Logout orderlocal session, then portalReverse order leaves a live local session behind
Every secret on this page is written as [see ~/.env.master]. Nothing here is a real value, and nothing ever should be.

Roles and what they unlock in i3

Four roles exist in the portal. i3 invents no fifth one.

Capabilityviewermanagerops_manageradmin
Read the dashboard and every listyesyesyesyes
Read the ledger and the audit logyesyesyesyes
Save and share viewsownownsharedshared
Acknowledge and snooze alertsnoyesyesyes
Assign an alert to a personnoyesyesyes
Mute a rule or change a throttlenonoyesyes
Edit digest schedules and recipientsnonoyesyes
Post a ledger correctionnonoproposeapprove
Sign an opening balancenoown storeanyany
Approve a write-offnonoproposefinance only
Edit classifier rulesnonoshadowlive
Replay a webhook eventnonoyesyes
Change app settings and flagsnononoyes
Read the access auditnonoyesyes
Hiding a control in the UI is a courtesy. Every one of these is also checked server side, so a direct API call from a viewer returns 403.

Persona to role map

A persona is a view preset and a default queue. It is never an authorisation decision, and it is never written into the JWT.

9 personas4 rolesderived at login
PersonaMaps to roleFeatures arrayLocation scopeLands onDefault saved viewDigest
Owner · Ahmadadminallall locationsdashboardMorning triagedaily 18:00 + weekly
Store managermanagercount.sign, transfer.receiverota-effective storeslocation-detailStore manager · MCCdaily 18:00
WH operatormanagergrn.post, carton.move, count.enterHQ_319, HQ_325grnReceiving todaynone · in-app only
Inbound coordinatorops_managerpo.edit, qc.decide, rts.shipHQ_319, suppliersinflowsInbound daydaily 18:00
CS agentmanagerreserve.place, case.linkall, read onlyreturnsOpen casesnone · in-app only
Plannerviewerexport.csvallinventory-liveBelow bufferweekly Mon
Marketingmanagersample.classify, influencer.dispatchHQ_319, poolssamplesSample QC clockdaily 18:00
Financeops_managerwriteoff.approve, close.runallreportsFinance closeweekly Mon 09:00
Admin · integrationsadminallallsettingsOps healthdaily 18:00
Viewer · fallbackviewernonehome location onlydashboardpage defaultnone
Persona is computed from (role, features[], rota-effective locations) and cached in i3's own profile table. A user may pin a different persona: that changes what they see, never what they may do.

Access audit

Every denial is a row, so "I cannot see the page" never needs a log dive

last 24hFull audit log ↗
TimeWhoEventPathRequiredObservedjtiResult
14:38:02qa-bot@minimalist.aelogin/dashboardviewerviewer8f3c21…allowed
14:31:47store.yas@minimalist.aedenied_role/api/notification-rulesops_managermanager4a91b0…403
14:22:10new.hire@minimalist.aedenied_no_app/dashboardapps.i3absentc204ef…permission state
14:22:31new.hire@minimalist.aeaccess_requested/api/access-requestviewerabsentc204ef…P3 raised to admin
13:57:19ahmad@minimalist.aelogin/drift/DRF-0142vieweradmin1b77aa…deep link preserved
13:41:05wh.lead@minimalist.aelogin/grnmanagermanager92de40…allowed
12:18:44ex.staff@minimalist.aedenied_revoked/inventory-liveviewerviewer7c0f13…jti revoked 11:02
11:52:30finance@minimalist.aelogin/reportsops_managerops_manager3ea5c8…allowed
11:04:12ahmad@minimalist.aerole_changedportal-manager → ops_manager-granted to inbound
10:22:08store.mcc@minimalist.aelogin/location-detailmanagermanager55b1d9…new device approved
09:47:55planner@minimalist.aedenied_role/api/views (shared)ops_managerviewer6d3a70…403
08:15:22-sso_unavailable/api/sso-revocations-timeout 5s-failed open · 3 known revoked still rejected
08:02:41marketing@minimalist.aelogin/samplesmanagermanagera11c62…allowed
07:38:09cs.agent@minimalist.aedenied_no_app/settingsadminmanagerbe49f1…permission state
Denials keep the jti so a run of failures can be correlated with a portal rotation. Access audit is never garbage collected.

State variants

A sign-in surface has more ways to fail than any other page, and every one of them must look intentional

Full library ↗
Loading
Redirecting to auth.minimalist-app.com…
The redirect is announced. Without this line an SSO bounce is indistinguishable from a hang, and users click twice.
Error
SSO unavailable. Retry in 30 s. Nothing was changed. Error id sso-9b17c2.
Already-signed-in users are unaffected: signature and expiry checks are local. Only new sign-ins are blocked.
Permission
You are signed in, but you have no grant on app i3.
Names the app, names who grants it, offers one action. Never a 500 and never a blank dashboard.
Logged out
Your session ended after 30 days. Sign back in to return to /drift/DRF-0142.
The destination is named, so the user knows the deep link is not lost.
Revoked
This session was ended by an administrator at 11:02.
Distinct from expiry. A revoked jti is rejected within 15 s of the portal recording it.
OTP error
999999
Boxes go red as a set, not individually. Attempts remaining are stated before the lockout, not after.

Sibling apps on the same cookie

38 registered apps · these are the ones i3 exchanges data with

app switcher
App codeHostRelationship to i3Direction
i3i3.minimalist-app.comthis appowns stock
inventoryinv.minimalist-app.comINV1 · dormant, read-only 2025 archiveretire at cutover
fulfillmentops.minimalist-app.comops2 · dispatch, AWB, returns scanpushes to i3
cs_dashboardcsd.minimalist-app.comreturn intent, replacement decisionspushes to i3
grngrn.minimalist-app.comretail and warehouse receivingpushes to i3
rfidrfid.minimalist-app.comencode, gate, zone, count, write-offpushes to i3
influencerinf.minimalist-app.comloan dispatch, return, giftpushes to i3
skusku.minimalist-app.comparent-child map, velocity, tierboth ways
replenishmentrep.minimalist-app.comreads canonical balance from i3reads i3
rotarota.minimalist-app.comstore roster drives location scopei3 reads
opsstatus.minimalist-app.comhealth scrape and app inventoryreads i3
countscounts.minimalist-app.comdaily retail countspushes to i3
Adding i3 to the registry is a coordinated change to the portal, not something i3 can do to itself. It is task T-01.29 in the module spec.

The rule this page enforces

One portal, one cookie, no exceptions

i3 has no login of its own. It has a redirect, a callback and a role gate. Every app in this estate works the same way, which is why a link from Slack to a drift case opens without a second sign-in.

Hiding a control is not a permission check. Every capability in the role table is enforced server side as well, so calling the endpoint directly with a viewer token returns 403 and writes an audit row.

Personas never reach the JWT. The cookie carries {role, enabled, features} and nothing more, because the cookie has a hard 3,500 byte ceiling across 38 apps and one careless addition breaks sign-in for everyone.

App code
i3
Roles
4 · viewer, manager, ops_manager, admin
Personas
9 · derived, never granted
2FA
TOTP, mandatory, no exemption
Session
30 days, aligned three ways
Deny renders
permission state, never a 500
Secrets on screen
never
Middleware is a drop-in file, identical in every app, currently v1.0.7.75
Connected to Dashboard Help Guided tour Notifications State library Settings Audit log Sibling events Locations Opening stock Command palette