This document describes the HTTP API exposed by the application using Next.js Route Handlers in the App Router (app/api/*/route.js).
All routes below are served under the /api prefix.
Frontend developers: For practical usage examples and the small
apiClienthelper layer, readdocs/frontend-api-usage.md. That document is the frontend-oriented single source of truth.
The API expects a valid server configuration.
Required environment variables:
MONGODB_URI — database connection string (used by lib/db.js).SESSION_SECRET — JWT signing secret for session cookies.NAS_ROOT_PATH — NAS mount root for storage operations.Optional environment variables:
SESSION_COOKIE_SECURE — override for the cookie Secure flag (true/false).The Search API can run with different provider backends.
SEARCH_PROVIDER (optional)
fs | qsirchfsNotes:
fs is a local/test fallback that traverses the NAS-like folder structure directly.qsirch is the intended production provider (indexed search on QNAP).If SEARCH_PROVIDER=qsirch, these variables are required:
QSIRCH_BASE_URL — base URL of the Qsirch service (must be reachable from inside the app container).
http://192.168.0.22:8080QSIRCH_ACCOUNT — QTS/Qsirch account used for server-to-server search.
QSIRCH_PASSWORD — password for the account.
QSIRCH_PATH_PREFIX — path prefix that contains the branch folders.
/NiederlassungenOptional Qsirch tuning:
QSIRCH_DATE_FIELD
modified | created (case-insensitive)modifiedQSIRCH_MODE
sync | async | auto (case-insensitive)syncNotes:
auto currently behaves like sync (placeholder for a later async implementation).Normalization rule (A.1)
lib/config/validateEnv.js accepts these values case-insensitively.QSIRCH_DATE_FIELD / QSIRCH_MODE, and trim for QSIRCH_PATH_PREFIX) so behavior matches validation.The environment can be validated via:
lib/config/validateEnv.jsscripts/validate-env.mjsIn Docker/production-like runs, execute node scripts/validate-env.mjs before starting the server to fail fast.
Note: You may see a Node warning like
MODULE_TYPELESS_PACKAGE_JSONwhen running env validation. The validation still succeeds; the warning is harmless.
Authentication uses a signed JWT stored in an HTTP-only cookie (auth_session).
To access protected endpoints:
POST /api/auth/login to obtain the cookie.Notes:
Secure and the app should run behind HTTPS.http://localhost:3000), you may set SESSION_COOKIE_SECURE=false in your local docker env file.RBAC is enforced on filesystem-related endpoints.
Role model:
branch — restricted to own branchadmin — access all branchessuperadmin — access all branchesdev — access all branchesResponse semantics:
User management is a separate capability from branch access.
Rules:
superadmin, devadmin, branchWhen an endpoint is guarded by this capability, it must return:
403 AUTH_FORBIDDEN_USER_MANAGEMENT
{
"error": {
"message": "Forbidden",
"code": "AUTH_FORBIDDEN_USER_MANAGEMENT"
}
}
Notes:
Most endpoints return JSON.
Success responses keep their existing shapes (unchanged).
Error responses always use this standardized shape:
{
"error": {
"message": "Human readable message",
"code": "SOME_MACHINE_CODE",
"details": {}
}
}
Notes:
error.message is intended for humans (UI, logs).error.code is a stable machine-readable identifier (frontend handling, tests, monitoring).error.details is optional. When present, it must be a JSON object (e.g. validation info).Binary endpoints
Some endpoints may return a non-JSON body on the 200 happy path (for example application/pdf).
Rules for such endpoints:
The API uses the following status codes consistently:
400 — invalid/missing parameters, validation errors401 — unauthenticated (missing/invalid session) or invalid credentials403 — authenticated but not allowed (RBAC / capability mismatch)404 — resource not found (branch/year/month/day/file does not exist)500 — unexpected server errors (internal failures)The API uses these machine-readable codes (non-exhaustive list):
Auth:
AUTH_UNAUTHENTICATEDAUTH_INVALID_CREDENTIALSAUTH_FORBIDDEN_BRANCHAUTH_FORBIDDEN_USER_MANAGEMENT (RHL-041; used by RHL-012)Validation (generic):
VALIDATION_MISSING_PARAMVALIDATION_MISSING_QUERYVALIDATION_INVALID_JSONVALIDATION_INVALID_BODYVALIDATION_MISSING_FIELDValidation (password management):
VALIDATION_WEAK_PASSWORDValidation (filesystem route params):
VALIDATION_BRANCHVALIDATION_YEARVALIDATION_MONTHVALIDATION_DAYVALIDATION_FILENAMEVALIDATION_FILE_EXTENSIONVALIDATION_PATH_TRAVERSALValidation (search):
VALIDATION_SEARCH_SCOPEVALIDATION_SEARCH_BRANCHVALIDATION_SEARCH_BRANCHESVALIDATION_SEARCH_DATEVALIDATION_SEARCH_RANGEVALIDATION_SEARCH_LIMITVALIDATION_SEARCH_CURSORVALIDATION_SEARCH_MISSING_FILTERStorage:
FS_NOT_FOUNDFS_STORAGE_ERRORSearch (backend/provider):
SEARCH_BACKEND_UNAVAILABLE (provider misconfiguration/unavailability)Internal:
INTERNAL_SERVER_ERRORRoute handlers use shared helpers:
lib/api/errors.js (standard error payloads + withErrorHandling)lib/api/storageErrors.js (maps filesystem errors like ENOENT to 404 vs 500)Secure cookie behavior, prefer HTTPS.http://localhost, many tools/browsers treat localhost as a special-case “secure context”. Behavior may vary between environments.This project reads delivery-note PDFs from a NAS mount. New scans can appear at any time.
The caching strategy is designed to:
All JSON API responses explicitly disable HTTP caching:
Cache-Control: no-storeThis is applied centrally by lib/api/errors.js (the json() / jsonError() helpers). The policy applies to both success and error responses.
For binary endpoints (e.g. PDF streaming), the route handler must explicitly set:
Cache-Control: no-storeAll API route handlers are forced to execute dynamically at request time:
export const dynamic = "force-dynamic";This avoids any static/ISR-like behavior for NAS-dependent endpoints and keeps auth/RBAC behavior safe.
To reduce repeated fs.readdir() calls, lib/storage.js implements a small process-local TTL cache after RBAC is enforced (RBAC is checked in the route handlers).
TTL defaults:
listBranches() / listYears() → 60 secondslistMonths() / listDays() / listFiles() → 15 secondsImportant:
Frontend code should call these endpoints with explicit “fresh data” settings:
Use credentials:
fetch(url, { credentials: "include", cache: "no-store" });
Do not rely on next: { revalidate: ... } for these endpoints. Freshness is controlled via:
Cache-Control: no-store (HTTP)GET /api/healthPurpose
Health check endpoint:
db.command({ ping: 1 })).NAS_ROOT_PATH.Authentication: not required.
Response 200 (example)
{
"db": "ok",
"nas": {
"path": "/mnt/niederlassungen",
"entriesSample": ["@Recently-Snapshot", "NL01", "NL02"]
}
}
POST /api/auth/loginPurpose
Authenticate a user and set the session cookie.
Authentication: not required.
Request body (JSON)
{ "username": "example.user", "password": "plain-text-password" }
Responses
200 { "ok": true }
400 (invalid JSON/body)
400 (missing username/password)
401 (invalid credentials)
500
GET /api/auth/logoutPurpose
Destroy the current session by clearing the cookie.
Authentication: recommended (but endpoint is idempotent).
Response
200 { "ok": true }GET /api/auth/mePurpose
Provide the current session identity for frontend consumers.
Semantics (frontend-friendly):
{ user: null } when unauthenticated{ user: { userId, role, branchId, email } } when authenticatedNotes:
role is one of: branch | admin | superadmin | dev.email is optional and may be null.POST /api/auth/change-password (RHL-009)Change password for the currently authenticated user.
GET /api/branchesReturns the list of branches (e.g. ['NL01', 'NL02']).
Authentication: required.
RBAC behavior
branch role → only own branchadmin / superadmin / dev → all branchesResponse 200
{ "branches": ["NL01", "NL02"] }
GET /api/branches/[branch]/yearsExample: /api/branches/NL01/years
Authentication: required.
GET /api/branches/[branch]/[year]/monthsExample: /api/branches/NL01/2024/months
Authentication: required.
GET /api/branches/[branch]/[year]/[month]/daysExample: /api/branches/NL01/2024/10/days
Authentication: required.
GET /api/files?branch=&year=&month=&day=Returns files for a given day.
Authentication: required.
GET /api/files/:branch/:year/:month/:day/:filenamePurpose
Stream (or download) a single PDF file from the NAS while enforcing authentication and branch-level RBAC.
GET /api/searchPurpose
Search delivery note content across PDFs.
Authentication: required.
RBAC behavior
branch role:
branchId403 AUTH_FORBIDDEN_BRANCHadmin / superadmin / dev role:
Filter rule:
q, from, to.400 VALIDATION_SEARCH_MISSING_FILTER.The endpoints and response shapes documented here (and in docs/frontend-api-usage.md) are considered API v1 for the first frontend implementation.
Rules:
When adding new endpoints:
route.js under app/api/....Enforce auth:
getSession() for protected endpoints401 AUTH_UNAUTHENTICATED when session is missingEnforce branch RBAC (when applicable):
canAccessBranch(session, branch)403 AUTH_FORBIDDEN_BRANCH when forbiddenEnforce user-management capability (only for user management APIs; RHL-012):
requireUserManagement(session)403 AUTH_FORBIDDEN_USER_MANAGEMENT when forbiddenUse the standardized error contract:
lib/api/errors.js (withErrorHandling, ApiError, helpers)Add route tests (Vitest).
Update this document.