# athena-auth

## Planned implementations

- `DRAGONFLY_URL` or `REDIS_URL` either one will be streamed down into a `caching_backend.url` accompanied with a `caching_backend.provider`
- Support a Caching backend

## Roadmap

See [ROADMAP.md](ROADMAP.md) for the current roadmap and planned backlog.

current version: `1.11.0`
Athena Auth–compatible authentication for Rust: Axum integration, plugin-style routes, and session-oriented APIs aligned with the reference OpenAPI description in `[better-auth.yaml](better-auth.yaml)`.

## Features

- **Athena Auth route parity**: Email/password, OAuth, sessions, password reset, email verification, 2FA, passkeys, API keys, admin, and organizations (see coverage tables below).
- **Modular crates**: `[crates/core](crates/core)` (types, adapters, OpenAPI builder), `[crates/api](crates/api)` (plugins).
- **OpenAPI**: Spec checked into the repo; aggregated JSON served at runtime (see OpenAPI section).

## Runbooks

- Onboarding: `[runbooks/ONBOARDING.md](runbooks/ONBOARDING.md)`
- Local dev: `[runbooks/LOCAL_DEVELOPMENT.md](runbooks/LOCAL_DEVELOPMENT.md)`
- Operations: `[runbooks/OPERATIONS.md](runbooks/OPERATIONS.md)`
- Troubleshooting: `[runbooks/TROUBLESHOOTING.md](runbooks/TROUBLESHOOTING.md)`
- Runbook index: `[runbooks/README.md](runbooks/README.md)`

## Docs

- Docs index: `[docs/README.md](docs/README.md)`
- OAuth providers (preconfigured + built-in): `[docs/oauth-preconfigured-providers.md](docs/oauth-preconfigured-providers.md)`
- Athena JS auth SDK bindings: `[docs/athena-js-auth-bindings.md](docs/athena-js-auth-bindings.md)`

## Athena Auth API coverage

Paths below are **relative to `AuthConfig.base_path`** (see `[crates/core/src/config.rs](crates/core/src/config.rs)`). For example, with `base_path = "/api/auth"`, sign-in is `POST /api/auth/sign-in/email`. The dispatcher strips this prefix before routing (`[src/core/auth.rs](src/core/auth.rs)`).

### Status legend


| Status          | Meaning                                                                                                                                        |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **Done**        | Registered and handled by core (`[src/core/auth.rs](src/core/auth.rs)`) and/or plugins (`[crates/api/src/plugins/](crates/api/src/plugins/)`). |
| **Partial**     | Implemented with a documented parity gap (path shape, semantics, or security posture).                                                         |
| **Not started** | No handler in this repository.                                                                                                                 |


For the endpoint set mirrored in `[better-auth.yaml](better-auth.yaml)`, everything below is **implemented** (nothing in that set is *Not started*).

### Sign-in and session


| Endpoint            | Method(s) | Status | Notes                                                                                                     |
| ------------------- | --------- | ------ | --------------------------------------------------------------------------------------------------------- |
| `/sign-in/social`   | POST      | Done   | `[OAuthPlugin](crates/api/src/plugins/oauth/mod.rs)`                                                      |
| `/get-session`      | GET, POST | Done   | `POST` is an extra supported by `[SessionManagementPlugin](crates/api/src/plugins/session_management.rs)` |
| `/sign-out`         | POST      | Done   |                                                                                                           |
| `/sign-up/email`    | POST      | Done   | Route omitted when signup is disabled in config                                                           |
| `/sign-in/email`    | POST      | Done   |                                                                                                           |
| `/sign-in/username` | POST      | Done   |                                                                                                           |


### Passwords and email verification


