SDK + OAuth guide
Connect the SDK via machine-to-machine (client credentials) or PKCE, step by step.
The Dalea SDK defaults to a workspace API key, but two OAuth 2.1 flows cover the cases a key can't:
| Flow | Actor | Credential class | Token lifetime |
|---|---|---|---|
| Client credentials (M2M) | A service — no user involved | OAuthClientCredentialsCredential | ~1 hour, re-minted automatically; no refresh token |
| Authorization code + PKCE | A signed-in Dalea user, via your app | OAuthPkceCredential | Access token ~1 hour + refresh token (auto-rotates) |
Both flows start from an OAuth application registered in a workspace (Settings → Workspaces → your workspace → Applications). The application's workspace role is a permission ceiling: tokens can never do more than that role allows, whichever flow minted them.
Machine-to-machine (client credentials)
Use this when a backend service needs its own standing identity — a LIMS sync, a data pipeline, a CI job — and tying it to a personal API key is wrong (people leave; services shouldn't).
When you enable machine-to-machine access on an application, Dalea provisions
a service account: a login-disabled machine identity enrolled in the
workspace with the application's role. Everything the service does —
permissions, row-level security, audit trails — is attributed to
Your App (service account), and it never consumes a human member seat.
1. Create an M2M application
- Settings → Workspaces → Applications → New application
Give it a recognisable name and an optional description — the name carries through everywhere the service acts: audit trails, the member list (as “Name (service account)”), and token attribution. Pick Confidential (Web App) — machine-to-machine requires a client secret, so public/PKCE-only clients can't use it.
- Tick “Enable machine-to-machine access (client credentials)”
This provisions the service account. Organizations have a tier-based service-account limit; the checkbox shows current usage. Redirect URIs and scopes are not needed for a machine-only app — both apply only when users also sign in through this application, so add them only in that dual-use case.
- Pick the workspace role
The most restrictive role that gets the job done. The service account acts as exactly this role — change the application's role later and the service account follows.
- Copy the client ID and secret once
The secret is displayed exactly once. Store it in your secret manager.
2. Mint and call
The credential requests a token from the token endpoint on first use, caches it until just before expiry, and transparently re-mints on a 401 — your code never touches the token.
import { DaleaClient, OAuthClientCredentialsCredential } from '@dalea/sdk';
const dalea = new DaleaClient({
baseUrl: 'https://dalea.app',
credential: new OAuthClientCredentialsCredential({
tokenEndpoint: 'https://dalea.app/api/v1/auth/oauth2/token',
clientId: process.env.DALEA_OAUTH_CLIENT_ID!,
clientSecret: process.env.DALEA_OAUTH_CLIENT_SECRET!,
}),
});
// Acts as the service account, in the application's workspace:
const { environments } = await dalea.data.environments.list();
// Writes work the same way — attributed to the service account, and allowed
// only when the application's workspace role permits them:
const { table } = await dalea.data.environments.createTable(environments[0].id, {
name: 'm2m_sync_demo',
description: 'Created by the machine-to-machine example',
});
No workspace configuration is needed: the token acts in the workspace the application is bound to. Per OAuth 2.1, the grant issues no refresh token — the short-lived access token simply gets re-minted from the client secret whenever it expires.
What an M2M token may do comes from the application's workspace role,
not from OAuth scopes. If you pass an explicit scope, it must be
non-OIDC (e.g. mcp:read) — requesting openid,
profile, email or offline_access is
rejected with invalid_scope, because there is no user identity to
grant.
Lifecycle
- Rotate the secret from the application's page; outstanding tokens expire naturally within the hour.
- Deleting the application revokes its tokens immediately and deactivates the service account (its audit history is preserved).
- The service account appears in member lists with a Service account badge; its role is managed only through the application, and it never counts toward the organization's member-seat limit.
On behalf of a user (authorization code + PKCE)
Use this when a person signs in to your application and it acts as them — each user sees exactly what their own Dalea account can see.
You can register the client two ways: as a workspace application (as above,
either type works — PKCE needs no secret), or via Dynamic Client
Registration at /api/v1/auth/oauth2/register for self-serve public
clients.
An authorize request may not exceed the client's registered scopes.
If you want refresh tokens, the registration itself must declare
offline_access — omitting scope at registration
gives you only openid profile email, and requesting
offline_access later fails with invalid_scope.
The flow: generate a PKCE pair, send the user to the authorize page, receive
?code=… on your redirect URI, exchange it for tokens.
import { DaleaClient, OAuthPkceCredential } from '@dalea/sdk';
const tokenEndpoint = 'https://dalea.app/api/v1/auth/oauth2/token';
// 1. PKCE pair — keep codeVerifier server-side for the exchange.
const { codeVerifier, codeChallenge } = await OAuthPkceCredential.generatePkcePair();
// 2. Send the user to the authorize page.
const authorize = new URL('https://dalea.app/api/v1/auth/oauth2/authorize');
authorize.searchParams.set('response_type', 'code');
authorize.searchParams.set('client_id', clientId);
authorize.searchParams.set('redirect_uri', 'https://myapp.example/callback');
authorize.searchParams.set('scope', 'openid profile email offline_access');
authorize.searchParams.set('code_challenge', codeChallenge);
authorize.searchParams.set('code_challenge_method', 'S256');
// … the user signs in, picks a workspace + role for your app, and consents.
// Dalea redirects to your callback with ?code=…
// 3. Exchange the code. The credential auto-refreshes on 401 from then on.
const credential = await OAuthPkceCredential.exchangeCode({
tokenEndpoint,
clientId,
code,
codeVerifier,
redirectUri: 'https://myapp.example/callback',
onTokens: (tokens) => persist(tokens), // called on every rotation — store them
});
const dalea = new DaleaClient({ baseUrl: 'https://dalea.app', credential });
const { environments } = await dalea.data.environments.list();
// Writes act as the signed-in user, within the role they granted your app
// during authorization:
const { table } = await dalea.data.environments.createTable(environments[0].id, {
name: 'pkce_demo',
description: 'Created on behalf of the signed-in user',
});
During authorization the user picks which of their workspaces (and which role) your application gets — the token is then bound to that choice, so again no workspace configuration in the client.
Troubleshooting
- 401 “Machine-to-machine access is not enabled for this client”
- The application exists but was created without the M2M checkbox. Create a new confidential application with machine-to-machine access enabled.
- 403 SERVICE_ACCOUNT_LIMIT_REACHED
- The organization is at its tier’s service-account limit. Remove an unused M2M application or upgrade the tier.
- invalid_scope at the token endpoint
- M2M: you requested an OIDC scope (openid/profile/email/offline_access) — drop it. PKCE: the authorize request asked for a scope the client never registered.
- 403 “not available for programmatic access”
- OAuth tokens (both flows) reach the public API surface only — the same operations the SDK exposes. First-party-only endpoints require a session.
- 401 right after rotating the client secret
- Expected for M2M: cached tokens survive up to an hour, then the SDK re-mints with whatever secret it holds. Deploy the new secret before the old tokens expire.