Skip to content
bitzorcas
中EN

Concept

Monorepo, deep modules, and red lines

The frontend organizes apps and packages as deep modules with strict dependency direction, enforced by red lines that keep the backend contract single-sourced and credentials out of reach.

Last updated

The frontend is not a loose collection of packages. It is organized around deep modules: a few packages own a wide surface of complexity and expose a narrow public API. Apps and pages import only from barrels, so the hard parts — the request pipeline, the auth session, the composite UI glue — have exactly one home.

1. What “deep module” means here

A deep module hides a broad implementation behind a small interface. In this codebase that is a literal choice: package barrels (src/index.ts) are the only sanctioned import path, and the index re-exports a deliberately short list.

PackageHidesExposes
@bitz/platform-sdkrequest pipeline, auth session, ProblemDetails parsing, tenant/permission context, generated OpenAPI contractcreatePlatformClient, PlatformProvider, usePlatform, PermissionGate, FeatureGate, createPlatformApi, config + error helpers
@bitz/widgetspagination, loading, empty, error, and permission glue; the PlatformResult mappingServerDataTable, AppShell, SidebarNav, HeaderBar, FormField, ConfirmAction, QueryState, EmptyState, ErrorState, PermissionGate
@bitz/componentsshadcn primitives and the cn helpera shadcn primitive barrel (dozens of UI re-exports) (button, sidebar, table, dialog, …)
@bitz/editorTiptap setupa useBitzEditor wrapper over StarterKit
@bitz/i18nReact Context + typed t()a swap-safe i18n seam (I18nProvider, useI18n, useT, tRaw, zhCN)

A page that imports ServerDataTable from @bitz/widgets does not know whether the table fetches via React Query, how it maps a ProblemDetails error to a retry state, or which gate hides the delete column. That is the point.

2. Dependency direction

generated from

apps/app
pages · routes

@bitz/widgets

@bitz/platform-sdk

@bitz/i18n

@bitz/components

@bitz/utils

src/contracts/
schema aliases

generated.d.ts
(openapi-typescript)

backend OpenAPI artifact

The arrow direction is fixed: apps depend on packages, packages depend only on lower-level packages, and nothing reaches into a package’s deep paths. @bitz/platform-sdk is the only package that talks to the backend; @bitz/widgets is the only package that composes data-fetching with UI states (and it depends on both the SDK and i18n, which the previous diagram omitted).

3. The request path through the web app

usePlatform().api

Page component

PlatformProvider
(one client + one session, memoized)

request pipeline
createPlatformClient

base URL · Authorization: Bearer · X-Client-Platform

credentials: 'include'
(HttpOnly refresh cookie auto-attached)

single fetch · AbortSignal.timeout(30s)

parse RFC 9457 ProblemDetails → AppError

React Query cache

There is exactly one request pipeline instance, created in PlatformProvider via useMemo and shared through React context (usePlatform().client). Individual pages never construct a client and never reimplement retry, refresh coordination, timeout, or ProblemDetails mapping — those all live in the one pipeline. See Platform contract layer for the field-level detail.

4. The auth-session state machine

AuthSession is the single owner of credential state and refresh coordination. Its transitions are an architectural contract, not an implementation detail:

login successrefreshOnce (singleton)requiresMfaMFA satisfiedlogoutrefresh fails / 401 retry still401clear session

Anonymous

Authenticated

MfaRequired

LoggedOut

RefreshFailed

The load-bearing invariant is the singleton refresh: concurrent 401s share one in-flight refreshOnce() promise. If the retried request is still a 401, the session calls forceRefreshFailed() and emits a refresh-failed event — this prevents a refresh storm. Listeners are isolated (each is try/catch’d) so a throwing listener cannot break the session.

5. Red lines

These are non-negotiable. Each one exists because its failure mode is a security or correctness regression, not a style issue, and each is implemented.

#Red lineStatusWhy
1Generated client is never hand-editedCurrentgenerated.d.ts mirrors the backend OpenAPI artifact; edits drift and are overwritten by regeneration.
2Frontend permission checks are UX onlyCurrentThe backend authorization pipeline is the single permission truth.
3No refresh token in localStorageCurrentRefresh is HttpOnly-cookie only; access token is a module-private field so no script can exfiltrate it.
4No permission fact in localStorageCurrentTenantId / UserId / permissions come from /api/auth/me or JWT claims, never self-declared.
5No hand-written backend DTOCurrentsrc/contracts/ may alias generated schemas but must not copy their fields.
6No scattered fetch in pagesCurrentAll HTTP goes through usePlatform().api.
7One request pipeline instanceCurrentPlatformProvider memoizes one client; no per-page HTTP adapter.

6. Why the structure is worth it

The cost of a deep-module, single-pipeline layout is upfront discipline: you cannot drop a fetch into a page, hand-copy a DTO, or read the refresh token. The payoff is that the system’s hard invariants hold by construction:

  • Security invariants live in one place. Token storage, refresh coordination, and ProblemDetails handling are all in @bitz/platform-sdk. A review of that one package audits the whole web app’s auth posture.
  • Backend changes propagate through regeneration. When a backend DTO changes, regenerating the client turns a silent runtime mismatch into a compile error.
  • Composite UI states are consistent. Every data-driven screen shares the same loading/empty/error/permission shapes because they come from @bitz/widgets, not from each page’s ad-hoc JSX.

7. Reviewing an architecture change

  1. Name the package the change lives in, and confirm it is a barrel import, not a deep-path import.
  2. If a new HTTP call is introduced, confirm it goes through usePlatform().api and not a page-level fetch.
  3. If a new permission check is introduced, confirm the backend enforces the same capability.
  4. If a backend contract changes, export OpenAPI and regenerate the client; only adjust schema aliases under src/contracts/.
  5. Run the workspace checks (yarn lint && yarn typecheck && yarn build) before commit — Husky and lint-staged enforce formatting and lint on staged files.

8. Source review

Terminal window
# Confirm barrels are the import surface and apps/app does not consume deep paths.
rg -n "from '@bitz/platform-sdk'|from '@bitz/widgets'" frontend/apps/app/src
rg -n "from '@bitz/widgets/src/|from '@bitz/platform-sdk/src/" frontend/apps/app/src
# Expected: the first command returns many matches; the second returns none.
# Confirm the single request-pipeline and auth-session home.
rg -n "createPlatformClient|class AuthSession|refreshOnce|forceRefreshFailed" \
frontend/packages/platform-sdk/src/client frontend/packages/platform-sdk/src/auth

Back to Frontend · Platform contract layer · Web admin app

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%