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 in app/main.py with prefix="/api". Inject deps via Depends(...) (get_db, get_vector_store, get_current_active_user, get_evaluate_policy).
- Request/response shapes are Pydantic
BaseModel. Errors via raise HTTPException(status_code, detail). Register static routes before dynamic {id} routes to avoid shadowing.
- All connections come from
SQLiteConnectionPool with PRAGMA foreign_keys = ON — rely on ON DELETE CASCADE. Schema is the SCHEMA constant; migrations are idempotent migrate_add_* functions registered in run_migrations.
- SQLite is sync; wrap blocking calls in
await asyncio.to_thread(...). Atomic multi-statement writes use BEGIN IMMEDIATE (clear any dangling tx with if conn.in_transaction: conn.rollback() first).
- Authorize via
await evaluate(user, "vault", vault_id, action); scope every user-data query by vault.
evaluate_policy vs _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_permission at backend/app/api/deps.py:476 still uses the standalone variant, which doubles per-request connection consumption under load. When adding new auth dependencies, prefer the DI-injected get_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 in api.ts (Document.id is a string).
- Pages are slim orchestrators composing local hooks + feature components (the DocumentsPage pattern). State via Zustand. Routes are lazy +
ProtectedRoute + MainAppShell.
strict TS, 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-icons icons (type IconSvgElement — a readonly array) with lucide-react icons (type React.ComponentType, rendered as forwardRef objects). When adding new nav items or any component that accepts a ComponentType | IconSvgElement union, discriminate with Array.isArray, NOT typeof === "function". Lucide forwardRef components have typeof === "object", not "function" — the wrong guard routes them to HugeiconsIcon, which spreads its icon prop ([...icon]) and throws TypeError: currentIcon is not iterable at 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.
HugeiconsIcon is 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.json covers only vite.config.ts. tsconfig.json covers src/ and references tsconfig.node.json as a composite project.
- Do NOT add files from
src/ to tsconfig.node.json's include array and do NOT import src/ files from vite.config.ts or vite.paths.ts. Doing so produces Output 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.ts and src/lib/, keep two copies with cross-reference comments. The subpath deployment helpers (normalizeBasePath) follow this pattern: canonical source in src/lib/normalize-base-path.ts, inline duplicate in vite.paths.ts.
See docs/engineering/conventions.md for the full detail and file references.
1---2name: engineering-conventions-43description: 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".4---56# RAGAPPv3 Engineering Conventions78The authoritative, detailed conventions live in **`docs/engineering/conventions.md`**.9Read it before non-trivial backend or frontend work. Match the pattern in the10file you are editing over anything summarized here.1112## Must-know invariants (summary)1314**Backend (`backend/app/`)**15- Routes export `router = APIRouter()`, registered in `app/main.py` with `prefix="/api"`. Inject deps via `Depends(...)` (`get_db`, `get_vector_store`, `get_current_active_user`, `get_evaluate_policy`).16- Request/response shapes are Pydantic `BaseModel`. Errors via `raise HTTPException(status_code, detail)`. Register static routes before dynamic `{id}` routes to avoid shadowing.17- All connections come from `SQLiteConnectionPool` with **`PRAGMA foreign_keys = ON`** — rely on `ON DELETE CASCADE`. Schema is the `SCHEMA` constant; migrations are idempotent `migrate_add_*` functions registered in `run_migrations`.18- SQLite is sync; wrap blocking calls in `await asyncio.to_thread(...)`. Atomic multi-statement writes use `BEGIN IMMEDIATE` (clear any dangling tx with `if conn.in_transaction: conn.rollback()` first).19- Authorize via `await evaluate(user, "vault", vault_id, action)`; scope every user-data query by vault.20- **`evaluate_policy` vs `_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_permission` at `backend/app/api/deps.py:476` still uses the standalone variant, which doubles per-request connection consumption under load. When adding new auth dependencies, prefer the DI-injected `get_evaluate_policy`.2122**Frontend (`frontend/src/`)**23- Single axios client in `lib/api.ts` (`VITE_API_URL`, Bearer + CSRF interceptors). Prefer options-object function signatures. IDs are typed as declared in `api.ts` (`Document.id` is a `string`).24- Pages are slim orchestrators composing local hooks + feature components (the DocumentsPage pattern). State via Zustand. Routes are lazy + `ProtectedRoute` + `MainAppShell`.25- `strict` TS, zero-warning lint (`eslint src --max-warnings 0`). Scripts: `typecheck`, `lint`, `test` (`vitest run`), `build`.2627**Repo**28- Three agent runners, three skill trees (`.claude/`, `.agents/`, `.opencode/`). Mirror repo-specific skills across all three, or keep them thin pointers to canonical docs.29- Before push/PR: run `ci-compatibility-audit`. For tests: `writing-tests` / `docs/engineering/testing.md`. For commits/PRs: `commit-pr`.3031**Hugeicons + lucide mixed icon discrimination**32- The navigation rail (`NavigationRail.tsx`) mixes `@hugeicons/core-free-icons` icons (type `IconSvgElement` — a readonly array) with `lucide-react` icons (type `React.ComponentType`, rendered as `forwardRef` objects). When adding new nav items or any component that accepts a `ComponentType | IconSvgElement` union, discriminate with `Array.isArray`, NOT `typeof === "function"`. Lucide `forwardRef` components have `typeof === "object"`, not `"function"` — the wrong guard routes them to `HugeiconsIcon`, which spreads its `icon` prop (`[...icon]`) and throws `TypeError: currentIcon is not iterable` at render time on every authenticated page.33- Canonical pattern: `function isHugeicon(icon): icon is IconSvgElement { return Array.isArray(icon); }` — use this as a JSX type guard.34- `HugeiconsIcon` is 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.3536**TypeScript tsconfig boundary (Vite context)**37- `tsconfig.node.json` covers only `vite.config.ts`. `tsconfig.json` covers `src/` and references `tsconfig.node.json` as a composite project.38- Do NOT add files from `src/` to `tsconfig.node.json`'s `include` array and do NOT import `src/` files from `vite.config.ts` or `vite.paths.ts`. Doing so produces `Output file has not been built from source file` — a TypeScript composite conflict because both tsconfigs would include the same file.39- When logic must be shared between `vite.paths.ts` and `src/lib/`, keep two copies with cross-reference comments. The subpath deployment helpers (`normalizeBasePath`) follow this pattern: canonical source in `src/lib/normalize-base-path.ts`, inline duplicate in `vite.paths.ts`.4041See `docs/engineering/conventions.md` for the full detail and file references.