Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 34 additions & 16 deletions vendor/docs/docs/content/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,40 @@ Rivet separates the process that decides *where* work runs from the process that
*runs* it. Everything else in these docs is a detail of one of those two sides,
or of the boundary between them.

```
┌──────────────────────────────────┐
your client ────▶│ Gateway │
│ routes to a specific workload │
├──────────────────────────────────┤
│ Control plane │
│ schedules, versions, persists │
└───────────────┬──────────────────┘
│ runner protocol
┌───────────────┴──────────────────┐
│ Runner (your process, your code)│
│ ┌────────┐ ┌────────┐ │
│ │ actor │ │ actor │ ... │
│ └────────┘ └────────┘ │
└──────────────────────────────────┘
```
<svg viewBox="0 0 720 360" role="img" aria-label="Your client calls the gateway, which routes to a specific workload. The gateway sits above the control plane, which schedules, versions, and persists. The control plane talks to a runner over the runner protocol. The runner is your own process running your code, and it hosts many actors." style="width:100%;max-width:720px;height:auto;display:block;margin:2.5rem auto;font-family:system-ui,sans-serif">
<defs>
<marker id="arch-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="rgb(var(--site-ink, 27 25 22))"/></marker>
<marker id="arch-arrow-hl" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="rgb(var(--runtime-highlight, 183 75 35))"/></marker>
</defs>

<text x="20" y="72" font-size="13" font-weight="600" fill="rgb(var(--site-ink, 27 25 22))">Your client</text>
<line x1="112" y1="67" x2="228" y2="67" stroke="rgb(var(--site-ink, 27 25 22))" stroke-width="1.4" marker-end="url(#arch-arrow)"/>

<rect x="230" y="28" width="470" height="120" rx="8" fill="rgb(var(--site-paper-mid, 227 227 229))" stroke="rgb(var(--site-ink, 27 25 22))" stroke-width="1.4"/>
<text x="252" y="58" font-size="14" font-weight="600" fill="rgb(var(--site-ink, 27 25 22))">Gateway</text>
<text x="252" y="78" font-size="12" fill="rgb(var(--site-ink-soft, 86 82 74))">routes to a specific workload</text>
<line x1="230" y1="92" x2="700" y2="92" stroke="rgb(var(--site-ink, 27 25 22))" stroke-width="1.4"/>
<text x="252" y="118" font-size="14" font-weight="600" fill="rgb(var(--site-ink, 27 25 22))">Control plane</text>
<text x="252" y="138" font-size="12" fill="rgb(var(--site-ink-soft, 86 82 74))">schedules, versions, persists</text>

<line x1="465" y1="149" x2="465" y2="209" stroke="rgb(var(--runtime-highlight, 183 75 35))" stroke-width="1.4" marker-start="url(#arch-arrow-hl)" marker-end="url(#arch-arrow-hl)"/>
<text x="479" y="184" font-size="12" fill="rgb(var(--site-ink-soft, 86 82 74))">runner protocol</text>

<rect x="230" y="210" width="470" height="122" rx="8" fill="rgb(var(--runtime-highlight, 183 75 35) / 0.14)" stroke="rgb(var(--site-ink, 27 25 22))" stroke-width="1.4"/>
<text x="252" y="240" font-size="14" font-weight="600" fill="rgb(var(--site-ink, 27 25 22))">Runner</text>
<text x="252" y="260" font-size="12" fill="rgb(var(--site-ink-soft, 86 82 74))">your process, your code</text>
<g fill="rgb(var(--site-paper, 239 239 239))" stroke="rgb(var(--site-ink, 27 25 22))" stroke-width="1.3">
<rect x="252" y="276" width="96" height="38" rx="6"/>
<rect x="364" y="276" width="96" height="38" rx="6"/>
<rect x="476" y="276" width="96" height="38" rx="6"/>
</g>
<g text-anchor="middle" font-size="12" fill="rgb(var(--site-ink, 27 25 22))">
<text x="300" y="300">actor</text>
<text x="412" y="300">actor</text>
<text x="524" y="300">actor</text>
</g>
<text x="600" y="300" font-size="13" fill="rgb(var(--site-ink-faint, 138 132 120))">…</text>
</svg>

## Control plane

Expand Down
22 changes: 22 additions & 0 deletions vendor/docs/docs/content/authentication.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
title: "Authentication"
description: "How clients authenticate to Rivet: secrets for your backend, short-lived JWTs for public clients, and ACL tokens for long-lived scoped access."
skill: true
---

