stralos-feature-onboarding is the Module Federation remote that owns the onboarding checklist. It renders a persistent launcher in the bottom-right corner for company admins and expands into a step-by-step panel that guides a new company through its initial configuration. It also owns the welcome dialog shown to admins while the Projects area is locked for setup.
What this remote owns
The remote exposes./widget, which exports the widget together with its pass-through RemoteProvider and REMOTE_KEY. The shell wraps the widget once. The always-loaded entry stays Chakra-free; the full panel loads on first open.
Routes: none. The checklist is a floating overlay, not a page.
Checklist sections and steps
The catalogue (src/catalogue.ts) defines every section and step in display order. Step ids are persisted server-side in organisation_settings.onboarding.reviewedSteps and must never be renamed.
Sections
The
estimate section is locked until a project id is known.
Section open state
The panel keeps at most one section open at a time (openSection: SectionId | null in OnboardingPanel). On mount, openSection is initialised to progress.firstIncompleteSection. Selecting a section header opens it and closes whichever was open. Selecting the already-open section’s header closes it, leaving openSection as null. When a section transitions to complete (detected via progress.firstIncompleteSection changing), the panel sets openSection to the new first incomplete section, or to null if none remains.
Step detection
src/checks.ts runs all reads in parallel under Promise.allSettled. A failed read, a non-2xx response, or a missing field yields undefined (shown neutral, never counted as done or not done). Steps are ticked by detection or by a person clicking Looks good (markReviewed PATCH).
GET /api/global/organisation-settings, GET /api/global/org/users, and GET /api/documents/drive-sources go through the shared-read store (see Cross-remote shared reads). All other detection reads go directly to the network.
Office step
Theoffice step ticks as soon as the organisation has at least one own active location. It does not depend on the organisation’s defaultLocationId. Each user’s default office falls back to the first-created office until they choose their own.
The step copy reads: “Projects run out of an office; your first office becomes everyone’s default until they choose their own.”
Resources and drive-sync steps
resources is hidden when the mvp feature flag is set. shared-drive is hidden unless the server’s drive-sync switch answers ON (drive-sources/onboarding-config enabled). Hidden steps are not shown and not counted.
State management
src/onboardingClient.ts handles the state reads and writes.
The PATCH body accepts
{ markReviewed?, unmarkReviewed?, dismissed?, completed? } and requires at least one field. Only admins may write; any member may read. A new organisation can store a null onboarding value. The API normalises it to { reviewedSteps: [], dismissedAt: null, completedAt: null }, so the checklist reads an empty progress state.
Cross-remote shared reads
Module Federation bundles each remote independently, so the sameGET issued by two remotes on one page load cannot coalesce at the network level. Three reads that this remote and the organisation remote (Settings page) make together are routed through the cross-remote shared-read store (src/sharedReads.ts).
The store lives on globalThis.__stralosSharedReadsV1 (contract v1). An entry keyed by ${organisationId}:${userId}\x00${url} is reused for up to 30 seconds. Reads that fail are never stored; if a joined promise rejects, the caller retries with its own transport.
Invalidation. Every onboarding PATCH (
markReviewed, dismissed, completed) calls invalidateSharedReads for the organisation-settings path in its finally block, so a write that is refused or dropped still forces the next read to go to the network. The organisation remote is responsible for invalidating org/users and drive-sources after its own writes.
Contract version. The global key is __stralosSharedReadsV1. If the store contract ever changes, the key must be bumped to V2 in every remote that carries a copy, so mixed deploys bypass each other instead of misreading entries written with a different shape.
Callers pass scope as ${organisationId}:${userId} from sharedReadScope() in src/session.ts. Passing null bypasses the store entirely (used for reads where the scope is not yet known). Passing force: true skips the cache and refreshes the stored entry.
Detection trigger
On first load, once the flags have answered and only for an eligible admin (the checklist flag is on and the session role isadmin), the widget reads organisation-settings alone. A member never costs the API a read. Only if the checklist is still open (neither completed nor dismissed) does it then call runDetection, passing readSettings: false to skip the duplicate settings read. This keeps a completed or dismissed organisation at one read per hard load regardless of how many check endpoints exist. A failed settings read shows nothing and spends no further reads.
After the initial load, runDetection is called when the panel opens, on a throttled route change, and on window focus. It never polls.
The billing read is not performed by this remote. The shell mounts the floating-end slot only inside its billing boundary’s unblocked branch, so a separate billing read would duplicate the shell’s own call.
Launcher open state on sign-in
The guide opens expanded on every sign-in. A minimise holds for the rest of that sign-in, across reloads and route changes, and the guide opens again on the next sign-in (src/launcher/launcherState.ts).
The sign-in id comes from src/launcher/signIn.ts:
stralos.auth.signInIdinlocalStorage, when the shell has written one. The shell writes it when a hand-off redemption or OIDC exchange stores a new session, and removes it inclearSession(see localStorage session keys). Token refresh and company switching leave it unchanged. The remote reads this key and never writes it.- Otherwise, a per-tab id in
sessionStorage, minted on first read. It survives reloads and route changes in that tab and is forgotten when the tab sees the session end (watchSessionEnd). A new tab gets a new id, so the guide opens there once.
localStorage under stralos.onboarding.launcher.<organisationId>.<userId> as { "minimisedIn": [<sign-in id>, …] }, newest last, keeping the last eight ids (MAX_REMEMBERED_SIGN_INS). Keeping it per company and per user means two admins sharing a browser each get their own state. Any other stored value, including the old bare "expanded" or "minimised" strings, reads as “not minimised in this sign-in”, so the guide shows. Storage that is absent, full or refused never breaks the launcher.
Welcome dialog
src/welcome/ implements the welcome dialog: a full-screen ui Dialog titled Welcome to Stralos. It is the front door to the shell’s Projects setup lock (see Projects setup lock) and loads as its own lazy chunk only when it is shown.
Gate
useWelcome (src/welcome/useWelcome.ts) opens the welcome only when all of these hold:
- The
onboarding-setup-lockPostHog flag is on: PostHog has answered and the value istrue. Still loading, a PostHog error, a missing or inactive flag, or a headless browser with no flags all count as off. This is the same flag that gates the shell’s Projects lock, so both surfaces switch on and off together. - The session role is
admin, and the person does not reach this company through a contractor company.src/session.tssetsviaContractorwhen the current membership instralos.auth.usercarries a non-blankviaContractorName. - This sign-in has not settled the welcome yet (see Settlement).
GET /api/global/organisation-setupanswerscomplete: falseandexempt: false.
onboarding-checklist flag or the checklist’s dismissed state. While it is shown, the checklist launcher and panel are hidden.
fetchSetupStatus (src/welcome/setupStatus.ts) fails open. Anything other than a 200 whose body has boolean complete, exempt, steps.office, steps.client and steps.rate (a 404, a 5xx, a dropped request, or a body of the wrong shape) returns null, and null never shows the welcome.
Contractor-companies flows
On/settings/client-companies, any path under /settings/client-companies/, and any path under /settings/contractor-invite/ (isContractorFlowPath in src/OnboardingWidget.tsx), an open welcome stays unseen. It shows once the person leaves those flows, so it never covers creating a client company, bringing data across, or accepting a contractor link.
Steps
The dialog lists the three steps that unlock Projects, in order:
Each step shows a tick when its
steps value is true. When every step is done, or the company is exempt, the dialog offers Create your first project. Focus lands on the first step still to do, otherwise on Create your first project.
While the dialog is open, a window focus event re-reads the setup status, so a step finished in another tab shows its tick when the admin returns. A failed re-read keeps the ticks already shown.
Settlement
The welcome is asked at most once per sign-in, using the same sign-in id as the launcher. A sign-in settles as soon as the setup read gives a real answer, whether or not the welcome then shows. A failed read settles nothing, so the next load asks again, but a failure never shows the welcome. Settlement is stored inlocalStorage under stralos.onboarding.welcome.<organisationId>.<userId> as { "settledIn": [<sign-in id>, …] }, keeping the last eight ids. When there is no sign-in id, or storage is absent or refused, the welcome reads as already settled, so a dialog that cannot remember being closed does not come back on every load.
Foreign-layer behaviour
The launcher and panel are aware of other remotes’ overlays. When a zag dialog withdata-placement="end" (a side sheet) is open and it is not owned by this remote, the launcher and panel slide left by 16px + sheet width over 200 ms. When no room remains (viewport − sheet < widget + 32), they step out instead. A centred modal also causes a step out. Stepping out preserves the panel’s open state; a Help panel that was open waits for the layer to close, then takes focus again.
Bottom offset
The launcher and panel sit above two optional spaces, both read from custom properties on<html> (BOTTOM_SPACE in src/launcher/launcherStyles.ts):
--stralos-bottom-bar-height: set by a page that owns a sticky bottom bar while the bar is mounted, and removed when it unmounts. Two bars publish it: the project settingsSettingsSaveBar(feature-project-settings) and the Company defaults sticky footer (OrganisationDefaultsSaveScopeinfeature-organisation, rendered for admins only). Each measures its bar with aResizeObserver, so the Setup pill sits just above Save changes without reserving empty page space. The launcher has no path-based special case:/settings/*shows the full pill, and only this property moves it.--stralos-assistant-pill-space: reserved for a host-owned bottom-end pill. The shell no longer sets it, because Talk with Doug is now a header button.
0px when unset, so the launcher does not move on pages without a bar.
Contract-v2 model
This remote follows the thin-host contract v2. The host providesreact, react-dom, and react-router-dom as ranged singletons declared in .ci/shell-federation-contract.json. @stralos-co-nz/ui and @stralos-co-nz/domain are bundled as exact pins. Fonts are the shell’s; nothing under src/ may import a @fontsource/* path.
The ./widget entry point must stay Chakra-free. The panel (src/panel/OnboardingPanel.tsx) is a dynamic import() on first open and mounts ui’s real RemoteProvider around itself.
Build and release pipeline
The pipeline follows the produce-gate-activate model described in Micro-frontends and Release model. A push tomain triggers deploy-dev.yml, which builds the remote, writes built-against.json, publishes the by-SHA bundle to remotes/onboarding/<sha>/, and nudges the reconciler. build-remote.yml builds and verifies on every push and pull request without publishing.
