engineeringprivacyfeatures

Building Document Import Without Ever Seeing Your Documents

D
Danny Vasquez-Khoury
December 27, 20256 min read

Here's the thing about building privacy-first software: the constraints that seem limiting at first are actually what force you to build something better.

This week we shipped two features in Postiller — Document Import and Annotation Queue. Both sound simple on paper. Both turned out to be engineering puzzles that I genuinely enjoyed solving.

The Challenge: Files Without a Cloud

Most apps that handle documents do something like this: user selects a file, upload it to a server, process it in the cloud, send back the results. Simple. Scalable. And completely incompatible with our architecture.

At Postiller, your data never leaves your device. Not "we promise not to look at it" — we literally cannot. There's no server to upload to. No cloud to process in. Everything happens on the iPhone in your hand.

So when a user wants to import a 50-page PDF or a Word document, we need to extract the text, generate embeddings for semantic search, chunk it for our RAG system, and store it locally. All on-device. All in a few seconds. All without ever phoning home.

Making PDFs Work (All of Them)

Apple's PDFKit framework makes extracting text from PDFs easy — for text-based PDFs. Documents created digitally, exported from Word, those work beautifully.

Scanned documents are different. If someone scanned a printed document or took a photo and saved it as PDF, there's no text layer. PDFKit gives you nothing.

The fix: detect when extraction fails and fall back to Apple's Vision framework for OCR. We render each page to an image and run text recognition on it. It's slower — a 50-page scanned PDF might take 10-15 seconds instead of 1 — but it works. And it's still entirely on-device.

The user doesn't need to know which path we took. They just know their document worked.

Word Documents Are Weird

Word documents (.docx) are technically ZIP archives containing XML files. In theory, you unzip, parse the XML, extract the text. In practice, Microsoft's XML structure puts every word in separate tags, every paragraph wrapped in its own element.

We built a simple parser that walks through the structure and extracts just the text content. It's not elegant. It doesn't preserve formatting. But it gets the text out — which is all we need. Postiller isn't a document viewer. It's a knowledge tool. We extract your highlights and discard the margins.

One Model, Two Source Types

Here's a decision that took longer to make than to implement: should we create a new Document model or extend our existing Bookmark model?

The requirements were similar — store text, generate embeddings, allow annotation, surface in search. The differences were minor — documents have no URL, they have a source file type instead.

We decided to extend Bookmark. One model, two source types. The alternative — separate models with shared behavior — would have meant duplicating embedding logic, search logic, annotation logic, and every feature we add in the future.

The trade-off: we had to update every place in the code that assumed a bookmark has a URL. We touched 8 files fixing these assumptions. Worth it. Now when we build features, we build them once.

Surfacing What Matters

The second feature — Annotation Queue — came from a realization about our product's core loop.

Postiller's name comes from "postil" — marginal notes on text. The whole point is that users annotate their saved content. Your notes capture why something mattered to you. Without them, you're just hoarding links.

But we had no way to surface unannotated content. Bookmarks that needed attention were mixed in with everything else.

The fix was straightforward: filter bookmarks without notes, show a count, add a toggle. The UX required more thought. We didn't want to nag. We wanted to invite. The filter shows an orange badge with the count — visible but not intrusive. When you switch to "Needs Notes" view, a banner explains why annotation matters.

We're not just organizing content. We're teaching a workflow.

How We Kept It Straight

Building two features simultaneously — with overlapping model changes and cascading code updates — could have been chaos. It wasn't.

We use SpecWeave for requirements management. Before writing any code, we spec'd both features in SpecWeave's hierarchy. Document Import had 34 requirements across 6 user stories. Annotation Queue had its own set. Each requirement tied to a user story, each story to a persona.

When I realized our Bookmark model needed changes, I could trace every downstream impact. SpecWeave showed me which requirements depended on URL-sourced content, which acceptance tests would need updating, and which assumptions we'd made that were now invalid.

The assumptions tracking was particularly useful. We had documented: "The existing Bookmark model can accommodate file-sourced content with minimal schema changes." That got validated. But we also discovered an undocumented assumption scattered across 8 files: "Every bookmark has a URL." Having the feature's scope clearly defined meant we knew what to look for.

The open questions feature helped too. Before building, we captured decisions:

  • "Should 'Bookmark' be renamed now that it includes files?" (No, internal refactor only)
  • "Should file imports be in Share Extension?" (Not in initial scope)
  • "What's the default title for imported documents?" (Filename, user can override)

These decisions — made upfront, documented permanently — meant I wasn't stopping mid-implementation to ask product questions. The spec was the source of truth.

If you're building anything non-trivial, spec it first. SpecWeave makes that practical instead of bureaucratic.

What I'd Do Differently

Looking back at this sprint:

The file parsing could be cleaner. We wrote manual code to avoid adding dependencies. It works, but it's the kind of code that makes me nervous. If we ever see weird document bugs, this is where I'll look first.

The OCR fallback is slow for large documents. 10+ seconds feels long, even with a progress indicator. There might be room to optimize, but that's for later. Ship it working, then ship it fast.

Why This Matters

I spent four years building systems that knew everything about people — what they bought, where they went, who they talked to. I was good at it.

Now I build the opposite.

When you import a confidential PDF into Postiller — maybe a draft business plan, medical records, private research — that document is processed entirely on your iPhone. The text goes into your local database. The original file is discarded. I couldn't access it even if I wanted to.

That's not a limitation. That's the point.

The engineering is harder. The constraints are real. But the result is software you can actually trust — not because we promise to be good, but because the architecture makes being bad impossible.

That's worth the extra work.


Danny Vasquez-Khoury is Lead Engineer at Postiller. He previously worked in quantitative finance before teaching himself iOS development to build things he believes in.

Share:

Create content like this with Postiller

Transform your ideas into polished content with AI, while keeping your data completely private.

Learn more