---
summary: Developer guide for integrating SigID into a confidential server-side app (Node and other backend runtimes) using the @sigid/backend SDK – login, token verification, logout, and webhooks without hand-rolling OAuth/OIDC.
tags:
  - developers
  - sdk
  - backend
  - oauth
  - oidc
  - confidential
categories:
  - For Developers
---

# Backend SDK For Confidential Servers

<!-- agent:page
You are a coding agent adding SigID login to a server-side app that holds its own
session (B2B SaaS, server-rendered app, existing cookie auth). Use @sigid/backend
so you do not hand-roll PKCE, code exchange, JWKS, RP-initiated logout, or webhook
verification. The server is a confidential OAuth client: it can hold a
client_secret. Configure issuer, clientId, clientSecret, and the exact redirect
URI. After exchange, mint YOUR OWN app session cookie from the verified ID-token
claims and discard OAuth artifacts; retain the id_token only if you want
RP-initiated logout. Protect backend APIs with validateAccessToken; never trust
frontend session state for authorization. Verify webhooks with the raw body
before parsing. Keep secrets server-side only.
-->

Use this page when your app is a **confidential server** with its own session
(B2B SaaS, server-rendered apps, an existing cookie/auth boundary that must stay
authoritative) and you want SigID login without hand-rolling the OAuth/OIDC
code flow.

`@sigid/client` is public-only – it never sends a `client_secret`. For server
apps that hold a secret, `@sigid/backend` provides the confidential-side
primitives browser apps already get from `@sigid/client`: PKCE/state/nonce,
confidential code exchange, ID-token verification, RP-initiated logout, webhook
verification, and access-token validation.

## Install

```bash
npm install @sigid/backend
```

## Configure

```typescript
import { createSigIdBackend } from "@sigid/backend";

const sigid = createSigIdBackend({
  issuer: process.env.SIGID_ISSUER!,
  clientId: process.env.SIGID_CLIENT_ID!,
  clientSecret: process.env.SIGID_CLIENT_SECRET!, // server-side only
  redirectUri: "https://app.example.com/auth/callback",
});
```

| Value | Where | Notes |
|---|---|---|
| Issuer | Dashboard / discovery | e.g. `https://auth.sigid.org` |
| Client ID | Dashboard Applications | The confidential application |
| Client secret | Dashboard Applications | **Never** ship to the browser |
| Redirect URI | Dashboard Applications | Exact match (scheme/host/port/path) |

## 1. Begin Login

```typescript
// Persist a fresh random value in an HttpOnly, Secure, SameSite=Lax cookie
// when the authorization response returns via the default GET callback.
const sessionBinding = appSession.oauthBinding;
const { url, state, nonce } = await sigid.buildAuthorizeUrl({ sessionBinding });
// Redirect the user to `url`. PKCE verifier, state, and nonce are generated
// and stored in the transaction store, keyed by `state`.
```

The store defaults to an in-memory store (single-process). For multi-instance
deployments, pass a shared store:

```typescript
import { type TransactionStore } from "@sigid/backend";
class RedisTransactionStore implements TransactionStore {
  async set(key, record, ttlSeconds) { /* redis SET ... EX ttlSeconds */ }
  async get(key) { /* redis GET */ }
  async remove(key) { /* redis DEL */ }
}
const sigid = createSigIdBackend({ /* ... */, store: new RedisTransactionStore() });
```

## 2. Handle The Callback

On your `/auth/callback` route, exchange the code and verify the ID token in one
call:

```typescript
const result = await sigid.exchangeAuthorizationCode({ code, state, sessionBinding });
// result.idTokenClaims is already signature/issuer/audience/nonce verified.
// Mint YOUR OWN app session cookie from result.idTokenClaims.
// Retain result.idToken only if you want RP-initiated logout.
```

`exchangeAuthorizationCode` uses `client_secret_basic` by default (SigID's
backend default); pass `authMethod: "client_secret_post"` to use the body
method. Nonce binding is enforced automatically from the stored transaction.

The mandatory `sessionBinding` must be an unguessable value read from the
initiating browser's cookie (or equivalent server-side session). For the
default query response mode, whose callback is a top-level GET navigation, use
an `HttpOnly; Secure; SameSite=Lax` cookie. A cross-site POST callback such as
`response_mode=form_post` requires `SameSite=None; Secure`, or a same-site GET
trampoline that restores the stricter cookie before exchange. Never derive the
binding from a callback parameter.

## 3. Logout

```typescript
const endSessionUrl = sigid.buildEndSessionUrl({
  idTokenHint: result.idToken,
  postLogoutRedirectUri: "https://app.example.com/",
});
// Redirect the user to `endSessionUrl`.
```

SigID's end-session flow decodes the `id_token_hint` to locate the application
and verify the post-logout redirect, so the ID token must be retained and passed
through. `buildEndSessionUrl` handles `id_token_hint` + `client_id` +
`post_logout_redirect_uri` for you.

## 4. Protect Backend APIs

```typescript
const claims = await sigid.validateAccessToken(token, {
  audience: "https://api.example.com",
  tenantId: process.env.SIGID_TENANT_ID!,
  scopes: ["projects:read"],
});
```

Single-tenant servers must always pass `tenantId`; multi-tenant servers must
scope every data lookup by `claims.tenantId`. See
[Verify Access Tokens](verify-tokens.md) and [Protect Backend APIs](protect-apis.md).

## 5. Verify Webhooks

```typescript
const { deliveryId, eventType } = await sigid.verifyWebhook({
  secret: process.env.SIGID_WEBHOOK_SECRET!,
  headers,
  rawBody, // the VERBATIM request body – do not re-serialize JSON
});
```

Suite is `sigid-webhook-v1` (HKDF-SHA256 over a canonical string). See
[Receive Webhooks](webhooks.md).

## Design Principle: Enable, Don't Take Over

`@sigid/backend` stays framework-agnostic and never owns your session. It exposes
pluggable store interfaces and tells you what to persist and how to verify – it
does not mandate a session implementation. Framework adapter packages
(`@sigid/express`, `@sigid/hono`, …) may wrap the guards on demand.

## Errors

All failures throw `SigIdBackendError` with a stable `code` and a `remediation`
hint. Handle them at your framework's error boundary:

```typescript
import { isSigIdBackendError } from "@sigid/backend";
try {
  await sigid.exchangeAuthorizationCode({ code, state, sessionBinding });
} catch (error) {
  if (isSigIdBackendError(error)) {
    console.error(error.code, error.remediation);
  }
}
```

## Next Pages

| Goal | Page |
|---|---|
| Token validation details | [Verify Access Tokens](verify-tokens.md) |
| Protecting routes | [Protect Backend APIs](protect-apis.md) |
| Events | [Receive Webhooks](webhooks.md) |
| OAuth reference | [OAuth And OIDC](../reference/oauth-oidc.md) |
