Scoped Tokens
Issue short-lived JWTs bound to a specific namespace or workspace. Designed for SaaS platforms that need to give end-users limited, temporary access without sharing long-lived API keys.
When to use scoped tokens — You're building a SaaS product on Oblien. Your backend holds an admin API key. When an end-user logs in, you issue them a namespace-scoped token that only lets them interact with workspaces in their tenant's namespace. The token expires automatically — no revocation needed.
How it works
┌─────────────────┐ POST /tokens ┌──────────────────┐
│ Your Backend │ ──────────────────────▶ │ Oblien API │
│ (admin API key) │ ◀────────────────────── │ returns JWT │
└────────┬────────┘ └──────────────────┘
│ hand JWT to user
▼
┌─────────────────┐ Bearer <jwt> ┌──────────────────┐
│ End User │ ──────────────────────▶ │ /workspace/* │
│ (browser/app) │ │ (namespace only)│
└─────────────────┘ └──────────────────┘- Your backend authenticates with its admin API key
- Calls
POST /tokensspecifying the scope (namespace or workspace) and TTL - Returns the short-lived JWT to the end-user
- The end-user uses
Authorization: Bearer <jwt>— scoped enforcement kicks in automatically
Issue a token
Requires an admin-scoped API key.
The namespace must exist before you issue a token for it. Create it first via client.namespaces.create() or oblien ns create. The API validates that the namespace (or workspace) exists and belongs to your account — you'll get a 400 if it doesn't.
const { token, expiresAt } = await client.tokens.create({
scope: 'namespace',
namespace: 'tenant-abc',
ttl: 900,
label: 'user-session-42',
});
// Hand the token to your end-userPOST /tokens{
"scope": "namespace",
"namespace": "tenant-abc",
"ttl": 900,
"label": "user-session-42"
}curl -X POST https://api.oblien.com/tokens \
-H "X-Client-ID: $OBLIEN_CLIENT_ID" \
-H "X-Client-Secret: $OBLIEN_CLIENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"scope": "namespace",
"namespace": "tenant-abc",
"ttl": 900
}'oblien tokens create --scope namespace --namespace tenant-abc --ttl 900
# With JSON output
oblien tokens create --scope namespace --namespace tenant-abc --json
# Workspace-scoped
oblien tokens create --scope workspace --workspace-id ws_abc123Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
scope | string | Yes | "namespace" or "workspace" |
namespace | string | When scope=namespace | Namespace slug to bind the token to |
workspaceId | string | When scope=workspace | Workspace ID to bind the token to |
ttl | number | No | Token lifetime in seconds (1–3600, default: 900) |
label | string | No | Human-readable label (included in JWT for debugging) |
Response 201
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresAt": "2026-03-15T12:15:00.000Z",
"scope": "namespace",
"ttl": 900
}Errors
| Status | Code | Cause |
|---|---|---|
| 400 | validation_error | Missing or invalid scope/namespace/workspace |
| 400 | validation_error | Namespace or workspace does not exist |
| 401 | missing_credentials | No API key provided |
| 403 | scope_denied | API key is not admin-scoped |
Using scoped tokens
The end-user passes the JWT as a Bearer token. It works everywhere API keys work — SDK, REST, cURL, and MCP.
// End-user client — initialize with the scoped token
import Oblien from 'oblien';
const userClient = new Oblien({ token });
// Only sees workspaces in the bound namespace
const workspaces = await userClient.workspaces.list();
// Can create workspaces (forced into the bound namespace)
const ws = await userClient.workspaces.create({
image: 'node-20',
config: { cpus: 2, memory_mb: 2048 },
});
// When the token is refreshed, swap it without re-creating the client:
userClient.setToken(newToken);const response = await fetch('https://api.oblien.com/workspace', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
});curl https://api.oblien.com/workspace \
-H "Authorization: Bearer $SCOPED_TOKEN"Scope permissions
Scoped tokens follow the exact same enforcement as scoped API keys:
Namespace scope
| Action | Result |
|---|---|
| List workspaces | Filtered to bound namespace only |
| Create workspace | Forced into bound namespace (server-side) |
| Access workspace in own namespace | Allowed |
| Access workspace in another namespace | Blocked (403) |
| Access other services (pages, CDN, etc.) | Blocked (403) |
| Create API keys or tokens | Blocked (403) |
Workspace scope
| Action | Result |
|---|---|
| Access bound workspace | Allowed (all sub-resources) |
| List workspaces | Blocked (403) |
| Create workspaces | Blocked (403) |
| Access any other workspace | Blocked (403) |
Token properties
| Property | Value |
|---|---|
| Format | Signed JWT (HS256), standard Authorization: Bearer header |
| Max TTL | 1 hour (3600 seconds) |
| Default TTL | 15 minutes (900 seconds) |
| Storage | Stateless — no DB row, no Redis entry |
| Revocation | Not supported (by design — short TTL makes it unnecessary) |
| Validation | JWT signature + expiry check only |
| Enforcement | Same middleware chain as API keys (requireScope, enforceScopeOnList, requireSandboxOwnership) |
Full SaaS example
End-to-end flow: customer signs up → create namespace → issue scoped token → user creates workspaces.
import Oblien from 'oblien';
// ─── Your backend (runs on your server with admin key) ─────────
const admin = new Oblien({
clientId: process.env.OBLIEN_CLIENT_ID!,
clientSecret: process.env.OBLIEN_CLIENT_SECRET!,
});
// 1. Customer signs up → create their namespace
const ns = await admin.namespaces.create({
name: `customer-${customerId}`,
type: 'production',
resource_limits: {
max_workspaces: plan.maxWorkspaces,
max_vcpus: plan.maxCpus,
max_ram_mb: plan.maxRamMb,
},
});
// 2. Set spending quota
await admin.namespaces.setQuota({
namespace: ns.data.slug,
service: 'sandbox',
quotaLimit: plan.monthlyCredits,
period: 'monthly',
overdraft: plan.monthlyCredits * 0.1,
onOverdraftAction: 'stop_workspaces',
});
// 3. Customer logs into your app → issue them a scoped token
const { token, expiresAt } = await admin.tokens.create({
scope: 'namespace',
namespace: ns.data.slug,
ttl: 3600, // 1 hour
});
// Return token + expiresAt to your frontend
// ─── Your frontend (runs in the user's browser) ────────────────
const userClient = new Oblien({ token });
// User can only see and create workspaces in their namespace
const workspaces = await userClient.workspaces.list();
const newWorkspace = await userClient.workspaces.create({
image: 'node-20',
config: { cpus: 2, memory_mb: 2048 },
});
// namespace is automatically set to the bound namespace — cannot be overridden
// When the token expires, get a fresh one and swap it in:
const fresh = await fetchNewTokenFromYourBackend();
userClient.setToken(fresh.token);API key scopes vs scoped tokens
Both enforce the same permissions. Choose based on your use case:
| API Key (client_id/secret) | Scoped Token (JWT) | |
|---|---|---|
| Lifetime | Until revoked | 1 hour max |
| Auth method | X-Client-ID + X-Client-Secret headers | Authorization: Bearer header |
| Storage | Row in database | Stateless (no storage) |
| Revocable | Yes (delete from dashboard or API) | No (expires naturally) |
| Best for | Backend services, CI/CD, long-running agents | End-user sessions, browser apps, temporary access |
| Creation | Dashboard UI or session auth | POST /tokens with admin API key |
Rule of thumb — Use API keys for your backend. Use scoped tokens for anything you hand to an end-user.