Self-hosted Gmail triage, powered by AI.

Miel fetches your Gmail, asks an AI to sort each message by priority and suggest labels, and shows you the result in a local web app where you review, apply, or reply in one pass.

It is open source and self-hosted: you run it on your own machine or your own server, against your own Google account. There is no signup, no hosted product, and no account with me — because there is nothing to sign up for.

Get the code on GitHub

What Miel does

It syncs messages from your Gmail account and stores them locally in Postgres.

It sends each message's sender, subject and preview to the AI for triage, which returns a priority and suggested labels along with its reasoning.

It surfaces the triaged inbox in a web UI where you can apply label suggestions, archive or trash threads, draft replies, and manage Gmail filters.

Who it is for

People with a noisy Gmail inbox who are comfortable running a Docker container, a Postgres database, and a couple of dev servers on hardware they control.

If you would rather not run software yourself, Miel is not for you — there is no hosted version to point you at.

Self-hosted and open source

The whole thing is open source. Clone the repository, read exactly what it does with your mail, and run it yourself.

Your mail, your database, your machine. Nothing is sent to a server I operate.

What you need to run it

Your own Google Cloud OAuth client. Miel talks to Gmail through the Google API on your behalf, and each self-hoster supplies their own client ID and secret — you create a project in the Google Cloud console, enable the Gmail API, and configure a consent screen for your own use. Plan for that setup cost before you start.

Credentials for your AI provider, so Miel can classify your messages and draft replies.

Docker for Postgres, and Bun to run the app.

Motivation

A busy Gmail inbox is mostly noise, and the work of deciding what matters is the part no filter rule ever got right. Rules match senders and subjects; they cannot read a message and tell you it is the one thing worth answering today.

Miel hands that judgement to an AI. It pulls in your recent mail, asks the AI to rank each message by priority and suggest labels, and shows you the result grouped high to low — so the triage pass you were doing by hand is already done when you sit down to it.

Nothing it proposes is applied behind your back. Priorities, label suggestions and new labels worth creating are all proposals; Gmail is not touched until you apply them. The tool does the reading, you keep the decision.

It is built to be self-hosted because mail is the last thing worth handing to someone else's server. You run it against your own Google OAuth client, your own AI credential and your own Postgres, on hardware you control. There is no hosted version and no account with me. The only thing that ever leaves your machine is what goes to the AI provider running your triage, and you pick which one that is: the local Claude Code CLI, which is what a fresh install uses, or Anthropic, Google or OpenAI over their APIs. What each of them is sent is spelled out further down this page.

Installation

Miel runs on Bun, with Postgres in Docker. You will also need a Google Cloud project with the Gmail API enabled and an OAuth client of type Web application.

You will need a credential for one AI provider, and one is already picked for you: claude-code, the local Claude Code CLI, is the default, and a fresh install runs triage, replies and filter suggestions through it. So the last prerequisite is that CLI on your PATH with a token from claude setup-token. Change the provider in settings to Anthropic, Google or OpenAI and the prerequisite becomes an API key from that vendor instead, with no CLI at all.

Two ways to run it: with Bun for development, or entirely in Docker if you would rather not install Bun at all.

  1. Clone the repository and install dependencies

    Every dependency comes from a public registry, so the install needs no token and no per-scope configuration.

    git clone [email protected]:lucasriondel/miel.git
    cd miel
    bun install
  2. Fill in your environment

    Copy the example file and supply three things: your Google OAuth client ID and secret, a random API secret that must match between API_SECRET and VITE_API_SECRET, and in production a TOKEN_ENCRYPTION_KEY. No AI credential goes in this file — nothing miel runs reads one from the environment. Every provider's credential is pasted in the app instead and stored encrypted in Postgres. Run claude setup-token now anyway: its token is required as shipped, since claude-code is the provider a fresh install triages with, and the next step is where you paste it. It is optional only if you switch every task to Anthropic, Google or OpenAI before your first sync. The rest of the file already has working localhost defaults.

    cp .env.example .env
    openssl rand -base64 32      # TOKEN_ENCRYPTION_KEY
    claude setup-token           # paste the result in Settings, not in .env
  3. Start Postgres

    The dev compose file starts the database on port 5435 and nothing else. Migrations apply themselves the next time the API boots, so there is no separate step for them.

    docker compose -f docker-compose.dev.yml up -d
  4. Run the app

    The root compose file builds and runs Postgres, the API and the web app together — the fastest way to self-host without installing Bun. The API applies any pending migrations itself on start, so nothing else is needed. Rebuild with --build after changing either VITE_API_BASE or VITE_API_SECRET, because both are baked into the bundle at image-build time.

    docker compose up -d --build
  5. Run the app

    The API comes up on port 3001 and the web app on port 3000. Open http://localhost:3000/app and click Connect with Google.

    bun dev
  6. Check which AI provider runs each task, and change it if you want

    Open Settings, then AI and Triage. All three tasks — triage, replies and filter suggestions — start on claude-code, the local Claude Code CLI, so a fresh install triages from its very first sync once that provider has its credential. Every provider's credential lives under Credentials in that same panel, one row each, stored encrypted in Postgres rather than in a file or an environment variable: an API key for Anthropic, Google or OpenAI, and for Claude Code the token from claude setup-token. Nothing is read from the environment, so a credential exists only once it is pasted here. Point any task at Anthropic, Google or OpenAI instead and that vendor's key is what it needs.

