Skip to content

Admin / Merchant Shell Split โ€” Design Spec โ€‹

Date: 2026-04-27 Status: Approved (pending implementation plan) Scope: Restructure apps/admin/ into two role-scoped shells (AdminShell, MerchantShell) with a shared library zone, enforce the boundary at lint + build time, and align the auth API so both shells have well-defined surfaces. Merchants who are also Lantern users get a working self-service experience that doesn't pierce admin gates.

Problem โ€‹

Iterating on the merchant integration surfaced a recurring pattern: every time a merchant-role user touches the admin app, something breaks (admin-only API gates, admin-shaped sidebar, "Merchant not found" landing failures, role-conditional UI cluttering shared components). The root cause is that apps/admin/ was built admin-first with merchants retrofitted via a merchantOnly flag and a MerchantsIndexRedirect. Every page assumes admin context.

Splitting into two shells with hard boundaries:

  • Eliminates role conditionals across every page
  • Gives merchants a clean, purpose-built experience
  • Lets admins enter "merchant mode" deliberately (with switcher privileges) when they need to see what merchants see
  • Sets up the eventual apps/merchant/ separate-app split as a mechanical lift, not a rewrite

Constraints โ€‹

  • Single PR, multi-commit. Each commit leaves the working tree green. No "halfway-migrated" intermediate state.
  • No regression in admin functionality. Every existing admin URL keeps working (via back-compat redirects for one release cycle).
  • Strict validation, not convention. Cross-shell imports fail lint with error severity. Build-time chunk-graph validation catches dynamic imports that lint can't see.
  • No new shared package. Inside one Vite project, @shared/* path aliases are sufficient. Package extraction (@lantern/ui) only when a second app needs the chrome.
  • Symmetric URL space. Both shells get a top-level prefix (/admin/*, /merchant/*). No "admin-at-root, merchant-prefixed" asymmetry that leaks into every routing conversation forever.

v1 Scope โ€‹

Architecture โ€‹

Two role-scoped shells. The top-level fork happens in App.jsx after Firebase auth state is resolved.

AdminShellMerchantShell
URL prefix/admin/*/merchant/:merchantId/*
AudiencecustomClaims.role === 'admin'role === 'merchant' (own merchantId) OR role === 'admin' (any merchantId, via switcher)
Default landing/admin/ (DashboardHome)/merchant/:id/overview
SidebarDashboard, Users, Merchants (list), Docs, Storybook, API, Config, Analytics, System, Billing, Profile, Feature TrackerOverview, Offers, Venues, Notes, Photos, Address, Profile, โ† Admin (admin-only), Sign out
Entry from the othern/aAdmin sidebar "Merchants" โ†’ /admin/merchants/all โ†’ row click โ†’ window.open('/merchant/:id/overview', '_blank')

Three guarantees:

  1. A merchant can never reach admin URLs. Real merchants accessing /admin/* are redirected to /merchant/<theirId>/overview. Enforced at the route guard, validated again at the API by requireAdmin.
  2. An admin can reach either shell. Admins entering merchant mode use elevated switcher privileges (any merchantId).
  3. No file in src/admin/ or src/merchant/ ever imports from the other. Enforced by ESLint import/no-restricted-paths (error severity) + a build-time chunk validator.

Directory layout โ€‹

apps/admin/src/
โ”œโ”€โ”€ App.jsx                     โ† role fork; URL-mode handlers
โ”œโ”€โ”€ main.jsx                    โ† Vite entry
โ”œโ”€โ”€ firebase.js                 โ† stays at root (re-exports; will progressively shrink)
โ”‚
โ”œโ”€โ”€ shared/
โ”‚   โ”œโ”€โ”€ components/             โ† LanternLogo, PageHeader, PageTabs, StyledSelect,
โ”‚   โ”‚                             SetAdminPassword, SetMerchantPassword, SetPassword,
โ”‚   โ”‚                             LoadingScreen, AccessDenied, LoginScreen,
โ”‚   โ”‚                             AdminMigrationBanner, ReinitializeLanternModal
โ”‚   โ”œโ”€โ”€ lib/
โ”‚   โ”‚   โ”œโ”€โ”€ apiClient.js
โ”‚   โ”‚   โ”œโ”€โ”€ authApi.js          โ† admin auth + merchant auth endpoint clients
โ”‚   โ”‚   โ”œโ”€โ”€ merchantApi.js      โ† role-aware: picks /auth/admin/merchants/:id vs /auth/merchant/me
โ”‚   โ”‚   โ”œโ”€โ”€ currentMerchant.js
โ”‚   โ”‚   โ””โ”€โ”€ env.js
โ”‚   โ”œโ”€โ”€ hooks/
โ”‚   โ””โ”€โ”€ styles/                 โ† styles.css, design tokens
โ”‚
โ”œโ”€โ”€ admin/
โ”‚   โ”œโ”€โ”€ AdminShell.jsx          โ† sidebar + Routes for /admin/*
โ”‚   โ”œโ”€โ”€ DashboardHome.jsx
โ”‚   โ”œโ”€โ”€ users/                  โ† UserManagement, UserDetailPanel, CreateAdminForm,
โ”‚   โ”‚                             CreateMerchantUserForm, AttachMerchantDialog
โ”‚   โ”œโ”€โ”€ merchants/              โ† MerchantsAll (list), MerchantsCreate, CreateMerchantForm
โ”‚   โ”œโ”€โ”€ docs/                   โ† DocsEditor, MarkdownEditor, DocsEmbed, DocPage,
โ”‚   โ”‚                             SelfHostedDocsEditor
โ”‚   โ”œโ”€โ”€ api/                    โ† ApiOverview, ApiReferencePage, ClientSdkPage, ClientSdkEmbed
โ”‚   โ”œโ”€โ”€ storybook/              โ† StorybookEmbed
โ”‚   โ”œโ”€โ”€ config/                 โ† ConfigOverview, ConfigServicesCors, ConfigRateLimits, ConfigVenue
โ”‚   โ”œโ”€โ”€ analytics/              โ† (existing analytics subtree)
โ”‚   โ”œโ”€โ”€ system/                 โ† SystemHealth
โ”‚   โ”œโ”€โ”€ billing/                โ† Billing
โ”‚   โ”œโ”€โ”€ feature-tracker/        โ† FeatureTracker
โ”‚   โ”œโ”€โ”€ profile/                โ† MyAdminProfile
โ”‚   โ””โ”€โ”€ lib/
โ”‚       โ””โ”€โ”€ adminApi.js         โ† admin-only API clients
โ”‚
โ””โ”€โ”€ merchant/
    โ”œโ”€โ”€ MerchantShell.jsx       โ† sidebar + Routes for /merchant/:merchantId/*
    โ”œโ”€โ”€ MerchantSwitcher.jsx    โ† dropdown; admin-only
    โ”œโ”€โ”€ tabs/                   โ† Overview, Offers, Venues, Notes, Photos, Address
    โ”œโ”€โ”€ offers/                 โ† OfferDetail, OfferForm, OffersList
    โ”œโ”€โ”€ profile/                โ† MyMerchantProfile (new)
    โ””โ”€โ”€ lib/
        โ””โ”€โ”€ merchantSelfApi.js  โ† merchant-only thin client

Component fates:

  • MerchantDetail.jsx โ€” deleted. Its responsibility (route shell + tab switching) is replaced by MerchantShell.jsx.
  • MerchantsIndexRedirect.jsx โ€” deleted. The role fork in App.jsx makes it obsolete.
  • MerchantSwitcher.jsx โ€” moves into the merchant shell (it's a merchant-shell affordance).
  • Existing merchants/tabs/Merchant*Tab.jsx files โ€” renamed (drop the "Tab" suffix) and moved to src/merchant/tabs/ since each is now a top-level page, not a tab strip entry.

Path aliases โ€‹

apps/admin/vite.config.mjs and apps/admin/vitest.config.mjs:

js
resolve: {
  alias: {
    '@admin': path.resolve(__dirname, 'src/admin'),
    '@merchant': path.resolve(__dirname, 'src/merchant'),
    '@shared': path.resolve(__dirname, 'src/shared'),
  },
}

Lint enforcement (apps/admin/.eslintrc.json) โ€‹

json
{
  "rules": {
    "import/no-restricted-paths": ["error", {
      "zones": [
        {
          "target": "./src/admin",
          "from": "./src/merchant",
          "message": "Admin code may not import from src/merchant. Move shared code to src/shared/."
        },
        {
          "target": "./src/merchant",
          "from": "./src/admin",
          "message": "Merchant code may not import from src/admin. Move shared code to src/shared/."
        },
        {
          "target": "./src/shared",
          "from": ["./src/admin", "./src/merchant"],
          "message": "src/shared may not depend on shell-specific code. Inline or invert the dependency."
        }
      ]
    }]
  }
}

App.jsx, main.jsx, and firebase.js at the root are the bootstrap layer. They are not in src/admin/ or src/merchant/ and are allowed to import from any of the three zones.

Build-time bundle validator โ€‹

tooling/scripts/check-shell-isolation.mjs runs after npm run build (added to npm run validate). Reads Vite's dist/.vite/manifest.json and asserts:

  • The chunk graph reachable from MerchantShell.jsx's entry includes no module under src/admin/.
  • The chunk graph reachable from AdminShell.jsx's entry includes no module under src/merchant/.

Catches dynamic import() paths that ESLint can't statically analyze.

URL space โ€‹

Admin shell route tree (/admin/*) โ€‹

/admin/                          โ†’ DashboardHome
/admin/users                     โ†’ UserManagement
/admin/merchants                 โ†’ redirects to /admin/merchants/all
/admin/merchants/all             โ†’ MerchantsAll (list โ€” entry to merchant mode)
/admin/merchants/new             โ†’ MerchantsCreate
/admin/docs/*                    โ†’ DocsEditor subtree
/admin/storybook                 โ†’ StorybookEmbed
/admin/api/*                     โ†’ ApiOverview / ApiReferencePage / ClientSdkPage
/admin/config/*                  โ†’ ConfigOverview / ConfigServicesCors / ConfigRateLimits / ConfigVenue
/admin/analytics/*               โ†’ analytics subtree
/admin/system                    โ†’ SystemHealth
/admin/billing                   โ†’ Billing
/admin/profile                   โ†’ MyAdminProfile
/admin/feature-tracker           โ†’ FeatureTracker

Note: there is no admin route that renders the merchant tabs. Admins view those only through the merchant shell. MerchantsAll rows include an "Open in merchant mode" affordance that calls window.open('/merchant/:id/overview', '_blank').

Merchant shell route tree (/merchant/:merchantId/*) โ€‹

/merchant/:id/                   โ†’ redirects to overview
/merchant/:id/overview           โ†’ Overview
/merchant/:id/offers             โ†’ OffersList
/merchant/:id/offers/new         โ†’ OfferForm (mode=create)
/merchant/:id/offers/:offerId    โ†’ OfferDetail
/merchant/:id/offers/:offerId/edit โ†’ OfferForm (mode=edit)
/merchant/:id/venues             โ†’ Venues
/merchant/:id/notes              โ†’ Notes
/merchant/:id/photos             โ†’ Photos
/merchant/:id/address            โ†’ Address
/merchant/:id/profile            โ†’ MyMerchantProfile

Route guards โ€‹

At the App.jsx fork (before any shell renders):

  • Real merchants accessing /admin/* โ†’ redirected to /merchant/<theirId>/overview
  • Real merchants accessing / or any non-admin/non-merchant path โ†’ redirected to /merchant/<theirId>/overview
  • Admins accessing / or any non-admin/non-merchant path โ†’ redirected to /admin/

Inside MerchantShell (additional self-merchant guard):

  • Real merchants whose URL :merchantId does not match their claimed merchantId โ†’ redirected to their own merchant
  • Admins are free to navigate to any :merchantId

Backwards-compatibility redirects โ€‹

For one release cycle, redirect old admin URLs to the new /admin/* namespace. The shipping list:

Old URLNew URL
/users/*/admin/users/*
/docs/*/admin/docs/*
/storybook/admin/storybook
/client-sdk/*/admin/api/client-sdk/* (note: was top-level, now nested under api)
/system/admin/system
/billing/admin/billing
/profile/admin/profile
/feature-tracker/admin/feature-tracker
/api/*/admin/api/*
/config/*/admin/config/*
/analytics/*/admin/analytics/*
/merchants/admin/merchants/all
/merchants/all/admin/merchants/all
/merchants/new/admin/merchants/new
/merchants/:merchantId/*/merchant/:merchantId/* (note: pluralโ†’singular)

Track removal via a TODO comment + a /schedule follow-up agent (~2 weeks after merge).

Backend โ€” shared handlers + dual route mounts โ€‹

The merchant data API needs to serve both:

  • Admins via /auth/admin/merchants/:merchantId/* (any merchantId)
  • Merchants via /auth/merchant/me/* (merchantId resolves from token claim)

Refactor:

  • Extract route handlers from services/api/auth/src/routes/adminMerchants.js into pure functions in a new services/api/auth/src/handlers/merchantHandlers.js.
  • adminMerchants.js becomes a thin router that wires handlers under admin paths with requireAdmin middleware (unchanged behavior from today).
  • New services/api/auth/src/routes/merchantSelf.js mounts a subset of those handlers under /me. A small middleware injects req.params.merchantId = req.user.merchantId before the handlers run.

New middleware: requireMerchant in services/api/auth/src/middleware/auth.js (mirror of requireAdmin).

What's mounted on each route:

Handler/auth/admin/merchants/:id/.../auth/merchant/me/...
getMerchantDetailโœ“ (any merchant)โœ“ (own merchant)
listMerchantsโœ“โœ— โ€” admin only
createMerchantUserForMerchantโœ“โœ— โ€” admin onboards
detachUserFromMerchantโœ“โœ— โ€” admin manages
associateVenueWithMerchantโœ“โœ— โ€” admin claims
disassociateVenueFromMerchantโœ“โœ— โ€” admin un-claims

For v1, only getMerchantDetail is mounted on /me. Adding more handlers later is a single-line change in merchantSelf.js.

Client-side routing: in src/shared/lib/merchantApi.js, getMerchantDetail(merchantId) checks the current Firebase token's role claim and dispatches to the admin URL or the /me URL accordingly. Each shell calls the same function.

Merchant write paths:

  • Offers (services/api/merchants/, port 8085) โ€” already merchant-accessible via Firestore rules. No change.
  • Photos (Firebase Storage) โ€” existing merchant-writable rules. No change unless gaps surface.
  • Business name / contact / notes / address โ€” currently admin-only via PATCH /auth/admin/users/merchant/:userId (and merchant business updates). Add merchant-self versions in the migration phase if/when needed; otherwise the merchant tabs initially treat those as read-only and admin handles edits.

Migration sequencing (phased) โ€‹

Each commit leaves the tree green.

Phase A โ€” Foundation (4 commits, no behavior change)

  1. Add path aliases to vite.config.mjs + vitest.config.mjs.
  2. Create empty src/shared/, src/admin/, src/merchant/ (with .gitkeep).
  3. Add ESLint import/no-restricted-paths rule (zones empty initially).
  4. Add tooling/scripts/check-shell-isolation.mjs wired into npm run validate.

Phase B โ€” Move shared infrastructure (5 commits) 5. Move design tokens / styles.css to src/shared/styles/. 6. Move LanternLogo.jsx to src/shared/components/. 7. Move PageHeader, PageTabs, StyledSelect, LoadingScreen, AccessDenied to src/shared/components/. 8. Move LoginScreen, SetAdminPassword, SetMerchantPassword, SetPassword, AdminMigrationBanner, ReinitializeLanternModal to src/shared/components/. 9. Move lib/apiClient.js, lib/authApi.js, lib/currentMerchant.js to src/shared/lib/.

Phase C โ€” Build the shell skeletons (4 commits) 10. Create src/admin/AdminShell.jsx (initially imports admin pages from old paths). 11. Create src/merchant/MerchantShell.jsx (with placeholder pages). 12. Update App.jsx with the role fork + back-compat redirects. 13. Add requireMerchant middleware to the auth API.

Phase D โ€” Migrate admin pages (7 commits) 14. UserManagement subtree โ†’ src/admin/users/ 15. Docs subtree โ†’ src/admin/docs/ 16. API/SDK subtree โ†’ src/admin/api/ 17. Analytics subtree โ†’ src/admin/analytics/ 18. Misc admin pages (Billing, SystemHealth, MyAdminProfile, FeatureTracker, StorybookEmbed) โ†’ src/admin/{billing,system,profile,feature-tracker,storybook}/ 19. Admin-side merchants management (MerchantsAll, MerchantsCreate, CreateMerchantForm + "Open in merchant mode" affordance) โ†’ src/admin/merchants/ 20. Update AdminShell.jsx imports to @admin/* paths.

Phase E โ€” Migrate merchant pages (6 commits) 21. MerchantOverviewTab โ†’ src/merchant/tabs/Overview.jsx (rename, drop "Tab" suffix). 22. MerchantOffersTab/VenuesTab/NotesTab/PhotosTab/AddressTab โ†’ corresponding pages in src/merchant/tabs/. 23. OfferDetail, OfferForm, OffersList โ†’ src/merchant/offers/. 24. MerchantSwitcher โ†’ src/merchant/MerchantSwitcher.jsx. 25. Delete MerchantDetail.jsx and MerchantsIndexRedirect.jsx. 26. Replace placeholders in MerchantShell.jsx with real imports.

Phase F โ€” Backend dual-mount (3 commits) 27. Extract handlers from routes/adminMerchants.js to handlers/merchantHandlers.js. Old route file becomes thin wiring. 28. Add merchantSelfRoutes at routes/merchantSelf.js. Mount under /auth/merchant/me. 29. Update src/shared/lib/merchantApi.js to do role-aware URL selection.

Phase G โ€” Wire merchant write paths (2 commits, may grow) 30. Audit each merchant-shell page that writes data; document which endpoints are already merchant-accessible vs need /me mounting. The audit's findings drive how many follow-on commits are needed in this phase โ€” if every write path turns out to use services/api/merchants/ (already merchant-accessible) or read-only fields, no further commits. If gaps exist, each gap = one additional commit (one per /me mount or new endpoint). Estimate 2-6 total commits including the audit. 31. Add /me mounts and/or new endpoints for any gaps surfaced by step 30.

Phase H โ€” Profile + tests (4 commits) 32. Create src/merchant/profile/MyMerchantProfile.jsx. 33. Component tests for MerchantShell (renders, redirects on role mismatch, switcher visibility). 34. Component tests for AdminShell (renders, "Merchants" opens new tab โ€” mock window.open). 35. Route-guard test: real merchant accessing /admin/users redirects to /merchant/<theirId>/overview.

Phase I โ€” Final validation + PR (1 commit + close-out) 36. Run npm run validate end-to-end. Manual smoke matrix. Open PR.

Estimated total: ~36-40 commits (Phase G's audit may add a few), each small and self-contained.

Testing โ€‹

Component tests (vitest + Testing Library) โ€‹

  • src/admin/__tests__/AdminShell.test.jsx:

    • Renders the admin sidebar with all expected items
    • "Merchants" sidebar item navigates to /admin/merchants/all
    • MerchantsAll row click calls window.open('/merchant/<id>/overview', '_blank')
    • Routes correctly resolve admin sub-pages
  • src/merchant/__tests__/MerchantShell.test.jsx:

    • Real merchant whose merchantId === url:merchantId renders the shell
    • Real merchant whose merchantId !== url:merchantId redirects to their own
    • Admin viewing any merchant renders the shell with the switcher visible
    • Real merchant does NOT see the switcher
    • Real merchant does NOT see the "โ† Admin" sidebar item
    • Admin DOES see the "โ† Admin" sidebar item
  • src/__tests__/App.test.jsx (or update existing):

    • Real merchant lands on /admin/X โ†’ redirects to /merchant/<theirId>/overview
    • Admin lands on / โ†’ redirects to /admin/
    • Old /users URL โ†’ redirects to /admin/users
    • Old /merchants/<id> URL โ†’ redirects to /merchant/<id>/overview
    • Real merchant lands on /merchant/<other-id>/... โ†’ redirects to their own
  • src/shared/lib/__tests__/merchantApi.test.js:

    • When current user is admin โ†’ getMerchantDetail(id) calls /auth/admin/merchants/:id
    • When current user is merchant โ†’ getMerchantDetail(id) calls /auth/merchant/me

Existing tests updated:

  • apps/admin/src/components/__tests__/AdminDashboard.test.jsx supersedes / merges into AdminShell.test.jsx. Legacy AdminDashboard component goes away.
  • CreateMerchantUserForm.test.jsx, SetMerchantPassword.test.jsx โ€” import paths shift; logic unchanged.

Backend tests โ€‹

  • services/api/auth/src/handlers/__tests__/merchantHandlers.test.js: confirms each handler reads req.params.merchantId correctly given different mock requests.
  • services/api/auth/src/routes/__tests__/merchantSelf.test.js: confirms the /me middleware injects req.params.merchantId = req.user.merchantId.

The auth-API still has minimal route-level integration coverage. This PR does not add the integration scaffolding (deferred per existing "Known Gap"); the unit-level tests above cover the structural change.

Lint enforcement validation โ€‹

A one-shot test fixture (tooling/scripts/lint-fixture-cross-zone.mjs) creates a temporary file with a deliberately-bad import, runs ESLint on it, asserts it fails, then deletes the file. Guards against the rule being silently disabled or misconfigured.

Build-time bundle validation โ€‹

tooling/scripts/check-shell-isolation.mjs runs after npm run build as part of npm run validate. Reads Vite's dist/.vite/manifest.json and asserts no admin module is reachable from MerchantShell and vice versa.

Manual smoke matrix โ€‹

ActionReal merchantAdmin
Sign inLands on /merchant/<theirId>/overviewLands on /admin/
Visit /admin/usersRedirected to own merchant overviewRenders user management
Visit /merchant/<otherId>/overviewRedirected to own merchantRenders other merchant's overview
Visit old /usersRedirects, then merchant guard fires โ†’ own merchantRedirects to /admin/users
Open /merchant/<id>/offersLoads offers for own merchant onlyLoads offers for the picked merchant via switcher
Click "Merchants" in admin sidebarn/aNavigates to /admin/merchants/all
Click row in admin merchants listn/aOpens /merchant/<id>/overview in a new browser tab
Click "โ† Admin" in merchant sidebarDoesn't appearReturns to /admin/
Sign outReturns to LoginScreenReturns to LoginScreen
Forgot passwordSame parallel-fan-out as todaySame

Files Touched โ€‹

New โ€‹

  • apps/admin/src/admin/AdminShell.jsx
  • apps/admin/src/admin/__tests__/AdminShell.test.jsx
  • apps/admin/src/merchant/MerchantShell.jsx
  • apps/admin/src/merchant/MerchantSwitcher.jsx (moved + lightly adapted)
  • apps/admin/src/merchant/__tests__/MerchantShell.test.jsx
  • apps/admin/src/merchant/profile/MyMerchantProfile.jsx
  • apps/admin/src/shared/lib/merchantApi.js
  • apps/admin/src/shared/lib/__tests__/merchantApi.test.js
  • services/api/auth/src/handlers/merchantHandlers.js
  • services/api/auth/src/handlers/__tests__/merchantHandlers.test.js
  • services/api/auth/src/routes/merchantSelf.js
  • services/api/auth/src/routes/__tests__/merchantSelf.test.js
  • tooling/scripts/check-shell-isolation.mjs
  • tooling/scripts/lint-fixture-cross-zone.mjs

Renamed/moved (no logic change beyond import-path updates) โ€‹

  • All admin-only pages from apps/admin/src/components/ โ†’ apps/admin/src/admin/<sub>/
  • All shared chrome from apps/admin/src/components/ โ†’ apps/admin/src/shared/components/
  • Merchant tabs from apps/admin/src/components/merchants/tabs/ โ†’ apps/admin/src/merchant/tabs/ (drop "Tab" suffix)
  • Merchant offer flows from apps/admin/src/components/merchants/tabs/Offer*.jsx and OffersList.jsx โ†’ apps/admin/src/merchant/offers/
  • apps/admin/src/lib/*.js โ†’ apps/admin/src/shared/lib/*.js

Modified โ€‹

  • apps/admin/src/App.jsx โ€” role fork, back-compat redirects, URL-mode handlers
  • apps/admin/vite.config.mjs โ€” path aliases
  • apps/admin/vitest.config.mjs โ€” path aliases (mirror)
  • apps/admin/.eslintrc.json โ€” import/no-restricted-paths rule
  • apps/admin/package.json โ€” eslint-plugin-import dep if not already present
  • services/api/auth/src/routes/adminMerchants.js โ€” becomes thin wiring; handlers extracted
  • services/api/auth/src/middleware/auth.js โ€” adds requireMerchant
  • services/api/auth/src/index.js โ€” mounts merchantSelfRoutes under /auth/merchant/me
  • tooling/scripts/validate.js โ€” wires the new check-shell-isolation + lint-fixture-cross-zone scripts

Deleted โ€‹

  • apps/admin/src/components/AdminDashboard.jsx (responsibility split into AdminShell + App.jsx)
  • apps/admin/src/components/merchants/MerchantDetail.jsx
  • apps/admin/src/components/merchants/MerchantsIndexRedirect.jsx
  • apps/admin/src/components/__tests__/AdminDashboard.test.jsx (superseded by AdminShell.test.jsx)

Intentionally untouched โ€‹

  • apps/web/ โ€” Lantern consumer app unchanged
  • firestore.rules, storage.rules โ€” no rule changes
  • All Cloud Run / Cloudflare deployment workflows
  • services/api/merchants/ โ€” already merchant-accessible

Out of Scope (deferred) โ€‹

  • apps/merchant/ separate-app split โ€” this PR sets up the prerequisite. Separate Vite project + Cloudflare target is a future move when independent deploys justify the cost.
  • @lantern/ui shared package extraction โ€” not needed inside one Vite project.
  • Greenfield merchant dashboard / KPIs / activity feed โ€” Q1's option (c). Merchant landing stays on Overview (today's read-only summary). Track via "Merchant home redesign" issue.
  • Merchant self-onboarding (signup, billing, plan selection) โ€” product decision; orthogonal to architecture.
  • "View as merchant" admin impersonation โ€” distinct from "admin enters merchant mode." Add when QA / support workflows need it.
  • Multi-role users (single uid both admin AND merchant) โ€” existing guards reject this state; spec doesn't change that.
  • Merchant-side mutation endpoints beyond what's already wired โ€” additive. Drop handlers into merchantHandlers.js, mount on /me, done. No architectural change required.
  • Removing the back-compat redirects โ€” ships in this PR; follow-up agent removes them ~2 weeks after merge.
  • Visual differentiation between admin and merchant shells โ€” both use the same design tokens. Visual distinction is a separate frontend-design pass.
  • Per-page test coverage parity โ€” out of scope; admin-side coverage isn't great today either.
  • Non-Vite bundlers / deployment changes โ€” Cloudflare Pages auto-deploy and Cloud Run deploys unchanged.

If the implementer thinks any of these are needed for the PR to ship, stop and re-confirm.

Built with VitePress