Every request to Rivet carries a credential, and the control plane checks what that credential may reach before routing it to an actor. This page covers the kinds available. To authorize a caller after it is connected, see [Permissions](/actors/docs/permissions).

Authentication is required by default. On a self-hosted control plane, `auth.insecure_allow_unauthenticated` restores the old unauthenticated behavior for a trusted network; do not enable it on anything publicly reachable.

## Choosing a Credential

| | Lifetime | Scope | Revoke | Available in |
| --- | --- | --- | --- | --- |
| [Admin token](/docs/deploy/self-host/control-plane/configuration#admin-token) | Static | Everything, every namespace | Rotate the secret and restart | Self-hosted |
| [Secret key](/docs/deploy/cloud#tokens) | Static | One namespace | Delete the key | Rivet Cloud |
| [JWT](/docs/jwt) | 1 hour by default, 24 hour maximum | One namespace, one grant set | Wait for expiry | Self-hosted, Rivet Cloud |
| [ACL token](/docs/deploy/self-host/control-plane/rbac) | Long-lived | Roles built from reusable policies | Delete the token | Enterprise |

Your backend can hold a secret, so it uses one directly. A browser or mobile app cannot, because anyone can read it out of the bundle, so mint a short-lived [JWT](/docs/jwt) per user on your backend and hand that out instead. Reach for [ACL](/docs/deploy/self-host/control-plane/rbac) when a credential has to outlive a session and stay revocable.

On Rivet Cloud the secret, publishable, and connection keys are created from the dashboard or the CLI. See [Tokens](/docs/deploy/cloud#tokens).
155 changes: 155 additions & 0 deletions vendor/docs/docs/content/jwt.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
---
title: "JWTs"
seoTitle: "Scoped JWT Authentication for Rivet"
description: "Mint short-lived, grant-scoped JWTs on your backend so a browser can reach one Rivet Actor without holding a secret, and renew them automatically from RivetKit."
skill: true
---

A Rivet JWT is a short-lived credential you mint on your backend and hand to an untrusted client. It carries grants saying exactly what the holder may reach, and the control plane verifies it before routing any request.

Use one whenever a browser or mobile app talks to Rivet directly. For which credential to use where, see [Authentication](/docs/authentication).

<svg viewBox="0 0 800 470" role="img" aria-label="Sequence: the browser logs in to your backend, your backend mints a scoped JWT from the Rivet control plane, the browser connects to Rivet with that JWT, and the control plane routes it to the user's Actor." style="width:100%;max-width:800px;height:auto;display:block;margin:2.5rem auto;font-family:system-ui,sans-serif">
<defs>
<marker id="auth-seq-call" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="rgb(var(--site-ink, 27 25 22))"/></marker>
<marker id="auth-seq-return" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 L10 5 L0 10 z" fill="rgb(var(--runtime-highlight, 183 75 35))"/></marker>
</defs>
<g stroke="rgb(var(--site-ink, 27 25 22))" stroke-width="1.4" fill="rgb(var(--site-paper-mid, 227 227 229))">
<rect x="24" y="20" width="142" height="48" rx="7"/>
<rect x="226" y="20" width="148" height="48" rx="7"/>
<rect x="436" y="20" width="168" height="48" rx="7" fill="rgb(var(--runtime-highlight, 183 75 35) / 0.14)"/>
<rect x="652" y="20" width="126" height="48" rx="7"/>
</g>
<g text-anchor="middle" fill="rgb(var(--site-ink, 27 25 22))" font-size="13" font-weight="600">
<text x="95" y="49">Browser</text>
<text x="300" y="49">Your backend</text>
<text x="520" y="49">Rivet control plane</text>
<text x="715" y="49">User Actor</text>
</g>
<g stroke="rgb(var(--site-ink-faint, 138 132 120))" stroke-width="1.3" stroke-dasharray="5 5">
<line x1="95" y1="68" x2="95" y2="440"/>
<line x1="300" y1="68" x2="300" y2="440"/>
<line x1="520" y1="68" x2="520" y2="440"/>
<line x1="715" y1="68" x2="715" y2="440"/>
</g>
<g font-size="12" fill="rgb(var(--site-ink-soft, 86 82 74))" text-anchor="middle">
<text x="197" y="105">POST /login</text>
<text x="410" y="151">issueToken()</text>
<text x="410" y="167" font-size="10" fill="rgb(var(--site-ink-faint, 138 132 120))">grant: actor_gateway read on user:alice</text>
<text x="410" y="211">JWT, expires in 1h</text>
<text x="197" y="255">session + JWT</text>
<text x="410" y="305">connect with JWT</text>
<text x="617" y="349">verify + check grants</text>
<text x="410" y="401">actions &amp; events</text>
</g>
<g stroke="rgb(var(--site-ink, 27 25 22))" stroke-width="1.4">
<line x1="95" y1="116" x2="298" y2="116" marker-end="url(#auth-seq-call)"/>
<line x1="300" y1="178" x2="518" y2="178" marker-end="url(#auth-seq-call)"/>
<line x1="95" y1="316" x2="518" y2="316" marker-end="url(#auth-seq-call)"/>
<line x1="520" y1="360" x2="713" y2="360" marker-end="url(#auth-seq-call)"/>
</g>
<g stroke="rgb(var(--runtime-highlight, 183 75 35))" stroke-width="1.4" stroke-dasharray="5 4">
<line x1="520" y1="222" x2="302" y2="222" marker-end="url(#auth-seq-return)"/>
<line x1="300" y1="266" x2="97" y2="266" marker-end="url(#auth-seq-return)"/>
<line x1="713" y1="412" x2="97" y2="412" marker-start="url(#auth-seq-return)" marker-end="url(#auth-seq-return)"/>
</g>
</svg>

## Quickstart

<Steps>

<Step title="Issue a token on your backend">

Authenticate the user however you already do, then call `issueToken` on the actor they own. It defaults to gateway read access scoped to that one actor, so there is nothing to spell out. Return only the token to the client.

<CodeGroup workspace>
<CodeSnippet file="examples/docs/general-jwt/quickstart/server.ts" title="server.ts" />
<CodeSnippet file="examples/docs/general-jwt/quickstart/registry.ts" title="registry.ts" />
</CodeGroup>

</Step>

<Step title="Use the token in RivetKit">

Pass `getToken` to `createClient`. RivetKit calls it whenever it needs a credential, caches the result, and calls it again when the token expires.

<CodeSnippet file="examples/docs/general-jwt/quickstart/client.ts" title="client.ts" />

</Step>

<Step title="Verify the scope">

Point the same token at a different actor. The control plane rejects it before your code runs:

```sh
curl -i -H "Authorization: Bearer $TOKEN" \
"$RIVET_ENDPOINT/gateway/$SOME_OTHER_ACTOR_ID/"
```

```
HTTP/1.1 403 Forbidden
x-rivet-error: auth.insufficient_permissions
```

</Step>

</Steps>

Issuing needs a credential that already holds the grants being handed out, so it only works from a server-side client. `expiresIn` is in seconds and defaults to the control plane's lifetime, capped at 24 hours. `subject` is your user identifier, opaque to Rivet, and shows up in token inspection. `issuedAt` and `expiresAt` come back as Unix milliseconds.

## Grants

`actor.issueToken` covers the common case. Pass `permissions` to widen what the holder may do to that actor; every grant stays scoped to its resolved ID. For namespace-wide operations such as creating actors, use `client.auth.issueToken` with an explicit grant list, which adds nothing on its own.

| Resource | Gates |
| --- | --- |
| `actor_gateway` | Connecting to an actor by ID: actions, events, and raw HTTP or WebSocket handlers. |
| `actor` | Resolving or creating actors by key, and the actors API. |
| `actor_kv` | Reading an actor's raw KV, which the [inspector](/actors/docs/debugging) needs. |
| `namespace`, `runner`, `runner_config`, `datacenter` | Control-plane management APIs. |

A grant is a resource, a `target` of `"any"` or `{ id }`, and operations drawn from `create`, `read`, `update`, `delete`, and `list`. A grant set is capped at 32 grants, 5 operations each, and 2048 bytes encoded.

<CodeSnippet file="examples/docs/general-jwt/grants.ts" />

### Resolving Versus Connecting

These are different grants, and confusing them is the most common mistake. `getOrCreate(key)` and `get(key)` route through the query path, which checks `actor` with `create` and `read`. Connecting to an actor you already have the ID for checks `actor_gateway` with `read` on that ID.

That distinction is what makes tight scoping possible. A token holding only `actor_gateway` `read` on one ID can reach that actor and nothing else. Resolve the ID on your backend, where you still hold the admin token, then grant against it.

## Expiration and Renewal

Expiry is enforced mid-flight, not just at connection time. When a token's `exp` passes, the control plane cancels in-flight requests and closes open WebSockets with code `1008` and `auth.token_expired`.

With `getToken` wired up this is invisible: RivetKit catches the close, calls `getToken({ forceRefresh: true })`, and reconnects. With a static `token` the connection dies and does not come back, so **always use `getToken` for connections that outlive the token**.

There is no revocation, so a leaked token is valid until it expires. Keep durations short and let renewal do the work.

## Inspecting a Token

`GET /auth/tokens/inspect`, called with the token itself, returns the namespace, subject, grants, and timestamps it actually carries. Use it when a request is rejected and you want to see what the holder was granted.

## Errors

| Error | Status | Cause |
| --- | --- | --- |
| `auth.invalid_token` | 401 | Malformed, unsigned, or signed by a key this cluster does not know. |
| `auth.token_expired` | 401 | Past `exp`. Refresh and retry. |
| `auth.insufficient_permissions` | 403 | Valid token, but no grant covers this resource, target, and operation. |
| `auth.issuance_disabled` | 400 | `auth.jwt.issuance_enabled` is off, so issuance will not mint. |

Over HTTP the code arrives in the JSON body and, through the gateway, in the `x-rivet-error` response header. On a WebSocket it arrives in the close reason.

## Configuration

Issuance and verification are on by default when a self-hosted control plane has an [admin token](/docs/deploy/self-host/control-plane/configuration#admin-token). Tokens are signed with `EdDSA`, the issuer is derived from the leader datacenter's public URL, and signing keys rotate every seven days with no operator action. Tune `auth.jwt.default_duration`, `auth.jwt.max_duration`, and `auth.jwt.audience` if the defaults do not suit you.

## Not Your Application's JWTs

This page is about credentials Rivet issues and verifies. Tokens from Clerk, Auth0, Supabase, or your own issuer are never seen by Rivet. Pass them as connection parameters and verify them inside the actor. See [Permissions](/actors/docs/permissions).

The two compose: a Rivet JWT decides which actor a client may reach, and your own token decides who the user is once they are there.

[`examples/jwt-counter`](https://github.com/rivet-dev/rivet/tree/main/examples/jwt-counter) is a runnable backend, client, and smoke test covering issuance, scoped access, and renewal.
56 changes: 56 additions & 0 deletions vendor/docs/docs/content/quickstart.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
title: "Quickstart"
description: "Pick the Actor type you are building and follow its quickstart. Every one runs on the same control plane."
---

Every Actor type on this site is built on the same primitive, so the control plane, deployment, and authentication work the same way whichever you start with. Pick the one closest to what you are building.

## Actors

The primitive itself. Start here if you are building your own durable processes, and pick the quickstart matching your stack.

<CardGroup>
<Card title="Node.js & Bun" href="/actors/docs/quickstart/backend">
A standalone backend with the RivetKit SDK.
</Card>
<Card title="React" href="/actors/docs/quickstart/react">
A frontend talking to Actors over the browser client.
</Card>
<Card title="Next.js" href="/actors/docs/quickstart/next-js">
Route handlers and server components.
</Card>
<Card title="Rust" href="/actors/docs/quickstart/rust">
The Rust SDK.
</Card>
<Card title="Effect.ts" href="/actors/docs/quickstart/effect">
Actors inside an Effect runtime.
</Card>
<Card title="Cloudflare Workers" href="/actors/docs/quickstart/cloudflare">
Serverless workers on Cloudflare.
</Card>
<Card title="Supabase Functions" href="/actors/docs/quickstart/supabase">
Serverless workers on Supabase.
</Card>
</CardGroup>

## Higher-level Actor types

Each of these is an Actor with a job already built into it, so you write less to get started.

<CardGroup>
<Card title="Workflows" href="/workflows/docs/quickstart">
Multi-step operations that replay instead of starting over.
</Card>
<Card title="Sandboxes" href="/agentos/docs/quickstart">
A filesystem, shell, and network for code you did not write.
</Card>
<Card title="Dynamic Apps" href="/dynamic-apps/docs/quickstart">
A backend per user, deployed the moment it is generated.
</Card>
</CardGroup>

## Next steps

- [Architecture](/docs/architecture) for the vocabulary the rest of these docs use.
- [Authentication](/docs/authentication) before you expose anything to a browser.
- [Deploy](/docs/deploy) to run the control plane and workers yourself, or on Rivet Cloud.
Loading
Loading