Highlights#
- Built and shipped an internal records-management system implementing the Italian protocollo informatico, replacing the company’s manual document-registration process (NestJS, PostgreSQL, Angular 19, AWS S3).
- Designed a tamper-evident daily register: a scheduled job seals each day’s registrations into canonicalized Italian-format XML (W3C Exclusive C14N) with a dual SHA-256 hash and a preservation-status workflow.
- Implemented encrypt-then-MAC document storage on S3 (AES-256-CBC, HMAC-SHA256 verified before every decryption), plus a SHA-256 fingerprint recorded per uploaded file.
- Enforced role-based access down to individual buttons: ~40 granular permission codes checked by NestJS guards against the database on the API side and by a structural directive in the Angular UI.
Architecture / How it works#
flowchart TD
subgraph Client["Angular 19 + Ionic PWA (EN/IT)"]
FORM["Protocol form
4-step stepper"]
PDF["Client-side PDF export
pdfmake, cancelled watermark"]
VIEW["In-app PDF preview"]
end
subgraph API["NestJS API"]
PROT["Protocols service
sequential numbering,
single transaction"]
DOCS["Documents service
SHA-256 fingerprint per file"]
ENC["Encryption service
AES-256-CBC + HMAC-SHA256"]
CRON["Daily register job
23:00 UTC"]
XML["Register XML
C14N + dual SHA-256 seal"]
RBAC["JWT + Google OAuth
permission guards"]
end
PG[("PostgreSQL")]
S3[("AWS S3
ciphertext + .meta sidecar")]
FORM --> RBAC --> PROT
PROT --> DOCS --> ENC --> S3
PROT --> PG
CRON --> XML
XML --> S3
XML --> PG
VIEW --> DOCS
Registering a document is one database transaction: the service assigns the next protocol number for the year, validates sender, recipient, operator, department and classification, encrypts each uploaded file and stores it in S3 with a sidecar metadata object, and records a SHA-256 fingerprint per document. A cron job at 23:00 UTC then seals the day: it builds the Italian register XML, canonicalizes it so the hash doesn’t depend on formatting quirks, and stores a combined hash of both the canonical XML and a normalized data structure. If the S3 upload fails mid-seal, a compensating cleanup deletes the orphaned file and the transaction rolls back.
Portfolio Case Study#
Problem. Italian organizations keep a protocol registry: incoming and outgoing documents get a sequential registration number, and a daily register has to prove that the day’s records were not altered afterwards. At goproject this was a manual process. ERMS replaced it with a web app that registers, classifies, stores and seals company documents. I was the lead developer (195 of the 199 commits across both repos), with a colleague supporting on review.
Approach. The design borrows blockchain’s core idea, registrations as transactions and each day as a sealed block, without pretending to be a blockchain: days are sealed independently rather than chained, and the writeup is deliberate about that distinction. The seal itself is redundant by design. It hashes the canonicalized XML and, separately, a normalized JSON structure of the same registrations, so integrity can be checked even if the XML serialization ever changes. Document storage is encrypt-then-MAC in the correct order: AES-256-CBC with a random IV per file, HMAC-SHA256 over the ciphertext, and the HMAC checked before any decryption is attempted. Cipher and MAC use independent keys.
The frontend is an Angular 19 / Ionic PWA in English and Italian (557-line translation files for each language), with a reusable config-driven table, tree and autocomplete component set that every list screen shares. A permission store built on NgRx coordinates with the HTTP interceptor: after a 401 triggers a token refresh, the interceptor waits until the refreshed token’s permissions are loaded before retrying the original request.
What was hard.
- Reproducible hashing over XML. A seal is worthless if regenerating the same register produces a different hash, and XML serialization gives no such guarantee. The fix was canonicalizing with W3C Exclusive C14N before hashing, and pairing that with the second hash over normalized raw data as a fallback verification path.
- The permission system. Getting RBAC right took several iterations: newly registered users initially had no roles, system roles could be deleted, and permissions had to be added to the JWT claims. It settled into seeded roles over ~40 permission codes, guards that re-check permissions from the database rather than trusting the token, and a frontend directive that hides any action the user can’t perform.
- Client-side PDF generation. The protocol record export went through multiple refactors before landing on a dedicated builder class separate from the service, with all strings translated so the PDF language is chosen independently of the UI language, and a diagonal red watermark rendered across cancelled records.
Outcome. The app went into internal use at goproject for registering company documents, replacing the manual workflow. It was built in about seven weeks (February to April 2025): two TypeScript repos with Docker Compose for local setup, Swagger API docs, TypeORM migrations and seeds, a small Jest suite on the backend, and full English/Italian localization.
Non-goals. Each day’s register is sealed independently; there is no hash chain linking consecutive days, and the XML is canonicalized but not digitally signed (canonicalization is the prerequisite if signing is added later). It is a single-organization tool: no multi-tenancy, and transmission of sealed registers to a long-term preservation provider is tracked as a status but performed manually.
Stack#
NestJS 10, TypeORM, PostgreSQL, AWS S3, Passport (JWT + Google OAuth), Angular 19, Ionic 8, NgRx, Angular Material, ngx-translate, pdfmake, ngx-extended-pdf-viewer, Docker.
