# Authentication

**You sign a JWT saying who the visitor is. Vatio verifies it and passes it to
your tools.** That is the whole of authentication — there is one mechanism, on
every channel.

Vatio holds only your **public** key, so it can check a token but never mint
one. Your backend stays the only thing that can say who someone is.

::: tip Users sign in with Supabase?
Skip the keypair and the signing: `vatio auth --supabase` names your project,
and the widget, WhatsApp and Instagram use the Supabase session as it is. See
[Supabase](https://vatio.ai/docs/integrations/supabase).
:::

## Set it up

Generate the keypair:

```bash
vatio auth --new-key
```

That writes two files, and they go to different places.

**`identity.pub` stays in the workspace.** It is a public key, so it is
committed like any other file and `vatio.yml` names it:

```yaml
# vatio.yml
auth:
  public_key: identity.pub
```

**`identity.pem` goes into your own backend**, as a secret — an environment
variable, or whatever credential store you already use. Your backend is what
signs, so it is the only thing that ever needs it. Load it, then delete the
file from the workspace directory; it has no job there.

```bash
# environment variable
VATIO_IDENTITY_PRIVATE_KEY="$(cat identity.pem)"

# or Rails credentials
bin/rails credentials:edit     # vatio: { identity_private_key: "..." }
```

**Never `vatio secrets set` the private key.** That store is read by Vatio, to
let your tools call your API. A private key in there would let Vatio *mint*
tokens for your users instead of only checking them, which is the one thing
this design exists to prevent. Vatio holds the public half and nothing else.

Mark every tool that needs a signed-in user:

```yaml
# tools/my_bookings.yml
access: private
```

Tools without `access: private` are public and run for anyone.

## The token

Sign it with `identity.pem` using **RS256**:

| Claim | Required | Value |
|---|---|---|
| `sub` | yes | Your user id. Becomes `$auth.subject`. |
| `aud` | yes | Your workspace slug, exactly. |
| `exp` | yes | Unix seconds. Keep it short — Vatio re-checks it on every tool call. |
| `name` | no | Fills the contact's name. |
| `email` | no | Fills the contact's email. |
| `phone_number` | no | Fills the contact's phone. |
| `metadata` | no | What the inbox shows about the person: a `profile` link and `badges`. See [Enrich the inbox](#enrich-the-inbox). |

Any other claim you add arrives as `$auth.claims.<name>`.

```ruby
# Ruby
JWT.encode(
  { sub: user.id.to_s, aud: "acme", exp: 1.hour.from_now.to_i,
    name: user.name, email: user.email },
  OpenSSL::PKey::RSA.new(ENV["VATIO_IDENTITY_PRIVATE_KEY"]), "RS256"
)
```

```js
// Node (jsonwebtoken)
jwt.sign(
  { sub: String(user.id), name: user.name, email: user.email },
  process.env.VATIO_IDENTITY_PRIVATE_KEY,
  { algorithm: "RS256", audience: "acme", expiresIn: "1h" }
);
```

```python
# Python (PyJWT)
jwt.encode(
    {"sub": str(user.id), "aud": "acme",
     "exp": int(time.time()) + 3600, "name": user.name},
    os.environ["VATIO_IDENTITY_PRIVATE_KEY"], algorithm="RS256",
)
```

`aud` is required because a signature proves *who* signed, not *what for*.
Without it, any other JWT you sign with the same key — a password reset, a
download link — would be accepted here as an identity.

## Enrich the inbox

Put a `metadata` object in the token and the inbox shows it in the
conversation header, so whoever answers knows who they are talking to without
opening your admin:

```json
{
  "sub": "42",
  "aud": "acme",
  "exp": 1790000000,
  "name": "Claudio Andrés",
  "metadata": {
    "profile": "https://acme.com/admin/users/42",
    "badges": {
      "plan": { "value": "Premium", "tone": "green" },
      "payment": { "value": "Overdue", "tone": "red" },
      "country": "CL"
    }
  }
}
```

| Key | Type | Shown as |
|---|---|---|
| `profile` | string, `http://` or `https://` URL | A **Profile ↗** link that opens in a new tab. Anything else shows a quiet "Invalid profile link" warning instead, with the value you sent in its tooltip. |
| `badges` | object of label → value | One chip per entry, `plan: Premium`, in the order you send them. A value is a string, number or boolean, or `{ "value": ..., "tone": ... }` to colour the chip. Up to 10, each label and value cut at 60 characters. |

`tone` is one of `green`, `amber`, `red` or `gray`. Leave it out, or send
anything else, and the chip takes your inbox's accent colour.

Both are optional, and any other key under `metadata` is ignored by the inbox.
The inbox reads it from the latest token for that conversation, so a
refreshed token updates the header. Tools see the whole object as
`$auth.claims.metadata`.

```js
// Node (jsonwebtoken)
jwt.sign(
  {
    sub: String(user.id),
    name: user.name,
    email: user.email,
    metadata: {
      profile: `https://acme.com/admin/users/${user.id}`,
      badges: {
        plan: { value: user.plan, tone: user.paying ? "green" : "gray" },
        country: user.country,
      },
    },
  },
  process.env.VATIO_IDENTITY_PRIVATE_KEY,
  { algorithm: "RS256", audience: "acme", expiresIn: "1h" }
);
```

Next: how the token reaches Vatio on
[each channel](https://vatio.ai/docs/authentication/sessions), and what a
[private tool](https://vatio.ai/docs/authentication/tools) sees once it does.
