AI Developer Guide¶
Instructions for AI coding assistants (Claude Code, Cursor, Copilot, etc.) working on the Bast platform monorepo.
Repository Structure¶
bast-admin-ui/ # pnpm monorepo root
├── apps/
│ ├── admin-ui/ # Main admin dashboard (React 19 + Vite 6, :3000)
│ ├── bff/ # Backend-for-frontend (Express + tRPC, :4000)
│ ├── chat-ui-beta/ # Chat interface (React 18 + Vite 6, :3001)
│ ├── bast-ux/ # UX component playground (React 18 + Vite 6, :3002)
│ └── patient-journey/ # Patient journey app (React 19 + Vite 7, :5173)
├── packages/
│ └── shared-styles/ # Design system (tokens, components, CSS)
├── tests/
│ └── e2e/ # Playwright E2E tests (release-gate, smoke, nightly)
├── openspec/ # OpenSpec change management (specs, changes, sequencing)
├── docs/ # Architecture docs, decisions, guides
│ └── architecture/YYYY-MM-DD/ # Date-stamped architecture decision records
└── .claude/
└── launch.json # Dev server configurations
Package manager: pnpm with workspaces
Branch: dev → remote origin/dev (Bitbucket)
Node version: Check .nvmrc if present; otherwise use latest LTS
Starting the Development Environment¶
All five services should be running during development. Start them using the launch configurations:
# If using Claude Code with preview_start:
preview_start("admin-ui") # → :3000
preview_start("bff") # → :4000
preview_start("chat-ui-beta") # → :3001
preview_start("bast-ux") # → :3002
preview_start("patient-journey") # → :5173
# If using terminal directly:
cd bast-admin-ui
pnpm --filter @bast/admin-ui dev # → :3000
pnpm --filter @bast/bff dev # → :4000
pnpm --filter @bast/chat-ui-beta dev --host 0.0.0.0 --port 3001
pnpm --filter bast-ux dev --host 0.0.0.0 --port 3002
pnpm --filter @bast/patient-journey dev # → :5173
The BFF proxies /trpc, /api, and /health requests from admin-ui on :3000 to :4000.
Build & Test¶
# Build
pnpm --filter @bast/admin-ui build
# Unit tests (vitest)
pnpm --filter @bast/admin-ui test # single run
pnpm --filter @bast/admin-ui test:watch # watch mode
pnpm --filter @bast/admin-ui test:coverage # with coverage
# UI contract tests (custom scripts)
pnpm --filter @bast/admin-ui test:bast-ux
Always verify build passes after making changes. The build catches TypeScript errors that the dev server may not surface.
Verification Workflow¶
After editing code while a preview server is running:
- Check server logs for build/compilation errors
- Check browser console for runtime errors
- Take a screenshot to confirm the UI renders correctly
- Note: The preview browser does NOT share cookies with the real browser. Auth uses httpOnly cookies set by the BFF, so you cannot log in through the preview. Verify via server logs and code review instead.
Code Conventions¶
Component Patterns¶
- Named exports only —
export function ComponentName(), neverexport default - Barrel files — Each component subdirectory has an
index.tsre-exporting public components - Existing:
auth/,customization/,platform/,operational/,setup/,cat-builder/ - Shared design system components imported from
@bast/shared-styles/components NeuCard,NeuButton,NeuInput,NeuSelect,NeuCheckbox,NeuRadio,NeuToggle,NeuAlertPill,StatusPill,ThemeLifecyclePill,ThemeVersionPillIconActionButton,InfraCard,Button
String Constants¶
All user-facing strings live in apps/admin-ui/src/constants/strings.ts, organized by feature area:
- COMMON, NAV, TOP_BAR, CAT, ORG, DELETE_MODAL, ABOUT, INVITATIONS, SETUP, TERMS, AUTH, ERRORS, FOOTER, INFRA_CARD
- Dynamic strings use template functions: TOP_BAR.PENDING_INVITATIONS(count)
- When adding new UI text, add the string constant first, then reference it in the component.
Styling¶
- Tailwind CSS v4 with design system tokens via CSS custom properties
- Use Tailwind arbitrary values for CSS variables:
bg-[var(--bg-elevated)],text-[var(--primary-text)] - Do NOT use inline
style={{}}for static CSS variable references — convert to Tailwind arbitrary values - Inline styles are acceptable only for truly dynamic/computed values (e.g., runtime-determined colors)
Typography — Semantic Heading Classes¶
Use semantic heading classes from @bast/shared-styles rather than composing raw type-ramp + font-weight. The hierarchy:
| Class | Scale | Weight | Use |
|---|---|---|---|
screen-title |
h3 (24px) | 200 | Page-level titles (<h2>) |
heading-3-strong |
h3 (24px) | 700 | Dashboard section headings with icon (<h3>) |
heading-4 |
h4 (19px) | 500 | Wizard steps, modal headers, auth state headings (<h4>) |
heading-5 |
h5 (15px) | 600 | Card item labels, form sub-sections, empty state titles (<h3>–<h5>) |
The raw type ramp classes (type-h1–type-h5) remain available for font-size-only control. For body text use type-body (14px) and type-body-sm (12px).
State Management¶
- tRPC + React Query for server state (via BFF)
- Auth context (
useAuth()) for authentication state — tokens stored in httpOnly cookies - localStorage accessed exclusively through
lib/typed-storage.ts— never use rawlocalStorage - Demo data gated behind
DEMO_MODEfromlib/demo-mode.ts(env var:VITE_DEMO_MODE, defaultstrue)
Form Validation¶
Use shared utilities from lib/validation.ts:
- validateEmail(), validateSlug(), validateRequired(), getPasswordRequirements()
- EMAIL_REGEX, SLUG_REGEX
Accessibility¶
- All icon-only buttons must have
aria-label - Modals/dialogs must include:
role="dialog",aria-modal="true",aria-labelledby="<title-id>" - Reference
DeleteConfirmModal.tsxas the gold standard (focus trap, initial focus, escape key, full ARIA) - AppShell includes a skip-to-content link targeting
#main-content
Lazy Loading¶
Route-level components are lazy-loaded via React.lazy():
- App.tsx — auth screens and ScreensGallery
- ContentRenderer.tsx — all dashboard and configuration tab components
- Pattern for named exports: const Comp = lazy(() => import('./path').then(m => ({ default: m.Comp })))
- Always wrap in <Suspense fallback={<Spinner />}>
Git & Deployment¶
Committing¶
# Stage specific files (preferred over git add -A)
git add <file1> <file2>
# Commit with descriptive message
git commit -m "$(cat <<'EOF'
<type>: <short description>
<optional body>
Co-Authored-By: <agent name> <noreply@anthropic.com>
EOF
)"
Commit types: feat, fix, chore, docs, refactor, test, style
Pushing¶
If push fails with permission errors, retry with escalated permissions (the developer will grant access).
Rules¶
- Work on the
devbranch - Never force-push to
dev - Never commit
.envfiles, credentials, or secrets - Include
Co-Authored-Bywhen an AI assistant authored or co-authored the changes
Documentation¶
When to Update Docs¶
- After completing a set of related changes — create an architecture decision record
- After adding new utilities, patterns, or conventions — update this guide
- After changing project structure — update the Repository Structure section above
Architecture Decision Records¶
Place in docs/architecture/YYYY-MM-DD/ with a descriptive filename:
# Title
**Date:** Month Year
**Status:** Complete | In Progress
**Scope:** Brief scope description
---
## Summary
Numbered list of changes with one-line descriptions.
## Fix N: Title
### Problem
What was wrong.
### Solution
What was done, which files were changed, what pattern was used.
## Files Changed
Table or list of new and modified files.
## Verification
How to confirm the changes work.
This Guide¶
Update this file when you establish new conventions, add new tools/utilities, or change workflows. Keep it as the single source of truth for AI assistants working on this codebase.
Key Files Reference¶
| File | Purpose |
|---|---|
apps/admin-ui/src/constants/strings.ts |
All UI string constants |
apps/admin-ui/src/lib/auth-context.tsx |
Auth provider, useAuth() hook |
apps/admin-ui/src/lib/trpc.ts |
tRPC client |
apps/admin-ui/src/lib/trpc-provider.tsx |
tRPC provider with auto-refresh |
apps/admin-ui/src/lib/platform-hooks.ts |
Platform data query hooks |
apps/admin-ui/src/lib/deployment-hooks.ts |
Deployment CRUD query hooks |
apps/admin-ui/src/lib/api-key-hooks.ts |
API key management hooks |
apps/admin-ui/src/lib/cat-builder-hooks.ts |
CAT builder data hooks |
apps/admin-ui/src/lib/analytics-hooks.ts |
Analytics dashboard hooks |
apps/admin-ui/src/lib/validation.ts |
Shared form validation |
apps/admin-ui/src/lib/authenticated-fetch.ts |
Shared fetch wrapper with credentials: 'include' for non-tRPC BFF calls |
apps/admin-ui/src/lib/org-services-hooks.ts |
Org services query hooks (KB, deployments, CATs, useApiInfo) |
apps/admin-ui/src/lib/typed-storage.ts |
Typed localStorage wrapper |
apps/admin-ui/src/lib/demo-mode.ts |
DEMO_MODE feature flag |
apps/admin-ui/src/lib/csv-export.ts |
CSV export utility for tabular data download |
apps/admin-ui/src/lib/owl-export.ts |
OWL ontology export utility for knowledge domain graphs |
apps/admin-ui/src/hooks/useDemoData.ts |
Demo data hook |
apps/admin-ui/src/components/AppShell.tsx |
Authenticated layout shell |
apps/admin-ui/src/components/ContentRenderer.tsx |
Route → component resolver |
apps/admin-ui/src/App.tsx |
Router configuration |
apps/admin-ui/src/globals.css |
Global styles + CSS fallbacks |
apps/admin-ui/vitest.config.ts |
Test configuration |
packages/shared-styles/src/components/index.ts |
Design system component exports |
apps/bff/src/services/jwt-utils.ts |
verifyAccessToken<T>() — uses jwt.verify() when JWT_SECRET is set, falls back to jwt.decode() for local dev |
.claude/launch.json |
Dev server configurations |
Authenticated Fetch Pattern¶
Non-tRPC BFF calls (e.g., file upload, rate-limit policies) must use the shared authenticatedFetch helper from lib/authenticated-fetch.ts:
import { authenticatedFetch, getBffBaseUrl } from '@/lib/authenticated-fetch';
const res = await authenticatedFetch(`${getBffBaseUrl()}/api/kb/upload`, {
method: 'POST',
body: formData,
});
This ensures credentials: 'include' is always set for cross-origin requests. Never use raw fetch() for BFF endpoints.
Known Issues¶
- GuestGuard AuthProvider error —
useAuth must be used within an AuthProvidererrors appear in console on guest routes (login/register). This is a pre-existing routing issue whereGuestGuardrenders outside theAuthProvidertree. The UI still renders correctly via the error boundary recovery.
Rules of Hooks — Common Pitfalls¶
Two violations have burned us. Both cause "Rendered more hooks than during the previous render" on re-render:
-
Never call a hook inside
.map()— hook count changes with array length. Usetrpc.useQueries()for dynamic parallel queries: -
Never declare hooks after a conditional
return— hooks after an early-return are skipped when the condition is true. Move ALLuseState/useEffect/custom hooks above everyif (...) returnguard.