Human-in-the-loop AI
An AI email productivity platform on Cloud Run with Supabase, integrating Gmail via OAuth. A Gemini-powered feature suggests board organization and automated rules with a human-in-the-loop accept/reject workflow.
Gmail works fine as a mailbox. It falls apart as a workflow tool once inbound email is also where work gets assigned, tracked, and followed up on: folders and labels can't tell you which message still needs an answer and which one is already handled. I built Flowmail to keep Gmail as the source of truth and add a board on top of it. A rules engine and a review-before-apply AI layer route messages into project-specific Kanban boards, and a workspace layer lets an owner bring teammates onto specific boards without handing over the whole inbox.

I wrapped Gmail access in a GoogleProvider behind a ProviderFactory before I needed a second provider, because the ingestion path is the part I didn't want to have to touch again later.
When more than one board's rules match the same email, the highest-priority board wins, with ties breaking on the lowest board ID. I wanted classification to be predictable to the person who wrote the rule, so I made it a hard requirement instead of leaving the outcome to whatever order the code happened to check things in.
The board and rule suggestion services never create anything directly. They produce a pending suggestion with the specific emails that justified it attached as evidence, and accepting one calls the same create-board or create-rule code a manual action would. I didn't want a second, less-audited write path just because a suggestion came from Gemini instead of a person.
I made board and workspace access checks return "not found" instead of "forbidden" when a user lacks access, so a failed authorization check can't be used to figure out which boards or workspaces exist in the first place.
A workspace has exactly one owner, and I didn't build a promote or transfer-ownership path. That was a scope call I made when I shipped workspaces, not something I ran out of time for.
I'd cached the one-time OAuth handoff code in an in-memory Map. That held up fine on a single process and broke the moment Cloud Run scaled past one instance, because the code could get written on one instance and read on another. I moved it to a Postgres table and consumed it with an atomic delete-and-return query so it can only ever be used once.
I hadn't set a prompt parameter on the auth URL, so Google was reusing a cached browser session instead of showing the account picker. Users landed on a waitlist-rejection page with nothing telling them Google had even been involved. I fixed it by forcing the account chooser on every login, and forcing re-consent on reconnect.
My original scheme derived a user's encryption key from a hardcoded master-key literal in source plus their email. Anyone with repo access could derive any user's key. I replaced it with envelope encryption: a random key per user, generated once and wrapped under a master key that lives only in an environment variable.
Rate limiting is still in-memory and scoped to a single process, the same assumption that broke the OAuth exchange codes. It hasn't caused a real incident yet, but I've flagged it as the next thing to fix before it does.
I apply database migrations straight to the live Supabase project from the CLI, and they aren't tracked in version control or run through CI. It's a real gap. I haven't automated it yet.
Flowmail is live and still gated behind an approved waitlist while it's in early access. Most of my recent work has gone into fixing rule-matching, sync, and login reliability under real usage rather than adding new surface area.