Shared back-office lib — @bricks-common/bo¶
Lives at
projects/common/back-office, consumed by everybricksoffice-*app (bricksoffice-projects,bricksoffice-invest). Single home for design system, hooks, utils, providers, core infra. Read this before adding code to a consumer app — almost always, what you're about to write belongs in BO. Keep in sync: BO public surface,#-import map, no-build-step, providers/router-coupling, or build/deploy → update this file in the same PR.Naming: "BO" or "back-office shared". Its design system is one part — don't conflate.
Architecture¶
projects/common/back-office/
├── package.json # name: @bricks-common/bo · ships RAW TS · "main": ./src/index.ts
├── components.json # shadcn: new-york / zinc / lucide
└── src/
├── index.ts # top-level barrel
├── core/ # api client, env, dayjs, appQueryClient
├── providers/ # HttpClientProvider, …
├── hooks/ # use-mobile, useFormSubmissionContext, useIdempotentSubmit
├── utils/ # cn
└── design-system/ # see ./styling.mdc
├── components/ inputs/ layouts/ modals/ tables/
├── text-editor/ forms/
└── styles/ # styles.css, tokens.css, tokens.ts, theme/
No build step. "main" and "types" both point at ./src/index.ts. Consumers compile BO source through pnpm workspace symlinks. Cmd+click lands on the actual .tsx.
Peer deps: react / react-dom / tailwindcss / @tanstack/* are peers because BO is source-only and ships JSX/hooks — duplicating any hook-bearing package across consumer + lib silently breaks context. Versions pinned in the workspace backoffice catalog (pnpm-workspace.yaml).
Public surface¶
Defined by exports in BO's package.json. Consumers import via these paths only:
| Subpath | Resolves to |
|---|---|
@bricks-common/bo |
src/index.ts (full barrel) |
@bricks-common/bo/design-system |
src/design-system/index.ts |
@bricks-common/bo/hooks |
src/hooks/index.ts |
@bricks-common/bo/utils |
src/utils/index.ts |
@bricks-common/bo/providers |
HttpClientProvider / useHttpClient |
@bricks-common/bo/core |
appQueryClient, processHttpResult, withGlobalErrorHandling, useSubmitWithError, formatDate/formatDateTime, formatCentsFull/formatCentsShort, parseEurosInputToCents, parseDecimalInput, createAdminAxios |
@bricks-common/bo/modules/auth |
AdminAuthProvider, useAdminAuth, LoginForm, AdminLoginPage, createAdminAuthClient, useAdminGoogleSignIn, AdminSignInError, buildAdminApiErrorMessage, getAdminApiErrorCode |
@bricks-common/bo/modules/component-stories |
Dev-only DS gallery — ComponentStories, componentStoriesNavItems, COMPONENT_STORIES_PATH. Gate behind import.meta.env.DEV. |
@bricks-common/bo/styles.css |
Tailwind + tokens entrypoint |
@bricks-common/bo/tokens.css |
tokens only |
No deep imports. @bricks-common/bo/design-system/components/button is intentionally not exposed. Export from the barrel first.
Internal authoring — #-prefixed package imports (never ../../)¶
All cross-folder imports inside BO use Node package.json "imports" aliases. Package-private (Node spec) — consumers can't see them. Same-folder imports stay relative.
| Alias | Resolves to |
|---|---|
#components/* |
src/design-system/components/*.tsx |
#layouts/* |
src/design-system/layouts/*.tsx |
#inputs/* |
src/design-system/inputs/*.tsx |
#tables/* |
src/design-system/tables/*.tsx |
#text-editor/* |
src/design-system/text-editor/*.tsx |
#forms/* |
src/design-system/forms/*.tsx |
#styles/* |
src/design-system/styles/*.ts |
#hooks/* |
src/hooks/*.ts |
#utils/* |
src/utils/*.ts |
// inside BO — correct
import { cn } from '#utils/cn'
import { Button } from '#components/button'
// inside BO — WRONG
import { cn } from '../../utils/cn'
import { cn } from '@bricks-common/bo/utils' // self-reference
Two tooling gotchas (project_bo_imports_field_gotchas):
- TS bundler resolution requires the file extension on the RHS (
*.tsx/*.ts). Without it, TS errorsImport specifier '#x/y' does not exist in package.json scope. Relative imports get extension-probed;imports-field substitution doesn't. - Rolldown does NOT honor
importsarray fallback. Multi-extension fallback typechecks fine butvite buildfails. One single-target entry per topical folder, dominant extension only. Need both → split into two prefixes.
After any imports change, verify with both pnpm --filter @bricks/bricksoffice-projects build (Rolldown) and pnpm --filter @bricks/bricksoffice-invest build (Rollup). Typecheck alone isn't sufficient.
Hooks (src/hooks/)¶
Cross-cutting only:
- use-mobile — viewport breakpoint (Sidebar reads it for mobile drawer).
- useFormSubmissionContext — exposes { isPending } from <Form />. Read via useFormSubmission().
- useIdempotentSubmit — used internally by <Form />.
Bar for adding: no DS/form/style dependency. Single-feature → that app.
Utils (src/utils/)¶
Currently just cn. Bar: does it appear in DS source files? If no, it doesn't belong here.
Routing & auth (consumer concern)¶
BO ships AdminAuthProvider + useAdminAuth. Consumer owns the router and the navigation reaction. Two non-obvious rules in bricksoffice-projects:
- Gate the router behind
auth.isHydrating. Auth hydrates async. Mount<RouterProvider>only after hydration, else_authenticated'sbeforeLoadseesisAuthenticated=falseand the user appears logged out on every reload.src/App.tsx: render<FullScreenLoader />whileisHydrating, mount the router after. beforeLoadis not reactive on context changes. TanStack Router runsbeforeLoadon navigation, not on context ref change.logout()alone doesn't redirect — the user stays on the current page until they navigate. Inroutes/_authenticated.tsx, watchauth.isAuthenticatedanduseNavigate()({ to: '/login' })in auseEffectwhen it flips false. Keep the navigation in the consumer's route layer — BO stays router-agnostic.
Build / deploy pruning gotchas¶
bricksoffice-projects Dockerfile: 4 stages (pnpm+turbo base → turbo prune <pkg> --docker → install + turbo run build → nginx static via entrypoint.sh that envsubst's $PORT into nginx.conf).
Two worked-around traps (project_turbo_prune_tsconfig_base):
turbo pruneproduces a strippedout/pnpm-lock.yamlthat breaks--frozen-lockfile— the build copies the full rootpnpm-lock.yamlinstead. Same for rootpackage.json+pnpm-workspace.yaml: turbo 2.10+ emptiespatchedDependenciesin the pruned workspace when the prune graph doesn't use the patched dep.turbo prune --dockerdoesn't copy roottsconfig.base.jsoneven though per-projecttsconfig.build.jsonextends it —COPYit explicitly.
Build-time env: VITE_API_URL baked at build. Runtime nginx env: just PORT. Deployed by Railway from railway.bricksoffice-projects.json.
What NOT to do¶
- ❌ Add a
dist/build step to BO — source-only on purpose. - ❌ Add a per-package
biome.json— root config authoritative. Seefeedback_no_per_package_root_configs. - ❌ Replicate any DS component / hook / util inside a consumer app — add it here.
- ❌ Couple BO to a router (
@tanstack/react-router,react-router,expo-router). Auth-driven navigation lives in the consumer's route layer. - ❌ Pass axios as a prop — use
<HttpClientProvider>+useHttpClient(). See data-fetching. - ❌ Hardcode user-facing copy in BO — BO has its own paraglide. See i18n.
- ❌ Write speculative code into empty scaffold folders (
core/,providers/,modals/,tables/,text-editor/) — wait for a spec. Seefeedback_scaffold_folders_no_speculation.