Selected work / DefendaMe

Engineering / DefendaMe

DefendaMe: documents, AI and operations

Backend and AI infrastructure for a legal product: local OCR, structured-report recovery, failure investigation, and search and operations decisions.

Vinícius Menezes

Backend and AI infrastructure · Working on the product since 2025

Full article: ~11 min · estimate

The work at a glance

The challenge was processing documents and digital evidence under constraints around privacy, the user’s device and the reliability of model output.

Where I contributed

I work on backend and AI infrastructure. I directed Sentry adoption, with Ryan participating, and contributed to page reprocessing, invalid-response recovery, structured logs and Doppler deployment integration.

People and relationshipsSynthetic data
Graph of connected people and a detail panel for the selected person in a synthetic DefendaMe case.

The product brings together people, relationships and associated information in a navigable interface. This screenshot uses a synthetic case and shows the team’s work on the product. Portuguese interface; synthetic case.

Open full screenshot (opens in a new tab)
Jump to a section

01 / OCR

Keeping page images local without another installation

Problem
Page images had to remain on the user’s device, without requiring another installation.
Decision
Run OCR in the browser and manage concurrency, memory and incremental saving.
Consequence
The team implemented local controls and per-page indexing tracking with retries.
Limit
Saving results did not guarantee recovery of the entire workflow. Desktop remains an alternative to validate.

Where each part of the document is processed

User’s device

PDF.jsPrepare pages
Tesseract.jsRecognize text
IndexedDBSave results

Page images remain on the device in this workflow.

Extracted text

Cloud

Search and analysis
PDF.js prepares pages, Tesseract.js recognizes text, and IndexedDB saves intermediate results on the device. Only extracted text proceeds to cloud search and analysis in this workflow. Saving a page does not guarantee recovery of all processing.

PDF.jsPage preparation

Tesseract.jsRecognition in the browser

IndexedDBIntermediate results

Read the full episode

In the local PDF workflow, page images had to remain on the user’s device because they could contain sensitive material. Extracted text could be sent to the cloud for search and analysis. That distinction shaped the design: processing images locally did not mean keeping every part of document processing out of the cloud.

We initially chose to run OCR in the browser. Our user was a nontechnical lawyer; we wanted to avoid requiring another application to use a web system. We used PDF.js to prepare pages and Tesseract.js to recognize text.

The cost of this choice appeared during execution. In our experience, processing used substantial memory and sometimes caused the browser to close. Clients with less capable machines struggled even more. Avoiding installation did not remove dependence on the device.

The team adjusted concurrency to the machine’s capacity, processed pages in batches, released rendering resources and recreated workers between batches. These workers ran in the browser; they were different from backend queue workers. We also implemented incremental saving of results in IndexedDB, the browser’s local storage database.

Saving intermediate results preserved some work, but did not guarantee automatic recovery from every interruption. Recovery also depended on access to the original content and reprocessing paths that were still incomplete.

After extraction, there was another boundary: indexing the text. We implemented per-page tracking, retries after failures and stable identifiers in Elasticsearch. Indexing the same page again should not create a second document. This protection applied to indexing; it did not make every effect in the pipeline idempotent. My contribution to this work included reprocessing failed pages.

The browser removed the initial installation step, but retained memory and execution constraints we had to manage. I consider a desktop application a possible next direction for more control over local processing. It remains an alternative to validate, including installation and maintenance, rather than a delivery described here.

Public documentation explains mechanisms; it does not establish the product’s implementation or results.

02 / Reports

The cost of obtaining a usable response

Problem
Lengthy JSON output needed to follow a schema and maintain legal quality.
Decision
Validate and recover responses while considering the cost of obtaining a usable report.
Consequence
Recovery evolved from corrections and revalidation to repairing problematic fields.
Limit
Changing models did not eliminate failures. There is no reproducible cost or legal-quality benchmark.

Generating a response was only part of the work

Model response
Parse JSON
Validate schema

structural failure

Repair fieldsvalidate again

accepted structure

Usable structure

Legal quality is a separate requirement.

Model output goes through JSON parsing and schema validation. Structural failures lead to correction and revalidation. An accepted structure can proceed to the system, but this does not establish the legal quality of its content.
Teaching example · synthetic data

Why field validation matters

This simplified example illustrates a type error in parseable JSON. It is not the proprietary schema, real model output or evidence of production recovery.

Before correction

