Project context for AI assistants¶
Use this file when working on the Smartsapp codebase so that suggestions stay consistent with the project’s structure and conventions.
What this repo is¶
- Product: School management SaaS (Smartsapp).
- Deployment: Kubernetes. Backend runs as a Spring Boot app; infra config lives in
infrastructure/. - Docs entrypoint:
README.mdat repo root links to all sub-READMEs.
Repo layout (high level)¶
| Path | Purpose |
|---|---|
| backend/ | Application backend. Spring Boot app with business modules. |
| infrastructure/ | Infra config (not app code): cloud_environment_setup/ (K8s, CDN, Terraform), core_system_dependencies/ (platform primitives + OSS integrations), operational_utilities/ (backup, observability, dev tools). See infrastructure/README.md. |
| ui-clients/ | Client applications. frontend-web-apps/ = web apps and shared library. mobile-hybrid-apps/ = mobile applications. |
| e2e/ | End-to-end tests (Maestro, Playwright, Postman). |
| docs/ | Project-wide documentation (including this file). |
Documentation structure¶
| Document | Path | Scope |
|---|---|---|
| PRD index | docs/PRD.md |
Links to every module's PRD |
| C4 architecture | docs/c4-architecture.md |
System-wide C1/C2; links to module C3/C4 docs |
| CI/CD | docs/CICD.md |
Pipeline, environments, deployment |
| Commit convention | docs/COMMIT_CONVENTION.md |
Conventional Commits format, enforced by git hook |
| Deployment | docs/deployment.md |
DigitalOcean dev deployment (temporary — production target is Hetzner) |
| CLI commands | docs/COMMANDS.md |
Quick reference for all Gradle, Podman, git hook, and deployment commands |
| Debugging | docs/DEBUGGING.md |
Tracing customer issues through logs, traces, and audit logs |
Each module may have a docs/ subfolder containing:
- A module PRD (e.g. School_PRD.pdf)
- A C3/C4 architecture doc (e.g. school-c4-architecture.md)
When creating or modifying a module, check its docs/ folder for domain context and architectural guidance.
Auto-generated docs — never hand-edit¶
The following files are regenerated from source by tooling. Manual edits will be clobbered on the next regeneration. When content in one of these files needs to change, edit the source (entity class, controller, OpenAPI spec, script config) and re-run the listed generator. Every file below carries an > Auto-generated by … / > Do not edit manually banner at the top — if you see that banner in a file anywhere else in the repo, treat it the same way even if it's not in this table.
| File(s) | Generator | Source of truth |
|---|---|---|
docs/system-erd.md |
./gradlew generateSystemErd (from backend/) |
All @Entity classes under backend/src/main/java/com/smartsapp/system/modules |
docs/modules/{attendance,audit,canteen,sample,school,wallet}/<module>-c4-architecture.md (one per module) |
./gradlew generateC4Docs (from backend/) |
Controllers, services, repositories, and entities of the module |
docs/sdk/staff-reference.md, docs/sdk/parent-reference.md |
python3 scripts/generate-sdk-docs.py (from repo root) |
ui-clients/sdk/specs/*.json (OpenAPI spec dumps) |
mkdocs.yml (the nav: section only — partial-file regen) |
python3 scripts/generate-mkdocs-nav.py |
A static NAV list at the top of the script + auto-discovered docs/modules/* entries |
ui-clients/sdk/src/**/types.gen.ts, ui-clients/sdk/src/**/zod.gen.ts, ui-clients/sdk/specs/*.json |
npm run sync (from ui-clients/sdk/) |
Backend OpenAPI spec (/v3/api-docs from a running backend) |
Rules of thumb:
generateC4DocsandgenerateSystemErdread from compiled class files (backend/build/classes/java/main). After editing entity/controller source, run./gradlew compileJavabefore regenerating — otherwise the generator will emit stale output based on the previous build.- SDK regen requires a running backend on the expected port (for
ui-clients/sdk/ npm run sync) to fetch the current OpenAPI spec. The SDK regen also refreshesdocs/sdk/*-reference.mdas a downstream step. - When you need to update an auto-generated file's content: edit the source and regenerate. When you need to update an auto-generated file's format (e.g. the ERD layout, the module-doc header): edit the generator tool itself (
backend/tools/c4docgen/src/main/java/com/smartsapp/tools/c4docgen/renderers/orscripts/generate-sdk-docs.py), not the output. - Adding a new backend module auto-adds its C4 doc on the next
generateC4Docsrun — no table edit needed (the path pattern above already covers it).
Backend system (Spring Boot) structure¶
- Location:
backend/. - Style: One deployable app; modules are packages, not separate Gradle/Maven modules.
- Base package:
com.smartsapp.system. - Architecture, module layout, naming conventions, and new-module instructions: See the Structure section of
backend/README.md. That is the source of truth — do not duplicate it here.
Conventions to follow¶
- Architecture & naming: Follow the conventions documented in
backend/README.md(naming conventions section). When suggesting changes, match the existing MVC / package-by-layer structure and naming patterns. - No secrets in repo; use env or a secrets store. Secret templates live in their respective deployment folders (e.g.
infrastructure/cloud_environment_setup/digitalocean/k8s/app-secret.yml.example). - Infrastructure YAMLs in
infrastructure/: change only when the user explicitly asks for infra or deployment changes. - Run tests after major changes: After any significant backend change (new features, refactors, dependency updates), run the test suite to verify nothing is broken:
JAVA_HOME=/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home ./gradlew -p backend test - Do not modify tests: Never modify, delete, or skip existing tests unless the user explicitly asks to update them. Tests are the safety net — treat them as read-only by default.
- Testing stack: Integration tests use Testcontainers (PostgreSQL, Kafka, Redis). JaCoCo enforces 90% minimum instruction coverage. ArchUnit enforces module boundaries and architectural rules. See the testing section of
backend/README.mdfor details. - OpenAPI spec accuracy:
OpenApiSpecExportTestvalidates all generated specs (unique operation IDs, non-empty response schemas, error responses have content, expected endpoints exist). When adding or modifying controllers, run the test suite to catch spec regressions before they reach the SDK. When adding new controller endpoints, add@Schema(example = ...)to response DTO fields so Swagger shows populated examples. - Database migrations are mandatory. Read docs/MIGRATION_MANAGEMENT_GUIDE.md before touching any
@Entity. Hibernate runs invalidatemode — if you add or change an entity field without a corresponding Flyway migration, the app will not start. The workflow: - Change the entity.
- Run
./gradlew atlasDiff -Pname=describe_change— Atlas generates the migration SQL by diffing entities against existing migrations. Review the generated SQL and hand-edit if needed (renames, backfills, constraints). - Run
./gradlew atlasValidate— replays all migrations against a throwaway Docker Postgres to catch conflicts. - Run
./gradlew bootRun— Flyway applies the migration, Hibernate validates.
Migration + entity change go in the same PR, always. Never edit an already-applied migration; write a new one to fix forward. The pre-commit hook runs atlasValidate automatically when migration or entity files are staged; CI runs it again on every PR.
- Git hooks: .githooks/pre-commit auto-runs backend tests when backend/ files are staged. .githooks/commit-msg enforces Conventional Commits format. Developers should always run git commit from the command line (not IDE Git UIs) so hook output — especially test failures — is visible.
- Never bypass the pre-commit hook (AI sessions): If git commit fails because the pre-commit hook reported an error, stop and report the failure to the user before doing anything else. Do not use --no-verify on your own initiative — even if the failure looks unrelated to your change, even if the hook seems flaky, even if the user is waiting. The hook is the local safety net that gates broken migrations, broken tests, and coverage drops from reaching main. Bypassing it once cascades: see commit 7f17dd4 which shipped five distinct CI breakers (stale atlas.sum, broken CreateMenuRequest test sites, removed @ConditionalOnBean, mismatched dashboard test expectations, sub-90% coverage) that all would have been caught locally. The user can always tell you to skip the hook explicitly; you can never decide that yourself.
- Commit messages: Must follow Conventional Commits format — type(scope): description. See docs/COMMIT_CONVENTION.md for allowed types and examples.
- Changelog: Update CHANGELOG.md under [Unreleased] when implementing features, fixes, or changes. See the guide at the bottom of that file for format and rules. Do not cut releases (rename [Unreleased] to a version) — that's a human decision.
- Logging: Follow the logging strategy in docs/LOGGING.md. Use SLF4J (LoggerFactory.getLogger), not Lombok @Slf4j. Log INFO on entity create/delete, WARN on business rule violations and expected errors, ERROR on unexpected failures. Never log request bodies or tokens. Never log-and-rethrow — either log or let GlobalExceptionHandler handle it.
Package manager (ui-clients)¶
ui-clients is an npm workspace. Always run npm install (for dev) or npm ci (for clean installs / CI) from ui-clients/ — the workspace hoists sdk, admin-portal, parent-portal, and parent-app into a single root node_modules. Never run npm install inside a sub-package directory (ui-clients/sdk/, ui-clients/frontend-web-apps/apps/admin-portal/, etc.) — it creates a nested package-lock.json that breaks the SDK symlink and drifts from the canonical root lockfile. If you see a nested package-lock.json anywhere under ui-clients/, delete it and reinstall from the root. The .gitignore has rules that prevent them from being committed.
Yarn is forbidden in ui-clients/. The workspace used to track both yarn.lock and package-lock.json in parallel, which caused silent drift — one developer's yarn install would add a dep that CI's yarn install --frozen-lockfile wouldn't see because another developer's npm install had updated the other lockfile. The repo now commits to npm only. yarn.lock is gitignored.
Node version: pinned in ui-clients/.nvmrc at 22. nvm use in ui-clients/ before installing. CI (bitbucket-pipelines.yml) and both Containerfiles also use node:22-alpine. The SDK's @hey-api/openapi-ts dev-dep requires >=20.19.0, so going below node 22 will break the install.
Exception: the documentation-portal sub-project under
infrastructure/is a separate Backstage workspace with its own.yarn/directory andyarn 4lockfile. It is not part of the ui-clients workspace; leave its yarn setup alone.
CI gates and the enforcement model¶
The real enforcement layer is the PR pipeline, not git hooks. Client-side hooks (.githooks/) are a fast-feedback convenience — any dev can bypass them with git commit --no-verify. They auto-install on first contact with either tree (no manual git config needed):
- Frontend / mobile / sdk devs: ui-clients/package.json:11
"postinstall": "git config core.hooksPath .githooks 2>/dev/null || true"fires onnpm install/npm ci. The|| trueis deliberate — CI and Docker images are built on alpine without git, and the hook-path config is only useful on dev machines. - Backend devs: backend/build.gradle.kts:109-125
installGitHookstask runs the samegit configand is wired intocompileJava, so any./gradlew build/test/bootRun/...triggers it.
git config --get core.hooksPath should return .githooks after either install. If it doesn't, the dev hasn't bootstrapped yet — it'll fix itself on the next install or compile.
What CI actually runs¶
bitbucket-pipelines.yml defines two parallel checks on every PR (and on main):
build-and-test—gradle test bootJaragainst the backend, with Testcontainers + JaCoCo + ArchUnit + OpenApiSpecExportTest. Honors[hotfix]in the commit message to skip tests (escape hatch — see "Open gaps" below).admin-portal-vitests—npm ciatui-clients/, thennpm run test:coverage(vitest with v8 coverage) infrontend-web-apps/apps/admin-portal, then the coverage delta gate (see below). The fullcoverage/directory is uploaded as an artifact on every run, so reviewers can browse per-file coverage from the Bitbucket pipeline UI.npm run typecheckandnpm run lintare not yet gated; see "Frontend gate buildout" below.
The missing piece (one-time human action)¶
CI running is necessary but not sufficient. Without a merge restriction, a dev can still merge a red PR through the Bitbucket UI. The repo owner must enable:
Repository settings → Branch restrictions → Merge checks → "Successful builds" on
main.
Combined with "Prevent direct pushes" + "Prevent rewriting history", this makes the pipeline binding. Until that toggle is flipped, the pipeline is advisory.
Coverage delta gate (live)¶
Coverage is gated by delta, not floor. The PR pipeline runs scripts/check-coverage-delta.mjs, which compares vitest's coverage-summary.json against the checked-in baseline at ui-clients/frontend-web-apps/apps/admin-portal/coverage-baseline.json and fails the build if any of lines, statements, functions, or branches drops by more than 0.5pp.
Why delta and not floor: rewards every contribution that adds tests, never punishes contributors who happen to touch low-coverage areas. There is no aspirational target that creates a cliff nobody wants to push over.
The ritual:
- Open a PR. Pipeline runs
npm run test:coveragethen the delta script. If your changes left coverage flat-or-better, the gate passes. - If coverage improved on at least one metric, the script prints a JSON block with the new baseline values. Paste that into coverage-baseline.json in the same PR to lock in the gain — otherwise the next PR can erode it back to where it was.
- If coverage dropped by more than 0.5pp, you have two choices: add tests to recover, or — if the drop is intentional (e.g. you deleted dead code that had no tests but was inflating denominators) — paste the new baseline into the file and explain in the PR description.
- Always run
npm run test:coveragelocally first before pushing if you suspect coverage moved. Faster than waiting for CI to tell you.
The current baseline (as of the file's _updated date) is honest and small: lines 0.44%, statements 0.44%, functions 12.08%, branches 16.58%. Out of 30,731 source lines, 138 are covered. The point of the delta gate is to make the trajectory monotonically up — not to set a number.
Frontend gate buildout (cleanup-then-tighten)¶
The admin-portal-vitests step in CI gates npm run test:coverage + the coverage delta gate. npm run lint and npm run typecheck are not yet gated because the baseline has pre-existing errors. Current state after the targeted cleanup commit:
npm run typecheck— ~35 errors remaining. Theform-utils.tszod 4 cluster is fixed (the useZodForm wrapper now uses dual<TInput, TOutput>generics constrained toFieldValues). Remaining errors are independent: stale SDK renames (targetClassIds→targetClassroomIdsin menu.api.ts andMenuItemUpsertForm.tsx), dead module re-exports (./contextinreports/index.ts,./datainfeeding-tickets/index.ts,../types/meal-ticketsinmeal-tickets.api.ts), an enum-vs-string mismatch inEditCategoryPage.tsx, and a long tail of unused-var noise.npm run lint— 52 errors, 192 warnings remaining (was 55/192). The targeted bugs are fixed:Math.random()in render (FeedingListTableSkeleton.tsxnow uses pre-baked widths), ref-mutation in render (useUrlState.tsupdated viauseEffect), theanyinDonutChart.tsx(now usesrechartsContentType), and theanyinform-utils.ts. Remaining errors are unused-var noise (StatCard._externalLinkAffordance), one no-restricted-syntax raw fetch inImagePickerModal.tsx, andreact-refresh/only-export-componentsintest-utils.tsx.
Remaining cleanup-then-tighten path:
- Stale SDK references:
targetClassIds→targetClassroomIdsinMenuItemUpsertForm.tsx:147andmenu.api.ts:468. Mechanical rename. - Dead re-exports: delete the three
./context,./data,../types/meal-ticketslines that point at non-existent modules. - EditCategoryPage schema: the form's
themeColoris typed asz.string()but consumers expect the enum. Tighten the schema or widen the consumer. - Long-tail unused-var cleanup: ~25 individual unused imports / dead branches / unused destructured props. Mechanical, but tedious.
- Add
npm run typecheckandnpm run linttoadmin-portal-vitestsinbitbucket-pipelines.ymlonce steps 1-4 pass cleanly. The yaml comment marks the spot.
Where to invest test effort (git-log signal)¶
Looked at the last 3 months of git log --grep='^fix' (46 commits) cross-referenced with file churn. Two findings worth knowing:
-
The bug signal in commit messages is dominated by infrastructure, not features. ~28 of 46 fix commits are deployment / CI / Kafka / probe / Docker plumbing. Only ~6 are real frontend feature bugs (button labels, filter widths, event propagation in menu actions, env wiring). Tests don't fix deployment churn. If the goal is "fewer fix commits," the highest-leverage work is likely K8s probe tuning, image build stabilization, and rollback automation — not frontend tests.
-
The few real frontend bug fixes cluster around two areas, both of which are also high-churn:
- Reports (ReportsContent.tsx — 13 commits in 3 months, the highest in
features/; plusReportsFilters.tsx,OrdersReportSection.tsx). Most of the real bug fixes here were UI polish (button labels, widths) but the high churn alone is a risk signal. - Feeding list (FeedingListContent.tsx — also 13 commits; plus
FeedingListFilterPanel.tsx,feeding-list.api.ts). One real integration bug was caught and fixed in a prior session of this conversation: a hand-maintained query type literal was stripping 9 of 14 backend filters at the API wrapper. That's exactly the kind of bug a contract/integration test would have caught at write-time.
- Reports (ReportsContent.tsx — 13 commits in 3 months, the highest in
-
Caveat: the absence of fix commits in the canteen ordering flow doesn't mean the flow is bug-free — it might just mean bugs there aren't being captured in commits. Without an error-reporting service (
monitoring.tsis currently a console-only stub at src/lib/monitoring.ts), you don't have visibility into runtime bugs at all. Wiring up Sentry (or similar) is a much higher-leverage signal investment than writing 100 speculative tests.
Recommended targeted test investments (in priority order, when test-writing time becomes available):
- Feeding list filter contract test — assert that all 14 SDK-supported filter fields actually reach the API. This is the kind of test that catches the bug we just shipped.
- Reports filter parameter passing — ReportsFilters → ReportsContent → useXReportQuery chain has high churn and lots of moving filter parts.
- Canteen ordering flow integration test — core revenue path, currently zero coverage on the frontend (backend has it well-covered). Worth one focused integration test even though git log doesn't flag it as buggy, because the blast radius of a regression here is the largest in the app.
- Skip everything else for now. The git log doesn't justify broader investment without a better bug signal first.
Open gaps (not yet addressed)¶
The 9-gap audit from earlier this session identified these still-open items, in rough priority order:
[hotfix]skip-tests escape hatch (bitbucket-pipelines.yml:23-28) — anyone can ship untested code by typing seven characters into a commit message. Either delete the conditional or require a second-reviewer approval on hotfix PRs.- Post-deploy smoke test, no rollback —
e2e/postman/smoke-tests.jsonruns after deployment. Failures notify but don't revert. The smoke alarm is in a building that's already on fire. - Only admin-portal is tested —
parent-portal/package.jsonhas notestscript;payment-portal/student-portal/web-posare empty stubs. Adding even a smoke-level vitest toparent-portalwould be a small lift. - Dual lockfiles in
ui-clients/— bothyarn.lockandpackage-lock.jsonexist; pipeline uses yarn, the historical pre-commit used npm. Pick one — the other will silently drift.
PRD maintenance guidelines¶
PRDs are markdown files in docs/modules/<module>/ and indexed in docs/PRD.md. Follow these rules when working with them:
AI SHOULD update PRDs when:
- Implementation diverges from the PRD (e.g., new field added to an entity) — the code is truth, the PRD must match
- Open questions in the PRD get resolved during development
- New entities or fields are added that fall under an existing PRD's scope
- The user explicitly asks for a PRD update
AI SHOULD NOT update PRDs when:
- Product direction changes (new features, scope changes, strategic pivots) — these come from product, not inferred from code
- Removing or deprecating features — product makes that call, not the AI
- Speculative additions — don't add requirements that might be needed
- Business rule changes — if code changes a business rule, flag it to the user rather than silently updating the PRD
In short: AI updates PRDs to keep them accurate with what was built. Product updates PRDs to define what should be built next.
How to use this file¶
- At session start: Read this file (or have it included in context) when helping with backend, structure, or conventions.
- When suggesting changes: Prefer following the package layout and dependency rule above; cite relevant paths and existing modules (e.g.
samplefor reference,schoolfor a full implementation) when suggesting new code or new modules. - When unsure: Prefer asking or suggesting one clear option rather than inventing a new pattern; keep consistency with the existing layout.