diff --git a/docs/apikeys.mdx b/docs/apikeys.mdx
index 38626760ebe..c3c2808f4b8 100644
--- a/docs/apikeys.mdx
+++ b/docs/apikeys.mdx
@@ -1,54 +1,165 @@
---
title: "API keys"
-description: "How to authenticate with Trigger.dev so you can trigger tasks."
+description: "Authenticate backend requests with environment-specific API keys."
---
-### Authentication and your secret keys
+**API keys authenticate backend requests to a specific Trigger.dev project and environment.** Each environment can have multiple keys, with optional scopes and restrictions attached.
-When you [trigger a task](/triggering) from your backend code, you need to set the `TRIGGER_SECRET_KEY` environment variable.
+
+ API keys grant access to your Trigger.dev environment. Store them in a secret manager or backend
+ environment variable, never commit them to source control, and never expose them in frontend code.
+
-Each environment has its own secret key. You can find the value on the API keys page in the Trigger.dev dashboard:
+## Find your API keys
-
+Open your project in the dashboard, select an environment, and open the **API keys** page.
+
+API keys belong to one environment. A Development key cannot access Production, and a Production key cannot access Staging.
- For preview branches, you need to also set the `TRIGGER_PREVIEW_BRANCH` environment variable as
- well. You can find the value on the API keys page when you're on the preview branch.
+ Every team member has their own Development environment and keys. Copy the Development key from
+ your own API keys page so local requests run against your machine.
-### Automatically Configuring the SDK
+## Configure the SDK
-To automatically configure the SDK with your secret key, you can set the `TRIGGER_SECRET_KEY` environment variable. The SDK will automatically use this value when calling API methods (like `trigger`).
+Set `TRIGGER_SECRET_KEY` in your backend environment. The SDK reads it automatically for operations such as triggering tasks and retrieving runs.
```bash .env
-TRIGGER_SECRET_KEY="tr_dev_…"
-TRIGGER_PREVIEW_BRANCH="my-branch" # Only needed for preview branches
+TRIGGER_SECRET_KEY="tr_prod_sk_…"
```
-You can do the same if you are self-hosting and need to change the default URL by using `TRIGGER_API_URL`.
+To configure the SDK in code, pass the key to `configure`:
+
+```ts Your backend code
+import { configure, tasks } from "@trigger.dev/sdk";
+import type { sendEmail } from "./trigger/send-email";
+
+configure({
+ secretKey: process.env.TRIGGER_SECRET_KEY,
+});
+
+await tasks.trigger("send-email", {
+ to: "user@example.com",
+});
+```
+
+If you self-host Trigger.dev, set `TRIGGER_API_URL` or pass `baseURL` to `configure`:
```bash .env
+TRIGGER_SECRET_KEY="tr_prod_…"
TRIGGER_API_URL="https://trigger.example.com"
-TRIGGER_PREVIEW_BRANCH="my-branch" # Only needed for preview branches
```
-The default URL is `https://api.trigger.dev`.
+The default API URL is `https://api.trigger.dev`.
-### Manually Configuring the SDK
+## Create a key
-If you prefer to manually configure the SDK, you can call the `configure` method:
+Create a separate key for each service or integration that accesses Trigger.dev.
-```ts
-import { configure } from "@trigger.dev/sdk";
-import { myTask } from "./trigger/myTasks";
+
+ Creating and revoking keys requires permission to manage API keys for the selected environment.
+ The dashboard disables these actions when your role does not have permission.
+
-configure({
- secretKey: "tr_dev_1234", // WARNING: Never actually hardcode your secret key like this
- previewBranch: "my-branch", // Only needed for preview branches
- baseURL: "https://mytrigger.example.com", // Optional
-});
+
+
+ Select the project and environment the integration needs to access, then open **API keys**.
+
+
+ Click **New API key**, enter a descriptive name, and optionally set an expiration date. Names can
+ contain up to 64 characters.
+
+
+ Select an access preset. For task-aware presets, choose all tasks or up to 10 task identifiers.
+
+
+ Copy the key into your secret manager or backend environment. Trigger.dev shows the complete
+ value only once.
+
+
+
+## Access presets
+
+Access presets define what a key can do. Some presets require a paid plan. The dashboard shows which presets your organization can use — see [pricing](https://trigger.dev/pricing).
+
+| Preset | Access |
+| --- | --- |
+| **Trigger only** | Trigger runs and batches for all or selected tasks. Trigger responses include scoped public access tokens for the runs and batches they create |
+| **Task operator** | Trigger all or selected tasks and inspect or operate on their runs |
+| **Observer** | Read runs, tasks, batches, logs, traces, and queues |
+| **Operator** | Observe and operate on runs and queues, and trigger tasks |
+| **Deploy only** | Deploy new versions, read deployment status, and read environment variables |
+| **Variables only** | Read and write environment variables in this environment |
+| **No restrictions** | Full access to the environment |
+
+**Trigger only** and **Task operator** can be restricted to selected tasks. Task restrictions use task identifiers, such as `send-email`. A request involving multiple tasks — such as a batch trigger — succeeds only when the key can access every task in the request, so a task-restricted key can batch-trigger only its selected tasks.
+
+## Expire and revoke keys
+
+Set an expiration date when creating a key if the integration only needs temporary access. An expired key stops authenticating automatically.
+
+Revoking a key takes effect immediately and cannot be reversed. Requests using the key fail, and the key can no longer create public access tokens. Create a replacement before revoking a key when you need to rotate it without interrupting the integration.
+
+Removing a team member does not revoke keys they created. Review and revoke their keys separately when their access changes.
-async function triggerTask() {
- await myTask.trigger({ userId: "1234" }); // This will use the secret key and base URL you configured
-}
+## Create public access tokens
+
+Keys can create scoped [Public Access Tokens](/realtime/auth) without receiving the environment's root key. Use `@trigger.dev/sdk` version 4.5.8 or later with keys.
+
+When a key calls `auth.createPublicToken()`:
+
+- The token must request at least one scope.
+- Its scopes cannot exceed the key's access.
+- It expires after 15 minutes by default.
+- Its expiration cannot exceed 30 days.
+- A numeric `expirationTime` is a Unix timestamp in seconds.
+
+Revoking or expiring a key does not revoke tokens it already created. Those tokens remain valid until their own expiration, unless the environment's root key is regenerated first.
+
+## Target Preview and Development branches
+
+Preview and named Development branches use their parent environment's keys. Select the branch by setting `TRIGGER_PREVIEW_BRANCH` alongside the environment key:
+
+```bash .env
+TRIGGER_SECRET_KEY="tr_preview_sk_…"
+TRIGGER_PREVIEW_BRANCH="feature/new-checkout"
```
+
+The SDK sends the branch automatically. When calling the API directly, send the same value in the `x-trigger-branch` header.
+
+## Root keys
+
+
+ Root keys are legacy, and are likely to be deprecated in the future. We recommend against using them.
+
+
+Each environment has a single legacy root key. It can be regenerated, which creates a new value immediately. The previous root key remains valid for 24 hours so you can update services without downtime, then stops authenticating.
+
+Public access tokens signed with the previous root key remain valid until the earlier of their own expiration and the end of the 24-hour grace period.
+
+## Self-hosting
+
+Self-hosted installations support multiple keys with **No restrictions**. The restricted access presets are available in Trigger.dev Cloud.
+
+Keep your instance and SDK current before creating keys. Calling a public-token API with a key on a server that does not support server-minted tokens returns an upgrade error; use the root key until the server is upgraded.
+
+## Security recommendations
+
+- Create one key per service or integration instead of sharing keys.
+- Choose the narrowest access preset and task selection that supports the integration.
+- Store keys in a secret manager and inject them as backend environment variables.
+- Set expiration dates for temporary integrations and deployment credentials.
+- Revoke keys when an integration or team member no longer needs access.
+- Never put an API key in frontend code. Use scoped [Public Access Tokens](/realtime/auth) for client-side access.
+
+## Next steps
+
+
+
+ Trigger tasks from your backend with an environment API key.
+
+
+ Create scoped public tokens for frontend and realtime access.
+
+
diff --git a/docs/realtime/auth.mdx b/docs/realtime/auth.mdx
index 025060c688b..147cb6448e9 100644
--- a/docs/realtime/auth.mdx
+++ b/docs/realtime/auth.mdx
@@ -128,9 +128,11 @@ const publicToken = await auth.createPublicToken({
```
- If `expirationTime` is a string, it will be treated as a time span
-- If `expirationTime` is a number, it will be treated as a Unix timestamp
+- If `expirationTime` is a number, it will be treated as a Unix timestamp in **seconds**
- If `expirationTime` is a `Date`, it will be treated as a date
+The expiration cannot be more than 30 days from now.
+
The format used for a time span is the same as the [jose package](https://github.com/panva/jose), which is a number followed by a unit. Valid units are: "sec", "secs", "second", "seconds", "s", "minute", "minutes", "min", "mins", "m", "hour", "hours", "hr", "hrs", "h", "day", "days", "d", "week", "weeks", "w", "year", "years", "yr", "yrs", and "y". It is not possible to specify months. 365.25 days is used as an alias for a year. If the string is suffixed with "ago", or prefixed with a "-", the resulting time span gets subtracted from the current unix timestamp. A "from now" suffix can also be used for readability when adding to the current unix timestamp.
### Auto-generated tokens