Jan Benjamin Yap

Freelance · Internal tool · 2026

Law Firm Document System

The firm logged every court document in a paper logbook. I replaced it with a secure web app built around how they already worked, from first commit to production in 5 weeks.

Role
Freelance software engineer
Stack
Next.js, Cloudflare D1 & R2, Google OAuth, Playwright

One request through the app

BrowserFirm staffIdentityGoogle OAuthSign inVerified emailCloudflare PagesNext.js appAllowlist check · every requestPagesLogbook · case foldersAPIRecords · correctionsFile routes8 MiB parts · streamedDrizzleORMD1RecordsAllowlistActivity historyR2No public URLNo hard deletesBrowserFirm staffIdentityGoogle OAuthAllowlist check · every requestCloudflare Pages · Next.jsPagesLogbook · case foldersAPIRecords · via DrizzleFile routes8 MiB parts · streamedD1RecordsAllowlistActivity historyR2No public URLNo hard deletes
Every request goes through the session and allowlist check before it reaches a page, an API route or a file. Records go through Drizzle to D1. PDFs go up to a private R2 bucket in 8 MiB parts and come back down as a stream. Local, staging and production each run their own copy of all of it.
5 weeks
first commit to production
21
audited event types
199
tests at release

The firm logged every incoming court document by hand, in a physical logbook. That made documents hard to find, left little record of who received or filed what, and meant a misplaced PDF could be lost for good. I built the web app that replaced the logbook, and it went from first commit to production in 5 weeks.

What they asked for was simple: something secure that fit the way they already worked. They didn't want a new process, so I built the app around the one they had.

Keeping the shape of the logbook

The General Logbook has the same columns the paper one did: the date, the pleading, the case title and who received it. I kept the firm's own words, so a document's title is labeled "Pleading" because that's what they call it. Received date and upload date are separate fields, and so are received by and uploaded by. A document can arrive days before anyone scans it, and the record should match what actually happened.

Cases are browsed the way the firm already filed them: court level, then civil or criminal, then location, then the case. The court levels run from the Municipal Trial Court up to the Supreme Court, plus an administrative level for PhilHealth cases. Every document is tagged by who filed it (petitioner, court or respondent), color-coded and always labeled in text.

The folders are only a view over the data, so correcting a case's details never moves a stored file. And if you start an upload from inside a case folder, that case is locked in, so a document can't end up filed under the wrong one.

Locked to the firm

Security was the other half of the brief. Sign-in is Google only, and the verified email has to match an active entry on an allowlist the admins manage. There's no public signup, and an email on the firm's domain doesn't get you in on its own. Access is checked again on every request, so revoking someone takes effect immediately across pages, APIs and downloads.

PDFs live in a private Cloudflare R2 bucket and never get a public URL. They're streamed through an authenticated endpoint, checked by their actual file signature on upload, and rejected if they're encrypted or malformed. Nothing is overwritten in place, and nothing is permanently deleted in production: admins can move a document to the trash and restore it later. Every sign-in, upload, download, correction and access change lands in an activity history that can't be edited from the app, 21 kinds of event in all, with before and after values for every correction.

Built on Cloudflare

It's a Next.js app on Cloudflare Pages, with D1 for the records and R2 for the files. Local, staging and production each have their own database, bucket, OAuth client and allowlist, and a misconfigured server refuses to start. At release, 132 unit tests and 67 end-to-end tests passed, all against a stand-in Google provider, so the suite never touches real accounts.

I used Drizzle so the database stays swappable. PDFs up to 1 GB go up in 8 MiB parts as resumable multipart uploads, which keeps every request under Cloudflare's size limit.