Foodops
All docs

Getting Started · Updated 2026-08-20 · v713150a

Onboarding — Creating a New Foodops Organization

Audio narration for this article is generated with the weekly docs refresh.

Plain-English summary

You create an account, confirm your email by code, then a short in-app wizard walks you through your business details, your locale (country / currency / timezone) and your first outlet — and you're in. About 10-15 minutes end-to-end if you're prepared with the details. (A legacy permission gotcha used to need a manual fix after signup; the part of it caused by a missing name is now resolved automatically — see Step 6 for the current status.)

WHERE ONBOARDING HAPPENS NOW (July-2026 launch): the legacy app.foodops.io monolith and its Razor onboarding wizard are RETIRED. Onboarding now runs in the admin app (admin.foodops.io): account sign-up at /register, email verification at /verify-email, then a React wizard /onboarding/business → /onboarding/locale → /onboarding/outlet (gated by OnboardingGate, which bounces any org whose /onboarding/status is not isCompleted back into the wizard). Identity (account + org) is handled by the Identity API (POST /registration/register, /registration/verify-email, /organisations/provision); the wizard's business / outlet / locale data is persisted by the sales API's OnboardingApiController (/v1/onboarding/simplified/*). [code: foodops-admin-app/apps/admin/src/App.tsx:465-494; components/onboarding/OnboardingGate.tsx:61-77; services/registrationService.ts:37-77; services/onboardingService.ts:79-122]

When you'd use this

  • First-ever Foodops organisation setup (you're a new restaurant owner)
  • Setting up a demo or test org for training
  • Partner onboarding a client and walking them through the flow
  • Voice agent or chat agent answering "how do I sign up?"

Summary

Complete walkthrough for creating a new Foodops restaurant organization from scratch on the new stack. Covers: account creation in the admin app (/register), email verification, the in-app onboarding wizard (business → locale → outlet), automatic free-trial start, and the changed status of the legacy app-permission gotcha (one root cause now auto-fixed; the app-grant itself still to be verified live).

Prerequisites

  • A valid email address (for account verification)
  • A phone number (Maldives +960 is default)

Step-by-Step Process

Step 1: Create an Account

  1. Navigate to https://admin.foodops.io (the legacy app.foodops.io is retired). The counter app (counter.foodops.io) also sends unauthenticated users to the login flow.
  2. On the login page, choose to register — the sign-up form is at /register.
  3. Fill in the registration form (RegisterPage):
    • Email: Your business email
    • Phone Number: Business contact (default country: Maldives +960)
    • Password: At least 6 characters [code: foodops-admin-app/apps/admin/src/pages/RegisterPage.tsx:37 — if (password.length < 6)]
    • Confirm Password: Must match
  4. Submit — this calls the Identity API POST /registration/register. [code: registrationService.ts:37-44]

Full name: the form does not ask for a name, but the Identity API now auto-derives a Full Name from your email at registration (FullName = DeriveFullNameFromEmail(email)), so your user always has a name set. This removes one root cause of the old permission gotcha (see Step 6). [code: haafai.app.identity/Haafai.Identity.Api/Controllers/RegistrationController.cs:72 @staging] Wizard shape: the brand onboarding config models the flow as plan → account → business → locale → outlets — account is /register; the business / locale / outlet steps are the in-app wizard below. [code: foodops-admin-app/apps/admin/src/config/brands.ts:51 — OnboardingStepKey]

Step 2: Verify Email

  1. A verification code (OTP) is emailed to you via the FoodopsEmailVerification template. [code: haafai.app.identity/Haafai.Identity.Api/Controllers/RegistrationController.cs:199-204 @staging]
  2. Enter the code on the /verify-email page (VerifyEmailPage) and confirm — this calls the Identity API POST /registration/verify-email. [code: foodops-admin-app/apps/admin/src/services/registrationService.ts:46-54]
  3. Check your spam folder if it doesn't arrive; resend if the UI offers it.

After verification you log in. On first login, OnboardingGate checks /onboarding/status; a brand-new user with no org is provisioned (Identity API POST /organisations/provision) and routed to /onboarding/business. [code: foodops-admin-app/apps/admin/src/components/onboarding/OnboardingGate.tsx:61-77]

Step 3: Subscribe to Foodops Plan

Changed on the new stack: org provisioning now auto-starts a free trial (IsOnTrial = true, trial-expiry set from ModelConstants.TrialPeriod), so you are not forced to pick a plan before you can use the app — you can complete the wizard and start working, then choose/confirm a plan from the subscription surface. [code: haafai.app.identity/Haafai.Identity.Api/Controllers/OrganisationsController.cs:331-338 @staging — IsOnTrial = true, TrialExpiryDate = DateTime.Today.AddMonths(...)] Subscription management (plans/prices) is documented in Subscription Plans.

The plan catalogue (managed via the subscription surface):

  1. Open the Subscription surface (SSO subscriptions page / "Get subscription now")

  2. You'll see all available plans organized by product:

    Foodops Plans:

    Plan What it is for
    Freemium One outlet, one counter, on the house — with a sales cap.
    Starter A single outlet finding its feet.
    Growth Two outlets, plus online ordering and inventory.
    Pro Multi-outlet, AI costing, and your own domain.
    Enterprise Custom modules, integrations and API. Contact sales.

    Prices, user counts and outlet limits are deliberately not repeated here. They are DB-backed and change with the lineup, and a second copy in prose is a copy that goes stale silently — this table named two plans that had been retired, at prices that no longer existed, and the website published it for weeks. See Pricing, which renders the live plan catalogue from identity-api. (Ahmed, 2026-07-05: publish names only.)

    Other Products Also Available:

    • Shops (Retail POS): Free Starter tier
    • Finance (Accounting): $15/mo Starter
    • Sales: Free
  3. Select your plan (e.g., Foodops Starter — Free Forever)

  4. Confirmation dialog shows: subscription details, included features, price

  5. Check "I agree to the Terms of Use"

  6. Click "Subscribe"

Subscription is now active. Status shows "Live" on the Subscription page.

Step 4: Enter Business Information

  1. After login the wizard opens at /onboarding/business (BusinessInfoPage) on admin.foodops.io.
  2. Fill in:
    • Business Name (required): Your legal business name (e.g., "Sunrise Cafe Pvt Ltd")
    • Business Registration Number: Company registration from MIRA or relevant authority (e.g., "C-0123/2024")
    • Business TIN Number: Tax Identification Number for GST purposes
    • Business Logo: Optional — used on receipts and invoices
    • For the foodops brand the Industry / Type-of-Business field is hidden — the industry is auto-stamped behind the scenes. [code: foodops-admin-app/apps/admin/src/config/brands.ts:82-89]
  3. Continue — saved via POST /v1/onboarding/simplified/business-info. [code: services/onboardingService.ts:90-100]

Step 4b: Choose Locale (NEW step)

The wizard now has a dedicated locale step at /onboarding/locale (LocalePage) — country, currency and timezone (defaults: Maldives / MVR / Indian/Maldives). Backed by /v1/onboarding/simplified/lookups plus the locale endpoints on the sales API's OnboardingApiController. [code: foodops-admin-app/apps/admin/src/pages/onboarding/LocalePage.tsx; services/onboardingService.ts:122]

Step 5: Configure Your Outlet

  1. The final wizard step is /onboarding/outlet (OutletSetupPage).
  2. Fill in:
    • Outlet Name (required): e.g., "Main Branch"
    • Address (required): Physical address (e.g., "M. Sunflower, Orchid Magu, Male'")
    • Contact Number: Customer-facing phone number for this outlet
  3. Complete — saved via POST /v1/onboarding/simplified/outlet. This marks onboarding complete, so OnboardingGate (isCompleted === true) stops redirecting and lets you into the app. [code: services/onboardingService.ts:106-115; components/onboarding/OnboardingGate.tsx:65-77]

Step 6: App Permissions (status CHANGED on the new stack)

On the legacy stack a new owner often hit "You currently don't have permission to use this application (foodops). To gain access, please reach out to your system administrator." right after setup, and had to grant themselves the app manually. On the new stack the picture has changed:

What's fixed: the registration form still doesn't ask for a name, but the Identity API now auto-derives a Full Name from your email at registration. That removes the legacy root cause where a blank name made the permission save silently fail. [code: haafai.app.identity/Haafai.Identity.Api/Controllers/RegistrationController.cs:72 @staging — FullName = DeriveFullNameFromEmail(email)]

What's still uncertain (FLAG — verify live): the new org-provisioning endpoint /organisations/provision creates the org, an Administrator role group containing every role, the org-user link and a full set of ApplicationUserRoles — but it shows no explicit ApplicationOrganisationApplications (per-app "enable foodops") insert, and its own doc-comment says it "mirrors the legacy OrganisationsRegistrationsController.New" — the very method the legacy bug was attributed to. (ApplicationOrganisationHelper.Create, called inside provision, lives in a shared package not in this repo, so it could enable apps there — unconfirmed.) Whether the "no permission to use foodops" error still reproduces after a fresh self-signup is therefore not determinable from code alone. [code: haafai.app.identity/Haafai.Identity.Api/Controllers/OrganisationsController.cs:279-432 @staging]

If you do hit it (workaround):

  1. As an admin, open the Users surface (SSO users at sso.hubits.io/blazor/users, or the admin app's user management).

  2. Edit the affected user — ensure the Name is set (now auto-filled from email) and that the relevant applications + the Owner role are granted.

    Edit User panel (legacy SSO) showing app permissions

  3. Update — the Identity API now publishes a session-invalidation event when a user's assignments are synced or access is removed, so satellite apps drop cached session-validation and enforce the new roles on the user's next request instead of waiting for the old cache TTL. [code: haafai.app.identity/Haafai.Identity.Api/Controllers/UsersController.cs — _sessionInvalidationPublisher.PublishUserAsync(id)] If the permission error still appears, a full log-out and log-back-in remains a safe fallback.

Step 7: You're In

After onboarding completes you land in the back-office admin app (admin.foodops.io) at /dashboard. To start ringing up sales, open the counter app (counter.foodops.io) — for the foodops brand the counter relabels its main page to "Orders", and you open a register session from there. [code: foodops.counter.app/src/components/layout/Sidebar.tsx:249]

The admin sidebar provides the back-office modules (Catalog, Sales, Expenses, Inventory, Reports, Settings); the counter provides the on-the-floor POS (Orders, Dine-In, Deliveries, Takeouts, Reports). See Navigation & Module Map.

Common Issues

Issue Cause Solution
"You currently don't have permission to use this application (foodops)" Legacy gotcha — the missing-name root cause is fixed (name auto-derived from email); whether the app-grant itself still misfires is unconfirmed on the new stack (verify live) If it appears: Users → Edit → confirm Name set + grant the application/Owner role → Update → fully log out and re-login
Still getting permission error after checking boxes Name field was empty when clicking Update Fill in the Name field first, then check boxes, then Update
Permission error persists after update Previously stale session-validation cache — now auto-invalidated on permission sync/removal via a session-invalidation publish in the Identity API [code: UsersController.cs] Should clear on your very next request automatically; if it doesn't, fully log out of both SSO and Foodops (clear cookies if needed), then log in fresh
Verification code not received Email in spam, or email service delay Check spam folder, wait 5 minutes, click "Resend"

Key concepts

  • Organization — the top-level account. Holds subscription, users, outlets, menu. One paying org per restaurant business.
  • Outlet — a single physical location. An org can have multiple (e.g., a chain); Starter plan is typically one outlet.
  • Subscription — the plan the organisation is on. Freemium is the free tier; the paid tiers rise from there. Drives feature gates, limits, pricing. Current lineup: Pricing.
  • App permission — per-user grant for each Hubits app (foodops, People, Finance). Separate from roles. The onboarding bug leaves this unchecked on the founder's account; must be manually fixed.
  • Identity layer — account sign-up, email verification and org provisioning are handled by the shared Identity API (/registration/*, /organisations/provision); the SSO surface (sso.hubits.io) shares this identity. Sign-up now starts in the admin app (admin.foodops.io/register), not the retired app.foodops.io. All Hubits apps share one identity.

Common questions

Q: Do I still have to manually grant myself foodops permission after signup? A: Maybe not. On the legacy stack this was a known bug. On the new stack the part caused by a missing name is fixed (the Identity API auto-fills your Full Name from your email), and provisioning grants you an Administrator role group with every role. Whether the per-app "enable foodops" grant still misfires is unconfirmed from code — verify on a fresh signup. If you do see the error: Users → edit your user → confirm Name + grant the application/Owner role → save → re-login. See Step 6.

Q: Can I change my plan later? A: Yes — Settings → Account Profile → Subscription, or at sso.hubits.io/subscription. Upgrade is immediate; downgrade takes effect at renewal.

Q: What if I picked the wrong org name? A: Settings → Account Profile — edit company name, registration, etc. Not a reset — just an edit.

Q: How do I add my first menu items after onboarding? A: See Menu Management. You'll need categories first, then items, then outlet assignment.

Q: Do I need to verify my TIN / GST registration during signup? A: Only required at tax time — you can configure GST settings later once registered. Starter can operate as non-GST until upgrade.

Test Account Details

For KB generation and Playwright testing, we use:

What changed since 2026-04-15 (NEW 2026-05)

  • haafai.app.identity is NOT dormant (CORRECTED 2026-06-04) — the prior pass recorded "no commits since the watermark", but the SSO repo had three: 56d11d3c (2026-04-18) "redirect to app onboarding wizard when creating a new organisation", ddbc6750 (2026-04-19) "filter application radio buttons to relevant apps in new organisation modal", and the merge ebaa30ac (2026-06-03) of feature/106-authentication-module (touches OrganisationsRegistrationsController + the New/Edit Organisation views). The known onboarding bug (Step 6 — manually grant foodops permission after signup) is still documented as present: the org-registration New() flow auto-creates the org, an Administrator role group, role-group membership, the ApplicationOrganisationUser link and user roles — but does not auto-create an app-module-level permission grant for "foodops". This could not be confirmed fixed from code, so the Step 6 workaround stands; re-verify live when an auth.json refresh is available. [code: haafai.app.identity/Haafai.Identity/Controllers/OrganisationsRegistrationsController.cs:45 (New) — ebaa30a]
  • "Salesmade" is a white-label retail-POS rebrand, NOT a Foodops onboarding change — the brief's driver 8054674 (2026-05-06) adds a Salesmade brand configuration, logo assets, a separate deploy/salesmade/ docker-compose, and a SalesmadeCounterPage (retail POS) to foodops.counter.app. It's a separate branded build of the counter app (a Shops/retail concept), with no effect on the documented Foodops org-creation flow.
  • Counter app auth migration to Haafai Identity API (2026-04-19, bdeb289)foodops.counter.app migrated its auth backend to consume the centralized Haafai Identity API. End-to-end the user-facing onboarding flow is unchanged, but the counter app is now coupled to the same SSO layer that the legacy monolith uses.
  • Login form refresh (2026-04-29, a942e43, and 2026-04-20 d0ea0c5)LoginForm layout polished, app name shown for clarity ("Foodops Counter").
  • ProtectedRoute stale-while-revalidate (2026-05-01, 6ccc19e) — session validation now uses stale-while-revalidate caching for snappier UI on returning users; doesn't change the auth flow but makes "logged-in app loads fast then re-validates in background" the default.
  • Sidebar restricted to counter; default redirect to /counter (2026-04-20, dc8ea98) — for new users coming through the counter app, the default landing is /counter not /orders — relevant for the post-onboarding "where do I land" question.

The legacy monolith Haafai.Restaurant.App/Views/Onboarding/* Razor wizard (OnboardingController / SimplifiedOnboardingController at app.foodops.io) is RETIRED at the July-2026 launch. The Business and Outlet steps it used to host now live in the admin app wizard (/onboarding/business, /onboarding/outlet) plus a new /onboarding/locale step, backed by the sales API's OnboardingApiController simplified endpoints (/v1/onboarding/simplified/*).

  • Simplified-onboarding locale endpoints (3ef93e9d 2026-05-16, hubits-api-sales)OnboardingApiController gained endpoints for locale lookups/updates during simplified onboarding (country/currency/timezone defaults). Backend support for the wizard's locale step; no change to the documented user-facing steps.

Related

  • User Stories: Settings & Organization — US-SET-001

  • Subscription Plans — detailed plan comparison

  • 2026-08-19: haafai.app.identity (UsersController.cs, within ba0ea73..8d8e660) fixes UsersController.Register — the endpoint used to invite an additional user into an existing org, distinct from the founder self-registration flow this entry documents in Step 1 (RegistrationController.Register at /register) — so it now threads outletIds: viewModel.OutletIds through to AssignUserSelectionsAsync. Per the commit's own subject, 'assign outlets on user invite and clear them on organisation removal', this implies invited users were previously NOT assigned to specific outlets at invite time (and outlet assignment wasn't cleared when a user was removed from an org) — both now fixed. This does not contradict anything currently documented: Step 6 and Key Concepts describe per-app permission grants and the Administrator role group for the org creator's own account and the Users-surface edit workflow, not the separate invite-a-teammate flow, and this entry's Outlet concept is only described as created in Step 5, never as a field on the user-invite form. Worth a line once a UI surface is confirmed live — whether the admin's 'invite user' form (reachable from the same Users surface Step 6 cites) exposes an outlet picker, since persistence now actually honors it. Separately: this diff's OrganisationsController.cs hunks (support-access org listing in GetUserOrganisations/SwitchOrganisation) are the same Support Access Grant feature already logged in the 2026-08-16 verification entry (ba0ea73..9e5212a) — not new here, since this diff's range is cumulative from the same base commit; no additional note needed for that portion. Also present in this commit range but not user-facing: two dependency-pin chores on haafai.domain (one explicitly marked '[REVERT BEFORE MERGE]' in its own commit subject), irrelevant to onboarding behavior. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-16: haafai.app.identity (OrganisationsController.cs, ba0ea73..9e5212a) adds a new 'Support Access Grant' access mode to GetUserOrganisations and SwitchOrganisation — both outside this entry's cited Provision range (lines 279-432) and not currently described anywhere in this entry. GetUserOrganisations now unions in orgs the caller reaches only via a live support-access grant (OrgAccessQueries.SupportAccessedOrgIdsAsync), returned as separate OrganisationListDto rows flagged IsSupportAccess = true; a code comment states the switcher renders these under a distinct 'SUPPORT ACCESS' heading, kept separate from the caller's own memberships (not independently confirmed here — frontend file not in this diff). The endpoint's existing self-heal (which resets a stale LastLoggedInApplicationOrganisationId back to a valid owned org) is fixed to also treat a support-accessed org id as valid, per the added comment: without this, an engineer switched into a client org via a grant would be silently evicted back to their own org on next load. SwitchOrganisation now authorizes via a shared OrgAccessQueries.CanActInOrgAsync predicate (membership OR live grant) instead of a direct membership-only query, so a support engineer with an active grant — not a member — can now switch into a client's organisation through the same endpoint a real org member uses. When the switch is grant-based rather than membership-based, it populates CallContext synchronously (the diff's own comment explains this must be sync because CallContext is AsyncLocal-backed and there's no middleware on this API to populate it) and writes an operation-log entry directly onto the target org: 'Hubits support entered this organisation. Access expires {expiry}.' This doesn't contradict anything documented — this entry's Step 6/Key-concepts claims about org creation, app permissions and the Administrator role group are untouched, and the flagged 'does provisioning grant the foodops app permission' open question is unaffected since Provision isn't in this diff. Worth documenting once a UI surface is confirmed live: (a) a new 'Support Access' entry could plausibly appear in the org switcher for any org, and (b) a customer inspecting their org's own activity/operation log could see a 'Hubits support entered this organisation' line and ask what it is — a natural Common-Questions candidate. Also worth noting alongside the existing 2026-08-09 ForceNew note (multi-org capability, not yet documented in this entry) since both extend org-membership semantics beyond the single-owner-org model this entry currently describes. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-09: hubits-api-sales (OnboardingApiController.cs, 4f94ad6..cdb3b32) fixes a persistence bug in the three simplified-onboarding write endpoints this entry cites for Steps 4 and 5: UpdateBusinessInfo (/v1/onboarding/simplified/business-info), CreateSimplifiedOutlet (/v1/onboarding/simplified/outlet) and CompleteSimplified (/v1/onboarding/simplified/complete). Per the new code's own doc-comment, the Sales API sets QueryTrackingBehavior.NoTracking globally as 'the house default,' so the old shared helper FindOnboardingForOrgAsync(id, tracked: true) — despite its tracked flag defaulting true — returned an UNTRACKED entity anyway, because that flag only decided whether to call .AsNoTracking() explicitly, not the query's actual tracking mode: 'field mutations are invisible to SaveChanges and the handler still returns 200 having persisted nothing.' The fix splits the helper into an explicit ReadOnboardingForOrgAsync (untracked) and FindOnboardingForWriteAsync (.AsTracking()), and CompleteSimplified's direct ApplicationOrganisations write path gains the same explicit .AsTracking(). This does not contradict anything currently documented — Steps 4/4b/5's claims that these POSTs save business info, outlet, and mark onboarding complete are accurate now that persistence actually lands — but it means that during whatever window this bug was live upstream (start point not determinable from this diff alone), a wizard user could have clicked through business-info/outlet/complete, received a 200, and still been silently bounced back into the wizard by OnboardingGate (which reads isCompleted off the very row this bug failed to persist) or lost profile/outlet data entered on those steps. Worth flagging if a 'wizard won't let me past locale/outlet' or 'my outlet didn't save' report ever surfaces for that period; not an edit to the current entry since the golden path is correct as of cdb3b32. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-09: haafai.app.identity (OrganisationsController.cs, 51c503b..731aa70) changes the Provision endpoint's (Step 6, lines 279-432) existing-org short-circuit. Previously ANY active org membership caused provision to silently return that org — reported to the caller as 'Organisation provisioned.', with the caller's requested Name discarded — which the commit's own comment says caught 'a user merely invited into someone else's restaurant.' That silent-return path is now gated behind a new ForceNew request flag: without it, behavior is unchanged (existing org returned) but the response is now explicit — a new Existing() envelope with message 'User already belongs to an organisation; returned the existing one. Pass forceNew to provision another.' and a Created:false flag. With ForceNew:true, a user who already belongs to an org can now provision a genuinely new one; the new org's membership row only gets IsDefault=true if this is the user's first org (IsDefault = !alreadyBelongsToAnOrg), so a forced second org won't clobber the existing default. This does not touch the golden path documented in Step 2 ('a brand-new user with no org is provisioned... routed to /onboarding/business') since existing is null for first-time signups regardless of ForceNew. It also does not resolve Step 6's flagged open question — no ApplicationOrganisationApplications insert is added or removed by this diff, so whether provisioning auto-grants the foodops app permission remains exactly as unconfirmed as before. Worth documenting as a new multi-org capability once a UI surface for 'create another organisation' from an existing account is confirmed live — not yet described anywhere in this entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-30: haafai.app.identity (a982fab..85abc6a) adds real rate-limiting to the registration/verification surface this entry cites for Steps 1-2. RegistrationController moves its OTP store from per-process IMemoryCache to IDistributedCache (fixing a genuine multi-instance bug: an OTP issued by one API instance was previously unverifiable if the follow-up request landed on a different instance behind the load balancer) and adds two new throttles: VerifyEmail now caps guesses via IOtpVerifyThrottle and returns a 'too many attempts' failure when tripped, and the resend endpoint now caps sends via IOtpSendThrottle and returns a cooldown message — previously, per the commit's own comment, resend 'had no cap at all.' Both are new failure modes for Step 2 ('enter the code on the /verify-email page... resend if the UI offers it') worth a line once the exact copy/cooldown length is live-verified — folds into the existing UI-TODO on that step. Separately, UsersController.Authenticate gains the same login-lockout throttle already used elsewhere (429 + Retry-After on repeated failures), and UnlockUser is fixed so an admin unlock leaves LockoutEnabled=true (previously it set this permanently false, silently exempting that user from brute-force protection forever) — neither is part of the documented onboarding flow, but both touch the Users surface Step 6 cites for the permission workaround. Also in this diff: a new call to _sessionInvalidationPublisher.PublishUserAsync appears in a UsersController code path that assigns role groups/applications, with a comment stating it 'published nothing before' — additional to the SyncAssignments/RemoveAccess calls already documented in Step 6/Common Issues and previously confirmed 'existing, unchanged' on 2026-07-18. The diff hunk doesn't resolve to a method-signature context line, so this can't be pinned to a specific endpoint from code alone; whether it narrows a real gap in the Step 6 workaround's 'clears on your next request automatically' claim is UNCLEAR — flag for live re-verify rather than editing Step 6 on this alone. Unrelated to Step 6's still-open question: OrganisationsController.Provision (plus CreateOrganisation/SwitchOrganisation/GetUserOrganisations) now also invalidates a new IUserSessionProfileCache entry on write — a caching layer this entry hasn't previously tracked — but this diff adds no ApplicationOrganisationApplications insert, so the flagged 'does provisioning grant the foodops app permission' question remains exactly as unconfirmed as before. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-27: haafai.app.identity (OrganisationsController.cs, 682f345..4d5cb5b, staging) closes cross-tenant authorization gaps on six routes this entry tracks the file for: GetOrganisation, UpdateOrganisation, GetOrganisationUsers, GetOrganisationApplications, TransferOwnership and GetOrganisationSubscription now all require the caller to be a member (or, for TransferOwnership, the current owner) of the target organisation via new CallerId/Denied() helpers, where previously any authenticated caller could read/rename ANY organisation, list its users/apps/subscription, or hijack ownership outright. This does not touch the Provision method (Step 6's cited lines 279-432) — the open question of whether org-provisioning auto-grants the foodops app permission is unaffected and still unconfirmed from code. It also doesn't contradict anything documented: a legitimate org member retains access to their own org (the membership check passes), so the golden onboarding/Step-6 workaround path is unchanged. One noted side effect worth a live check if it ever surfaces in support: TransferOwnership is now permanently unusable on any legacy organisation whose OwnerId is null (accepted trade-off per the commit's own comment — no owner exists to satisfy the new check), which would need an out-of-band DB fix rather than the in-app flow. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-19: foodops.counter.app (ProtectedRoute.tsx, aad7025..99c4a32) adds a 10s watchdog (AUTH_GATE_MAX_WAIT_MS) around the existing auth-init/loading gate cited under Step 7 and the stale-while-revalidate note above. Previously a validateSession call that never settled (e.g. a hung IndexedDB read on a degraded connection) left the user stuck on a bare 'Loading...' spinner indefinitely. Now, if the gate is still unresolved after 10s, it swaps to an actionable overlay ('This is taking longer than usual' + Reload / Sign-in buttons) instead of navigating away — deliberately preserving the current URL (including /sessions/:id deep links) so a late-arriving validation still lands the user on the right page rather than bouncing through /login. This does not contradict anything documented here: the redirect-to-login-when-unauthenticated logic and the stale-while-revalidate caching for returning users are both untouched by this diff — this only bounds the unresolved case. Worth a line in Step 7 / Common Issues as a 'counter app stuck on Loading...' troubleshooting entry (reload, or wait ~10s for the fallback) once live-verified; not applicable to the admin-app onboarding wizard itself since this file is counter-app-only. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-18: hubits-api-sales (OnboardingApiController.cs, acfa332..074c027) added a class-level [ApiKeyOrSessionAuth] guard, so every /v1/onboarding/simplified/* endpoint — including GET /simplified/lookups, which backs the Step 4b locale step — now requires a valid session/API key. The controller's own doc comment previously called this endpoint 'anonymous-friendly' ('the wizard runs before a normal session is fully wired'); the corrected comment now says the opposite: 'the wizard runs post-login, so the session token is present even before the org context is fully wired.' This does not contradict anything documented here — Step 2 and OnboardingGate already describe the wizard as running only after login/verification, so the golden path is unaffected. It's noted because this entry already tracks onboarding auth/permission edge cases closely (see Step 6): a session that expires mid-wizard would now surface as a 401 on the locale step where the endpoint's prior anonymous-friendly behavior might have tolerated it — unconfirmed from code, flag for live re-verify if a mid-wizard session-timeout report ever surfaces. (auto-applied by the truth pipeline) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-18: haafai.app.identity (443714b..101b2e5) layers a new session-epoch mechanism on top of the documented permission-sync invalidation. UsersController.SyncAssignments and RemoveAccess now also bump the user's SecurityStamp (wrapped in a non-fatal try/catch — explicitly 'the role change is already saved and the pub/sub below must still fire') immediately before the existing, unchanged _sessionInvalidationPublisher.PublishUserAsync(id) call cited in Step 6 / Common Issues. Per the commit subjects in this range ('surface SecurityStamp as session epoch on validate-session', 'add cheap GET /api/auth/session-epoch endpoint') — files outside this entry's cited globs, so not directly confirmed here — this SecurityStamp is exposed as a lightweight session-epoch value so a satellite SPA can self-detect a stale session on next focus/navigation without a forced logout, i.e. a second, softer propagation path alongside the existing pub/sub fan-out. This does not contradict the documented claim (the publish call itself is untouched and still fires unconditionally on sync/removal); it's an additive robustness improvement worth a mention in Step 6 / the 'Permission error persists after update' row once the session-epoch endpoint itself is verified. Also in this range: OrganisationsController.GetUserOrganisations gained self-healing for a stale LastLoggedInApplicationOrganisationId (falls back to the user's first valid active org) — a different method than the cited Provision (lines 279-432) and unrelated to the documented onboarding/app-permission-grant question; no impact on this entry. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-05: haafai.app.identity (UsersController.cs, 822bf2c..fa91390) now enforces org-scoped authorization on user/role-management endpoints — GetUsers, GetUser, GetUserRoles, GetUserApplications, UpdateUser, SyncAssignments, UnlockUser and ChangePassword all require the caller to pass CanViewUsersInOrgAsync/CanManageUserAsync/CanManageUsersInOrgAsync checks (previously these endpoints had no such per-org guard visible in code). ChangePassword additionally now blocks anyone but the account holder from changing the password of a user who owns an organisation anywhere. This doesn't contradict the documented Step 6 workaround (admin edits a user's Name/app-grant via the Users surface, then Update) or the session-invalidation-on-sync behavior cited from this same file — _sessionInvalidationPublisher.PublishUserAsync is untouched in the diff — but it means the admin performing the workaround must themselves hold org-level manage rights, which is a new precondition worth confirming live doesn't produce an unexpected 403 for support staff attempting the fix. (auto-applied by the truth pipeline; adversarially gated)

Warning: 2026-06-29 — new-stack re-verify for July-10 launch (onboarding MOVED) · codebase Verified_by: codebase · Last verified: 2026-06-29 · foodops-admin-app@4173811 (main) + haafai.app.identity@31e7739 (staging) + foodops.counter.app@32669a9 (main) + hubits-api-sales@663dae0 (main) Found where onboarding actually happens now: the legacy app.foodops.io Razor wizard is retired. Onboarding runs in the admin app (admin.foodops.io): sign-up /register (RegisterPage → Identity API POST /registration/register, password min 6), verify /verify-email (POST /registration/verify-email, FoodopsEmailVerification OTP), then OnboardingGate provisions the org (POST /organisations/provision) and routes through /onboarding/business → /onboarding/locale → /onboarding/outlet (persisted via sales API /v1/onboarding/simplified/*). brands.ts models steps plan→account→business→locale→outlets; foodops auto-stamps industry (no industry field). NEW: a dedicated locale step; org auto-starts a free trial (IsOnTrial=true) so plan choice is no longer a blocking pre-onboarding gate. The hubits-onboarding-api Rust scaffold is not used. Permission bug status CHANGED: root cause #1 (blank name) is FIXED — RegistrationController.cs:72 auto-derives FullName from email; but OrganisationsController.Provision (lines 279-432) grants the full Administrator role group yet shows no explicit ApplicationOrganisationApplications (enable-foodops) insert and self-describes as mirroring the legacy New(), so whether the "no permission to use foodops" error still reproduces is NOT determinable from code — flagged UI-TODO for a fresh live signup (ApplicationOrganisationHelper.Create is in a shared package, unread). Dropped legacy haafai.app.foodops citation; re-pinned all SHAs.

Warning: Code-verified 2026-05-04 · codebase only (auth.json stale) Verified_by: codebase · Last verified: 2026-05-04 Globs curated to three real repos: haafai/haafai.app.identity (Razor pages + API controllers for SSO/registration/users), haafai/haafai.app.foodops (the multi-step Razor onboarding wizard), and haafai/foodops.counter.app (post-onboarding auth UI). Drift detector v2 had this entry as HIGH-rank #21 due to fallback to all 191 counter.app commits — that was a glob mismatch (src/auth/** matched nothing; the real path is src/components/authentication/**). With curated globs: haafai.app.identity is dormant (no commits since watermark — known onboarding bug is still present), haafai.app.foodops Onboarding is dormant, foodops.counter.app has a small-but-real signal (counter auth migration to Haafai Identity API, login form refresh, stale-while-revalidate session). UI labels in the documented Steps 1-7 likely still accurate but unverified — promoting to playwright re-verify is recommended once auth.json refresh is available.

Note: 2026-06-04 — Content re-verify (CHANGED) · codebase Verified_by: codebase · Last verified: 2026-06-04 Re-verified against haafai.app.identity@ebaa30a (master), foodops.counter.app@06d4e4c (main), hubits-api-sales@720407c (main). Correction to the prior "dormant" claim: haafai.app.identity had 3 commits since the watermark — 56d11d3c (redirect to onboarding wizard on org creation), ddbc6750 (filter app radio buttons in new-org modal), and the 2026-06-03 merge ebaa30ac of feature/106-authentication-module. The known onboarding permission bug is kept as documented — OrganisationsRegistrationsController.New() still does not auto-grant the foodops app-module permission, so the Step 6 workaround stands (could not confirm a fix from code; live re-verify recommended). Brief's driver 8054674 ("Salesmade") is a white-label retail-POS rebrand of counter.app, not a Foodops onboarding change — no impact on the documented flow. Also noted: OnboardingApiController locale endpoints (3ef93e9d). Documented Steps 1-7 UI labels still unverified live (auth.json stale) — promote to Playwright when available.

Additional verification events are appended here by the weekly KB sync and by manual re-verification passes. See _meta/failure-log.md for the global failure index.

Go beyond POS.
Choose the total solution.

Focus on creating the best experience for your guests while we handle the rest.