7.4 KiB
Code quality
The four rules that apply identically to both projects: no dead code, comment the why, no starter scaffolding, and a mock is only a mock behind a seam.
Last verified: 2026-07-30 against commit
d3ec723.
1. No dead code
Unused variables, imports/usings, parameters, and private members are removed — not left behind, not commented out, and not suppressed.
| Project | How it surfaces | Gate |
|---|---|---|
| Client | @typescript-eslint/no-unused-vars, raised from eslint-config-next's default warn to error in eslint.config.mjs |
Dead code fails npm run check |
| Server | CS0168 (declared, never used), CS0219 (assigned, never read), CS0169 (private field never used), IDE0005 (unnecessary using) |
The gate is zero new warnings, so dead code is a gate failure |
Delete it — don't silence it. No #pragma warning disable, no throwaway discards, no _ =
assignments to quiet an analyzer, no file-wide ESLint disable.
Two sanctioned opt-outs, both narrow:
- Client: a deliberately-unused binding is prefixed with
_—_event,catch (_err). - Server: a parameter that must exist to satisfy an interface or delegate signature but is genuinely
unused stays, named conventionally, with a one-line
// whyonly if the reason isn't obvious.
When a lint disable is genuinely correct — a deliberate browser-only read after mount that trips
react-hooks/set-state-in-effect is the real example in this codebase — use a scoped
// eslint-disable-next-line <rule> with a one-line reason on the line above. Never a file-wide disable,
and never in preference to fixing the code.
2. Comment the why, never the what
Code that needs a comment to be understood usually needs a better name instead. Reach for the name first, then a small helper, then a comment.
Don't write a comment that restates what the code already says:
// ❌ restates the obvious
// increment the retry counter
retryCount++;
// ❌ restates the obvious
// set the access token
setClientCookie(COOKIE_NAMES.ACCESS_TOKEN, token);
No XML-doc or JSDoc that merely echoes a function's name either.
Do add a tight comment where a non-obvious decision, constraint, business rule, workaround, ordering or security requirement, or deliberate deviation is not evident from the code. Explain the reasoning, not the mechanics:
// ✅ captures a constraint the code can't express on its own
// Payment gateway rejects amounts above 50M IRR per call; split larger settlements upstream.
if (amount > MaxPerCallRial) …
The models to follow in this codebase:
| File | What its comment earns |
|---|---|
client/src/app/[locale]/layout.tsx |
Why <html> lives in the [locale] layout and not above it |
client/src/lib/auth/token.ts |
Why the JWT exp check is UX-only and never a security boundary |
client/src/layout/config.ts |
Why the two chrome-bar heights are measured rather than guessed, and must stay in sync with the bars |
client/middleware.ts |
Why the matcher lists bare '/' explicitly alongside the catch-all regex |
Delete comments that no longer match the code. A wrong comment costs more than no comment.
3. Don't reintroduce starter scaffolding
Both projects were derived from open-source starters. Their branding, demo/showcase pages, and
_TITLE_/_DESCRIPTION_ placeholders were intentionally removed. Don't add them back — not as a
convenience, not while editing docs, not as an example.
Specifically:
- No placeholder page, showcase route, or "example component" gallery.
- No
_TITLE_/_DESCRIPTION_/ lorem-ipsum copy anywhere, including in message files. - No starter README boilerplate reinstated into a project README.
PlaceholderScreenexists for a genuinely not-yet-built screen and must not be reachable from a shell's navigation./admin/notificationsis the current example: it is a placeholder, and it is deliberately absent fromAdminLayout's nav for that reason.
4. A mock is only a mock behind a seam
Some integrations are intentionally out of scope and must be mocked, not invented: real PSP and BNPL connections, the Shahkar / MoH / INO / criminal-record vendors, MinIO/S3 credentials, the سامانه مودیان enrollment. Reaching one is not a blocker.
The only sanctioned form of "not real yet" is:
- An interface. Server: an interface in
Application/Contracts/, implemented twice, selected by configuration inAddCrossCuttingSeams— and the default is the mock, with a typo falling closed to the mock. Client: the domain'sApiinterface inservices/{domain}/types.ts, implemented byclientApi.tsandmockApi.ts, selected inapis/index.tsbyUSE_{DOMAIN}_MOCK. - Selection by registration, never by branching. No
if (mock)inside a handler, hook, or component. Swapping a mock for the real thing is a one-line registration change and touches no caller. - A record, in
docs/status/: the seam (interface name + file), what is faked, why, the config keys it reads, and step-by-step how to make it real — which provider, which settings, which methods, what to test.
An unrecorded mock is a defect, because the next agent cannot tell a deliberate stand-in from a bug.
Two mocks in this repo are deliberate MVP endpoints, not stand-ins waiting for a vendor:
ICredentialVerifier / ILicenseVerificationService stay mock because manual MoH / INO / eNamad review
is the intended MVP — there is no public B2B API. Don't "finish" them.
5. Scale and cost are part of correctness
Every decision should consider what it costs at scale, not only whether it works once:
Server — indexing, pagination on every unbounded list, caching read-heavy and reference data behind the cache seam, idempotency and locks on the money path, the DB constraint as the authoritative backstop behind every friendly pre-check.
Client — query caching with a deliberate staleTime so you never refetch what you already hold,
invalidation on mutation, re-render cost (stable references, select to subscribe to a slice, state
colocated low), and bundle size.
And in both: the seam that lets a mock become real without touching a caller.
6. Configuration lives in files, not a secret store
dotnet user-secrets is not used in this repo, and the <UserSecretsId> was removed from
Baya.Web.Api.csproj, so that store is not read at all. Any instruction telling you to set a value
with dotnet user-secrets is stale.
| Where config lives | What |
|---|---|
server/src/API/Baya.Web.Api/appsettings.*.json |
Server config, including dev crypto keys |
client/.env.development / .env.production |
Client config |
root docker-compose.yml |
The deployment's container-specific overrides |
This is a deliberate pre-launch trade for a demo deployment, which means the repo contains live
credentials. Before onboarding real users they must be rotated and the secret half moved out of git —
see DEPLOY.md "Going to Production". Never hardcode a secret in code either way:
keys, connection strings, and tokens come from configuration bound to typed settings, never a literal in
a handler, service, or component.
One value is load-bearing and must never change: Seams:FieldEncryption:Key / :HashKey decrypt all
existing PII and derive the phone-lookup hash. Changing them makes every PII read throw and every phone
lookup miss.