Skip to content

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.md at 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:

  • generateC4Docs and generateSystemErd read from compiled class files (backend/build/classes/java/main). After editing entity/controller source, run ./gradlew compileJava before 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 refreshes docs/sdk/*-reference.md as 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/ or scripts/generate-sdk-docs.py), not the output.
  • Adding a new backend module auto-adds its C4 doc on the next generateC4Docs run — 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.md for details.
  • OpenAPI spec accuracy: OpenApiSpecExportTest validates 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 in validate mode — 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 and yarn 4 lockfile. 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 on npm install / npm ci. The || true is 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 installGitHooks task runs the same git config and is wired into compileJava, 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):

  1. build-and-testgradle test bootJar against the backend, with Testcontainers + JaCoCo + ArchUnit + OpenApiSpecExportTest. Honors [hotfix] in the commit message to skip tests (escape hatch — see "Open gaps" below).
  2. admin-portal-vitestsnpm ci at ui-clients/, then npm run test:coverage (vitest with v8 coverage) in frontend-web-apps/apps/admin-portal, then the coverage delta gate (see below). The full coverage/ directory is uploaded as an artifact on every run, so reviewers can browse per-file coverage from the Bitbucket pipeline UI. npm run typecheck and npm run lint are 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:

  1. Open a PR. Pipeline runs npm run test:coverage then the delta script. If your changes left coverage flat-or-better, the gate passes.
  2. 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.
  3. 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.
  4. Always run npm run test:coverage locally 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. The form-utils.ts zod 4 cluster is fixed (the useZodForm wrapper now uses dual <TInput, TOutput> generics constrained to FieldValues). Remaining errors are independent: stale SDK renames (targetClassIdstargetClassroomIds in menu.api.ts and MenuItemUpsertForm.tsx), dead module re-exports (./context in reports/index.ts, ./data in feeding-tickets/index.ts, ../types/meal-tickets in meal-tickets.api.ts), an enum-vs-string mismatch in EditCategoryPage.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.tsx now uses pre-baked widths), ref-mutation in render (useUrlState.ts updated via useEffect), the any in DonutChart.tsx (now uses recharts ContentType), and the any in form-utils.ts. Remaining errors are unused-var noise (StatCard._externalLinkAffordance), one no-restricted-syntax raw fetch in ImagePickerModal.tsx, and react-refresh/only-export-components in test-utils.tsx.

Remaining cleanup-then-tighten path:

  1. Stale SDK references: targetClassIdstargetClassroomIds in MenuItemUpsertForm.tsx:147 and menu.api.ts:468. Mechanical rename.
  2. Dead re-exports: delete the three ./context, ./data, ../types/meal-tickets lines that point at non-existent modules.
  3. EditCategoryPage schema: the form's themeColor is typed as z.string() but consumers expect the enum. Tighten the schema or widen the consumer.
  4. Long-tail unused-var cleanup: ~25 individual unused imports / dead branches / unused destructured props. Mechanical, but tedious.
  5. Add npm run typecheck and npm run lint to admin-portal-vitests in bitbucket-pipelines.yml once 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:

  1. 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.

  2. 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/; plus ReportsFilters.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.
  3. 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.ts is 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 passingReportsFiltersReportsContentuseXReportQuery 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 rollbacke2e/postman/smoke-tests.json runs after deployment. Failures notify but don't revert. The smoke alarm is in a building that's already on fire.
  • Only admin-portal is testedparent-portal/package.json has no test script; payment-portal/student-portal/web-pos are empty stubs. Adding even a smoke-level vitest to parent-portal would be a small lift.
  • Dual lockfiles in ui-clients/ — both yarn.lock and package-lock.json exist; 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. sample for reference, school for 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.