Coding Standards
Coding StandardsStandards: Drop — Fintech Payment App
Project:
{{PROJECT_NAME}}Drop — Remittance + QR Payments Version:{{VERSION}}1.0 Date:{{DATE}}2026-02-23 Author:{{AUTHOR}}John (AI Director) Status:Draft | In Review |Approved Reviewers:{{REVIEWERS}}Alem Bašić (CEO)
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | Initial |
Purpose
These standards apply to all code in {{PROJECT_NAME}}Drop. When automated tools enforce a rule, the tool wins. When in doubt, optimize for readability — the next personAI agent reading your code mayis betrying youto inunderstand sixintent, months.not guess it.
Non-negotiable: All new code must pass automatedTypeScript lintingstrict check, ESLint, and Prettier formatting checks before merge. No exceptions.
Fintech non-negotiable: Parameterized SQL everywhere. No stored balances. No stored card numbers or CVV. These are encoded as automated tests — failing these means P0.
1. Language-Specific Conventions
1.1 Naming Conventions
JavaScriptTypeScript /(Drop's TypeScript:primary language):
| Type | Convention | Example |
|---|---|---|
| Variables | camelCase |
, |
| Functions | camelCase |
, |
| Classes | PascalCase |
|
| Interfaces | PascalCase (no I prefix) |
User, |
| Types | PascalCase |
, |
| Enums | PascalCase |
, |
| Constants | UPPER_SNAKE_CASE |
, |
| Files — components | PascalCase.tsx |
|
| Files — utilities | camelCase.ts |
, |
| Files — API routes | route.ts (Next.js convention) |
app/api/auth/login/route.ts |
| Test files | {name}.test.ts |
auth.test.ts, db.test.ts |
| E2E test files | {name}.spec.ts |
user-flows.spec.ts |
Python:Drop-specific constants:
| const QR_MERCHANT_FEE_RATE = 0.01; // 1%
const MIN_REMITTANCE_NOK = 100;
const MAX_REMITTANCE_NOK = 50_000;
// Supported corridors
const SUPPORTED_CURRENCIES = ['RSD', | |
| | |
| | |
| | |
| |
1.2 File Organization
src/drop-app/src/
├── {{MODULE_1}}/app/
│ ├── index.tsapi/ # PublicNext.js exportsAPI onlyRoutes
│ │ ├── {{MODULE_1}}.service.tsauth/
#│ Business logic│ │ ├── {{MODULE_1}}.controller.login/route.ts
#│ HTTP handlers│ │ ├── {{MODULE_1}}.types.register/route.ts
#│ Types/interfaces│ │ └── me/route.ts
│ │ ├── {{MODULE_1}}.test.transactions/
│ │ │ ├── remittance/route.ts
#│ Unit tests│ │ └── {{MODULE_1}}.integration.test.qr-payment/route.ts
├│ │ └── shared/rates/
# Shared utilities, no business logic│ │ ├── errors/route.ts # GET /api/rates
│ │ └── [currency]/route.ts # GET /api/rates/RSD
│ └── (app)/ # Protected pages (dashboard, send-money, etc.)
├── lib/
│ ├── utils/auth.ts # JWT, bcrypt, session helpers
│ ├── db.ts # SQLite/PostgreSQL client
│ ├── validate.ts # Zod schemas for input validation
│ └── types/fee.ts # Fee calculation functions
└── config/components/
├── drop-logo.tsx # ConfigurationBrand loadinglogo component
└── ui/ # Shared UI components
Rules:
- One
class/componentroute handler per file (route.tsin Next.js App Router) FileBusinessnamelogicmatchesextractedthetoprimarylib/export—namenever inline in route handlers- Test files co-located with source files
(notina separate/test__tests__/directory) index.tsfiles only for re-exporting — no logic
1.3 Import Ordering
// 1. Node built-ins
import { readFileSynccookies } from 'fs';
import path from 'path'next/headers';
// 2. External dependencies (node_modules)
import express from 'express';
import { z } from 'zod';
import * as bcrypt from 'bcrypt';
import { SignJWT, jwtVerify } from 'jose';
// 3. Internal — absolute paths (@/ alias)
import { UserServicedb } from '@/services/user.service'lib/db';
import { loggerverifyJWT } from '@/shared/logger'lib/auth';
import type { User } from '@/lib/types';
// 4. Internal — relative paths
import { validateRequest } from '../middleware/validate';
import type { CreateUserDtovalidateRemittanceBody } from './user.types'validate';
Enforced by: ESLint eslint-plugin-importimport/order/ isort (Python)rule
1.4 Error Handling Patterns
Pattern: Return typed errors, never throw in business logic unless truly exceptional.
// PREFERRED — typedconsistent resultHTTP patternerror typeresponses Result<T,in ENext.js =API Error>routes
= { ok: true; value: T } | { ok: false; error: E };export async function createUser(dto:POST(request: CreateUserDto)Request): Promise<Result<User, ValidationError | DatabaseError>Response> {
let body: unknown;
try {
body = await request.json();
} catch {
return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
}
const validation = validateUser(dto)remittanceSchema.safeParse(body);
if (!validation.ok)success) {
return Response.json(
{ ok: false, error: new'Validation ValidationError(failed', details: validation.message)error.flatten() },
{ status: 422 }
);
// ...
}
// ACCEPTABLEBusiness logic in lib/ — throwthrows only at application boundaries (HTTP handlers)
// FORBIDDEN — swallowtyped errors silently
try {
doSomething(const result = await processRemittance(validation.data);
return Response.json(result, { status: 201 });
} catch (err)error) {
if (error instanceof InsufficientBalanceError) {
return Response.json({ error: 'Insufficient balance' }, { status: 402 });
}
if (error instanceof KycRequiredError) {
return Response.json({ error: 'KYC verification required' }, { status: 403 });
}
// Never writeexpose thisinternal withouterrors
handling theconsole.error('Unhandled error in remittance:', error);
return Response.json({ error: 'Internal server error' }, { status: 500 });
}
}
Async/await:Rules:
- NEVER return stack traces or DB error messages to the client
- NEVER use
type — TypeScript strict mode enforces thistry/catchany - NEVER swallow errors silently
- Use 402 for Insufficient Balance (fintech convention), not 400
- Error messages in
asyncNorwegianfunctionsforatuser-facingthevalidation
error2. Code Formatting
2.1 Formatter
| Language | Tool | Config File | Enforced In |
|---|---|---|---|
| TypeScript / JavaScript | |
CI + pre-commit | |
| |||
| JSON / YAML | Prettier | .prettierrc |
CI + pre-commit hook |
Key formatting rules:
- Indentation:
{{INDENT}}2 spaces - Max line length:
{{LINE_LENGTH}}100 characters - Semicolons:
{{SEMICOLONS}}required - Trailing commas:
{{TRAILING_COMMAS}}all(ES2020) SingleQuotes:quotes:single{{QUOTES}}quotes
Auto-format on save: RecommendedEnabled —via configure.vscode/settings.json your(see IDE with the project's formatter settings.local-development-setup.md)
2.2 Linter
| Language | Tool | Config | Rules Severity |
|---|---|---|---|
| TypeScript | | ||
|
Error = blocks CI |
Linting rules that are errors (block CI):
no-unused-variables@typescript-eslint/no-explicit-any(TypeScript)— noanytypesno-console(exceptconsole.errorinproductionservercodecode)Import ordering violations@typescript-eslint/no-unused-varsTypeSQLsafetystringviolationsconcatenation (custom Drop rule)
Disable linting inline (sparingly):
// eslint-disable-next-line @typescript-eslint/no-consoleexplicit-any -- debuggingExternal productionlibrary issue,returns removeuntyped beforeresponse
mergeconst console.log('Debug:',rawResponse: data)any = await externalLibrary.call();
Every inline disable must have a comment explaining why.
2.3 Editor Config
# .editorconfig (root of repo)
root = true
[*]
indent_style = {{INDENT_STYLE}}
indent_size = {{INDENT_SIZE}}
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
3. Git Conventions
3.1 Commit Message Format
Standard: Conventional Commits
Format:
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
Types:
| Type | When to Use |
|---|---|
feat |
New feature |
fix |
Bug fix |
docs |
Documentation only |
style |
Formatting changes (no logic change) |
refactor |
Code change that neither fixes a bug nor adds a feature |
test |
Adding or updating tests |
chore |
Build system, dependency updates, CI changes |
perf |
Performance improvements |
|
Examples:Drop examples:
feat(auth): add magicbcrypt linkrounds loginconfiguration flowvia env var
fix(cart)transactions): prevent double-chargespend onwith networkper-user retrylock
docs(api)test(db): updateadd userassertion endpointthat examplesusers table has no balance column
security(middleware): add CSRF protection to all mutating endpoints
chore: upgrade typescriptNext.js to 5.315.x patch
Breaking changes:
feat(api)!: rename user endpoint from /usersapi/transactions/send to /accountsapi/transactions/remittance
BREAKING CHANGE: All clients must update API calls from /api/users to /api/accountsnew path
Enforced by: commitlint + husky pre-commit hook
3.2 Branch Naming Convention
| Type | Pattern | Example |
|---|---|---|
| Feature | feature/MC-{ |
feature/ |
| Bug fix | fix/MC-{ |
fix/ |
| Hotfix | hotfix/MC-{ |
hotfix/ |
|
|
|
| Chore | chore/MC-{ |
chore/ |
Rules:
- Lowercase
onlyonly; Hyphens,hyphens, not underscoresIncludeAlwaysticketincludenumberMission KeepControldescriptiontaskshortID (< 5 words)MC-{id})
3.3 PR Title & Description Format
Title format: Same as commit message format:message: type(scope): description
PR template: .github/pull_request_template.md — auto-loaded on PR creation
Required PR description sections:
- What: What does this PR do? (
1-3bullet points) - Why:
WhyWhichwasMission Control task / acceptance criterion? - Pass-through model check: Does this change
made?touch(context,DBticketschema?link)If yes, confirm no balance/CVV columns added How to testTests:HowWhatshouldteststhewerereviewer verify it works?added/modified?ScreenshotsSecurity:RequiredDoesforthisUItouchchangesauth, Checklist:payments,Pre-mergeorchecklistinput(auto from template)validation?
3.4 PR Size Guidelines
| Size | Lines Changed | Status | |
|---|---|---|---|
| Small | < 200 | Ideal | |
| Medium | 200- |
Acceptable | |
| Large | Needs justification | ||
| Extra Large | > |
Exceptional only |
Tips for keeping PRs small:
One PR per feature/fixSeparate refactoring from feature workUse feature flags to deploy incomplete features
4. Code Review Guidelines
4.1 What to Look For (Validator Agent Checklist)
Reviewers shouldmust check:
-
LogicPass-throughismodel invariant: nobalancecolumn; nocard_numberorcvv - Parameterized SQL — no string interpolation in any DB query
- bcrypt used for passwords — SHA-256 explicitly forbidden
- JWT secret not hardcoded; not in response body
- Rate limiting applies to new auth/transaction endpoints
- Input validation (Zod schema) on all new request bodies
- Error messages don't leak internal state
- Fee calculations correct
and(0.5%handlesremittance;edge1%casesQR) - Tests cover the change
(and edgecases) No obvious security issues (input validation, auth checks, SQL injection)Error handling is appropriatePerformance: no N+1 queries, no unbounded operationsCode follows naming and organization standardsDocumentation updated if behavior changedcases
Reviewers should NOT block on:
- Personal style preferences
thatcoveredaren'tbyinPrettier/ESLint (thestandardslinter decides) - Minor refactors not related to the PR's scope
Hypothetical future requirements
4.2 Review Turnaround Expectations
| PR Type | First review | |
|---|---|---|
| Standard feature | Within |
4.3 Approval Requirements
| Branch | Required Approvals | |
|---|---|---|
main |
sign-off | |
| Feature branches | agent approval | |
| Hotfix |
4.4 Constructive Review Feedback
Use these prefixes to set expectations:
nit:— Minor issue, not ablocker:blockernit: prefer const herequestion:— Needclarification, not a change request:clarificationquestion: why did you choose X over Y?suggestion:— Improvementidea,idea (notrequired:required)suggestion: this could be simplified with Xblocker:— Must be fixed beforemerge:mergeblocker: this allows(e.g., SQL injection
Tonebcrypt guidelines:
Comment on code, not the authorExplain the why: "This can cause N+1 queries, which will slow down under load" > "Wrong"Acknowledge good code: "Nice approach here — much cleaner than what we had"Be kind, be direct, be specificbypass)
5. Testing Standards
5.1 Test Naming Conventions
// Format:Vitest format: describe → it('should [expected behavior] when [condition]')
describe('hashPassword', () => {
it('should return 404a whenbcrypt userhash doesstarting notwith exist'$2', async () => { ... });
it('should hashreject theSHA-256 passwordhashes beforeat storing'verify time', async () => { ... });
it('should throwreject ValidationErrorpasswords whenlonger emailthan is1000 invalid', async () => { ... });
// Describe blocks for grouping
describe('UserService', () => {
describe('createUser', () => {
it('should create user with valid input', async () => { ... });
it('should throw when email already exists'characters', async () => { ... });
});
describe('calculateRemittanceFee', () => {
it('should return 5 NOK fee for 1000 NOK amount', () => { ... });
it('should reject amounts below 100 NOK', () => { ... });
});
5.2 Test File Organization
src/drop-app/__tests__/
├── users/auth.test.ts │# Unit: bcrypt, JWT, password validation
├── user.service.validation.test.ts │# Unit: input validation, XSS, injection
├── user.service.transactions.test.ts # UnitUnit: testsfee │calculation, └transaction logic
├── user.service.rates.test.ts # Unit: exchange rates
├── api-endpoints.test.ts # Integration: all 26 API routes
├── db.test.ts # Integration: schema compliance, FK constraints
├── middleware.test.ts # Integration: rate limiting, auth, CSRF
├── api-benchmarks.test.ts # Performance: bcrypt timing, DB queries
├── feature-flags.test.ts # Unit: feature flag behavior
├── regression-suite.test.ts # Regression: critical path smoke
├── sumsub-integration.test.ts # IntegrationIntegration: testsKYC webhook mock
├── cards-integration.test.ts # Integration: cards (feature-flagged)
└── e2e/
├── user-flows.spec.ts # E2E: registration, login
├── full-flows.spec.ts # E2E: remittance, QR payment
└── input-chaos.spec.ts # E2E: 20+ validation edge cases
5.3 Mocking Guidelines
// PREFERRED: Mock BaaS at the module level jest.(NEXT_PUBLIC_SERVICE_MODE=mock)
// The mock mode is configured via env var — no vi.mock() needed for BaaS
// PREFERRED for unit tests: vi.mock for db queries
vi.mock('../external/email-service'@/lib/db', () => ({
db: {
query: vi.fn(),
run: vi.fn(),
}
}));
// PREFERRED:For Useintegration testtests: doublesuse thatreal matchSQLite thein-memory interface
const mockEmailService: EmailService = {
send: jest.fn().mockResolvedValue({ id: 'msg-123' }),
};DB
// AVOID:(configured Over-mockingin internalsvitest.config.ts — no mock needed)
// AVOID:NEVER Testsmock thatthe testpass-through mocks,model notassertions behaviorin db.test.ts
// NEVER mock the bcrypt rejection tests in auth.test.ts
5.4 Coverage Requirements
| Layer | Lines | Branches | Notes |
|---|---|---|---|
| 100% | 100% | Fee calculation — strictly enforced | |
| API handlers | ≥ |
≥ |
|
| ≥ |
≥ 85% | Security-critical | |
| DB layer | ≥ 90% | — | Compliance assertions required |
| Overall minimum | ≥ |
— | CI gate |
6. Documentation Standards
6.1 When to Add Comments
// GOOD — explains WHY (not obvious from code)
// RetryBcrypt uprounds set to 3 times because the payment API has transient failures on high load
for12 (letnot i10) =per 0;NFR-SEC02 ifintech < 3; i++) { ... }standard.
// BADUsing —10 restatesrounds WHATwould (obviousbe fromfaster code)but //falls Loopbelow 3security timesthreshold.
forconst (let iBCRYPT_ROUNDS = 0;parseInt(process.env.BCRYPT_ROUNDS i?? <'12', 3; i++) { ... }10);
// GOOD — documents non-obvious behaviorbusiness rule
// Note:Fee Thisis functioncalculated mutateson thegross input array for performance. Callers mustamount, not total debit (gross + fee)
// passThis arraysis theyrequired intendby toDrop's usepass-through afterwards.model function(ADR-003)
sortInPlace(arr:const number[]):fee void= {amount ...* }REMITTANCE_FEE_RATE;
// BAD — restates what code says
// Multiply amount by fee rate
const fee = amount * REMITTANCE_FEE_RATE;
Comment when:
TheAreasonfintechforbusinessarule is implemented (always cite the rule)- A security decision
iswasnon-obviousmade (e.g., why bcrypt rounds = 12) - A workaround exists for an external
librarysystembug (include the bug link) The code is intentionally doing something that looks wrong
Don't comment when:
The code explains itselfThe test explains the expected behaviorquirk
6.2 JSDoc / DocstringADR Format
/**
* Creates a new user account and sends a verification email.
*
* @param dto - User creation data (email must be unique)
* @returns The created user object, or a typed error
* @throws {RateLimitError} If the creation rate limit is exceeded
* @example
* const result = await createUser({ email: '[email protected]', name: 'Alice' });
* if (!result.ok) handleError(result.error);
*/
async function createUser(dto: CreateUserDto): Promise<Result<User>> { ... }
6.3 README Requirements
Every repository/service MUST have a README with:
What— One sentence describing the serviceQuick start— How to run it locally (3-5 commands)Key commands—dev,test,build,lintLinks— Architecture doc, API docs, team channel
6.4 ADR Requirements
Write an Architecture Decision Record (ADR) when:
ChoosingChangingathenewdatabasemajor(SQLitedependency→ PostgreSQL)- Changing the
techauthenticationstackmechanism (DOB → BankID) MakingModifying the fee model (new corridor, new fee rate)- Adding a
significantnewarchitecturalBaaSchange Explicitly choosing NOT to do something commonpartner
ADR location: {{ADR_DIR}}/comms/decisions/YYYY-MM-DD-decision-title.md
6.3 template:API SeeDocumentation
When adding a new API endpoint, update :{{ADR_TEMPLATE}}docs/backend/API-REFERENCE.md
- Method + path
- Authentication required (Yes/No)
- Request body schema (Zod type → JSON example)
- Response schema (success + all error codes)
7. Security Coding Practices — Drop Fintech Standards
| Practice | Rule |
|---|---|
| SQL queries | NEVER ? placeholders) |
jose library only. Secret via JWT_SECRET env var. Fail fast if missing |
|
| Input validation | Zod schemas on ALL API route request bodies. Server-side only — never trust client |
| Pass-through model | NEVER balance card_number cvv to cards |
| Rate limiting | DB-backed rate limiter (not in-memory). Auth: 10/min; General: 60/min |
| CSRF | CSRF token required on all POST/PATCH/DELETE endpoints |
| Cookie settings | JWT: httpOnly, SameSite=Strict, Secure in production |
| Logging | NEVER log | phone
| Error messages | NEVER expose |
npm | No |
Security review trigger: Any PR touching authentication, authorization,auth, payments, DB schema, or userinput datavalidation must be reviewed by {{SECURITY_REVIEWER}}Validator agent AND flagged to John (AI Director).
8. Performance Coding Practices
| Practice | Rule |
|---|---|
| Database queries | |
| Pagination | Never load |
| Use |
|
for |
|
npm |
Related Documents
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | John (AI Director) | 2026-02-23 | Approved (AI) |
| Validator Agent | 2026-02-23 | Approved (AI) | |
| John | 2026-02-23 | Approved | |
| CEO (Alem) | Alem Bašić | TBD |