---
title: "Security & Authentication"
description: "How services authenticate with each other and how user access is controlled"
source: /docs/security
---


Overview [#overview]

MastraKit uses a layered security model. Each service authenticates requests independently. No service blindly trusts another — every call is verified.

```
Browser ──cookie──> Web Worker ──JWT──> Auth Worker
                       │                    │
                       │ JWT                 │ JWKS
                       ▼                    ▼
                   API Worker          Mastra Worker
                       │                    │
                       │                    │ shared secret
                       │                    ▼
                       └──JWT──────> Metering Worker
```

Auto-Generated Secrets [#auto-generated-secrets]

The deploy wizard (`npx mastrakit deploy`) automatically generates these secrets. You never need to create them manually:

| Secret                           | Format           | Purpose                                   |
| -------------------------------- | ---------------- | ----------------------------------------- |
| `BETTER_AUTH_SECRET`             | 64-char hex      | Signs JWTs and encrypts JWKS private keys |
| `SESSION_SECRET`                 | 64-char hex      | Encrypts Mastra Studio session cookies    |
| `METERING_SERVICE_CLIENT_ID`     | 32-char hex      | Identifies Mastra when calling Metering   |
| `METERING_SERVICE_CLIENT_SECRET` | 64-char hex      | Authenticates Mastra-to-Metering calls    |
| `METERING_MASTER_API_KEY`        | `mtr_admin_sk_*` | Bootstrap admin access to Metering        |

These are generated once during the first deploy and stored in `scripts/env/.env.dev-secrets`. If you lose them, you can regenerate — but changing `BETTER_AUTH_SECRET` invalidates all existing sessions and JWKS keys (you'll need to clear the `jwks` table in the auth database).

User Authentication (JWT Flow) [#user-authentication-jwt-flow]

All user-facing requests are authenticated via JWTs issued by the Auth service.

How it works [#how-it-works]

1. **User signs in** via the Web app (Google OAuth, GitHub, email/password, magic link)
2. **Auth service** creates a session and issues a JWT signed with an Ed25519 key
3. **Session cookie** (`__Secure-better-auth.session_token`) is set on `.yourdomain.com` with `SameSite=None`, `Secure`, `HttpOnly`
4. **Web worker** receives the cookie on every request, forwards it to Auth to get a JWT
5. **Other services** (API, Mastra, Metering) verify the JWT against Auth's JWKS endpoint

JWT verification [#jwt-verification]

Every service that needs to verify user identity does the same thing:

```
Service receives: Authorization: Bearer <JWT>
    │
    ▼
Fetch JWKS from: {AUTH_URL}/api/auth/jwks
    │
    ▼
Verify JWT signature using Ed25519 public key
    │
    ▼
Extract claims: sub (userId), organizationId, role
```

Services use the `jose` library's `createRemoteJWKSet` which caches the JWKS keys automatically.

JWT claims [#jwt-claims]

| Claim            | Description                                                 |
| ---------------- | ----------------------------------------------------------- |
| `sub`            | User ID                                                     |
| `email`          | User email                                                  |
| `organizationId` | Active organization ID                                      |
| `role`           | Organization role (`owner`, `admin`, `member`, `developer`) |
| `iss`            | Auth service URL                                            |
| `exp`            | Expiration timestamp (15 minutes)                           |

Cookie configuration [#cookie-configuration]

The auth service sets cross-subdomain cookies so all services under your domain can read the session:

| Attribute  | Value             | Reason                             |
| ---------- | ----------------- | ---------------------------------- |
| `Domain`   | `.yourdomain.com` | Shared across all subdomains       |
| `SameSite` | `None`            | Cross-origin requests (web ↔ auth) |
| `Secure`   | `true`            | HTTPS only                         |
| `HttpOnly` | `true`            | Not accessible to JavaScript       |

Service-to-Service Authentication [#service-to-service-authentication]

Mastra → Metering (Shared Secret) [#mastra--metering-shared-secret]

Mastra calls Metering for two operations:

* **Event ingestion** (`POST /ingest`) — reports token usage and tool executions
* **Balance check** (`POST /balance/check`) — pre-flight credit verification before running the agent

These are authenticated with a shared secret pair:

```
Mastra Worker                              Metering Worker
    │                                          │
    │  POST /ingest                            │
    │  CF-Access-Client-Id: <shared-id>        │
    │  CF-Access-Client-Secret: <shared-key>   │
    │─────────────────────────────────────►    │
    │                                          │  Validates:
    │                                          │  - header ID matches CF_ACCESS_CLIENT_ID env
    │                                          │  - header secret matches CF_ACCESS_CLIENT_SECRET env
    │                                          │  - rejects 401 if mismatch
```

**How the secrets are configured:**

| Worker   | Env Variable                     | Contains                            |
| -------- | -------------------------------- | ----------------------------------- |
| Mastra   | `METERING_SERVICE_CLIENT_ID`     | The shared client ID                |
| Mastra   | `METERING_SERVICE_CLIENT_SECRET` | The shared secret                   |
| Metering | `CF_ACCESS_CLIENT_ID`            | Same client ID (validates incoming) |
| Metering | `CF_ACCESS_CLIENT_SECRET`        | Same secret (validates incoming)    |

The deploy wizard generates these automatically and pushes them to both workers. Conductor's `deploy.sh` reads them from `.env.dev-secrets` and injects them during deployment.

**Development mode:** If `CF_ACCESS_CLIENT_ID` is not set on the Metering worker, it runs in open mode (allows all requests). This makes local development easier but should never be used in production.

API → Metering [#api--metering]

The API worker calls Metering with the user's JWT (forwarded from the original request). Metering verifies it against Auth's JWKS endpoint, same as any other service.

Web → Mastra (Service Binding) [#web--mastra-service-binding]

On Cloudflare Workers, the Web worker communicates with Mastra via a **Service Binding** — a direct in-memory connection that bypasses the public internet. No authentication headers are needed for the binding itself. The Web worker forwards the user's JWT to Mastra, which verifies it independently.

Metering Admin Access [#metering-admin-access]

Administrative endpoints on the Metering service (rate cards, contracts, credit grants) require an API key:

| Endpoint              | Auth Required                  | Purpose               |
| --------------------- | ------------------------------ | --------------------- |
| `POST /ingest`        | Shared secret (service)        | Event ingestion       |
| `GET /balance`        | JWT or shared secret           | Balance queries       |
| `POST /balance/check` | JWT or shared secret           | Pre-flight checks     |
| `GET /usage/*`        | JWT or shared secret           | Usage reports         |
| `CRUD /rate-cards`    | Admin API key                  | Pricing configuration |
| `CRUD /contracts`     | Admin API key or shared secret | Contract management   |
| `POST /credits/grant` | Admin API key or shared secret | Manual credit grants  |

API key format [#api-key-format]

Metering API keys follow the format `mtr_admin_sk_<random>` or `mtr_sk_<random>`. They are hashed with SHA-256 before storage.

The `METERING_MASTER_API_KEY` (auto-generated by the deploy wizard) is a bootstrap key that grants full admin access. Use it to create scoped API keys for specific use cases.

RBAC (Role-Based Access Control) [#rbac-role-based-access-control]

The Mastra service enforces role-based permissions:

| Role        | API Access          | Studio Access |
| ----------- | ------------------- | ------------- |
| `owner`     | Full (read + write) | No            |
| `developer` | Full (read + write) | Yes           |
| `admin`     | Read-only           | No            |
| `member`    | None                | No            |

Studio access is restricted to the `developer` role. This keeps the agent development environment separate from regular org management. Org owners manage users and billing; developers build and test agents.

Roles are assigned per-organization in the Auth service. A user can be an `owner` in one org and a `member` in another.

OAuth Social Login [#oauth-social-login]

Google and GitHub OAuth are configured with a centralized callback proxy:

```
Browser → Google → oauth.yourdomain.com/callback/google → auth-worker/api/auth/callback/google
```

The OAuth proxy (`oauth.yourdomain.com`) enables multiple workspace deployments (Conductor) to share a single Google/GitHub OAuth app. Each workspace sets an `oauth_ws` cookie before redirecting to the OAuth provider. The proxy reads this cookie to route the callback to the correct workspace's auth service.

Required for Google OAuth [#required-for-google-oauth]

1. **Google Cloud Console**: Create OAuth 2.0 Client ID
2. **Authorized redirect URI**: `https://oauth.yourdomain.com/callback/google`
3. **Environment variables**: `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` on the Auth worker

Both must be set — if only one is provided, Google sign-in fails silently with a 500 error.

Required for GitHub OAuth [#required-for-github-oauth]

1. **GitHub Developer Settings**: Create OAuth App
2. **Authorization callback URL**: `https://oauth.yourdomain.com/callback/github`
3. **Environment variables**: `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET` on the Auth worker

Security Checklist [#security-checklist]

Before going to production, verify:

* [ ] `BETTER_AUTH_SECRET` is unique and not shared between environments
* [ ] `METERING_SERVICE_CLIENT_ID` and `SECRET` are set on both Mastra and Metering
* [ ] `CF_ACCESS_CLIENT_ID` is set on Metering (not running in open dev mode)
* [ ] `MASTER_API_KEY` is set on Metering for admin access
* [ ] Google/GitHub OAuth redirect URIs point to your OAuth proxy
* [ ] Both `GOOGLE_CLIENT_ID` and `GOOGLE_CLIENT_SECRET` are set (or neither)
* [ ] `AUTH_URL` is set on all services that verify JWTs (API, Mastra, Metering)
* [ ] JWKS endpoint (`{AUTH_URL}/api/auth/jwks`) is reachable from all services
* [ ] All session cookies use `Secure` and `HttpOnly` flags
* [ ] `SameSite=None` is only used with `Secure=true`
