Crivo
Crivo labels business emails as productive or unproductive and drafts a reply when one is needed. Hugging Face models run behind a FastAPI back end, and a Next.js dashboard shows the results and a history of every email processed.
I built it alone in six days.

The problem
AutoU asked for this app in its hiring process. I framed the demo around a finance team whose inbox mixes client requests with bank notices, newsletters, and system alerts, and deployed it on Render’s free tier so evaluators could open it without installing anything.
Decisions
Remote inference over a local model
Render’s free tier gives the back end 512 MB of RAM, and running bart-large-mnli in the same process needs about 1.6 GB, so I called the Hugging Face Inference API instead of loading the model. The price is cold starts on two fronts: the hosted model can answer 503 while it loads, which the back end turns into Portuguese error messages (also for timeouts), and Render puts the idle back end to sleep, which the warm-up overlay covers.
English labels on Portuguese email
I tried the multilingual xlm-roberta-large-xnli and went back to bart-large-mnli the same day, because it gave lower confidence on emails that were obvious requests for action. I kept English labels and an English hypothesis template, and when the old ‘productive’/‘unproductive’ labels put newsletters in the productive class, I rewrote them to describe the email (‘a direct request requiring action’ against ‘informational or promotional content’).
LLM replies with a deterministic fallback
A generated reply reads better than a template, but an external LLM call can fail or time out, so any error falls back to the template engine and the user still gets a draft. Emails flagged as automated skip the LLM and get an internal action, since nobody is on the other end to read a reply.
What I built
- A FastAPI service split into extraction, classification, metrics, seed, and health modules, on PostgreSQL through async SQLAlchemy 2.0 and asyncpg, with the schema managed by Alembic migrations.
- A batch endpoint that streams progress as Server-Sent Events and commits each result to the database as soon as it is ready. The browser’s EventSource cannot send a POST, so the front end reads the POST response stream with a TextDecoderStream and parses the events itself.
- A reply pipeline: Qwen2.5-72B-Instruct, through the Hugging Face chat completion API, writes a short Portuguese reply tied to the email’s content. When that call fails, a Python template engine picks a template by sub-type and fills in the extracted amounts, dates, first line, and the name from the greeting.
- A Next.js 16 and React 19 app with Overview, Classify, and History pages, built on TanStack Query hooks, shadcn/ui, a Recharts daily chart, an SVG donut chart, and SVG confidence rings.
- A warm-up overlay that polls the health endpoint, with exponential backoff up to 10 seconds between retries, while Render wakes the back end. A seed endpoint clears the tables and loads 40 pre-classified sample emails in Portuguese spread over 30 days, so the dashboard has data on the first visit.
How it works
A Next.js front end talks to a FastAPI back end over REST. An email comes in as pasted text or as a .txt or .pdf file of up to 10 MB. Files go first to an extraction endpoint, which reads PDFs with pypdf and decodes text files as UTF-8 with a Latin-1 fallback. The text then goes to facebook/bart-large-mnli on the Hugging Face Inference API for zero-shot classification, and the winning label becomes Produtivo or Improdutivo with its confidence score and a fixed explanation for that label.
Rule-based code then looks for automated-message phrases such as “não responda este email”, picks a sub-type (request, proposal, complaint, scheduling, negotiation, notification, newsletter) from keyword counts, and pulls amounts in R$, dates, and percentages with regex. Emails that expect an answer get a draft reply from Qwen2.5-72B-Instruct. Automated ones get an internal action instead, such as filing a bank statement under reconciliation. Each email and its classification are saved in two PostgreSQL tables through async SQLAlchemy.
Batch mode posts a list of texts to one endpoint that answers with Server-Sent Events: one event per email, then a summary event with totals and average confidence. The dashboard reads totals, the label split, average confidence, and a daily series from a metrics endpoint that does the aggregation in SQL, and the history page pages through the same tables.



Built with
- Front end
- Next.js 16, React 19, TypeScript, Tailwind CSS v4, shadcn/ui, TanStack Query v5, Recharts v3, react-dropzone, Sonner
- Back end
- Python, FastAPI, Uvicorn, Pydantic v2 (pydantic-settings), SQLAlchemy 2.0 (async), Alembic, pypdf
- AI
- Hugging Face Inference API (huggingface_hub AsyncInferenceClient), facebook/bart-large-mnli (zero-shot), Qwen/Qwen2.5-72B-Instruct
- Data
- PostgreSQL, asyncpg
- Infrastructure
- Render (front end, API and database on the free tier)
- Tooling
- pnpm, ESLint