This document defines the architectural standards for the Next.js migration and future feature work. The existing React/Vite code is migration material, not the target architecture.
This revision hardens the original guide with explicit input validation, sanitization, security, financial-integrity, error-handling, testing, and observability rules so the system is safe by default, not just organized by default.
- Prefer server rendering and server data access by default.
- Keep client-side JavaScript limited to components that need browser APIs, local state, event handlers, or client navigation.
- Keep financial operations, authentication, authorization, and database access on the server.
- Use strict TypeScript types across every module.
- Keep UI, domain logic, validation, and data access separate.
- Build for extension through typed configuration and shared abstractions rather than duplicated page logic.
- Treat every input from a client, webhook, or third-party provider as untrusted until it has been validated and, where it will be rendered or stored, sanitized.
- Fail closed: on any unexpected error, return a safe generic response and log the detail server-side rather than leaking internals.
- Every operation that changes a wallet balance or transaction record must be atomic and idempotent.
- Every
page.tsxis a Server Component by default. - Every
layout.tsxis a Server Component by default. - Do not add
"use client"to route files unless there is a documented, unavoidable reason. - A page should compose server data and UI components; it should not contain a large form or presentation implementation.
- Add
"use client"only to the smallest component that requires interactivity. - Client components receive serializable props from their server parents.
- Use Server Actions or Route Handlers for mutations and server-side operations.
- Use
useRouter,usePathname, and other Next navigation APIs only inside client components. - Route Handlers that receive webhooks or third-party callbacks (payment providers, airtime/data vendors) must verify the request's authenticity (signature, shared secret, or IP allowlist) before any processing, and must respond quickly, deferring heavy work to a background job.
src/
app/
(auth)/
layout.tsx
login/page.tsx
register/page.tsx
reset-password/page.tsx
(user)/
layout.tsx
dashboard/page.tsx
airtime/page.tsx
data/page.tsx
education/page.tsx
airtime-to-cash/page.tsx
wallet/page.tsx
transactions/page.tsx
profile/page.tsx
admin/
layout.tsx
login/page.tsx
page.tsx
analytics/page.tsx
pricing/page.tsx
users/page.tsx
transactions/page.tsx
profile/page.tsx
api/
webhooks/
[provider]/route.ts
components/
user/
layout/
dashboard/
services/
wallet/
transactions/
profile/
admin/
layout/
dashboard/
analytics/
pricing/
users/
transactions/
profile/
shared/
ui/
forms/
feedback/
navigation/
lib/
auth/
db/
services/
validations/
permissions/
security/
errors/
logging/
env.ts
utils/
types/
auth.ts
user.ts
admin.ts
wallet.ts
transaction.ts
service.ts
api.ts
errors.ts
middleware.ts
prisma/
public/
Route files belong in src/app/. They define URL structure, metadata, server-side composition, and loading/error boundaries. They should not become general-purpose component directories.
All reusable UI belongs in src/components/. Separate user, admin, and shared components. A component should have one clear responsibility and a typed props interface.
Business logic, database access, authorization, service integrations, validation schemas, and utility functions belong in src/lib/. These modules must not import browser-only APIs unless explicitly isolated for client use.
lib/validations/is the single source of truth for the shape and constraints of every external input. Do not duplicate ad hoc checks inside components, Server Actions, or Route Handlers.lib/security/holds sanitizers, rate-limit helpers, and CSRF/webhook-verification utilities.lib/errors/holds typed domain errors and the mapping from domain errors to safe client-facing responses.lib/logging/holds the structured logger and the audit-log writer.lib/env.tsparses and validatesprocess.envat startup against a schema; the app must fail to start if required config is missing or malformed.
Shared domain types belong in src/types/. Prefer named types or interfaces for users, roles, wallets, transactions, services, API results, and form contracts. Avoid repeating object shapes across components. Every Zod schema in lib/validations/ exports its inferred type via z.infer; do not hand-write a parallel interface that can drift from the schema.
- User and admin routes must have separate layouts.
- User and admin navigation components must be separate.
- User and admin permissions must be checked on the server.
- Admin UI must not rely on client-side redirects as its security boundary.
- Shared primitives may live under
components/shared, but user-specific and admin-specific workflows must remain distinct. - Admin login, and any admin action that approves, reverses, or adjusts a financial record, must be rate-limited and written to the audit trail.
- Use strict TypeScript settings.
- Do not use
any; define a type, useunknown, or narrow the value safely. - Type component props explicitly.
- Type Server Action inputs and outputs.
- Type API responses and error results.
- Use discriminated unions for transaction states, roles, service categories, and result states where appropriate.
- Validate external input at runtime with Zod before using it in business logic.
- Keep source files at or below 200 lines whenever practical.
- Extract meaningful sections into components instead of splitting code mechanically.
- Extract forms, tables, charts, modals, navigation, cards, and complex sections into focused files.
- Keep business rules out of large JSX blocks.
- Keep route files thin and readable.
- A file exceeding 200 lines should trigger a review to determine whether it has more than one responsibility.
- Do not store authentication tokens, transaction PINs, OTPs, or financial state in
localStorage,sessionStorage, or any client-readable storage. - Use secure, HTTP-only,
SameSite-protected session cookies for authentication. - Keep server state on the server and pass only the data required by the UI.
- Use client state only for transient interaction state such as open modals, field values, and selected tabs.
- Use database transactions for wallet changes, purchases, refunds, and reversals.
- Keep mock data in dedicated typed modules until real Prisma-backed data replaces it.
- Never log PINs, OTPs, passwords, full card/account numbers, or session tokens, even at debug level.
The initial service set is Airtime, Data, Education, Airtime to Cash, Wallet, Transactions, and Profile.
- Define supported services through typed configuration.
- Do not duplicate provider, pricing, validation, and transaction-label logic across pages.
- Betting, Electricity, and Cable TV remain deferred and must not be added to the initial user or admin workflows.
- New services should add a domain module and configuration rather than bypassing existing boundaries.
- Every Server Action and Route Handler defines a Zod schema for its input and parses it before touching business logic; reject anything that doesn't parse with a structured, generic validation error.
- Never trust a client-supplied price, discount, fee, or wallet amount. Recompute or re-validate it server-side against the source-of-truth service configuration before executing a purchase or transfer.
- Validate and normalize domain-specific formats before use or before calling a downstream provider: phone numbers, network/provider codes, meter numbers, account numbers, and email addresses.
- Enforce explicit max lengths on all free-text fields (names, support messages, notes) to bound storage size and rendering cost.
- Sanitize any user-supplied text before it is rendered as HTML (e.g., with a library such as DOMPurify); never pass unsanitized input to
dangerouslySetInnerHTML. - Use the Prisma query builder or fully parameterized raw queries only — never build a query by concatenating user input into a string.
- Validate file uploads (if introduced) by type, size, and content — not by filename or client-reported MIME type alone.
- Reject unknown/extra fields on strict-parsed schemas rather than silently dropping or coercing them.
- Set strict security headers on every response — Content-Security-Policy,
X-Content-Type-Options: nosniff,Referrer-Policy,Strict-Transport-Security, and a frame-ancestors/X-Frame-Options policy — viamiddleware.tsornext.config. - Server Actions get Next.js's built-in origin checks; any Route Handler that mutates state on behalf of a browser session must additionally verify the request origin/referer or a CSRF token.
- Rate-limit authentication, OTP/PIN verification, password reset, and every financial mutation endpoint, per-IP and per-account, to slow brute-force and abuse.
- Hash transaction PINs and passwords with a strong, salted algorithm (argon2 or bcrypt); never store, log, or return them in plaintext.
- Verify webhook signatures from payment, airtime, and data providers before processing; treat any unverified or malformed webhook as untrusted and discard it.
- Keep all secrets (API keys, provider credentials, session/signing secrets) in environment variables validated through
lib/env.ts; never commit them or hard-code them in source. - Apply least privilege to admin accounts, service accounts, and API keys — scope each credential to only what it needs.
- Log security-relevant events (failed logins, PIN lockouts, admin actions, permission denials) to an append-only audit trail that ordinary application code cannot edit or delete.
- Store every monetary value as an integer in the smallest currency unit (kobo), never as a floating-point number.
- Every wallet-affecting operation (top-up, purchase, refund, reversal) runs inside a database transaction and accepts an idempotency key, so a retried or duplicated request cannot double-charge or double-credit a wallet.
- Treat the transaction ledger as the source of truth; the cached wallet balance is a derived value that must reconcile against it.
- When a call to a downstream provider can succeed while our own write fails (or vice versa), record the attempt and resolve it through a reconciliation/retry job — never leave it as a silent, unresolved fire-and-forget call.
- Any manual balance adjustment by an admin must be justified with a reason, tied to an admin identity, and written to the audit trail.
- Never return a raw exception message, stack trace, or database error to the client; map every error to a safe, user-facing message with a stable error code.
- Centralize error handling through typed domain errors (in
lib/errors/) caught at a single boundary per Server Action or Route Handler, rather than ad hoc try/catch scattered through UI code. - Log full error detail server-side — with a request/correlation ID and relevant non-sensitive context — through the structured logger in
lib/logging/, notconsole.log. - Add an
error.tsxboundary per route segment so an unhandled error degrades that segment instead of crashing the app.
- Unit-test all business logic and validation schemas in
lib/. - Integration-test every Server Action and Route Handler that touches the wallet, a transaction, or authentication.
- Cover critical paths — login, PIN verification, airtime/data purchase, wallet top-up, refund/reversal — with at least smoke-level end-to-end tests.
- Treat passing tests as a required, non-optional part of the Definition of Done and CI, not an afterthought.
- Emit structured logs for every financial mutation and admin action, tagged with a correlation/request ID that ties client request, server processing, and downstream provider call together.
- Add basic error-rate and uptime monitoring for API routes and Server Actions so failures surface before users report them.
- Keep the audit trail queryable by admins but not writable by ordinary application paths.
- Parse and validate all required environment variables at startup with a schema (
lib/env.ts); fail fast on missing or malformed config instead of failing later mid-request. - Pin dependency versions and run automated vulnerability scanning (
npm audit, Dependabot, or equivalent) in CI. - Separate environment configuration (and secrets) cleanly between development, staging, and production; production secrets must never be reachable from a lower environment.
- Establish strict Next.js, TypeScript, Tailwind v4, ESLint, and Prisma configuration, plus a runtime-validated environment schema.
- Define shared domain types and remove unsafe
anyusage. - Create separate user and admin layouts.
- Convert route files into thin Server Components.
- Extract interactive forms and controls into focused Client Components.
- Replace React Router with Next.js navigation inside client components only.
- Move mock data into typed modules.
- Add Zod validation schemas for every Server Action and Route Handler input, and sanitize any user-supplied content before it is rendered or persisted.
- Introduce Prisma repositories and database transactions, including idempotency keys for every wallet-affecting operation.
- Replace simulated authentication with secure server sessions, hashed PINs/passwords, and rate-limited login/OTP endpoints.
- Add security headers, CSRF protection for non-Server-Action mutations, and signature verification for all provider webhooks.
- Add structured logging, an append-only audit trail for financial and admin actions, and centralized typed error handling.
- Add automated checks for type safety, file size, linting, tests, dependency vulnerabilities, and build health.
A feature is complete when:
- Its route remains server-first.
- Interactive behavior is isolated in the smallest necessary Client Component.
- User and admin ownership is clear.
- Props, domain data, validation, and mutation results are typed.
- All external input is validated with Zod and, where rendered, sanitized.
- Any financial mutation is wrapped in a database transaction and is idempotent.
- No secret, PIN, password, or stack trace is exposed to the client or written to logs.
- Errors are caught by a typed boundary and return safe, coded responses to the client.
- Relevant unit and/or integration tests exist and pass for new business logic.
- Security headers and rate limiting cover any new public or authentication endpoint.
- No source file unnecessarily exceeds 200 lines.
- Loading, error, empty, success, and mobile states are handled.
- Authentication and authorization are enforced server-side.
- Lint, typecheck, tests, and production build pass.