Document management system
I built a web application in C# that registers official correspondence for a government office, stamps barcodes on scanned letters and follows each one through approval, entirely on the office network.
- Year
- 2026
- Context
- For the Egyptian Commercial Service
- Built with
- C#, .NET 9, Blazor Server, SQLite
Overview
The Egyptian Commercial Service handles its official correspondence on paper: letters that arrive, letters that go out, and internal memos. Each one needs a reference number, a record of who handled it, and the signature of an administrator. I built a web application that registers this correspondence, follows each document through its steps and keeps every version of the scanned file.
The application is written in C# on .NET 9. The interface uses Blazor Server, so clerks only need a browser, and the data lives in SQLite on one Windows machine in the office. It runs on the office network and does not need the internet for its core work.
The current version is a rebuild. The first version, written in January and February 2026, was an Avalonia desktop client that talked to an ASP.NET Core API backed by SQL Server, with Arabic text recognition, full text search and drawn signatures. In March 2026 I replaced it with the web version and dropped the features the office did not use in its daily work. I tagged version 1.0.0 on 26 March 2026.
How it works
There are three kinds of documents.
- An incoming letter is registered, stamped with a barcode on its first page, printed so an administrator can sign it, scanned again, given a recorded action and marked as delivered.
- An outgoing letter starts as a draft. An administrator approves or rejects it, a rejected draft can be submitted again, and an approved letter is signed, uploaded and marked as sent.
- An internal memo follows the outgoing path.
When a clerk registers a document, the server gives it a barcode in the form ECS.YYYYMMDD.NNNN and a reference number made of a yearly counter for its direction, an Arabic letter for that direction and the year. It also sets a deadline from the urgency: 7 days for normal, 5 days for urgent, 48 hours for very urgent, or a custom number of days.
The label is a Code 128 image drawn with ZXing.Net and SkiaSharp. The clerk drags it over the first page in a PDF.js viewer, which reports its position as fractions of the page size. PDFsharp multiplies those fractions by the page size in points and draws the label there, so the zoom level does not matter.
Every change to a file is saved as a new version, and every change of status goes into a timeline with the old and the new value.
Design decisions
- A browser application instead of a desktop one. With Blazor Server, nothing has to be installed on the clerks' computers, and an update means replacing the files on one machine.
- SQLite instead of SQL Server. The office has few users, and SQLite in WAL mode lets them read while one of them writes, without a separate database server to install and maintain.
- Paper signatures instead of electronic ones. The first version had a pad where an administrator drew a signature. The office signs on paper, so the web version prints the first page, and the scan of the signed page replaces page 1 as a new version of the PDF.
- No text recognition for now. Arabic recognition on real scans was not reliable enough to depend on, so the system stores the PDF as it is, and search covers the reference number, subject, barcode and organization names.
- Confidentiality in one place. There are five levels, from Public to Top Secret. Every query for documents goes through one filter, which shows administrators everything and shows clerks only Public and Internal documents.
- PDFsharp for editing PDFs, because it can replace a page and draw an image on it without a commercial license.
Problems I ran into
- Arabic text recognition. The first version used Tesseract with its Arabic model, then switched to EasyOCR, called from C# as a Python process, because it read Arabic better. An earlier Python prototype used Qari, a vision model for Arabic text with 2 billion parameters, but through a hosted service, which does not fit an office without internet. None of them was reliable enough on real scans, so the web version has no recognition.
- Arabic search in SQL Server. Full text search needs the Arabic word breaker, language 1025, and the SQL Server image I used in Docker for development did not support it. I made that migration do nothing there and kept a plain
LIKEsearch as a fallback. - Right to left text. In the desktop client, Arabic letters did not appear until I set a list of fallback fonts, and chart legends stayed in English because they came straight from enum names.
- File streams in Blazor. Replacing page 1 failed with "Synchronous reads are not supported", because PDFsharp reads synchronously and the upload stream in Blazor does not allow it. The upload is now copied into a
MemoryStreamfirst. - Schema changes. The server creates the database with
EnsureCreated, which does not add new columns to an existing table, so at startup it runs a short list ofALTER TABLEstatements and ignores the error when a column already exists.
Results
Version 1.0.0 covers the three document paths, barcode labels and stamping, replacement of page 1, topics that group the letters of one conversation, links between a reply and the letter it answers, user management with two roles, five confidentiality levels, deadlines and a timeline for every document. The repository has 408 commits from January to March 2026, across both versions.
The server can be published as a Windows service that listens on the office network. It limits uploads to 50 MB, and each time it starts it copies the database to a backup folder and keeps the last 7 copies. A /health endpoint reports that it is running, and a GitHub Actions workflow builds the server and shared projects on every push.
The web version has no automated tests. The 21 tests in the repository belong to the first desktop version and cover its text recognition and search services.
What I'd do next
- Make the web version fully offline. The page still loads its fonts from Google Fonts and its icons from a CDN, so on a network without internet the text falls back to system fonts and the icons are missing. The application should serve these files itself.
- Take backups with the SQLite backup API. The server copies the database file directly, and in WAL mode the latest changes can still sit in the separate WAL file, so a plain copy can miss them.
- Make deletion match the design. Documents have an
IsDeletedflag and a query filter that hides deleted rows, but the delete action still removes the row and its files. - Add tests for the workflow rules, such as which status can follow which, because those rules are spread across several methods.
- Try Arabic text recognition again with a model that runs on the office machine.