Skip to content
Kourrier
Kourrier 0.1The e-mail layer of your Kubernetes platform

Events in.E-mail out.

Your services publish what happened. Declarative routes, versioned in Git, decide which e-mails go out. Asynchronous, self-hosted, provider-agnostic, written in Go.

01An event comes in

CloudEvent · via NATSusers

user.signed_up

Who
Jane · jane@example.com
Language
E-mail verified

Change the event and watch it go through.

02Routes decide

route · welcome

Matched
  • Ifit's user.signed_up
  • andthe e-mail is verified

send welcome to Jane in FR

route · order-receipt

  • Ifit's order.paid
  • andthe total is above 0

send order-receipt to the customer

Written in CEL, versioned in Git. No route, no e-mail.

03The e-mail goes out

To
Jane <jane@example.com>
Subject
Bienvenue sur Kourrier, Jane !

Bonjour Jane,

Votre compte a bien été créé. Vous pouvez dès maintenant vous connecter.

KOURRIER · AWS SES · KOURRIER · AWS SESSent12 NOV 2026

Queued, then sent by a worker through SES.

01Decoupled

Your business code stops knowing e-mail exists.

Services publish domain events in the CloudEvents format. Kourrier consumes them and works out which e-mails to send. Adding a notification is adding a route, reviewed in a pull request like the rest of your platform.

Before

Before: e-mail inside the business transaction

  • SMTP is slow? The signup is slow.
  • SMTP is down? The signup fails.
  • A new e-mail means a change in the service.

One transaction

Signup service

Saves the user

…and sends the e-mail

Template, locale, SMTP

SMTP

Slow or down

After

After: publish the fact, route it in Git

  • The service never waits for e-mail, and never fails because of it.
  • Retries, templates and quotas live in one place.
  • A new e-mail is a new route, in a pull request.

Signup service

Publishes user.signed_up

NATS

Event bus

Kourrier

Routes in Git

SES

or SMTP

Two doors, one road

Send a specific e-mail now (password reset, OTP)

Direct API call

Notify after a business fact, without coupling

Events

Send only if the database transaction commits

Events + outbox

Several e-mails for one fact (customer, admin, accounting)

One route per e-mail
02How it works

One binary. Interchangeable doors in, one road out.

A hexagonal Go monolith. The HTTP API, the CloudEvents sink and the NATS JetStream consumer all hand over to the same submit service, so validation, idempotency, statuses, retries and metrics are identical whichever door the mail came through.

01

Doors in

  • NATS JetStream (pull, explicit ack)
  • POST /v1/events (CloudEvents)
  • POST /v1/emails (direct)
02

Sorting

  • CEL routes, compiled at startup
  • Bounded cost per evaluation
  • Backpressure on the broker
03

Submit

  • Allowed senders
  • Idempotency key
  • Status: queued
04

Queue

  • Redis Streams + consumer groups
  • Delayed set for retries
  • Dead-letter stream
05

Workers

  • Template from S3, cached
  • Distributed rate limit (GCRA)
  • Retries with backoff
06

Out

  • AWS SES
  • SMTP
  • console · filesystem

Beside the road · Templates

In S3, MinIO, RustFS or a local folder, cached by the workers. A file dropped in the bucket is live within a minute, without a redeploy.

Beside the road · Admin port :9090

/metrics for Prometheus, /livez and /readyz for the probes. JSON logs on stdout for Loki, with addresses masked.

03Reliability

Mail doesn't get lost. It gets retried, then kept.

At-least-once delivery on Redis Streams. A transient failure is retried with exponential backoff and jitter. A permanent one, or eight failed attempts, goes to a dead-letter queue that is never emptied silently, and can be replayed once you've fixed the cause.

At the provider

Time is compressed. Defaults: first retry after 10 s, doubled each time with jitter, capped at 30 min, 8 attempts before the DLQ.

Tracking · e-mail 0192f1b4

  1. t+0 squeued

    Accepted and queued in Redis Streams

KOURRIER · WORKERS · KOURRIER · WORKERSQueued12 NOV 2026

Status

queued

Attempts · 0 of 8 before the DLQ

Provider · AWS SES

Pods die, jobs don't

Jobs left pending by a dead Pod are claimed back by the others.

No duplicates

Idempotency-Key on the API, route + source + id on events: resends and redeliveries are absorbed.

Quota respected

A distributed GCRA rate limiter on Redis keeps you under your SES quota, however many replicas run.

Backpressure

When e-mails leave slower than events arrive, the backlog stays in NATS instead of piling up in Redis.

04Routes as code

