This is a Next.js project bootstrapped with create-next-app.
First, run the development server:
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun devOpen http://localhost:3000 with your browser to see the result.
You can start editing the page by modifying app/page.tsx. The page auto-updates as you edit the file.
This project uses next/font to automatically optimize and load Geist, a new font family for Vercel.
To learn more about Next.js, take a look at the following resources:
- Next.js Documentation - learn about Next.js features and API.
 - Learn Next.js - an interactive Next.js tutorial.
 
You can check out the Next.js GitHub repository - your feedback and contributions are welcome!
The easiest way to deploy your Next.js app is to use the Vercel Platform from the creators of Next.js.
Check out our Next.js deployment documentation for more details.
This project includes a lightweight useApi hook used across features. A few testing tips:
- Prefer to assert hook-driven UI changes using RTL's 
waitFororact()rather than relying on internal timing within hooks. Tests that await the publicrefresh()method should wrap assertions inwaitForso they observe committed state. useApipreviously used extra macrotask waits to stabilize Jest runs; that behavior has been removed in favor of clearer test assertions. If you encounter flakiness in CI, prefer adding explicitwaitFor()in the test rather than reintroducing hidden waits in the hook.- Use 
clearApiCache()fromsrc/hooks/use-api.tsin test setup/teardown if you need to reset internal caches between tests. 
If you'd like, I can add a short CONTRIBUTING.md with testing conventions for this repository.
Structured logging is provided by a lightweight zero-dependency logger in src/lib/logger.ts. It supports:
- Log levels (debug, info, warn, error) with 
LOG_LEVELenv control. - Pretty colored output in development; JSON lines in production (override with 
LOG_PRETTY=1). - Redaction of obvious sensitive keys (password/token/secret/authorization).
 - Scoped loggers via 
createLogger('scope'). - Safe error serialization with 
serializeError(error). 
Example:
import { createLogger, serializeError } from '@/lib/logger'
const log = createLogger('inventory')
try {
  // ...
  log.info('item_created', { id })
} catch (e) {
  log.error('create_failed', { error: serializeError(e) })
}Initial minimal tracing utilities live in src/lib/observability/context.ts (no external dependencies yet). They provide:
createRequestContext()– generatesrequestId&traceId.startSpan(name, ctx)andwithSpan(name, ctx, fn)– lightweight timing spans.- API routes (inventory & users) now include 
x-request-idandx-trace-idheaders for correlation. 
Planned future upgrade path:
- Introduce OpenTelemetry SDK for real span objects and exporters.
 - Propagate context via middleware and async context APIs.
 - Auto-enrich logs with active span/trace IDs directly from OTel context.
 
Until then, search logs using requestId / traceId to correlate a single request across layers.
Role & action based authorization is centralized in src/lib/authorization/policies.ts providing:
can(user, action)-> booleanrequirePermission(user, action)-> throwsAuthorizationErrorif deniedlistRolePermissions(role)-> list actions
Defined actions (initial set): inventory:create|update|delete|read, user:list, order:create|update|read|delete.
Current role mappings (simplified):
- 
ADMIN: all actions
 - 
MANAGER: inventory (create/update/read), user:list, order (create/update/read)
 - 
SUPERVISOR: inventory:read, user:list, order:read
 - 
THIRD_PARTY_CLIENT: order:read only
 - 
Default window: 60s
 - 
Default max: 30 requests per identity (user ID or fallback IP) per verb grouping
 
Returned headers on instrumented routes:
X-RateLimit-Limit– maximum allowed in the windowX-RateLimit-Remaining– remaining quotaX-RateLimit-Reset– epoch seconds when window resetsRetry-After– (429 only) seconds until next allowed attempt
Future upgrade path: swap internal map storage with Redis using atomic INCR + PX + TTL; expose async interface without changing calling code. Could add token bucket or sliding window for burst smoothing.
Lightweight Prisma query performance tracking is implemented in src/lib/prisma-performance.ts and automatically attached in src/lib/db.ts when enabled.
Capabilities:
- 
Optional capture of raw SQL text and parameter JSON for slow query analysis
 - 