{
  "event": "Synthetic event A",
  "sourceIds": "document-a"
}

After field correction

{
  "event": "Synthetic event A",
  "sourceIds": [
    "document-a"
  ]
}
The sourceIds field must be an array. Correcting that type allows the example structure to validate; it does not verify whether the event is true. Download the complete example ↓

ZodSchema validation

GPT-5Generation in the described workflow

JSONStructure consumed by the system

Read the full episode

Legal reports needed to follow a schema validated with Zod, often with lengthy JSON responses. There were three separate requirements: the JSON had to be parseable, its fields had to follow the schema, and its content needed legal quality. Passing structural validation did not demonstrate that the analysis was correct.

We evaluated models against these requirements using the product’s workloads. When structure failed, repairing or repeating generation added time and cost. Price per call was therefore insufficient: what mattered was the cost of obtaining a usable report, including discarded attempts and corrections.

During this process, we moved from Gemini 2.5 Pro to GPT-5. The switch did not eliminate failures. I worked on recovery of invalid responses through sanitization, correction and further validation. The implementation later evolved to repair only the problematic fields instead of indiscriminately regenerating the entire response.

There were also compatibility adjustments between schemas and the Structured Outputs API. Part of the difficulty was in the integration, rather than the model alone. These changes support an account of a model choice accompanied by recovery work, rather than a conclusion about the entire Gemini family or a guarantee of legally correct output from the adopted models.

The trade-off was retaining validation and recovery in the system even after changing the model. We do not have a reproducible comparison that quantifies how much this choice reduced failures or total cost. The decision was to evaluate responses we could actually use, rather than the advertised price per token alone.

Public documentation explains mechanisms; it does not establish the product’s implementation or results.

Events organized over timeSynthetic data
Timeline of a synthetic case, with events, dates, people involved and a reference to the associated document.

The timeline presents events with dates, people and a reference to the associated document. It illustrates how structured information appears in the interface, using synthetic data. Portuguese interface; synthetic case.

Open full screenshot (opens in a new tab)

03 / Observability

Understanding what happened during real use

Problem
It was difficult to understand what happened during product use.
Decision
Combine error capture and stage events with structured logs and correlation.
Consequence
Mechanisms were implemented to connect events and investigate executions at instrumented points.
Limit
Instrumentation did not cover every path or demonstrate faster diagnosis.

SentryError capture and context

JSONStructured logs

CorrelationConnecting events

Read the full episode

I directed the adoption of Sentry to better understand failures during product use, with Ryan participating in the implementation. We combined browser error capture with events from stages such as upload, OCR and analysis, at instrumented points in the client and server. Context included the operation in progress and start, completion or failure events.

I later worked on the evolution of structured JSON logs and correlation between executions. Depending on the instrumented point, records included execution, service, stage, duration and attempt. A correlation identifier connected events that would otherwise require reading isolated messages.

This added mechanisms for investigating processing, but did not provide complete tracing of every path or demonstrate a measured reduction in diagnosis time. The result I can describe here is the implemented instrumentation and its evolution.

Useful investigation context also needed limits. We applied masking and filters to restrict the transmission of sensitive content. More telemetry required more care with collection; these mechanisms were not a guarantee of complete anonymization.

Public documentation explains mechanisms; it does not establish the product’s implementation or results.

04 / Search

Moving responsibility for embedding generation

Problem
The application ran embeddings and fed two search layers.
Decision
Move embedding generation to the ingestion flow associated with Elasticsearch.
Consequence
Model execution moved off the application server, bringing text and vector search together.
Limit
Inference still consumed resources, and dependence on search infrastructure increased.

The same responsibility, in a different place

Before

Document text
Application
Elasticsearch · text
Embeddings in the app
PostgreSQL · vectors

After

Document text
Application
Ingestion and embeddings
Elasticsearch · text + vectors

The highlight follows the responsibility that moved. The computational cost of inference still exists.

Before, the application sent text to Elasticsearch and ran embeddings for PostgreSQL. After, it sent text to search infrastructure, whose ingestion flow took on embeddings and brought text and vector search together. This is a conceptual view, not a detailed inference mechanism or internal topology.

ElasticsearchText and vector search

PostgreSQLPrevious vector destination

Read the full episode

We used Elasticsearch for text search and PostgreSQL for vector search. The application also ran the embedding model over the texts, sharing processing and memory with product features.

The application fed two search layers and ran the embedding model.

