2026-05-16

SQL over your Fastmail inbox — and why we built faist

Fastmail shipped an official MCP server on National Email Day. Plug it into Claude or ChatGPT and the model can read mail, draft replies, check your calendar, look up a contact. It's a clean implementation of the live-ops side of an inbox assistant.

faist is the other half: the historical index. Every message you've ever received, mirrored into a Postgres database you own, with full-text and vector search on top — and exposed to your agent as plain SQL.

What you get that Fastmail's MCP doesn't

Fastmail's MCP is fantastic for "what's on my calendar tomorrow?" and "draft a reply to Sarah." It's tools-over-JMAP — fast, scoped, conversational. But it's also live-ops: the model walks the inbox per question, and there's no shared index it can aggregate across.

faist adds four things on top:

  1. Full historical backfill. Every email since you started using Fastmail, indexed once, queryable forever. The model doesn't need to paginate through JMAP to count what you got from acme.com last year.
  2. SQL, not just tools. SELECT date_trunc('month', received_at), count(*) FROM jmap.emails WHERE from_address ILIKE '%@acme.com' GROUP BY 1 is a query you can run from Claude Code today.
  3. Hybrid lexical + semantic search. pgroonga BM25 for keyword precision, pgvector for semantic recall, fused with reciprocal rank fusion. One endpoint, both modalities.
  4. Your data, your database. faist never owns your mail. It writes into the jmap schema of your own Supabase project under a scoped jmap_indexer role. We can't read it; if you delete the project, the data is gone.

Why SQL is the right surface for agents

Tools are easy for humans to reason about — one tool per intent, named like a verb. But agents are good at compositional thinking, and SQL is the most compositional surface we've ever built for tabular data. Group-by, window functions, joins across emails, email_chunks, and jmap_accounts come for free.

A few queries faist users run from Claude:

-- "who emailed me most this quarter that I never replied to?"
SELECT from_address, count(*) AS got, sum((reply_count = 0)::int) AS unreplied
FROM jmap.emails
WHERE received_at > now() - interval '3 months'
GROUP BY 1
ORDER BY unreplied DESC
LIMIT 20;

-- "find every receipt over $500 in 2025"
SELECT received_at, from_address, subject
FROM jmap.emails
WHERE to_tsvector('english', subject || ' ' || body_text) @@ plainto_tsquery('receipt invoice')
  AND body_text ~ '\$([5-9][0-9]{2}|[1-9][0-9]{3,})'
  AND received_at BETWEEN '2025-01-01' AND '2026-01-01'
ORDER BY received_at;

You wouldn't write these as tool calls. You'd write them as SQL — and an agent will write them for you the moment it has the schema.

How it fits together

JMAP API  ──►  faist  ──►  YOUR Supabase
                │
                └──► OpenAI / local embeddings (optional)

faist polls JMAP, normalizes messages, chunks bodies, computes embeddings, and writes everything into your Supabase. It exposes three HTTP surfaces and one MCP endpoint:

  • GET /schema — introspects the jmap.* tables in your Supabase
  • GET /query?sql=… — runs arbitrary SELECTs as the scoped jmap_indexer role
  • POST /similarity-search — hybrid pgroonga + pgvector with RRF
  • POST /mcp — Streamable HTTP MCP server exposing the same as three tools (schema, sql-query, query) to Claude Code, VSCode's native MCP, Cursor, Cline, Continue

You can use it as an HTTP API, as a SQL endpoint, or as an MCP server — same data, three doors.

Try it

2 minutes, a Fastmail API token and a Supabase account. If you've been frustrated that your agent can talk to your inbox but can't aggregate, count, or filter across it, that's the gap faist fills.