Sep 2026 · 4 min read
PublishedBuild Log #3: The Encrypted PDF That Wasn't Password-Protected
The schema exists now. This is the first stage where an actual uploaded file touches it: intake validation, and the extraction pipeline that turns an accepted PDF into raw text.
Reject cheap things first
Every uploaded file gets its own row and its own status the moment it's submitted, before anything expensive happens to it. Validation runs cheapest-check-first: wrong extension, then size limit, then a corrupted-file check, then password protection — so a five-hundred-page file never gets fully parsed just to fail on page count, and a corrupted file never reaches the password check at all.
A real bug the requirements doc didn't anticipate
Some banks encrypt a statement PDF to block printing or copying, but leave the actual user password empty. Those files open in any PDF reader with no password prompt at all — which means they're not what "password-protected" is supposed to mean, even though a naive check for "is this PDF encrypted" would say yes and reject it.
A PDF can refuse to let you copy its text and still hand that same text to anyone who opens it. That isn't password protection, and treating it like one would have rejected a normal, harmless statement.
The fix only rejects a file when actually attempting to decrypt it with an empty password fails — the real, correct definition of "this file needs a password I don't have."
Extraction: native text first, OCR only when it has to be
Most statements have real embedded text, which is fast and exact to extract. A page only falls back to OCR if it has fewer than about forty usable characters of embedded text — and if any single page in a document needs OCR, the whole document falls back together, not just that page. A statement split between two extraction methods risks silently losing the transactions on whichever page the split happened on, which is exactly the kind of quiet inaccuracy this project exists to avoid.
Both paths — native and OCR — converge on the exact same output shape, so nothing downstream ever needs to know or care which one ran.
Next
The background job queue — so a hundred statements can process without freezing the app or letting one bad file take the rest down.