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:

FlowActorCredential classToken lifetime
Client credentials (M2M)A service — no user involvedOAuthClientCredentialsCredential~1 hour, re-minted automatically; no refresh token
Authorization code + PKCEA signed-in Dalea user, via your appOAuthPkceCredentialAccess 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Scopes are not permissions here

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.

Declare every scope at registration

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.

What's next