DB_PERF_ENABLE=1– turn the feature on (defaults to on in development unless explicitly set0) - 
SLOW_QUERY_MS=150– threshold in ms for marking a query as slow - 
DB_PERF_MAX_SAMPLES=50– max retained slow query samples (oldest dropped first) - 
DB_PERF_VERBOSE=1– log non-slow queries at debug level - 
DB_PERF_CAPTURE_SQL=1– include SQL text in stored/logged sample (may contain identifiers) 
import { getSlowQuerySamples } from '@/lib/prisma-performance'
const samples = getSlowQuerySamples()
// [{ ts, durationMs, query?, params?, target? }, ...]Each slow sample logs an event slow_query with minimal metadata (duration + model/target) to avoid leaking SQL unless explicitly configured. Use these samples to:
- Identify candidate indexes / query rewrites
 - Correlate with application-level spans (timestamps close to span durations)
 - Establish baseline before deeper instrumentation (e.g. full OpenTelemetry + exporter)
 
Future possible enhancements:
- Export metrics to Prometheus (histogram + counter for slow queries)
 - Add basic percentile tracking without external deps
 - Integrate trace/span IDs into query events once tracing context propagation improves
 
An authenticated ADMIN-only endpoint exposes recent slow query samples:
GET /api/_internal/db-perf?limit=25
{
"samples": [
{ "ts": 1696032000000, "durationMs": 342, "target": "User", "query": "SELECT ..." }
],
"total": 42
}
Security:
- Protected by `diagnostics:read` action (only ADMIN mapped).
- Do NOT enable `DB_PERF_CAPTURE_PARAMS` or `DB_PERF_CAPTURE_SQL` in production unless reviewed; may contain sensitive data.
- Keep endpoint unlinked from public navigation; path intentionally namespaced under `_internal`.
Operational Tips:
- To flush samples quickly for a test session: temporarily lower `SLOW_QUERY_MS`.
- For ad-hoc debugging, set `DB_PERF_VERBOSE=1` to see all query durations in logs while still retrieving slow samples via the endpoint.
### Performance Summary Endpoint
`GET /api/_internal/db-perf/summary` (ADMIN only) returns aggregate counters:
Example response:
```json
{
	"totalQueries": 1284,
	"totalDurationMs": 53210,
	"avgMs": 41.4,
	"slowCount": 37,
	"slowRate": 0.0288,
	"slowThresholdMs": 150,
	"capturedSlowSamples": 37,
	"maxSlowSamples": 50,
	"globalP95Ms": 210,
	"sampleWindow": 200,
	"targets": [
		{ "target": "Order", "count": 600, "slow": 20, "avgMs": 52.3, "maxMs": 412, "p95Ms": 180, "slowRate": 0.033 },
		{ "target": "User", "count": 400, "slow": 9,  "avgMs": 12.1, "maxMs": 88,  "p95Ms": 40,  "slowRate": 0.0225 }
	],
	"generatedAt": 1696032000123
}
Notes:
p95MsandglobalP95Mscomputed from a rolling window (sizeDB_PERF_SAMPLE_WINDOW, default 200) using a simple sorted index method.targetssorted descending by query count for quick hotspot identification.- Use 
slowRate(slow / count) per target to prioritize tuning even if raw durations differ. - Adjust window via 
DB_PERF_SAMPLE_WINDOWto trade memory vs smoothing; small windows react faster, large windows stabilize noise. 
Future enhancement: add lightweight rolling window histogram to approximate percentiles.
Testing:
isSlowQueryclassification covered bysrc/lib/__tests__/prisma-performance.test.ts(boundary: below, equal, above threshold)- Additional integration validation can be done by temporarily setting 
SLOW_QUERY_MS=0and exercising API routes to force slow classification 
Implemented in src/lib/unified-validation.ts to provide a single, typed entry point for validating body, query, and route params using Zod schemas.
Key Features:
- Separate optional schemas: 
body,query,params. - Query value coercion (
"2" -> 2,"true" -> true) enabled by default. - Size guard (
maxJsonBytes, default 1MB) with explicit413 JSON_TOO_LARGEerrors. - Content-Type enforcement (default 
application/json). - Consistent structured error payload: 
{ error, message, details: [{ path, code, message }] }. - Strong typing of returned 
body/query/paramsobjects. 
Example Usage (Inventory POST):
const validate = buildValidator({
  body: z.object({
    name: z.string().min(1),
    sku: z.string().min(1),
    description: z.string().optional(),
    unitPrice: z.union([z.number(), z.string()]).transform(v => Number(v)),
  }),
})
const { body } = await validate(request)Error Handling:
Wrap the validator in try/catch and return validationErrorResponse(err) if err instanceof HttpError.
Migration Strategy:
- Existing older helpers (
validation.ts,request-validation.ts) remain for backwards compatibility. - New or refactored routes should adopt the unified validator for consistency.
 - Gradually phase out ad-hoc manual 
await request.json()+ inline checks. 
Testing:
- Unit tests in 
src/lib/__tests__/unified-validation.test.tscover: success path, body failure, query coercion. - Future enhancements: add integration tests asserting 400 error shape on malformed payloads.
 
All new/refactored routes may use helpers in src/lib/api-errors.ts to ensure a consistent payload structure.
Shape:
{ "error": "CODE", "message": "Human readable", "details": [ ...optional structured data... ] }Helpers:
Inventory & Users routes now emit unified error responses for common failure modes (authz, rate limit, validation, internal errors).
Benefits:
Planned Enhancements:
apiSuccess now supports a lightweight built-in ETag mechanism to help clients leverage conditional requests and reduce payload transfer for unchanged resources.
Options:
apiSuccess({
  data,
  etag: true, // auto-generate weak ETag from JSON body hash
  // OR provide custom string (quoted automatically if not already)
  // etag: 'resource-v1'
  requestHeaders: request.headers, // supply to enable 304 evaluation
  headers: { 'Cache-Control': 'public, max-age=30' },
})Behavior:
- When 
etag: true, a weak ETagW/"<hash>"is produced using a fast FNV-1a style hash of the serialized JSON. - When 
etag: '<value>', the helper normalizes to a quoted (strong) tag unless you pass something already starting withW/or a quote. - If the incoming request supplies 
If-None-Matchand it matches (case-sensitive) the final ETag (weak or strong), a304 Not Modifiedis returned with no body. - Matching is tolerant: it strips weak prefixes and quotes for comparison to handle clients that supply normalized values.
 
Notes / Caveats:
- The hash is for cache validation only (not cryptographic integrity). For stronger guarantees use a content digest header or real build fingerprint.
 - Only applied when you provide 
etag– no implicit generation. - Future enhancement could include automatic ETag injection on selected routes via middleware.
 
Client Tip:
Include If-None-Match header from a previous response's ETag to leverage 304s and save bandwidth.
To eliminate repetitive boilerplate for assigning and propagating correlation identifiers, routes can wrap their handlers with withApiContext from src/lib/observability/context.ts.
Features:
- Automatically reads inbound 
x-request-id/x-trace-idheaders (if present) or generates new IDs. - Supplies a lightweight 
RequestContextobject{ requestId, traceId, startTime }to your handler as the second argument. - Ensures the final 
Responseincludes bothx-request-idandx-trace-id(without overwriting if you already set them). - Safe in both real Next.js runtime and Jest test environment (graceful handling of polyfilled 
Headers). 
Example:
import { withApiContext, withSpan } from '@/lib/observability/context'
import { apiSuccess, apiError, unauthorized } from '@/lib/api-errors'
import {
  getUserFromRequest,
  requireAuth,
  BasicAuthRequest,
} from '@/lib/jwt-auth'
export const GET = withApiContext(async (request: Request, ctx) => {
  try {
    const user = getUserFromRequest(request as unknown as BasicAuthRequest)
    requireAuth(user)
    const data = await withSpan('users.list', ctx, async () => {
      /* ... */
    })
    return apiSuccess({ data })
  } catch (e) {
    if (e instanceof Error && e.message === 'Unauthorized')
      return unauthorized()
    return apiError({
      status: 500,
      code: 'INTERNAL_SERVER_ERROR',
      message: 'Internal error',
    })
  }
})Testing:
- A dedicated test (
with-api-context.test.ts) validates header injection, propagation of incoming IDs, and error surfacing. - If you unit test a wrapped handler directly, just call the exported function with a standard 
Request– the wrapper manages context. 
Migration Guidelines:
- Replace previous manual generation of correlation IDs (if any) with the wrapper.
 - Remove ad-hoc header setting (
x-request-id,x-trace-id) inside handlers—they'll be injected automatically. - Use the provided 
ctxfor span creation or structured logging enrichment. - Prefer 
withSpan('operation', ctx, fn)for nested instrumentation. 
Why not a Middleware?
- The App Router's edge/runtime nuances plus desire for a testable pure function made a wrapper simpler at this stage. Middleware-based propagation can be layered later without changing handler signatures.
 
Future Enhancements:
- Integrate with AsyncLocalStorage / OpenTelemetry context for transparent span propagation.
 - Automatic log enrichment via an adapter logger bound to the current context.
 
buildValidator previously required a NextRequest. It now accepts a standard Request (optionally with a nextUrl: URL property). This simplifies testing and decouples core validation from the Next.js runtime type.
Update Notes:
- Existing code that passed a 
NextRequestcontinues to work (structurally compatible withRequest). - Query parsing falls back to 
new URL(req.url).searchParamswhennextUrlis absent. - No functional changes to validation semantics; only the accepted input type broadened.
 
When combining withApiContext and apiSuccess({ etag: true }) you get:
- Stable correlation headers for tracing.
 - Automatic conditional 304 responses when content unchanged.
 - Uniform success / error payload envelopes.
 
Example pattern:
export const GET = withApiContext(async (request, ctx) => {
  const data = await withSpan('inventory.list', ctx, () =>
    service.list(/*...*/)
  )
  return apiSuccess({ data, etag: true, requestHeaders: request.headers })
})Q: Do I need to wrap every route immediately? No. New or actively modified routes should adopt the wrapper; legacy routes can migrate opportunistically.
Q: Will adding the wrapper break streaming responses?
Not currently supported for streaming because the wrapper clones/injects headers once. Planned enhancement: detect ReadableStream bodies and preserve them.
Q: How are IDs generated? Simple UUID (or Math.random fallback) truncated for requestId; full 32 hex chars for traceId.
Q: Can I override the generated IDs?
Yes—supply your own x-request-id / x-trace-id headers in the incoming request; they propagate through unchanged.
To simplify accessing the current requestId / traceId deep inside services or repositories without threading a context parameter, the project now includes a minimal AsyncLocalStorage-backed helper.
Location: src/lib/observability/async-context.ts
Exports:
runWithRequestContext(ctx, fn)– internal utility used bywithApiContextto establish the context scope.getCurrentRequestContext()– retrieve{ requestId, traceId, startTime }for the active request (orundefinedif none).
Usage Example (inside a service method):
import { getCurrentRequestContext } from '@/lib/observability/async-context'
import { createLogger } from '@/lib/logger'
const log = createLogger('inventory.service')
export async function doWork() {
  const ctx = getCurrentRequestContext()
  if (ctx)
    log.debug('work_started', {
      requestId: ctx.requestId,
      traceId: ctx.traceId,
    })
  // ... perform logic ...
}Behavior & Limitations:
- In Node runtimes with 
async_hooks, context flows across async/await, microtasks, and timers. - In environments lacking 
AsyncLocalStorage(some edge runtimes), a no-op shim returns only synchronous access (most handlers still work; deep async boundaries will not retain context). - No automatic log injection yet—explicitly include IDs where needed (future enhancement could wrap logger factory).
 
Testing:
async-context.test.tsasserts that identifiers remain stable acrossPromise.resolve()andsetTimeout()boundaries.
Roadmap Enhancements:
- Tie into a future OpenTelemetry context if/when tracing SDK is adopted.
 - Provide a 
scopedLogger()that automatically pulls current context. - Add request duration metric tagging with trace IDs.
 
Opt-Out:
- If you prefer explicit propagation, simply ignore 
getCurrentRequestContext(); existing handlerctxparameter remains authoritative.