- ProblemStatic onboarding frontend feeding NAS-backed uploads, SQLite records, and automated PDF dossier generation through a tunnel-exposed local API.
- RoleSystem design, backend/API implementation, infra debugging
- TimeframeIncremental build + production hardening
- StackNext.js • Node.js • Docker • Cloudflare Tunnel
- FocusNext.js • Node.js • SQLite
- ImpactUpload path: Working end-to-end
Problem
Teacher onboarding was fragmented across a static form, ad hoc document collection, and manual reconciliation. Staff could capture profile details, but supporting documents arrived through separate channels and were stored without a deterministic link back to the teacher record.
That created operational drift. Structured data, uploaded files, and final dossiers lived in different places, which meant there was no single persistence path, no stable retrieval model, and too much manual handling for a workflow intended for non-technical staff.
The system needed one pipeline: submit teacher data, upload supporting documents, persist both reliably, and generate a dossier automatically without asking operators to fix storage or naming problems by hand.
Context
The architecture was constrained by the real runtime environment. The frontend was a static Next.js deploy, while the API and persistence lived on local/NAS infrastructure rather than a public cloud backend. That meant the design had to bridge a static public client and a private origin without exposing the internal host directly.
ASUSTOR NAS was the primary persistence layer for uploaded files and generated dossiers. Structured metadata lived in SQLite so the system could query and link records cleanly, but the file system of record remained on mounted NAS storage rather than object storage.
Cloudflare Tunnel provided secure public exposure of the local API. That kept inbound ports closed, but it also introduced a real dependency on tunnel state, transport configuration, container networking, and origin correctness. The solution had to work for non-technical staff, so those infra details had to be absorbed by the system rather than pushed back to the user.
Architecture
- Frontend
- Static Next.js deployment hosted separately from the API runtime.
- Teacher onboarding form collects structured profile data plus supporting documents.
- Sends multipart uploads to `/api/upload/teacher` and structured JSON to `/api/teacher/application`.
- The frontend stays intentionally simple: capture, validate basic fields client-side, then hand persistence and dossier generation to the backend.
- Backend
- Upload endpoint `/api/upload/teacher` handles document intake, request authentication, validation, filename sanitization, and NAS writes.
- Application endpoint `/api/teacher/application` normalizes the teacher payload, persists structured data to SQLite, and triggers PDF dossier generation.
- CORS and shared-secret authentication are enforced at the API boundary on both routes.
- The API contract explicitly separates binary upload handling from structured teacher metadata so retries and validation stay predictable.
- Storage
- NAS is the file system of record for uploads and generated dossiers.
- SQLite stores teacher profile data, record identifiers, and references to persisted file paths.
- Per-teacher NAS path structure is deterministic:
- `teachers/{teacherId}/application.json`
- `teachers/{teacherId}/documents/{documentType}-{originalFilename}`
- `teachers/{teacherId}/dossier/{teacherId}-dossier.pdf`
- Infrastructure
- Cloudflare Tunnel exposes the API publicly without directly opening the NAS host to the internet.
- Docker containers isolate the API runtime and simplify deployment between local development and NAS execution.
- Local and NAS runtime paths remain separate, but they must agree on ports, secrets, payload contracts, and container networking.
- The design accepts a hybrid topology on purpose: static frontend outside, private API inside, persistent storage on NAS.
- Dossier generation
- PDFKit generates a teacher dossier from the structured SQLite-backed application data.
- The output PDF is written next to the teacher’s uploaded documents so staff retrieve one folder instead of reconstructing a case manually.
- This keeps the final onboarding artifact derived from persisted data rather than transient browser state.
- Request flow
- 1. Teacher completes the onboarding form on the static frontend.
- 2. Supporting files are uploaded as multipart form data to `/api/upload/teacher`.
- 3. The API validates the request, writes files to the NAS path for that teacher, and returns stored references.
- 4. The frontend submits the structured application JSON to `/api/teacher/application`.
- 5. SQLite persists the teacher record and links it to the NAS file paths.
- 6. PDFKit generates the dossier and stores it alongside the uploaded documents.
Demo
End-to-end onboarding flow: upload → storage → API → dossier generation
Security / Threat Model
- Unauthorized uploads would turn a public endpoint into an abuse path if requests were not gated.
- Secret mismatch between frontend and backend would reject legitimate submissions while looking like an application failure.
- CORS misconfiguration could either block the real frontend or overexpose the API surface.
- Cloudflare Tunnel reduces direct exposure but does not replace application-level authorization or origin validation.
- File validation issues such as unsafe filenames, unexpected MIME types, or oversized payloads could corrupt storage assumptions.
- Breaking the linkage between teacher metadata and uploaded documents would recreate the manual reconciliation problem the system was meant to remove.
Debugging Journey
- Stale container secret mismatch
- Symptom: the frontend submitted requests correctly, but authenticated API calls were still rejected.
- Diagnosis: the running backend container was still using an older shared secret even though the code and local env had been updated.
- Fix: restarted the correct container with the intended env and re-validated the exact request path end-to-end.
- Cloudflare tunnel not running
- Symptom: the API worked on the internal host, but the public hostname failed completely.
- Diagnosis: the tunnel process was not active, so the public entry point had no working route to the origin.
- Fix: restarted the tunnel service first, then resumed application debugging only after public ingress was restored.
- QUIC vs HTTP2 tunnel instability
- Symptom: connectivity through the tunnel was intermittent even when the API itself was healthy.
- Diagnosis: the failure pattern was at the tunnel transport layer rather than inside the application runtime.
- Fix: switched the tunnel path to the more stable HTTP2 transport mode for this origin.
- Container network isolation issue
- Symptom: the tunnel was up, but it still could not consistently reach the backend container.
- Diagnosis: the tunnel service and API container were not resolving each other on the intended Docker network and origin port.
- Fix: corrected container addressing and network routing so the tunnel pointed at the actual backend service.
- 502 bad gateway from the public hostname
- Symptom: the hostname resolved and the tunnel answered, but requests returned 502.
- Diagnosis: Cloudflare could not forward successfully to a healthy origin, which narrowed the fault to the origin target rather than DNS.
- Fix: corrected the backend target and re-tested the origin directly before re-verifying the public path.
- CORS blocked the static frontend
- Symptom: browser submissions failed even when the API was reachable from the server side.
- Diagnosis: preflight handling and allowed headers did not match the actual static frontend origin and request shape.
- Fix: allowlisted the exact frontend origin and added proper `OPTIONS` and header handling for the upload and application routes.
- Payload mismatch (`personal.email` vs `email`)
- Symptom: the request completed partially, but the persisted teacher record was missing the expected email field.
- Diagnosis: the frontend payload was nested while the backend parser assumed a flatter contract.
- Fix: normalized the backend parser to the real request shape and aligned the API contract at the boundary instead of patching around it downstream.
Tradeoffs & Lessons
NAS vs cloud storage — NAS kept persistence local, predictable, and inexpensive, but it also coupled success to mounted storage health, directory discipline, and local infrastructure availability.
Single shared secret vs full auth system — the shared secret was appropriate for a controlled internal onboarding flow and fast to deploy, but it is not a substitute for a proper user auth model if the system grows beyond trusted operators.
Static frontend vs dynamic backend — a static Next.js deploy reduced hosting complexity, but it made request contracts, CORS correctness, and boundary validation non-negotiable.
Uploads vs structured data separation — keeping binary upload handling separate from structured teacher records simplified validation and retries, but only because SQLite became the authoritative linkage layer between records and file paths.
Hybrid local/NAS runtime — the architecture worked well once stabilized, but most real failures lived at the seams between components: tunnel to origin, container to container, browser to API, and frontend schema to backend parser.
Results
The final system delivers a working onboarding path for non-technical staff: teacher applications persist to SQLite, supporting documents persist to deterministic NAS paths, the public API works reliably through Cloudflare Tunnel, and PDF dossiers are generated automatically alongside the uploaded files. The value was not only in building the happy path, but in hardening the real production path until storage, API behavior, and dossier output were stable under actual deployment conditions.
Stack
FAQ
Where is teacher data stored?
Structured teacher metadata is stored in SQLite, while uploaded documents and generated dossiers are stored on the ASUSTOR NAS under per-teacher directories.
How is security enforced?
The public API is exposed through Cloudflare Tunnel and still requires application-level controls: shared-secret authentication, explicit CORS policy, and server-side file validation.
What happens if the NAS is offline?
The upload and dossier pipeline loses its persistence target. Requests should fail clearly rather than pretending the onboarding completed successfully.
How are documents linked to teacher records?
SQLite stores the teacher identifier and the NAS file references, making the database the authoritative layer that links structured application data to uploaded documents and generated dossiers.