| Endpoint                   | Method(s)    | Status | Notes                                                                                                                                                                                                                       |
| -------------------------- | ------------ | ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/forget-password`         | POST         | Done   |                                                                                                                                                                                                                             |
| `/reset-password`          | POST         | Done   |                                                                                                                                                                                                                             |
| `/reset-password/{token}`  | GET          | Done   | Dynamic `token` path segment                                                                                                                                                                                                |
| `/verify-email`            | GET          | Done   |                                                                                                                                                                                                                             |
| `/send-verification-email` | POST         | Done   |                                                                                                                                                                                                                             |
| `/change-email`            | POST         | Done   | Core by default; when `[UserManagementPlugin](crates/api/src/plugins/user_management/mod.rs)` registers `POST /change-email`, plugin flow takes precedence and sends verification unless `update_without_verification=true` |
| `/change-password`         | POST         | Done   |                                                                                                                                                                                                                             |
| `/update-user`             | POST         | Done   | Core                                                                                                                                                                                                                        |
| `/delete-user`             | POST, DELETE | Done   | `POST` is core by default, but plugin-owned when `[UserManagementPlugin](crates/api/src/plugins/user_management/mod.rs)` registers it (verification-gated by default); `DELETE` remains core                                |
| `/list-sessions`           | GET          | Done   | Optional via session plugin config                                                                                                                                                                                          |
| `/revoke-session`          | POST         | Done   |                                                                                                                                                                                                                             |
| `/revoke-sessions`         | POST         | Done   |                                                                                                                                                                                                                             |
| `/revoke-other-sessions`   | POST         | Done   |                                                                                                                                                                                                                             |


Route precedence on collisions: if a request matches both a core route and a plugin route with the same HTTP method + path, plugin `on_request` runs first. If no plugin handles the request, core fallback still applies.

### OAuth, accounts, and probes


| Endpoint                | Method(s) | Status | Notes                                |
| ----------------------- | --------- | ------ | ------------------------------------ |
| `/link-social`          | POST      | Done   |                                      |
| `/list-accounts`        | GET       | Done   |                                      |
| `/delete-user/callback` | GET       | Done   | Core; token query completes deletion |
| `/unlink-account`       | POST      | Done   |                                      |
| `/refresh-token`        | POST      | Done   |                                      |
| `/get-access-token`     | POST      | Done   |                                      |
| `/ok`                   | GET       | Done   | Core                                 |
| `/error`                | GET       | Done   | Core                                 |


**OAuth redirect:** Provider callbacks use `**GET /callback/{provider}`** (`[OAuthPlugin](crates/api/src/plugins/oauth/mod.rs)`), not a single hard-coded `/oauth/callback`. Register that pattern in your OAuth app (**Partial** vs static-path examples in some docs).

### Two-factor authentication


| Endpoint                            | Method(s) | Status | Notes |
| ----------------------------------- | --------- | ------ | ----- |
| `/two-factor/get-totp-uri`          | POST      | Done   |       |
| `/two-factor/verify-totp`           | POST      | Done   |       |
| `/two-factor/send-otp`              | POST      | Done   |       |
| `/two-factor/verify-otp`            | POST      | Done   |       |
| `/two-factor/verify-backup-code`    | POST      | Done   |       |
| `/two-factor/generate-backup-codes` | POST      | Done   |       |
| `/two-factor/enable`                | POST      | Done   |       |
| `/two-factor/disable`               | POST      | Done   |       |


### Passkeys


| Endpoint                                 | Method(s) | Status | Notes                                                                                                  |
| ---------------------------------------- | --------- | ------ | ------------------------------------------------------------------------------------------------------ |
| `/passkey/generate-register-options`     | GET       | Done   |                                                                                                        |
| `/passkey/generate-authenticate-options` | POST      | Done   |                                                                                                        |
| `/passkey/verify-registration`           | POST      | Done   | Full server-side WebAuthn verification via `[passkey-server](https://crates.io/crates/passkey-server)` |
| `/passkey/verify-authentication`         | POST      | Done   | Full server-side WebAuthn assertion verification + signature counter checks                            |
| `/passkey/list-user-passkeys`            | GET       | Done   |                                                                                                        |
| `/passkey/delete-passkey`                | POST      | Done   |                                                                                                        |
| `/passkey/update-passkey`                | POST      | Done   |                                                                                                        |


Related Origin Requests support is available through a top-level endpoint:

- `GET /.well-known/webauthn` (enabled only when passkey related origins are configured)
- Response shape: `{ "origins": ["https://site-1.example", "https://site-2.example"] }`

### Admin


| Endpoint                      | Method(s) | Status | Notes               |
| ----------------------------- | --------- | ------ | ------------------- |
| `/admin/set-role`             | POST      | Done   | Requires admin role |
| `/admin/create-user`          | POST      | Done   |                     |
| `/admin/list-users`           | GET       | Done   |                     |
| `/admin/list-user-sessions`   | POST      | Done   |                     |
| `/admin/unban-user`           | POST      | Done   |                     |
| `/admin/ban-user`             | POST      | Done   |                     |
| `/admin/impersonate-user`     | POST      | Done   |                     |
| `/admin/stop-impersonating`   | POST      | Done   |                     |
| `/admin/revoke-user-session`  | POST      | Done   |                     |
| `/admin/revoke-user-sessions` | POST      | Done   |                     |
| `/admin/remove-user`          | POST      | Done   |                     |
| `/admin/set-user-password`    | POST      | Done   |                     |
| `/admin/has-permission`       | POST      | Done   |                     |


### Admin email, audit, and tenant operations


| Endpoint                       | Method(s) | Status | Notes |
| ------------------------------ | --------- | ------ | ----- |
| `/admin/api-key/create`        | POST      | Done   |       |
| `/admin/athena-client/create`  | POST      | Done   |       |
| `/admin/athena-client/list`    | GET       | Done   |       |
| `/admin/oauth-client/create`   | POST      | Done   | Managed callback URL registry for social OAuth callers |
| `/admin/oauth-client/list`     | GET       | Done   | Managed callback URL registry for social OAuth callers |
| `/admin/audit-log/list`        | GET       | Done   |       |
| `/admin/email/list`            | GET       | Done   |       |
| `/admin/email/get`             | GET       | Done   |       |
| `/admin/email/create`          | POST      | Done   |       |
| `/admin/email/update`          | POST      | Done   |       |
| `/admin/email/delete`          | POST      | Done   |       |
| `/admin/email-failure/list`    | GET       | Done   |       |
| `/admin/email-failure/get`     | GET       | Done   |       |
| `/admin/email-failure/create`  | POST      | Done   |       |
| `/admin/email-failure/update`  | POST      | Done   |       |
| `/admin/email-failure/delete`  | POST      | Done   |       |
| `/admin/email-template/list`   | GET       | Done   |       |
| `/admin/email-template/get`    | GET       | Done   |       |
| `/admin/email-template/create` | POST      | Done   |       |
| `/admin/email-template/update` | POST      | Done   |       |
| `/admin/email-template/delete` | POST      | Done   |       |
| `/admin/email-event-type/list` | GET       | Done   | Canonical event taxonomy |


### API keys


| Endpoint          | Method(s) | Status | Notes |
| ----------------- | --------- | ------ | ----- |
| `/api-key/create` | POST      | Done   |       |
| `/api-key/get`    | GET       | Done   |       |
| `/api-key/update` | POST      | Done   |       |
| `/api-key/delete` | POST      | Done   |       |
| `/api-key/list`   | GET       | Done   |       |


### Organizations


| Endpoint                              | Method(s) | Status | Notes |
| ------------------------------------- | --------- | ------ | ----- |
| `/organization/create`                | POST      | Done   |       |
| `/organization/update`                | POST      | Done   |       |
| `/organization/delete`                | POST      | Done   |       |
| `/organization/set-active`            | POST      | Done   |       |
| `/organization/get-full-organization` | GET       | Done   |       |
| `/organization/list`                  | GET       | Done   |       |
| `/organization/invite-member`         | POST      | Done   |       |
| `/organization/cancel-invitation`     | POST      | Done   |       |
| `/organization/accept-invitation`     | POST      | Done   |       |
| `/organization/get-invitation`        | GET       | Done   |       |
| `/organization/reject-invitation`     | POST      | Done   |       |
| `/organization/check-slug`            | POST      | Done   |       |
| `/organization/remove-member`         | POST      | Done   |       |
| `/organization/update-member-role`    | POST      | Done   |       |
| `/organization/get-active-member`     | GET       | Done   |       |
| `/organization/leave`                 | POST      | Done   |       |
| `/organization/list-invitations`      | GET       | Done   |       |
| `/organization/has-permission`        | POST      | Done   |       |


### Extra routes (extensions)


| Endpoint                               | Method(s) | Notes                                                                                |
| -------------------------------------- | --------- | ------------------------------------------------------------------------------------ |
| `/api-key/verify`                      | POST      |                                                                                      |
| `/api-key/delete-all-expired-api-keys` | POST      |                                                                                      |
| `/organization/list-members`           | GET       |                                                                                      |
| `/organization/list-user-invitations`  | GET       |                                                                                      |
| `/set-password`                        | POST      | `[PasswordManagementPlugin](crates/api/src/plugins/password_management/mod.rs)`      |
| `/change-email/verify`                 | GET       | `[UserManagementPlugin](crates/api/src/plugins/user_management/mod.rs)` when enabled |
| `/delete-user/verify`                  | GET       | User management plugin when enabled                                                  |
| `/reference/openapi.json`              | GET       | From `[BetterAuth::openapi_spec](src/core/auth.rs)`                                  |
| `/health`                              | GET       |                                                                                      |


## Legacy `/auth/`* documentation

Older snippets described JWT-style `POST /auth/register` and `POST /auth/login`. The Athena Auth–aligned surface uses `POST /sign-up/email`, `POST /sign-in/email`, and session handling as defined in `[better-auth.yaml](better-auth.yaml)`. Use the tables above for new work.

## Documentation

This repository serves API endpoints and OpenAPI JSON only. Interactive docs/admin static pages are maintained in a separate repository.

## OpenAPI documentation

- **Repository (Athena Auth routes)**: `[openapi.yaml](openapi.yaml)` and `[athena-auth.yaml](athena-auth.yaml)` — synced specs for the current Rust server/plugin route surface.
- **Upstream reference**: `[better-auth.yaml](better-auth.yaml)` — upstream compatibility baseline.
- **Runtime**: `GET {base_path}/reference/openapi.json` (e.g. `GET http://localhost:3000/api/auth/reference/openapi.json` when `AUTH_BASE_PATH=/api/auth`).

Use either with Swagger UI, Postman, or code generation tooling.

## Setup

If you are new to this repository, start with `[runbooks/ONBOARDING.md](runbooks/ONBOARDING.md)` and then use this README as reference.

### Prerequisites

- Rust (1.93.1 or later)
- PostgreSQL database (or Supabase/ScyllaDB via athena_rs)

### About athena_rs

This project uses the [athena_rs](https://crates.io/crates/athena_rs) crate (v0.82.2), which is a database gateway API that supports multiple backends:

- **Native PostgreSQL** (via SQLx)
- **Supabase**
- **ScyllaDB**
- **Neon**

The `DATABASE_URL` should point to your database connection string, and the `DATABASE_KEY` is used for authentication (empty for local PostgreSQL).

### Installation

1. Clone the repository:
  ```bash
   git clone https://github.com/xylex-group/athena-auth.git
   cd athena-auth
  ```
2. Copy the example environment file and configure:
  ```bash
   cp .env.example .env
  ```
3. Edit `.env` with your configuration. For machine-local overrides, add only the differing keys to `.env.local`:
  ```env
   DATABASE_URL=postgresql://postgres:postgres@localhost:5432/athena_auth
   DATABASE_KEY=your_database_key_here
   JWT_SECRET=your_jwt_secret_key_here_change_in_production_minimum_32_chars
   BASE_URL=http://localhost:3000
   AUTH_BASE_PATH=/api/auth
   PASSWORD_ARGON2_MEMORY_COST=1024
   PASSWORD_ARGON2_TIME_COST=2
   PASSWORD_ARGON2_PARALLELISM=1
   TRUSTED_ORIGINS=http://localhost:3000,http://localhost:3001,https://app.formations.company,https://speedrun-formations.vercel.app
   ATHENA_AUTH_COOKIE_DOMAIN=.formations.company
   PORT=3000
  ```
   The server loads `.env` first and then `.env.local`. Explicit process env still wins over both.
   Scheme-less `BASE_URL` and `TRUSTED_ORIGINS` values are accepted. Public hosts default to `https`, while localhost-style hosts and values with `sslmode=disable` default to `http`.
4. Optional: add `config.yaml` for auth/session runtime config, observability, and multitenancy. Use `[config.yaml.example](config.yaml.example)` as a template:
  ```yaml
   auth:
     base_url: ${BASE_URL:-http://localhost:3000}
     base_path: ${AUTH_BASE_PATH:-/api/auth}
     trusted_origins:
       - ${PRIMARY_TRUSTED_ORIGIN:-http://localhost:3000}
       - https://app.formations.company
       - https://speedrun-formations.vercel.app
     password:
       argon2:
         memory_cost: 1024
         time_cost: 2
         parallelism: 1
     passkey:
       related_origins: []
     session:
       expires_in_seconds: 604800
       update_age_seconds: 86400
       disable_session_refresh: false
       cookie_name: athena-auth.session-token
       cookie_same_site: lax
       trusted_proxy_headers:
         - x-forwarded-for
         - x-real-ip
       ip:
         headers:
           - x-forwarded-for
           - x-real-ip
         disable_tracking: false # set true to force NULL session ipAddress
       intelligence:
         enabled: false
         detect_country_change: true
         geoip:
           enabled: false
           provider: ipwhois # ipwhois | ipapi
           base_url: https://ipwho.is
           api_key_env: ATHENA_AUTH_GEOIP_API_KEY
           cache_ttl_seconds: 86400
   observability:
     metrics:
       enabled: true
       path: /metrics
       dashboard_enabled: true
       dashboard_path: /metrics/view
       prefix: athena_auth
     loki:
       enabled: false
       url: http://localhost:3100
       labels:
         service: athena-auth
         environment: local
     logging:
       format: pretty # pretty | json
       wide_events: true
       environment: local
       region: workstation
       deployment_id: local-dev
       instance_id: athena-auth-server
       commit_hash: dev
   multitenancy:
     enabled: false
     header_name: x-athena-tenant
     default_tenant: default
     schema_mapping:
       users: users
       sessions: sessions
     tenants:
       default:
         database_url_env: DATABASE_URL
         search_path: athena
         schema_mapping:
           users: tenant_users
   schema_detection:
     enabled: false
     auth_schema: ${ATHENA_AUTH_SCHEMA:-athena}
     write_mapping_file: false
     output_path: schema-mapping.generated.yaml
     min_column_match_ratio: 0.6
     table_substitutions:
       users:
         - user
         - auth_users
       sessions:
         - session
         - auth_sessions
  ```
  Numeric Argon2 values in `config.yaml` can be overridden with
  `ATHENA_AUTH_CONFIG__AUTH__PASSWORD__ARGON2__MEMORY_COST`,
  `ATHENA_AUTH_CONFIG__AUTH__PASSWORD__ARGON2__TIME_COST`, and
  `ATHENA_AUTH_CONFIG__AUTH__PASSWORD__ARGON2__PARALLELISM`. Legacy aliases
  `PASSWORD_ARGON2_MEMORY_COST`, `PASSWORD_ARGON2_TIME_COST`, and
  `PASSWORD_ARGON2_PARALLELISM` are also accepted.
  - If `ATHENA_AUTH_CONFIG_FILE` is not set, the server checks `./config.yaml`.
  - If the file is missing, YAML-based auth/session, observability, and multitenancy extras stay at defaults and startup behavior remains unchanged.
  - Config values support inline env templates:
    - `${ENV_NAME}` requires the env var to exist.
    - `${ENV_NAME:-fallback}` uses `fallback` when the env var is missing.
  - Environment overrides are always final (env wins over YAML) using:
    - `ATHENA_AUTH_CONFIG__<PATH_SEGMENT_1>__<PATH_SEGMENT_2>...`
    - Example: `ATHENA_AUTH_CONFIG__AUTH__BASE_URL=https://auth.example.com`
    - Example: `ATHENA_AUTH_CONFIG__SCHEMA_DETECTION__AUTH_SCHEMA=tenant_auth`
  - Legacy env aliases are preserved for compatibility:
    - `BASE_URL`, `AUTH_BASE_PATH`, `TRUSTED_ORIGINS`, and `PASSKEY_RELATED_ORIGINS`
  - Session IP capture is controlled by `auth.session.ip.disable_tracking` (or `ATHENA_AUTH_CONFIG__AUTH__SESSION__IP__DISABLE_TRACKING`).
  - When trusted proxy headers are missing, Axum falls back to the connection remote address for session IP capture.
  - `auth.session.intelligence.geoip` is optional and fail-open: cached IP lookups are reused and failed provider calls do not block sign-in.
  - Free provider adapters included: `ipwhois` (`https://ipwho.is`) and `ipapi` (`https://ipapi.co`).
  - When multitenancy is enabled, requests are routed by `header_name` (fallback aliases: `x-athena-client`, `x-athena-tenant`).
  - `schema_detection.auth_schema` sets the runtime schema for all auth/internal tables (default `athena`).
  - `schema_mapping` supports logical table-name mappings plus per-tenant overrides for strict schema compatibility between tenants.
  - `schema_detection` can probe live table/column layouts, evaluate ranked fallback tables per logical table, and (optionally) write a generated YAML mapping report when substitutions are detected.
### Direct Browser Auth On Sibling Subdomains

If the browser should call Athena Auth directly at `https://auth.formations.company`
while your app and SSR live at `https://app.formations.company`, configure a shared
parent-domain session cookie:

```env
BASE_URL=https://auth.formations.company
AUTH_BASE_PATH=/
TRUSTED_ORIGINS=https://app.formations.company
ATHENA_AUTH_COOKIE_DOMAIN=.formations.company
```

That produces a session cookie shaped like:

```http
Set-Cookie: athena-auth.session-token=<token>; Path=/; Secure; HttpOnly; SameSite=Lax; Domain=.formations.company
```

This is the deployment contract:

- `TRUSTED_ORIGINS` allows the browser on `app.formations.company` to call the auth host with credentials.
- `ATHENA_AUTH_COOKIE_DOMAIN` widens the cookie from host-only `auth.formations.company` to the shared parent `.formations.company`, which lets both direct browser auth calls and sibling-subdomain SSR use the same session.

Without `ATHENA_AUTH_COOKIE_DOMAIN`, the cookie stays host-only on the auth host, so the browser can talk to auth but SSR on `app.formations.company` will not receive that session cookie. No Next.js proxy is required when the shared parent-domain cookie is configured correctly.

5. Optional manual schema setup (the server now self-heals at startup by creating the configured auth schema and applying `sql/provision.sql` automatically if objects are missing; root `schema.sql` is a minimal legacy example):
  ```bash
   psql -d athena_auth -f schema.sql
  ```
6. Build and run the HTTP server:
  ```bash
   cargo run --bin server --features server
  ```

The server listens on `HOST`/`PORT` (defaults `0.0.0.0:3000`). Health check: `GET http://localhost:3000/api/auth/health` (adjust host/port and `AUTH_BASE_PATH` if you override them).

- Prometheus metrics endpoint is mounted at `observability.metrics.path` (default `/metrics`) when enabled in `config.yaml`.
- A self-contained HTML metrics dashboard is mounted at `observability.metrics.dashboard_path` (default `/metrics/view`) when metrics and `dashboard_enabled` are enabled.
- WebAuthn related origins endpoint is mounted at `/.well-known/webauthn` when `auth.passkey.related_origins` (or `PASSKEY_RELATED_ORIGINS`) is non-empty.
- Loki log shipping is enabled only when `observability.loki.enabled=true` and `observability.loki.url` is set.
- `observability.logging.format` supports `pretty` and `json`. Use `json` when you want grep-friendly local logs or Loki/Grafana ingestion.
- `observability.logging.wide_events=true` emits one request-completion event per HTTP request with `request_id`, `trace_id`, route, tenant, cookie/auth diagnostics, timing, and status metadata.
- On startup, the server ensures `schema_detection.auth_schema` (default `athena`) exists and applies the core auth DDL from `sql/provision.sql` inside that schema. The DB user therefore needs `CREATE SCHEMA` and DDL permissions in the target database.
- When startup self-healing actually repairs missing schema/tables, it writes an audit row to `athena.audit_log_ddl` if that table already exists.

## Debug and observability pages

When local auth requests fail because the browser origin is not trusted, use the debug pages before guessing at `TRUSTED_ORIGINS`.

- Demo schema/origin debugger: `http://localhost:3001/schema-debug`
  - Shows live output from the demo proxy route `GET /api/auth/debug/schema`.
  - Includes tenant resolution, auth base-path/runtime state, and trusted-origin rejection diagnostics.
  - The trusted-origin section shows the delivered origin/referer, the normalized allowlist the server expected, recent rejected attempts, and a ready-to-copy `TRUSTED_ORIGINS=...` line that would allow the failing origin.
- Server schema debug endpoint: `http://localhost:3000/debug/schema`
  - Exposes the same runtime schema/config snapshot directly from the Rust server.
  - Includes `origin_rejection_debug`, which records recent server-side origin rejections so operators can compare what was sent vs what the server expected.
- Metrics scrape endpoint: `http://localhost:3000/metrics`
  - Prometheus-compatible plaintext metrics for scraping.
- Metrics dashboard: `http://localhost:3000/metrics/view`
  - Human-readable HTML dashboard backed by the same metrics registry.

### Local observability stack

When you need to take a `traceId` from an auth error payload and find the corresponding request locally, use the dedicated stack under [ops/observability/local](ops/observability/local/README.md).

1. Start the stack with [scripts/start-local-observability.ps1](scripts/start-local-observability.ps1). If Docker Desktop is installed but not running, the script now attempts to start it for you and waits for the daemon to become ready.
2. Run the server with [scripts/run-server-local-observability.ps1](scripts/run-server-local-observability.ps1).
3. Open the pre-provisioned dashboard at `http://localhost:3002/d/athena-auth-local-observability/athena-auth-local-observability`.
4. Run the server with `powershell -File .\scripts\run-server-local-observability.ps1`. The script pins the local observability server to `PORT=3000` and `BASE_URL=http://localhost:3000` so Prometheus and Grafana line up with the expected local endpoints.
5. For direct log lookup, run `powershell -File .\scripts\find-local-trace.ps1 -TraceId <traceId>`.
6. If Docker Desktop is down but you still want local trace lookup, run `powershell -File .\scripts\run-server-local-observability.ps1 -LogPath .\.local\athena-auth-observability.ndjson` and then query the captured file with `powershell -File .\scripts\find-local-trace.ps1 -TraceId <traceId> -LogPath .\.local\athena-auth-observability.ndjson`.
7. If you want the raw Loki query experience, open Grafana Explore at `http://localhost:3002/explore` and query `{service="athena-auth"} |= "<traceId>"`.

The local config at [ops/observability/local/config.local-observability.yaml](ops/observability/local/config.local-observability.yaml) enables:

- JSON logs
- Wide request completion events
- Loki shipping to `http://127.0.0.1:3100`
- Prometheus scraping of the server's `/metrics` endpoint
- A pre-provisioned Grafana dashboard plus direct trace lookup tooling

Recommended workflow for "wrong origin" failures:

1. Open `/schema-debug` in the demo app and reproduce the failing auth action.
2. Inspect the trusted-origin rejection list to see the delivered origin, expected origin set, and suggested `TRUSTED_ORIGINS` entry.
3. Add the required origin or wildcard to both the demo app environment and the Rust server environment, then restart both processes.
4. Confirm the server metrics/dashboard still load if you are validating a deployment-level issue instead of a browser-only issue.

### DB-backed email templates (multi-tenant ready)

Built-in auth email flows now resolve templates through `email_templates` when available, with safe fallback to built-in defaults if no row exists (or lookup fails).

- Placeholder format: `{{variable_name}}`
- Locale lookup order: requested locale -> locale language base -> `en`
- Active row selection: `is_active = true`, newest `updated_at` wins

Default `template_key` values:

- `verification_email`
- `password_reset_email`
- `change_email_confirmation_email`
- `account_deletion_confirmation_email`
- `two_factor_otp_email`
- `organization_invitation_email`

Canonical `eventType` values live in `email_event_types` and can be listed via
`GET /admin/email-event-type/list`.

Template assignment is explicit through `email_templates.event_type`, with runtime
resolution order:

1. `eventType` override match (active + locale-aware)
2. `templateKey` fallback match (active + locale-aware)
3. built-in Rust defaults

Seeded event taxonomy includes current live flows plus future-ready entries, for example:

- `user.email.verify`
- `user.sign-in.email`
- `organization.member.invite`
- `organization.create`

In multitenancy mode, each tenant runtime resolves templates from its own schema/runtime tables, so different tenants can use different branding/copy without code changes.

## Database schema

The application uses these core tables:

- **users**: user identities and profile/security flags
- **sessions**: session tokens, user binding, and client metadata
- **accounts**: OAuth/email provider account links
- **api_keys**: API keys associated with users
- **ip_profiles** *(optional)*: IP-level GeoIP cache keyed by normalized IP (migration `010`)
- **session_ip_profiles** *(optional)*: per-session location/security projection linked to `ip_profiles` (migration `010`)

See `schema.sql` for the complete schema definition.

## Docker deployment

### Using Docker Compose

The repo ships a self-contained local stack in `[docker-compose.yml](docker-compose.yml)`:

```bash
docker compose up --build
```

This stack:

- Start a PostgreSQL database
- Start the athena-auth server on `http://localhost:3000`
- Serve auth routes at `http://localhost:3000/api/auth/*`
- Let the server self-heal the auth schema from `sql/provision.sql` at startup
- Use the dedicated container runtime config at `[ops/docker/config.yaml](ops/docker/config.yaml)` instead of copying a local `config.yaml`

Useful endpoints after startup:

- `GET http://localhost:3000/api/auth/health`
- `GET http://localhost:3000/api/auth/reference/openapi.json`
- `GET http://localhost:3000/metrics`

For Compose-specific overrides, export the namespaced `ATHENA_AUTH_DOCKER_*` variables documented in `[.env.example](.env.example)` before running Compose. See `[runbooks/DOCKER.md](runbooks/DOCKER.md)` for the common ones.

### Building Docker image manually

The image now copies `[ops/docker/config.yaml](ops/docker/config.yaml)` to `/app/config.yaml` and sets `ATHENA_AUTH_CONFIG_FILE=/app/config.yaml` by default.

```bash
docker build -t athena-auth .
docker run -p 3000:3000 \
  -e DATABASE_URL=postgresql://postgres:postgres@host.docker.internal:5432/athena_auth \
  -e DATABASE_KEY="" \
  -e JWT_SECRET=your_secret_key_at_least_32_characters_long \
  -e BASE_URL=http://localhost:3000 \
  -e AUTH_BASE_PATH=/api/auth \
  -e PUBLIC_AUTH_BASE_PATH=/api/auth \
  athena-auth
```

To use a different runtime config, mount it and override `ATHENA_AUTH_CONFIG_FILE` at runtime.

### Cloudflare Worker and Direct Postgres

`wrangler.jsonc` deploys a Worker that fronts the Rust server running in a
Cloudflare Container.

Use `wrangler secret put DATABASE_URL` to provide the Rust server's PostgreSQL
connection string. The supported Cloudflare deployment path is direct Postgres
from the containerized Rust server via `DATABASE_URL`.

## Example usage

An example usage script is provided in `examples/api_usage.sh`:

```bash
chmod +x examples/api_usage.sh
./examples/api_usage.sh
```

This script demonstrates:

1. User registration
2. User login
3. Token verification
4. Getting user information
5. Creating an API key
6. Listing API keys
7. Revoking an API key

## Development

Build the project:

```bash
cargo build
```

Run the HTTP server in development:

```bash
cargo run --bin server --features server
```

Run with release optimizations:

```bash
cargo run --release --bin server --features server
```

## Testing and quality gates

Run the baseline checks before pushing:

```bash
cargo fmt --check
cargo clippy
cargo test
```

Run compatibility suites with the split test targets:

```bash
cargo test --test compat_endpoint_tests -- --nocapture
cargo test --test compat_field_tests -- --nocapture
cargo test --test compat_coverage_tests -- --nocapture
cargo test --test compat_consistency_tests -- --nocapture
cargo test --test compat_unit_tests -- --nocapture
cargo test --test compat_admin_tests -- --nocapture
cargo test --test compat_organization_tests -- --nocapture
cargo test --test compat_passkey_tests -- --nocapture
```

For snapshot/property/dual-server commands, use the canonical matrix in `[compat-tests/README.md](compat-tests/README.md)`.

## Security notes

- Always use a strong, random `JWT_SECRET` in production
- Store database credentials securely (use environment variables)
- API keys are hashed before storage using bcrypt
- Passwords are hashed using bcrypt with default cost factor
- Session tokens default to 7 days (`AuthConfig.session.expires_in`); JWT helper tokens default to 24 hours (`AuthConfig.jwt.expires_in`)

## Technology stack

- **Framework**: Axum 0.8
- **Database**: athena_rs 0.82.2 (multi-database gateway)
- **Authentication**: JWT (jsonwebtoken), bcrypt
- **Async runtime**: Tokio
- **Serialization**: Serde

## License

MIT License - see LICENSE file for details