Since Elasticsearch was already part of the infrastructure, we moved responsibility for embedding generation to the ingestion flow associated with search. The application began sending text, and we brought text and vector search together in Elasticsearch.

The computational cost of inference still existed. The change removed model execution from the application server and concentrated indexing in the search infrastructure. It also increased our dependence on that infrastructure.

The diagrams represent this transfer of responsibility. They do not detail the inference mechanism or internal topology.

Public documentation explains mechanisms; it does not establish the product’s implementation or results.

05 / Operations

More control also meant more responsibility

Problem
Long-running workloads, platform costs and configuration drift created friction.
Decision
The team moved to a Droplet and adopted Doppler to centralize configuration.
Consequence
We gained control and became directly responsible for deployment, proxies, containers and process recovery.
Limit
I argued for staying on Vercel. Today I would separate the web layer from persistent services; that alternative was not measured.

VercelPrevious managed operations

DigitalOceanInfrastructure operated by the team

DopplerDeployment configuration

Three ways to allocate operational responsibility
AlternativeWhat it offersResponsibility and limits
Keep everything on Vercel · position argued at the timePreserve the platform’s managed operations.Long-running workloads and per-seat cost remained the reported friction.
Move everything to a Droplet · team decisionControl over processes and dependencies.The team took on deployment, proxies, containers, memory and recovery.
Web on Vercel, services elsewhere · retrospective viewPreserve a managed web layer and move workloads requiring a different model.The alternative I would prefer today; cost and availability were not compared under equivalent conditions.
Read the full episode

Running embeddings, indexing and analysis could take many minutes. Accommodating these workloads in Vercel’s execution model was becoming inconvenient. The team decided to move to a DigitalOcean Droplet; the web layer moved as well because, in our assessment, the machine’s fixed cost was more convenient than the platform’s per-seat cost.

I was among those who most strongly argued for staying on Vercel because of the managed operations work it handled. We reached an agreement to migrate, with my reservations still in place. The decision and the work of dealing with its consequences were collective.

We gained control over processes and dependencies, but became directly responsible for deployment, the proxy, communication between containers, memory and process recovery. We encountered problems in these layers and spent time investigating them. For a small team, this work competed for attention with product features.

In retrospect, I consider moving the entire application a mistake. I would have preferred keeping the web layer on Vercel and moving only the services that needed a different execution model. This is my assessment today, rather than an alternative whose cost or availability we measured under equivalent conditions.

Configuration brought another concrete problem. The application had accumulated hundreds of environment variables, including configuration and secrets, and differences between environments were among the bugs we frequently encountered. We evaluated alternatives, particularly HashiCorp Vault, and chose Doppler for its relative simplicity in our context. I contributed directly to deployment integration, using the CLI to provide variables to the process.

Centralizing configuration did not guarantee that it reached each service. A later fix addressed a variable available in Doppler that Compose was not passing into a container. The integration changed configuration management; we remained responsible for checking its propagation.

Public documentation explains mechanisms; it does not establish the product’s implementation or results.

06 / Deliveries

What this account supports

We implemented local OCR controls, per-page indexing tracking and retries, structured-response recovery and instrumentation for investigating failures. These deliveries had different limits: saving a page did not ensure recovery of all processing; validating JSON did not ensure legal quality; capturing events did not ensure coverage of every failure.

The most important part of these decisions was defining the responsibility of each stage. The browser avoided another installation, but depended on the lawyer’s machine. The model needed to produce a usable structure, and the application still needed to validate it. More open infrastructure gave us control, but also required operations work. Much of the engineering happened at these boundaries.

07 / Authorship

Authorship, evidence and limits

DefendaMe is built by a team. I use “we” for collective decisions and implementations and identify my contribution in each episode. Ryan participated in the Sentry implementation; OCR optimizations and infrastructure changes also involved other people’s work.

This text brings together my account and descriptions of code, documents and history examined during editorial research. The public references below explain mechanisms; they do not establish DefendaMe’s results. I do not publish proprietary code, customer data or metrics without traceable measurement.

Technical references

Public documentation explains mechanisms; it does not establish the product’s implementation or results.

Does your product face a similar problem?

Engineering projects

If your product faces difficulties with documents, AI, integrations or operations, describe what happens today.

Describe my problem →

Professional opportunities

To discuss my work in backend, product engineering and AI infrastructure.

Discuss an opportunity →View professional profile ↗