The goal
The office chased missing client documents by hand, typed figures from its bookkeeping software into Excel tax-estimate templates, and kept capital declarations in a workbook. The goal was one system that collects documents from clients, tracks what is still missing per report type and year, and produces the recurring tax paperwork automatically - reachable from outside the office while the files stay on the office's own server.
The solution
A Hebrew RTL web app (Vue 3 + Express + MongoDB) with 10 admin sections and 3 public client pages. Clients get a personal portal link by branded email, fill dynamic forms and upload documents (images are converted to PDF and filed per client). On top of that sit two automation modules: tax estimates pulled straight from the paperless.tax REST API and written into the accountant's own Excel templates and folders, and capital declarations (form 1219) edited in the browser and printed as PDF. It runs as a Windows service on the client's server behind Cloudflare, deployed by a self-hosted GitHub Actions runner.
Highlights
- Tax estimates produced straight from the bookkeeping API into the office's own Excel templates and folders
- Capital declaration workbook replaced by a web workspace that prints form 1219 with all its fields
- Client document collection with personal portal links, image-to-PDF filing and per-year tracking
- Self-hosted on the client's Windows Server with push-to-deploy and daily backups
- Kept out of search engines and AI crawlers by design
The challenge
The tax-estimate Excel template carries the office's logo, styles and printer settings and goes out to clients, and the accountant edits it freely - a hard-coded cell map would silently write numbers one row off after any inserted row.
Located each target cell by its Hebrew label instead of a fixed reference, and wrote values by patching the sheet XML in place so every other part of the file stays byte-identical; formulas are left alone and recalculated by Excel on open.
The bookkeeping system, the payment exports and the office's own client list have no shared key - and spouses report under one income-tax file while each is known by their own ID elsewhere.
Built an explicit matching layer with strict rules (the office's client number compared exactly, ID numbers padded only for comparison), a scope step that understands shared tax files, and a measured match report on screen instead of a silent guess.
The system had to be reachable from any browser while the documents stay on the office's own Windows Server, with no platform scheduler or deploy pipeline in front of it.
Moved from Vercel + Blob to self-hosting: one Node process serves the API and the built SPA as an NSSM Windows service behind Cloudflare, backups run inside the process, and a self-hosted GitHub Actions runner on the server deploys every push and fails loudly instead of reporting a false success.
Report amounts from the bookkeeping API arrive in agorot, and a partnership keeps one set of books while tax is assessed per partner.
Encoded both rules in dedicated parsers - amounts divided once at the source, and partnerships expanded into one estimate per partner using the share recorded in the bookkeeping system.
What was delivered
- Vue 3 admin app + public client portal (Hebrew RTL, mobile-responsive)
- Express + MongoDB API with JWT admin auth
- Tax-estimate and capital-declaration automation modules
- Windows Server deployment scripts, NSSM service and GitHub Actions workflow
- Playwright E2E suites and server test scripts
Results
10
Admin sections
10
Data models
96
Commits
Hebrew RTL
Interface
What it taught me
- When the output goes to the client of a client, a failed run is better than a wrong number - anchor on labels, not cell addresses
- Measure how well two systems identify the same people before joining them, and show the gap on screen
- On a self-hosted box, anything that can fail silently (backups, deploys, an expiring connection) needs a loud, scheduled check