How to contribute

Issues and pull requests are welcome. The repository is the whole product — there is no private fork and no hosted variant with extra code in it.

CONTRIBUTING.md at the repository root is the place to start: how to get the stack running, the checks every pull request is gated on, and the conventions the codebase follows — one component per file, business logic in core services rather than in API routes, external systems reached only through the Effect services in packages/core/src/google and the Claude service beside them. CLAUDE.md beside it has the same ground in full detail, addressed to the coding agents that work on this repository.

Before opening a pull request, keep the checks green. Type checking is the one that matters most, since the codebase is strict TypeScript throughout.

Good first contributions: a Gmail feature the triage flow does not cover yet, a rough edge in the review UI, or documentation that was wrong or missing when you tried to follow it.

  1. Run the checks before opening a pull request

    bun run typecheck
    bun run lint
    bun run format:check
    bun run test

What Miel asks Google for

Miel requests five Google permissions. Below is each one: the wording Google itself will show you on the consent screen, and the feature that needs it.

The consent wording is reproduced as Google writes it, not softened. The mail permission is described there as permanently deleting your email — Miel never does that, but you will read that sentence when you connect an account, and you should read it here first.

Google permissions Miel requests, with Google's consent-screen wording and the feature that requires each
PermissionWhat Google's consent screen saysWhat Miel uses it for
Gmail modifyRead, compose, send, and permanently delete all your email from Gmailhttps://www.googleapis.com/auth/gmail.modifyFetch messages for triage; apply and remove labels; archive or trash the threads you select.
Gmail sendSend email on your behalfhttps://www.googleapis.com/auth/gmail.sendSend the replies you have reviewed and approved. Nothing is sent without you pressing send.
Gmail basic settingsSee, edit, create, or change your email settings and filters in Gmailhttps://www.googleapis.com/auth/gmail.settings.basicList your Gmail filters, and create the ones you accept from Miel's suggestions.
User info: profileSee your personal info, including any personal info you've made publicly availablehttps://www.googleapis.com/auth/userinfo.profileShow the connected account's display name and avatar in the app.
User info: emailSee your primary Google Account email addresshttps://www.googleapis.com/auth/userinfo.emailIdentify which account is connected.

Miel never permanently deletes mail: trashing a thread moves it to Gmail's bin, where Gmail's own thirty-day rule takes over. The mail permission still cannot be narrowed while archiving and trashing are features — the read-only and labels permissions together cover neither changing labels in bulk nor trashing a thread.

What Miel sends to the AI provider

Miel does not classify your mail itself. It sends it to the AI provider your deployment is configured against, which returns a priority, suggested labels and its reasoning. Your message content therefore leaves your machine and is processed by that provider under their terms. This is the one part of Miel that is not purely local, and if it is not acceptable to you, do not connect an account.

For triage, each request carries the sender, subject, the short preview snippet Gmail provides and the labels already on the message — up to 15 messages per request by default, and at most 50 if you raise the batch size in settings. Through the local CLI, the provider may ask Miel for a full message body when the preview is not enough to decide; a provider called over its API cannot, and is never sent one.

Drafting a reply is the one request that carries a whole message body, and it is truncated to the first 8,000 characters — anything past that is never sent. Filter suggestions send the sender, subject and snippet only.

You supply your own AI provider credentials, so the account these requests are billed to and governed by is yours, not mine. Read that provider's terms before you connect an account — you choose which one runs each task in settings, and the setup guide names the one Miel uses by default.

What Miel stores, for how long, and how to have it deleted: Privacy policy. The terms it is offered under: Terms of service.

Contact

Questions about Miel, or about what it does with your data: [email protected]