Test your notifications in CI, before they reach anyone.

Routes are written in CEL, the expression language of Kubernetes: no loops, no I/O, a bounded cost. Two commands work offline, with no Redis and no broker, so a broken route fails the pull request, not production.

Open

Add the order receipt e-mail

#128 · routes.yaml

+ route · order-receipt

  • Ifit's order.paid
  • andthe total is above 0

send order-receipt to the customer

Checks · offline, no Redis, no broker

  • kourrier routes check

    2 routes · syntax, CEL types, tenants and senders

  • kourrier routes test

    order-paid.json → 1 e-mail: order-receipt to jane@example.com

Ready to merge: the ConfigMap rolls out to the Pods.

The language of Kubernetes

CEL already validates CRDs and admission policies. No loops, no I/O.

Bounded cost

Each evaluation is capped, so a route can't slow the dispatcher down.

Fails loudly

A missing field rejects the route instead of sending a half-empty e-mail.

05Kubernetes-native

A Helm chart that's ready for production on day one.

Hardened by default, observable out of the box. Templates change in the bucket without a redeploy; routes change through a ConfigMap and a rolling update that stops by itself if the new file is invalid.

$ helm install kourrier deploy/helm/kourrier

Deployment

Non-root, read-only rootfs, seccomp, probes, graceful stop

Secret

Tenants and key fingerprints; a change restarts the Pods

ConfigMap

Routes; an invalid file stops the rollout by itself

HPA · PDB

Scales with load, survives node drains

NetworkPolicy · Ingress

Only the traffic it needs

ServiceMonitor

Prometheus scrapes :9090/metrics

PrometheusRule

Seven alerts, each with its runbook

Grafana dashboard

A labelled ConfigMap, picked up by Grafana

Outside the Pod

  • NATS JetStream

    events in

  • Redis ≥ 7

    queue, statuses, limits

  • S3 · MinIO · RustFS

    templates

  • AWS SES · SMTP

    e-mail out

Grafana · Kourrier · Overviewillustrative values

Sent (24h)

18,432

Failed (24h)

3

Success ratio (1h)

99.98%

Delivery latency p95

412 ms

Queue backlog

0

Dead-letter queue

0

E-mails sent / s by tenant

Seven alerts, with their runbook

  • KourrierHighFailureRate
  • KourrierDeadLetters
  • KourrierQueueBacklog
  • KourrierHighRetryRate
  • KourrierEventsRejected
  • KourrierEventSourceDown
  • KourrierEventsBackpressure

~40 MB

static binary

~53 MB

distroless, non-root image

Sends nothing by default

The default driver prints e-mails instead of sending them.

Hashed API keys

Per-tenant keys; only their SHA-256 fingerprints are stored.

Allowed senders

A tenant can only send from the addresses it was given.

Escaped and masked

HTML escaped automatically, addresses masked in the JSON logs.

06Try it

No AWS account. No Internet. Three commands.

Locally, Kourrier prints e-mails in the console instead of sending them. Edit a template, send again, see the result straight away.

01

make up

Redis and Kourrier in Docker, API on :8180

02

make send

sends the demo “welcome” e-mail

03

make logs

shows the rendered e-mail

To Jane <jane@example.com>

Welcome to Kourrier, Jane!

Hi Jane, your account is ready. You can sign in right away.

KOURRIER · CONSOLE · KOURRIER · CONSOLENot sentLOCAL

Need the real drivers? make up-full loads templates from S3 (RustFS) and sends over SMTP to Mailpit, still without leaving your machine.

07Decisions

Every choice is written down.

Fourteen architecture decision records explain what was chosen, what was ruled out, and why. They're in French, in the repository.

  1. ADR 001Go and a modular monolith
  2. ADR 002Asynchronous queue on Redis
  3. ADR 003Templates stored in S3 / MinIO
  4. ADR 004Provider abstraction and test drivers
  5. ADR 005Native observability: Prometheus, Loki, Grafana
  6. ADR 006Redis Streams, retries and dead-letter queue
  7. ADR 007Idempotent sends
  8. ADR 008Authentication and multi-tenant isolation
  9. ADR 009Distributed rate limiting
  10. ADR 010Template format, locales and cache
  11. ADR 011Event-driven entry (CloudEvents)
  12. ADR 011Template validation, catalogue and preview in the API
  13. ADR 012Declarative routing with CEL
  14. ADR 013NATS JetStream as the first broker
Read the ADRs

To: your platform team

Hand your e-mail to the platform.

Kourrier is on GitHub, with its Helm chart, its Grafana dashboard and its documentation.