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
Two steps in one card. i3 renders neither of them: the portal owns both. This is the reference so nobody rebuilds it.
The single source of truth for every unit of physical stock. Requires a @minimalist.ae account.
Only @minimalist.ae accounts · TOTP 2FA required
Enter the 6-digit code from your authenticator app.
Can't access your codes? Request a reset from IT.
The round trip once, then the per-request work forever after
One cookie, every app on the domain. Never a per-app login.
| Property | Value | Why it matters |
|---|---|---|
| Cookie name | _mst_session | Read by every app's middleware, no per-app cookie exists |
| Domain | .minimalist-app.com | Deep links between siblings work without a second sign-in |
| Flags | httponly secure samesite=Lax | Not readable from JavaScript, not sent cross-site |
| Algorithm | HS256 | Verified locally, so the portal being slow does not slow every request |
| Shared secret | SSO_JWT_SECRET | [see ~/.env.master] identical across every app |
| Lifetime | 30 days | Session, JWT and cookie expiry are all aligned. Any mismatch produces a redirect loop |
| Claims | sub · email · name · iat · exp · jti · apps{} | sub is the only stable identity, email can change |
| Per-app grant | apps[i3] = {role, enabled, features[]} | Missing or enabled:false is a hard deny, not a downgrade |
| Size budget | under 3,500 bytes | Currently 2,140 B. Never add per-app metadata to the apps dict |
| Revocation | jti vs /api/sso-revocations | 15 s consumer cache, so a rotation propagates in seconds not days |
| Revocation failure | fail open | Portal outage does not lock everyone out, but known-revoked jti stay rejected |
| Thin cookie mode | /api/sso-validate | When the apps claim is dropped, the payload is fetched with a 60 s cache |
| Thin cookie failure | fail closed | No payload means logged out. A silent downgrade would be worse |
| Logout order | local session, then portal | Reverse order leaves a live local session behind |
Four roles exist in the portal. i3 invents no fifth one.
| Capability | viewer | manager | ops_manager | admin |
|---|---|---|---|---|
| Read the dashboard and every list | yes | yes | yes | yes |
| Read the ledger and the audit log | yes | yes | yes | yes |
| Save and share views | own | own | shared | shared |
| Acknowledge and snooze alerts | no | yes | yes | yes |
| Assign an alert to a person | no | yes | yes | yes |
| Mute a rule or change a throttle | no | no | yes | yes |
| Edit digest schedules and recipients | no | no | yes | yes |
| Post a ledger correction | no | no | propose | approve |
| Sign an opening balance | no | own store | any | any |
| Approve a write-off | no | no | propose | finance only |
| Edit classifier rules | no | no | shadow | live |
| Replay a webhook event | no | no | yes | yes |
| Change app settings and flags | no | no | no | yes |
| Read the access audit | no | no | yes | yes |
A persona is a view preset and a default queue. It is never an authorisation decision, and it is never written into the JWT.
| Persona | Maps to role | Features array | Location scope | Lands on | Default saved view | Digest |
|---|---|---|---|---|---|---|
| Owner · Ahmad | admin | all | all locations | dashboard | Morning triage | daily 18:00 + weekly |
| Store manager | manager | count.sign, transfer.receive | rota-effective stores | location-detail | Store manager · MCC | daily 18:00 |
| WH operator | manager | grn.post, carton.move, count.enter | HQ_319, HQ_325 | grn | Receiving today | none · in-app only |
| Inbound coordinator | ops_manager | po.edit, qc.decide, rts.ship | HQ_319, suppliers | inflows | Inbound day | daily 18:00 |
| CS agent | manager | reserve.place, case.link | all, read only | returns | Open cases | none · in-app only |
| Planner | viewer | export.csv | all | inventory-live | Below buffer | weekly Mon |
| Marketing | manager | sample.classify, influencer.dispatch | HQ_319, pools | samples | Sample QC clock | daily 18:00 |
| Finance | ops_manager | writeoff.approve, close.run | all | reports | Finance close | weekly Mon 09:00 |
| Admin · integrations | admin | all | all | settings | Ops health | daily 18:00 |
| Viewer · fallback | viewer | none | home location only | dashboard | page default | none |
Every denial is a row, so "I cannot see the page" never needs a log dive
| Time | Who | Event | Path | Required | Observed | jti | Result |
|---|---|---|---|---|---|---|---|
| 14:38:02 | qa-bot@minimalist.ae | login | /dashboard | viewer | viewer | 8f3c21… | allowed |
| 14:31:47 | store.yas@minimalist.ae | denied_role | /api/notification-rules | ops_manager | manager | 4a91b0… | 403 |
| 14:22:10 | new.hire@minimalist.ae | denied_no_app | /dashboard | apps.i3 | absent | c204ef… | permission state |
| 14:22:31 | new.hire@minimalist.ae | access_requested | /api/access-request | viewer | absent | c204ef… | P3 raised to admin |
| 13:57:19 | ahmad@minimalist.ae | login | /drift/DRF-0142 | viewer | admin | 1b77aa… | deep link preserved |
| 13:41:05 | wh.lead@minimalist.ae | login | /grn | manager | manager | 92de40… | allowed |
| 12:18:44 | ex.staff@minimalist.ae | denied_revoked | /inventory-live | viewer | viewer | 7c0f13… | jti revoked 11:02 |
| 11:52:30 | finance@minimalist.ae | login | /reports | ops_manager | ops_manager | 3ea5c8… | allowed |
| 11:04:12 | ahmad@minimalist.ae | role_changed | portal | - | manager → ops_manager | - | granted to inbound |
| 10:22:08 | store.mcc@minimalist.ae | login | /location-detail | manager | manager | 55b1d9… | new device approved |
| 09:47:55 | planner@minimalist.ae | denied_role | /api/views (shared) | ops_manager | viewer | 6d3a70… | 403 |
| 08:15:22 | - | sso_unavailable | /api/sso-revocations | - | timeout 5s | - | failed open · 3 known revoked still rejected |
| 08:02:41 | marketing@minimalist.ae | login | /samples | manager | manager | a11c62… | allowed |
| 07:38:09 | cs.agent@minimalist.ae | denied_no_app | /settings | admin | manager | be49f1… | permission state |
A sign-in surface has more ways to fail than any other page, and every one of them must look intentional
38 registered apps · these are the ones i3 exchanges data with
| App code | Host | Relationship to i3 | Direction |
|---|---|---|---|
| i3 | i3.minimalist-app.com | this app | owns stock |
| inventory | inv.minimalist-app.com | INV1 · dormant, read-only 2025 archive | retire at cutover |
| fulfillment | ops.minimalist-app.com | ops2 · dispatch, AWB, returns scan | pushes to i3 |
| cs_dashboard | csd.minimalist-app.com | return intent, replacement decisions | pushes to i3 |
| grn | grn.minimalist-app.com | retail and warehouse receiving | pushes to i3 |
| rfid | rfid.minimalist-app.com | encode, gate, zone, count, write-off | pushes to i3 |
| influencer | inf.minimalist-app.com | loan dispatch, return, gift | pushes to i3 |
| sku | sku.minimalist-app.com | parent-child map, velocity, tier | both ways |
| replenishment | rep.minimalist-app.com | reads canonical balance from i3 | reads i3 |
| rota | rota.minimalist-app.com | store roster drives location scope | i3 reads |
| ops | status.minimalist-app.com | health scrape and app inventory | reads i3 |
| counts | counts.minimalist-app.com | daily retail counts | pushes to i3 |