No description
  • TypeScript 98.3%
  • CSS 1.7%
Find a file
theolagache e46bc77489
All checks were successful
Release / version (push) Successful in 30s
Release / build-amd64 (push) Successful in 2m17s
Release / build-arm64 (push) Successful in 22m16s
Release / merge (push) Successful in 45s
Release / release (push) Successful in 15s
Merge pull request 'Update release.yml' (#36) from feat/update into main
Reviewed-on: #36
2026-08-05 05:06:26 +00:00
.claude add: new auth,senders breach detection,aliceconnect,ux fixes 2026-08-02 11:51:58 +02:00
.forgejo/workflows Update release.yml 2026-08-05 07:05:56 +02:00
app fix 2026-08-04 21:53:48 +02:00
config add: extension options 2026-08-02 21:57:08 +02:00
db gpdr 2026-08-02 15:25:08 +02:00
docs/superpowers docs: alias create & label implementation plan 2026-06-29 09:10:36 +02:00
public logo 2026-08-02 15:25:08 +02:00
src fix 2026-08-04 21:53:48 +02:00
.dockerignore fixes 2026-06-25 08:56:22 +02:00
.env.example add: missing route api 2026-08-04 21:37:46 +02:00
.gitignore Update .gitignore 2026-08-02 15:27:43 +02:00
.nvmrc fixes 2026-06-25 08:56:22 +02:00
CITATION.cff add: extension 2026-08-02 21:57:08 +02:00
CODE_OF_CONDUCT.md fixes 2026-06-25 08:56:22 +02:00
components.json fix 2026-06-28 18:13:18 +02:00
CONTRIBUTING.md add: extension 2026-08-02 21:57:08 +02:00
docker-compose.yml add: extension 2026-08-02 21:57:08 +02:00
dockerfile fix: ci 2026-08-05 06:54:19 +02:00
drizzle.config.ts gpdr 2026-08-02 15:25:08 +02:00
env.ts add: missing route api 2026-08-04 21:37:46 +02:00
instrumentation.ts gpdr 2026-08-02 15:25:08 +02:00
LICENSE fixes 2026-06-25 08:56:22 +02:00
next-env.d.ts fix 2026-08-04 21:53:48 +02:00
next.config.ts fix 2026-06-28 18:13:18 +02:00
package.json add: extension options 2026-08-02 21:57:08 +02:00
pnpm-lock.yaml feat: add services registry + ServiceBadge + ServiceCombobox 2026-06-29 09:27:33 +02:00
pnpm-workspace.yaml fix 2026-06-28 17:51:49 +02:00
postcss.config.mjs chore: install Next.js 16, TanStack, next-safe-action — drop React Router 2026-06-28 13:29:50 +02:00
README.md add: extension 2026-08-02 21:57:08 +02:00
SECURITY.md fixes 2026-06-25 08:56:22 +02:00
tsconfig.json fix 2026-06-28 18:13:18 +02:00

Alice

Self-hosted email alias manager for Stalwart

Beta software. Alice is functional but under active development. You may encounter bugs or breaking changes between releases. Back up your database before upgrading.

Features

  • Aliases on demand — Bitwarden creates one on every sign-up, no manual step
  • Your own apps too — Alice has its own API, and pairs mobile devices without sharing secrets
  • Know where each alias came from — tag it with the service you gave it to
  • See what arrives — mail received per alias, and when the last one came in
  • Spot a leak — spam counts and unknown senders, flagged per alias
  • Find anything — search by address or service, filter by status and activity
  • Bulk everything — enable, disable or delete a whole selection at once
  • CSV in and out — export the full list, import with a preview before anything is written
  • Sign in with OIDC — including Stalwart's own OAuth server

Alice sits on top of Stalwart and speaks two APIs: an addy.io-compatible one for Bitwarden, and its own for everything you write yourself.

How it works

Bitwarden ──► /api/v1/…        addy.io emulation
your apps ──► /api/alice/v1/…  Alice's own API
                   │
                   ▼
             Stalwart (JMAP)
                   │
                   ▼
       alias@relay.example.com
       forwards to you@mail.example.com
  1. You register a client in Alice and give it the key it hands you.
  2. When the client needs an alias, it calls Alice.
  3. Alice creates a new address on Stalwart under your relay domain.
  4. Incoming mail to that alias is delivered to your real inbox by Stalwart.

Both surfaces share the same authentication and the same domain layer; only the response format differs. /api/v1 is frozen — its shape is dictated by addy.io, and Bitwarden appends those paths to the server URL you give it.

Authentication is handled via OIDC — you can use any standard identity provider, including Stalwart's built-in OAuth server.

Username matching. When a user logs in, Alice reads their OIDC name claim and looks for a Stalwart account with that exact username. If no matching account is found, login is denied. This means the username in your identity provider must match the Stalwart account name one-to-one. Using Stalwart as the OIDC provider is the simplest way to guarantee this, since accounts are the same on both sides.

Prerequisites

  • A running Stalwart instance with an admin API key
  • An OIDC provider — any standard provider works; Stalwart's built-in OAuth server is the recommended choice (see Username matching)
  • Docker and Docker Compose
  • Bitwarden (self-hosted or cloud, any plan) — only if you want the addy.io integration

Installation

1. docker-compose.yml

Copy the block below into a docker-compose.yml and fill in your values.

services:
  alice:
    image: git.aktech.fr/alice/alice:latest
    restart: unless-stopped
    ports:
      - "3000:3000"
    volumes:
      - alice_data:/app/data
    environment:
      NODE_ENV: production
      PROJECT_DATABASE_URL: file:./data/app.db
      PROJECT_AUTH_SECRET: "change-me-at-least-32-chars"
      PROJECT_AUTH_URL: "https://alice.example.com"
      PROJECT_STALWART_URL: "https://mail.example.com"
      PROJECT_STALWART_API_KEY: "your-stalwart-admin-api-key"
      PROJECT_RELAY_DOMAIN: "relay.example.com"
      PROJECT_ALIAS_FORMAT: "words"
      PROJECT_OIDC_ISSUER: "https://mail.example.com"
      PROJECT_OIDC_CLIENT_ID: "alice"
      PROJECT_OIDC_CLIENT_SECRET: "your-oidc-client-secret"

volumes:
  alice_data:

2. Run migrations and start

docker compose run --rm alice pnpm db:migrate
docker compose up -d

Alice will be available at the URL set in PROJECT_AUTH_URL.

Volumes

One volume, /app/data, holds:

Path What it is
/app/data/local.db the embedded database — aliases live in Stalwart, but clients, GDPR requests and the privacy-contact directory live here
/app/data/templates/ request templates of your own, read on every start (see Request templates)

Losing this volume loses your API keys, your erasure request history and your own privacy contacts. Aliases themselves are safe — they belong to Stalwart.

To edit templates from the host, bind-mount that subdirectory instead:

    volumes:
      - alice_data:/app/data
      - ./templates:/app/data/templates:ro

Environment variables

Variable Description Example
PROJECT_DATABASE_URL SQLite file path file:./data/app.db
PROJECT_AUTH_SECRET Random secret, min 32 chars openssl rand -hex 32
PROJECT_AUTH_URL Public URL of this Alice instance https://alice.example.com
PROJECT_STALWART_URL Stalwart admin URL https://mail.example.com
PROJECT_STALWART_API_KEY Stalwart admin API key
PROJECT_RELAY_DOMAIN Domain used for generated aliases relay.example.com
PROJECT_ALIAS_FORMAT Alias local-part format: words, hex, uuid words
PROJECT_OIDC_ISSUER OIDC discovery base URL https://auth.example.com
PROJECT_OIDC_CLAIM Claim holding the Stalwart username preferred_username
PROJECT_GDPR_ARCHIVE_URL Tarball of the privacy-contact directory, {ref} substituted see below
PROJECT_GDPR_REF Commit or tag to pin that directory to 02eb6f24…
PROJECT_GDPR_TEMPLATES_DIR Where your own templates are read from ./data/templates
PROJECT_OIDC_CLIENT_ID OIDC client ID
PROJECT_OIDC_CLIENT_SECRET OIDC client secret

Alias formats

Value Example output
words fox-oak-342@relay.example.com
hex a3f8b2c1@relay.example.com
uuid 550e8400-e29b-41d4-a716-446655440000@relay.example.com

Bitwarden configuration

Once Alice is running:

  1. Open Alice in your browser and log in via your OIDC provider.

  2. Go to Clients → Add client, name it Bitwarden, and copy the key. It is shown once and never again.

  3. In Bitwarden, go to Settings → Generator.

  4. Select Username as the type, then Forwarded email alias.

  5. Choose Addy.io as the service.

  6. Fill in the two fields:

    Field Value
    API access token Your Alice API key (from step 2)
    URL Your Alice instance URL (e.g. https://alice.example.com)
  7. Click Generate — Bitwarden will call Alice and return a fresh alias.

From that point on, every time you create a new login in Bitwarden with the username generator set to "Forwarded email alias", a new alias is created automatically.

Leak detection

An alias given to one service should only ever hear from that service. Alice watches for the two ways that stops being true, and reports them separately because the remedy differs.

Spam. How many of an alias's messages landed in Junk. An address that receives nothing but spam has been sold or scraped — the fix is to disable it, which the warning offers directly. Disabling one alias never touches the others.

Unknown senders. The sending domains seen on the alias. One domain is the normal case and raises nothing. From the second onwards, Alice lists the ones you have not vouched for: mark a domain Expected and it stops being reported, withdraw the approval later and it comes back.

Approvals are the point rather than an afterthought. Plenty of legitimate services send from more than one domain — transactional mail through SendGrid, newsletters through Mailchimp, amazon.com alongside amazon-ses.com — so without a way to say "this one is fine" the warning would be permanent noise and you would learn to ignore it.

The privacy-contact directory

Alice does not keep a list of privacy contacts of its own. It loads one from a git archive at startup, into its database.

PROJECT_GDPR_ARCHIVE_URL=https://codeload.github.com/datenanfragen/data/tar.gz/{ref}
PROJECT_GDPR_REF=02eb6f247e80ee4b423f9ccb68be4cc7ca23ab26

Request templates

The letters Alice sends come from the same archive, under templates/<locale>/<kind>.txt, and are stored in the database as upstream. Every sync wipes and rewrites them.

Two constructs:

{name} replaced with a value
[flag>text] kept when the flag is set, dropped otherwise

Braces Alice does not recognise are left alone on purpose — the reminder and complaint templates use them for passages a human must write, and the UI refuses to send while any remain.

Writing your own

Drop a file in the templates directory, mirroring the upstream layout:

data/templates/
└── fr/
    └── erasure-default.txt

It is read on every start and wins over the upstream template of the same locale and kind. Files are the interface rather than a form in the UI: a mounted directory is something you can version, review and back up.

Alice fills these placeholders:

Template Placeholders Flags
erasure-default {id_data} — the alias, and nothing else erase_all, has_fields
admonition {request_date}, {request_article}

{id_data} carries only the alias address. That is the identifier the controller already holds, so it is enough to identify you — while adding a name or a postal address would hand them personal data they never had.

To remove a custom template, delete the file and restart: local templates are reloaded wholesale, so a deleted file actually disappears.

Clients

Anything that talks to Alice's API is a client holding its own key: Bitwarden, a script, a mobile app. Revoking one leaves the others untouched.

AliceConnect API

Authenticate with Authorization: Bearer <client key>.

GET /api/alice/v1/me username, relay domain, available alias formats
GET /api/alice/v1/aliases the list; add ?usage=true for mail counts
POST /api/alice/v1/aliases { format?, service? } → a generated address
GET·PATCH·DELETE /api/alice/v1/aliases/{address} one alias, address URL-encoded
PATCH·DELETE /api/alice/v1/aliases { addresses[], … } — one Stalwart write

The list returns an ETag; send it back as If-None-Match and you get a 304 when nothing changed. Errors carry a machine-readable code: { "error": { "code": "unknown_domain", "message": "…" } }.

Usage is opt-in because it costs an extra round trip to Stalwart, and it is billed per alias — ask for it when you display it, not on every poll.

Connecting a device

Native apps must not ship the OIDC client secret, so they sign in through the device authorization grant against Alice — never against Stalwart directly:

1. POST /api/auth/device/code       → show the user_code to the user
2. user opens /device, enters the code, approves
3. poll POST /api/auth/device/token → access_token
4. POST /api/auth/api-key/create    → the client key, stored for good
5. POST /api/auth/sign-out          → discard the access_token

Step 5 matters: the token from step 3 is a full session valid for days, more powerful than the key it just created, and it appears nowhere in the Clients list. Once the key is stored, the token has no further use.

Stalwart setup

Alice needs permission to add and remove email addresses on Stalwart. Make sure:

  • The PROJECT_STALWART_API_KEY belongs to an admin account.
  • Your relay domain (e.g. relay.example.com) is configured as a local domain in Stalwart.
  • Each Alice user's primary account (their OIDC username) already exists as a Stalwart account before they log in.

Using Stalwart as the OIDC provider

Stalwart ships with a built-in OAuth 2.0 / OpenID Connect server, which makes it the simplest identity provider to use with Alice — accounts are shared, so usernames always match.

To configure it, create an OAuth client in Stalwart and point Alice at it:

PROJECT_OIDC_ISSUER=https://mail.example.com
PROJECT_OIDC_CLIENT_ID=alice
PROJECT_OIDC_CLIENT_SECRET=your-client-secret

The discovery endpoint Stalwart exposes (/.well-known/openid-configuration) is picked up automatically.

If you use a third-party provider (Authelia, Keycloak, Authentik…), make sure the name claim in the issued token matches the Stalwart account name exactly. Alice uses that value to look up the account on every login.

Roadmap

More protocols The API layer is already split so that only serialisation is protocol-specific — authentication and the Stalwart layer are shared. Adding SimpleLogin or Firefox Relay means writing a serialiser, not a second integration. The paths those clients impose can be arbitrary; route groups keep the tree organised regardless.

Contributions toward any of these are welcome — see CONTRIBUTING.md.