> ## Documentation Index
> Fetch the complete documentation index at: https://chahine.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Tokens

> How to issue and use Meridian auth tokens.

Meridian uses custom ed25519-signed tokens — smaller than JWT, constant-time verification.

## Bootstrap your first admin token

The HTTP token endpoint requires a token with `admin: true`. To generate one locally from your signing key (no server needed):

```bash theme={null}
cargo run --example gen_demo_token -- <MERIDIAN_SIGNING_KEY> 1 --admin
```

Store the output as `$ADMIN_TOKEN`. You can then use it to issue scoped client tokens via the HTTP API.

> [!TIP]
>
> For production, generate client tokens server-side in your own backend using the same signing key via the HTTP API below. Use `gen_demo_token` only for local bootstrapping.

## Token format

```
base64url(msgpack(claims)) + "." + base64url(ed25519_signature)
```

Claims:

| Field | Type | Description |
| - | - | - |
| `namespace` | `string` | The namespace this token grants access to |
| `client_id` | `number` | Unique numeric ID for this client |
| `expires_at` | `number` | Expiry timestamp in milliseconds |
| `permissions` | `object` | V1 glob lists or V2 fine-grained rules (see below) |

## Issue a token via HTTP

```bash theme={null}
curl -X POST http://localhost:3000/v1/namespaces/my-room/tokens \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "client_id": 42, "ttl_ms": 3600000, "permissions": { "read": ["*"], "write": ["*"] } }'
```

Response:

```json theme={null}
{ "token": "eyJ..." }
```

## Permissions — V1 (glob lists)

V1 permissions use glob patterns matched against the CRDT key (`crdt_id`). Issue a V1 token with the `permissions` field:

```bash theme={null}
curl -X POST http://localhost:3000/v1/namespaces/shop/tokens \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": 42,
    "permissions": {
      "read": ["*"],
      "write": ["or:cart-42", "pr:room-*"]
    }
  }'
```

| Pattern | Matches |
| - | - |
| `"*"` | All keys (full access) |
| `"gc:*"` | All GCounter keys |
| `"or:cart-42"` | Only `or:cart-42` |
| `"or:cart-*"` | All cart keys for any user |
| `"lw:title"` | Only the `lw:title` key |

## Permissions — V2 (fine-grained rules)

V2 tokens add per-rule **op masks**, **per-rule TTLs**, and a **rate limit override**. Issue a V2 token with the `rules` field:

```bash theme={null}
curl -X POST http://localhost:3000/v1/namespaces/shop/tokens \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": 42,
    "rules": {
      "r": [{ "p": "*" }],
      "w": [
        { "p": "gc:views", "o": 1 },
        { "p": "or:cart-42" }
      ],
      "rl": 200
    }
  }'
```

### Rule fields

| Field | Type | Description |
| - | - | - |
| `p` | `string` | Glob pattern matched against `crdt_id` |
| `o` | `number` | Op mask bitmask — which operations are allowed. Absent = all ops |
| `e` | `number` | Per-rule expiry timestamp in ms. Absent = no extra expiry |

Rules are evaluated **first-match-wins** in order. The token-level `expires_at` still applies.

### Op mask constants

Each CRDT type uses its own bit positions:

| CRDT | Op | Mask |
| - | - | - |
| GCounter | increment | `0x01` |
| PNCounter | increment | `0x01` |
| PNCounter | decrement | `0x02` |
| ORSet | add | `0x01` |
| ORSet | remove | `0x02` |
| LwwRegister | set | `0x01` |
| Presence | update | `0x01` |
| RGA | insert | `0x01` |
| RGA | delete | `0x02` |
| Tree | add node | `0x01` |
| Tree | move node | `0x02` |
| Tree | update node | `0x04` |
| Tree | delete node | `0x08` |

Combine masks with bitwise OR: `0x01 | 0x02 = 0x03` allows both operations.

### Example: read-only observer + increment-only counter

```bash theme={null}
# Can read everything, can only increment gc:views (no decrement), rate-limited to 50 req/s
curl -X POST http://localhost:3000/v1/namespaces/analytics/tokens \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": 99,
    "rules": {
      "r": [{ "p": "*" }],
      "w": [{ "p": "gc:views", "o": 1 }],
      "rl": 50
    }
  }'
```

### Example: time-limited write rule

```bash theme={null}
# Write access to or:promo expires at a specific time, read access is permanent
curl -X POST http://localhost:3000/v1/namespaces/shop/tokens \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": 7,
    "ttl_ms": 86400000,
    "rules": {
      "r": [{ "p": "*" }],
      "w": [{ "p": "or:promo", "e": 1740000000000 }]
    }
  }'
```

### `{clientId}` template — per-agent key scoping

Pattern values in V2 rules may contain the literal placeholder `{clientId}`. The server expands it to the token's `client_id` before matching. This lets you issue a single token template that automatically scopes each agent to its own keys:

```bash theme={null}
# Token issued to agent 42 — write access is restricted to "or:cart-42"
curl -X POST http://localhost:3000/v1/namespaces/shop/tokens \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": 42,
    "rules": {
      "r": [{ "p": "*" }],
      "w": [{ "p": "or:cart-{clientId}" }]
    }
  }'
```

Agent 42 can write `or:cart-42` but not `or:cart-99`. Agent 99, issued with the same rule template, can only write `or:cart-99`.

This is especially useful for multi-agent systems where each agent must be isolated to its own slice of the namespace:

```json theme={null}
{
  "client_id": 7,
  "rules": {
    "r": [{ "p": "pr:agents-{clientId}" }],
    "w": [{ "p": "pr:agents-{clientId}" }, { "p": "gc:work-{clientId}" }]
  }
}
```

### Rate limit override

The `rl` field (V2 only) sets a per-token rate limit in requests per second. This overrides the server default (100 req/s) for this specific token — useful to give trusted services higher throughput or restrict untrusted clients.

```json theme={null}
{ "rules": { "r": [{ "p": "*" }], "w": [{ "p": "*" }], "rl": 500 } }
```

## Backward compatibility

V1 and V2 tokens coexist without migration. Old V1 tokens continue to work unchanged. The server detects the version automatically from the token payload.

> [!WARNING]
>
> A token with `write: ["*"]` (V1) or `w: [{ "p": "*" }]` (V2) can write to **any** key in the namespace. In multi-user namespaces, always scope write permissions to the keys the client actually owns.

## Inspect a token

`GET /v1/namespaces/:ns/tokens/me` decodes the caller's token and returns its claims as JSON. Useful for debugging — no msgpack tooling needed.

```bash theme={null}
curl http://localhost:3000/v1/namespaces/shop/tokens/me \
  -H "Authorization: Bearer $TOKEN"
```

```json theme={null}
{
  "namespace": "shop",
  "client_id": 42,
  "expires_at": 1743000000000,
  "permissions": {
    "v": 2,
    "r": [{ "p": "*" }],
    "w": [{ "p": "or:cart-{clientId}" }]
  }
}
```

V1 tokens return `permissions` with `read`, `write`, and `admin` fields instead.

## Verify a token (SDK)

`MeridianClient.create()` automatically parses and validates the token before connecting:

```ts theme={null}
import { Effect } from "effect";
import { MeridianClient, TokenExpiredError, TokenParseError } from "meridian-sdk";

await Effect.runPromise(
  MeridianClient.create(config).pipe(
    Effect.catchTag("TokenExpiredError", () => Effect.die("renew your token")),
    Effect.catchTag("TokenParseError", () => Effect.die("invalid token")),
  )
);
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.