# Authentication

> Choose how the Nodaro SDK authenticates, with StaticTokenAuth, CallbackAuth or supabaseAuth, and share one browser login across your subdomains.

Source: https://nodaro.ai/docs/developers/sdk/auth

An **auth provider** tells the Nodaro SDK which token to send. Before every request, the client calls the provider's `getToken()` and sends the result as `Authorization: Bearer <token>`. When the provider returns `null`, the request is sent without the header, as an anonymous request.

## Choose a provider

| Provider | Use it for | Where the token comes from |
| --- | --- | --- |
| [`StaticTokenAuth`](#statictokenauth) | Server code, scripts, scheduled jobs | A fixed API token or OAuth access token |
| [`CallbackAuth`](#callbackauth) | Tokens that expire and need a refresh, custom session stores | Your function, called before every request |
| [`supabaseAuth`](#supabaseauthsupabase) | A browser app whose users sign in to the same Nodaro instance | The user's live session, refreshed automatically |
| Your own object | Anything else | Any object with a `getToken()` method |

## Tokens you can use

| Token | Looks like | Who it acts as | Get it from |
| --- | --- | --- | --- |
| API token | `ndr_` followed by 64 hex characters | You | **Settings › API Tokens** in Nodaro |
| OAuth access token | `ndr_app_` followed by 64 hex characters | A user who approved your app | The OAuth code exchange, [`client.oauth.exchangeCode()`](https://nodaro.ai/docs/developers/sdk/developer-apps) |
| Session token | A signed-in session | The signed-in user | The Nodaro sign-in, through `supabaseAuth` |

- **An API token** is a permanent credential with no spending cap. It works until you deactivate or delete it, so keep it on a server. See [API authentication](https://nodaro.ai/docs/developers/api/authentication) for limits and rate settings.
- **An OAuth access token** carries only the scopes the user approved. Use it when your app acts for other people. See [OAuth](https://nodaro.ai/docs/developers/oauth).
- **On a self-hosted Community edition install**, the app does not offer API tokens. Sign in and use your session token instead, with `supabaseAuth` or `CallbackAuth`.

## The Auth interface

```ts
interface Auth {
getToken(): Promise<string | null>
}
```

Any object with this shape can be the `auth` option of `createClient`. The three providers below implement it.

## StaticTokenAuth

```ts
new StaticTokenAuth(token: string)
```

Wraps one fixed token. Use it when the token does not change while your process runs: an API token, or an OAuth access token your server obtained through the authorization-code flow.

<TypeTable
type={{
token: {
type: 'string',
required: true,
description: "The token to send with every request: an API token (ndr_...) or an OAuth access token (ndr_app_...).",
},
}}
/>

```ts

const client = createClient({
baseUrl: "https://app.nodaro.ai",
auth: new StaticTokenAuth(process.env.NODARO_TOKEN!),
})
```

## CallbackAuth

```ts
new CallbackAuth(fn: () => string | null | Promise<string | null>)
```

Calls your function before every request and sends the token it returns. The function can be synchronous or asynchronous. Return `null` to send the request without a token.

<TypeTable
type={{
fn: {
type: '() => string | null | Promise<string | null>',
required: true,
description: "Returns the token for the next request, or null for an anonymous request.",
},
}}
/>

Use it to refresh tokens, to read a custom session store, or to rotate credentials:

```ts

const client = createClient({
baseUrl: "https://app.nodaro.ai",
auth: new CallbackAuth(async () => {
const session = await sessionStore.read()
if (!session) return null
if (Date.now() > session.expiresAt - 60_000) {
await refresh(session)
}
return session.accessToken
}),
})
```

## supabaseAuth(supabase)

```ts
supabaseAuth(supabase: SupabaseLikeClient): Auth
```

Reads the signed-in user's token from a Supabase v2 client before every request. Use it in a browser app whose users sign in to the same Nodaro instance, for example your own frontend for a self-hosted install. The Nodaro editor uses the same provider. Because the token is read live, a refreshed session is picked up automatically.

<TypeTable
type={{
supabase: {
type: 'SupabaseLikeClient',
required: true,
description: "A Supabase v2 client, or any object whose auth.getSession() method returns the current session. Nothing else is called.",
},
}}
/>

```ts

const supabase = createSupabase(
import.meta.env.VITE_SUPABASE_URL,
import.meta.env.VITE_SUPABASE_ANON_KEY,
)

const client = createClient({
baseUrl: import.meta.env.VITE_API_URL ?? "",
auth: supabaseAuth(supabase),
})
```

When nobody is signed in, the request is sent without a token.

## createSharedSupabaseClient(options)

```ts

createSharedSupabaseClient<Db = any>(options: {
url: string
anonKey: string
cookieDomain?: string
}): SupabaseClient<Db>
```

Creates a browser Supabase client that keeps the session in **cookies** instead of local storage. With `cookieDomain`, several apps on sibling subdomains share one login: a user who signs in on one is signed in on all of them, and signing out anywhere signs out everywhere.

<TypeTable
type={{
url: {
type: 'string',
required: true,
description: "The Supabase project URL of the Nodaro instance.",
},
anonKey: {
type: 'string',
required: true,
description: "The public anonymous key of that project.",
},
cookieDomain: {
type: 'string',
description: "A parent domain, such as .example.com, to share the session across its subdomains. Omit it for cookies that stay on the current host.",
},
}}
/>

```ts

const supabase = createSharedSupabaseClient({
url: SUPABASE_URL,
anonKey: SUPABASE_ANON_KEY,
cookieDomain: ".example.com",
})

const client = createClient({ baseUrl: "", auth: supabaseAuth(supabase) })
```

- `cookieDomain` applies only when the page's host is that domain or one of its subdomains. On any other host, such as `localhost` or a preview URL, cookies stay on the current host, so local development keeps a separate session per origin.
- On the first load, an existing session in local storage moves into the cookie and the old entry is removed. A user who was already signed in stays signed in. Expired sessions are discarded.
- This export lives at the separate path `@nodaro/sdk/supabase`, so the main package does not depend on Supabase. Install `@supabase/supabase-js` and `@supabase/ssr` to use it.

## Scopes and missing permissions

An OAuth access token carries the scopes the user approved, such as `workflows:read` or `workflows:execute`. The reference page of each resource names the scope its methods need. When a token lacks one, the method throws a `ForbiddenError` whose `missingScope` names it:

```ts

try {
await client.workflows.run(workflowId)
} catch (err) {
if (err instanceof ForbiddenError && err.missingScope) {
requestConsentFor([err.missingScope]) // send the user through OAuth again
} else {
throw err
}
}
```

API tokens and session tokens are not limited by scopes. See [Errors](https://nodaro.ai/docs/developers/sdk/errors) for every error class.

## Browser rules

- **Never ship an API token to a browser.** Anyone can read it from the page, and it acts as you.
- **OAuth tokens in the browser** work only from origins listed in your developer app's `allowedOrigins`. See [OAuth and developer apps](https://nodaro.ai/docs/developers/sdk/developer-apps).
- **Session tokens from `supabaseAuth`** are not checked against that list.
- **Keep secrets on the server.** The OAuth code exchange needs your client secret, so run [`client.oauth.exchangeCode()`](https://nodaro.ai/docs/developers/sdk/developer-apps) only in server code.

## Frequently asked questions

### Which auth provider should a server use?

StaticTokenAuth. Pass it an API token that starts with ndr_, or an OAuth access token that starts with ndr_app_, read from an environment variable.

### How do I refresh an expiring token with the SDK?

Use CallbackAuth. The SDK calls your function before every request, so the function can check the expiry, refresh the token and return the new one.

### Can I put an API token in a browser app?

No. An API token acts as you, with no spending cap, and anyone can read it from browser code. In the browser, sign users in and use their session, or use OAuth.

### What does ForbiddenError.missingScope mean?

The OAuth token is valid but was not granted the scope the endpoint needs, for example workflows:execute. Ask the user to grant that scope, then retry with the new token.
