name: engineering-conventions description: > RAGAPPv3 engineering conventions — backend (FastAPI + SQLite + LanceDB), frontend (React + TypeScript + Vite + Vitest), database/migration/RBAC patterns, and the multi-agent skill layout. Load before implementing or refactoring backend routes/services/migrations or frontend pages/components, or when you need to know "how does this repo do X".
RAGAPPv3 Engineering Conventions
The authoritative, detailed conventions live in docs/engineering/conventions.md.
Read it before non-trivial backend or frontend work. Match the pattern in the
file you are editing over anything summarized here.
Must-know invariants (summary)
Backend (backend/app/)
- Routes export
router = APIRouter(), registered inapp/main.pywithprefix="/api". Inject deps viaDepends(...)(get_db,get_vector_store,get_current_active_user,get_evaluate_policy). - Request/response shapes are Pydantic
BaseModel. Errors viaraise HTTPException(status_code, detail). Register static routes before dynamic{id}routes to avoid shadowing. - All connections come from
SQLiteConnectionPoolwithPRAGMA foreign_keys = ON— rely onON DELETE CASCADE. Schema is theSCHEMAconstant; migrations are idempotentmigrate_add_*functions registered inrun_migrations. - SQLite is sync; wrap blocking calls in
await asyncio.to_thread(...). Atomic multi-statement writes useBEGIN IMMEDIATE(clear any dangling tx withif conn.in_transaction: conn.rollback()first). - Authorize via
await evaluate(user, "vault", vault_id, action); scope every user-data query by vault. evaluate_policyvs_evaluate_policy: There are two variants._evaluate_policy(db, principal, resource_type, resource_id, action)accepts an injected DB connection — use this in FastAPI dependency chains.evaluate_policy(principal, resource_type, resource_id, action)opens its own pool connection — legacy backward-compatibility variant.require_vault_permissionatbackend/app/api/deps.py:476still uses the standalone variant, which doubles per-request connection consumption under load. When adding new auth dependencies, prefer the DI-injectedget_evaluate_policy.
Frontend (frontend/src/)
- Single axios client in
lib/api.ts(VITE_API_URL, Bearer + CSRF interceptors). Prefer options-object function signatures. IDs are typed as declared inapi.ts(Document.idis astring). - Pages are slim orchestrators composing local hooks + feature components (the DocumentsPage pattern). State via Zustand. Routes are lazy +
ProtectedRoute+MainAppShell. strictTS, zero-warning lint (eslint src --max-warnings 0). Scripts:typecheck,lint,test(vitest run),build.
Repo
- Three agent runners, three skill trees (
.claude/,.agents/,.opencode/). Mirror repo-specific skills across all three, or keep them thin pointers to canonical docs. - Before push/PR: run
ci-compatibility-audit. For tests:writing-tests/docs/engineering/testing.md. For commits/PRs:commit-pr.
Hugeicons + lucide mixed icon discrimination
- The navigation rail (
NavigationRail.tsx) mixes@hugeicons/core-free-iconsicons (typeIconSvgElement— a readonly array) withlucide-reacticons (typeReact.ComponentType, rendered asforwardRefobjects). When adding new nav items or any component that accepts aComponentType | IconSvgElementunion, discriminate withArray.isArray, NOTtypeof === "function". LucideforwardRefcomponents havetypeof === "object", not"function"— the wrong guard routes them toHugeiconsIcon, which spreads itsiconprop ([...icon]) and throwsTypeError: currentIcon is not iterableat render time on every authenticated page. - Canonical pattern:
function isHugeicon(icon): icon is IconSvgElement { return Array.isArray(icon); }— use this as a JSX type guard. HugeiconsIconis safe for any prop that resolves to an actual hugeicons array export. Never pass a React component type to it, even if it looks like a valid JSX element.
TypeScript tsconfig boundary (Vite context)
tsconfig.node.jsoncovers onlyvite.config.ts.tsconfig.jsoncoverssrc/and referencestsconfig.node.jsonas a composite project.- Do NOT add files from
src/totsconfig.node.json'sincludearray and do NOT importsrc/files fromvite.config.tsorvite.paths.ts. Doing so producesOutput file has not been built from source file— a TypeScript composite conflict because both tsconfigs would include the same file. - When logic must be shared between
vite.paths.tsandsrc/lib/, keep two copies with cross-reference comments. The subpath deployment helpers (normalizeBasePath) follow this pattern: canonical source insrc/lib/normalize-base-path.ts, inline duplicate invite.paths.ts.
See docs/engineering/conventions.md for the full detail and file references.