The BitzOrcas frontend lives under frontend/ in the backend monorepo, as a peer of the .NET src/ tree. It was imported with git subtree add from the standalone bitzeditor repository, so it ships with its own package.json, yarn.lock, and a vendored Yarn 4 release. The two sides never share source — they are connected only by a typed contract and a documented authentication flow.
1. Where the frontend sits
| Concern | Backend (.NET) | Frontend (TypeScript) |
|---|---|---|
| Repository location | src/, tests/, src/Hosts | frontend/apps, frontend/packages |
| Package manager | NuGet + Central Package Management | Yarn 4.17.0 (packageManager field; .yarn/releases/yarn-4.17.0.cjs vendored) |
| Runtime | .NET 10 (Api, JobHost, AppHost) | Node; browser / native shells |
| Contract surface | emits artifacts/openapi/openapi-v1.json | @bitz/platform-sdk consumes it |
| Auth truth | JWT + WebRefreshCookieService | access token in memory; refresh token in an HttpOnly cookie |
The frontend must not live inside src/. It is a peer subtree with an independent toolchain, exactly so a backend build never depends on Node and a frontend build never depends on the .NET SDK.
2. Two stacks, one monorepo
There are two apps, and they are not the same stack. The web admin and the mobile shell share a Yarn workspace but little else:
| App | Stack | React | Bundler | Backend bridge |
|---|---|---|---|---|
apps/app (web admin) | Vite SPA, RR7, Jotai, TanStack Query 5, Tailwind v4 | 19 | Vite 8 + rolldown, Babel 8 (dev-only Locator) | @bitz/platform-sdk + @bitz/widgets |
apps/app-mobile (mobile shell) | Taro 4 multi-target (H5 / WeChat / Harmony), Capacitor 8 native (Android / iOS) | 18 | Taro’s Vite 4 runner, Babel 7 | none — independent Jotai store; no platform-sdk |
The version split is deliberate: Taro 4 currently targets React 18, and its build toolchain pins Vite 4 / Babel 7, which is why the mobile workspace is resolutions-locked to those versions. See Mobile shell for the full picture.
3. The web app’s request path
The whole app shares one request-pipeline instance (created in PlatformProvider via useMemo) and one AuthSession. Pages never construct a client; they call usePlatform().api.<method>(...). See the platform contract layer for SDK details and the QUERY protocol for paged, list, and complex read transport.
4. Tech stack (web app)
| Layer | Choice | Notes |
|---|---|---|
| Language | TypeScript 6+ | strict; each workspace tsc --noEmit |
| Package manager | Yarn 4.17.0 | nodeLinker: node-modules; .yarn/releases/yarn-4.17.0.cjs vendored; registry registry.npmmirror.com |
| UI framework | React 19+ | @bitz/app |
| Bundler | Vite 8 + rolldown | dev proxy keeps the browser same-origin to avoid CORS |
| Router | React Router 7 | react-router-dom; lazy routes via import.meta.glob; route-level RouteGuard |
| Client state | Jotai | atoms; URL state via nuqs |
| Server state | TanStack React Query 5 | the only data-fetching surface in the web app |
| Styling | Tailwind CSS v4 | @tailwindcss/vite |
| UI primitives | shadcn patterns (Radix / Base UI) | @bitz/components — a shadcn primitive barrel (dozens of UI re-exports) |
| Rich text | Tiptap 3 | @bitz/editor — a useBitzEditor wrapper over StarterKit |
| Icons | unplugin-icons + @iconify-json/lucide | auto-imported |
| Build helpers | unplugin-auto-import, unplugin-svgr | |
| i18n | @bitz/i18n | Context + type-safe t(); currently zh-CN only, multi-locale deferred |
| PWA | vite-plugin-pwa | registerType: prompt; manifest FD WORK |
| Quality | ESLint + typescript-eslint, Prettier, Husky, lint-staged, commitlint | Conventional Commits; Chinese subjects allowed |
5. Workspace layout
frontend/├── apps/│ ├── app/ @bitz/app — Web admin SPA (React 19 + Vite 8)│ └── app-mobile/ @bitz/app-mobile — Taro 4 + Capacitor 8 (independent stack)└── packages/ ├── platform-sdk/ @bitz/platform-sdk — deep module: request pipeline, auth session, gates, contract types ├── widgets/ @bitz/widgets — composite business widgets (AppShell, ServerDataTable, FormField …) ├── components/ @bitz/components — shadcn primitive barrel (forms, tables, overlays, …) + `cn` helper ├── editor/ @bitz/editor — Tiptap `useBitzEditor` wrapper ├── i18n/ @bitz/i18n — Context + type-safe t() (zh-CN bundle) ├── materials/ @bitz/materials — Design System 1.2 token source, generator, and runtime ├── hooks/ @bitz/hooks — `useDebounce` (scaffold-stage) ├── utils/ @bitz/utils — `slugify`, `cx`, `formatDate` └── scripts/ @bitz/scripts — scaffold banner helperapps/* and packages/* are the two Yarn workspace globs. A page imports from package barrels (@bitz/widgets, @bitz/platform-sdk) and never reaches into a package’s deep file paths. materials now owns the token source and generation gate; hooks, scripts, and editor remain relatively thin packages.
6. Contract with the backend
The web app speaks to the backend through one channel, not ad-hoc HTTP:
- The backend emits an OpenAPI artifact at
artifacts/openapi/openapi-v1.jsonviascripts/build/export-openapi.sh. @bitz/platform-sdkconsumes the artifact;generate-clientrefreshessrc/client/generated.d.ts, and module contract files only bridge generated schemas.- All runtime calls flow through the singleton request pipeline — which injects base URL,
Authorization: Bearer, andX-Client-Platformheaders, sendscredentials: 'include'so the HttpOnly refresh cookie travels automatically, and parses RFC 9457 ProblemDetails into a typedAppError. - Drift between artifact and shipped API is guarded by
scripts/build/check-openapi-drift.sh.
Authentication follows a fixed contract: login returns an access token in the JSON body and a refresh token in an HttpOnly cookie the browser carries automatically. Refresh reads the cookie; logout revokes the refresh token server-side and clears the cookie.
7. Red lines
These are implemented architectural invariants.
| # | Red line | Status |
|---|---|---|
| 1 | Generated client is never hand-edited | Current. Change the backend contract, then export and regenerate. |
| 2 | Frontend permission checks are UX only | Current. The backend authorization pipeline is the single permission truth. |
| 3 | No refresh token in localStorage | Current. Web refresh is HttpOnly-cookie only; access token is a module-private field. |
| 4 | No permission fact in localStorage | Current. TenantId / UserId / permissions come from /api/auth/me or JWT claims. |
| 5 | No hand-written backend DTO | Current. Module contracts may only alias schemas from generated.d.ts. |
| 6 | No scattered fetch in pages | Current. All HTTP goes through usePlatform().api. |
| 7 | One request pipeline instance | Current. PlatformProvider memoizes one client for the whole app. |
8. Getting started
# from the backend monorepo rootcd frontend
# strict install against the lockfileyarn install --immutable
# web admin dev server (Vite, port 6800, dev proxy → Api Host 6881)yarn dev
# mobile H5 shell (separate stack; see Mobile shell page)yarn dev:mobileThe dev proxy means the browser sees same-origin /api/* on 6800, so there is no CORS in development. When VITE_API_BASE_URL is set instead, the frontend calls the backend directly and the backend must permit the origin.
9. Learning routes
| Goal | Reading order |
|---|---|
| Understand the architecture | This page → Architecture & red lines → Platform contract layer |
| Understand list requests | QUERY protocol → Platform contract layer |
| Understand sign-in and account security | This page → Identity frontend → Platform contract layer |
| Build a web admin screen | Platform contract layer → Web admin app |
| Ship a mobile build | Mobile shell → Toolchain & contributor flow |
| Contribute a change | Toolchain & contributor flow |
- 01/09
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.
- 02/09
The platform contract layer
@bitz/platform-sdk is the only sanctioned bridge between the web app and the .NET backend — it owns the request pipeline, auth session, ProblemDetails mapping, gates, Identity API, and generated OpenAPI contract.
- 03/09
HTTP QUERY and automatic POST fallback
How BitzOrcas uses RFC 10008 QUERY for paged, list, and complex read requests, including Platform SDK fallback, OpenAPI 3.1 representation, CORS, gateways, and a future .NET 11 / OpenAPI 3.2 migration.
- 04/09
Web admin app
@bitz/app is the React 19 + Vite 8 admin SPA. It composes widgets, the platform-sdk context, Jotai and React Query, and reaches the backend same-origin through a Vite dev proxy.
- 05/09
Admin workspaces and navigation
BitzOrcas organizes the admin application into seven server-governed tenant and Host workspaces with sticky page context, breadcrumb switching, and personal pinned views.
- 06/09
Identity frontend
Complete usage and implementation reference for BitzOrcas Web Identity: encrypted credential sign-in, CAPTCHA, MFA, passwords, admissions, authorization, organizations, application credentials, and tenant governance.
- 07/09
Mobile shell — Taro + Capacitor + Harmony
'@bitz/app-mobile is an independent Taro 4 + React 18 stack compiled to H5, WeChat mini-program, and HarmonyOS Next hybrid, then wrapped as native Android and iOS with Capacitor 8. It does not use platform-sdk or widgets.'
- 08/09
Toolchain, scripts, and contributor flow
How to install, lint, typecheck, and build the BitzOrcas frontend monorepo with Yarn 4, and the commit conventions enforced by Husky and commitlint.
- 09/09
Page Development
A recipe-driven frontend page workflow, concept map, redlines, and done checks for C# backend developers.
10. Source review
From the backend monorepo root:
# Confirm the frontend subtree, the pinned Yarn runtime, and the two-app layout.rg -n "packageManager" frontend/package.jsonrg -n '"react"' frontend/apps/app/package.json frontend/apps/app-mobile/package.jsonls frontend/apps frontend/packagesA source change to the frontend contract, package layout, or red lines forces this page to be reviewed rather than silently going stale.