modernx-admin — First-run (tenant-less) workspace behavior
A brand-new signup has an org and a session but no connected Magento store
yet. Until 2026-07-30 that state caused a 500 storm on /signup and
/login, because a tenant-scoped bootstrap query ran unconditionally on
every route. Two conventions now keep a tenant-less workspace from crashing
anywhere in the app.
Convention: client-side tenant bootstraps only mount inside (admin)
Section titled “Convention: client-side tenant bootstraps only mount inside (admin)”app/providers.tsx is the ROOT client provider tree — it mounts for every
route, including unauthenticated /login and /signup. It owns only
QueryClientProvider and ToastProvider. Anything that bootstraps
tenant-scoped data (like StoreContextProvider, lib/store-context.tsx)
must NOT go here.
StoreContextProvider instead mounts from components/platform/AdminProviders.tsx,
which app/(admin)/layout.tsx renders inside the authenticated shell —
after the layout’s session/getCurrentUser() check. A signed-out request to
/login or /signup never reaches this tree, so it never fires
StoreContextProvider’s listStoreViews query.
Why this matters: listStoreViews is a 'use server' action, and Next.js
server actions POST to the current page URL. Mounting its caller at the
root meant the action ran as POST /signup and POST /login — not an
admin route — for every page load, authenticated or not.
When adding a new client provider that reads tenant-scoped data, mount it
under AdminProviders/(admin), not app/providers.tsx.
Convention: tenant-scoped server actions degrade to empty, never throw
Section titled “Convention: tenant-scoped server actions degrade to empty, never throw”getActiveTenant() returning null (no connected store) is an expected,
common state — every fresh signup starts there — not an error. Actions that
depend on an active tenant follow a “degrade to empty, never throw”
contract for exactly that case:
listStoreViews()(lib/backends/magento/actions/stores.ts) catchesMagentoNotConfiguredErrorand resolves to[]. Any other error (transport, auth, schema) still rejects — this only absorbs the no-tenant case, not general Magento failures.app/(admin)/page.tsxresolveshasConnectedStoreserver-side (tenant !== null) and skips its pinned-reports query entirely when there’s no tenant, same patterngetActiveTenant()itself documents.
This is intentionally narrow: it lives in the action layer, not in
tenantMagentoFetch itself, because every other merchandising action
relies on that throw to drive its own per-page error UI. Widening the
catch into the shared fetch layer would silently turn real store failures
into “no data” states across the app.
Result: the dashboard has a real first-run state
Section titled “Result: the dashboard has a real first-run state”HomeDashboard (components/dashboard/HomeDashboard.tsx) takes a
server-resolved hasConnectedStore prop. When false, it renders a
“No connected store yet” empty state with an indigo “Connect a store” CTA
to /stores, and every merch query (listRules, listBanners,
listRuleProposals, listRecentHistory, listFeedHealthAlertsAction) is
passed enabled: hasConnectedStore so none of them fire for a tenant-less
workspace — instead of a grid of '—' tiles with silent failed queries
underneath.
Source: modernx-admin PR #399.