> ## Documentation Index
> Fetch the complete documentation index at: https://archie.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Node SDK

> Use @archie/node-sdk to verify Archie JWTs, manage users, and call your project's GraphQL and REST APIs with service-role access from your server. It also ships middleware for Express, Fastify, and AWS Lambda.

`@archie/node-sdk` is the server SDK for Archie. Use it in trusted backend code to verify JWTs, manage users as an admin, run service-role GraphQL and REST requests, and protect routes with framework middleware.

It is built for backend developers who run their own server or serverless functions next to an Archie project. For browser code, use the `@archie/js-sdk` package instead.

<Warning>
  The Node SDK authenticates with a **service-role API key**. This key has admin-level access. Use it only in server-side code, load it from environment variables, and never ship it to a browser, mobile app, or public repository.
</Warning>

## Install

Install the SDK together with `@archie/js-sdk`, which is a required peer dependency.

```bash theme={null}
# npm
npm install @archie/js-sdk @archie/node-sdk

# pnpm
pnpm add @archie/js-sdk @archie/node-sdk
```

Framework integrations are optional. Install `express` or `fastify` only if you use them.

The SDK requires Node.js 18.0.0 or later.

## Quick start

Create an admin client with your project ID and service-role API key.

```typescript theme={null}
import { createAdminClient } from '@archie/node-sdk';

const archie = createAdminClient({
  projectId: 'your-project-uuid',
  apiKey: 'your-service-role-api-key',
  apiUrl: 'https://your-project.archiecore.com',
  environment: 'master',
  retries: 3,
  timeout: 15_000,
});

// Health check
const healthy = await archie.ping();

// Verify a JWT
const { user, error } = await archie.auth.verifyToken(token);

// List users
const { data, error: listErr } = await archie.auth.listUsers({ page: 1, limit: 20 });

// Service-role GraphQL
const { data: roles } = await archie.graphql.query('{ roles { items { id name } } }');
```

## Configuration

`createAdminClient(options)` returns an `ArchieAdminClient`. Only `projectId` and `apiKey` are required.

| Option | Type | Default | Description |
| - | - | - | - |
| `projectId` | `string` | None | **Required.** Project UUID. |
| `apiKey` | `string` | None | **Required.** Service-role API key. |
| `environment` | `string` | `'master'` | Environment name. |
| `apiUrl` | `string` | `'https://api.archie.dev'` | Base URL for the API. |
| `authUrl` | `string` | Same as `apiUrl` | Base URL for the Auth service. |
| `timeout` | `number` | `30000` | Request timeout in milliseconds. Set `0` to disable. |
| `retries` | `number` | `0` | Max retry attempts for transient errors (429, 500–504, network). |
| `retryDelay` | `number` | `200` | Initial delay in ms for exponential backoff with jitter. |
| `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch implementation for testing or proxies. |
| `logger` | `ArchieLogger` | `{}` | Logger for request lifecycle events. |

The client exposes these modules:

* `archie.auth` for JWT verification and admin user operations.
* `archie.graphql` for service-role GraphQL queries and mutations.
* `archie.rest` for service-role REST requests.
* `archie.notifications` for emitting notifications from the server.

## Verify JWTs

Use `archie.auth.verifyToken(token, options?)` to verify JWTs issued by Archie. The SDK checks signatures against the project's JWKS (JSON Web Key Set). It caches keys for 5 minutes and shares one fetch between concurrent calls.

```typescript theme={null}
const { user, error } = await archie.auth.verifyToken('Bearer eyJhbG...');
```

The `token` can include a `Bearer ` prefix. The SDK strips it for you.

| Option | Type | Description |
| - | - | - |
| `options.requiredRoles` | `string[]` | Require at least one of these roles. Returns 403 if missing. |
| `options.requireEmailVerified` | `boolean` | Require `emailVerified: true`. Returns 403 if false. |
| `options.clockTolerance` | `number` | Clock tolerance in seconds. Default: `30`. |

The call resolves to a `VerifyTokenResult`:

```typescript theme={null}
interface VerifyTokenResult {
  user: TokenUser | null;
  error: ArchieError | null;
}

interface TokenUser {
  id: string;
  email: string;
  firstName?: string;
  lastName?: string;
  roles: string[];
  exp: number;
  iat: number;
  emailVerified: boolean;
}
```

```typescript theme={null}
// Basic verification
const { user, error } = await archie.auth.verifyToken(jwt);
if (error) {
  console.error(error.code); // AUTH_TOKEN_INVALID, AUTH_TOKEN_EXPIRED, etc.
}

// Require the admin role
await archie.auth.verifyToken(jwt, { requiredRoles: ['admin'] });

// Require a verified email
await archie.auth.verifyToken(jwt, { requireEmailVerified: true });
```

## Manage users as an admin

Admin operations use service-role authentication and return `{ data, error }`.

| Method | Returns `data` of |
| - | - |
| `archie.auth.createUser(params)` | The created user |
| `archie.auth.listUsers(params?)` | `{ users: User[], total: number, page: number, limit: number }` |
| `archie.auth.getUser(userId)` | `User` (`id`, `email`, `firstName`, `lastName`, `roles`) |
| `archie.auth.updateUser(userId, updates)` | The updated user. `updates` is a `Partial<User>`. |
| `archie.auth.blockUser(userId)` | `boolean` |
| `archie.auth.unblockUser(userId)` | `boolean` |
| `archie.auth.deleteUser(userId)` | `boolean` |
| `archie.auth.resendVerification(userId)` | `boolean` |
| `archie.auth.resetUserPassword(userId, newPassword)` | `boolean` |

<AccordionGroup>
  <Accordion title="Create a user">
    ```typescript theme={null}
    const { data, error } = await archie.auth.createUser({
      email: 'user@example.com',
      password: 'SecurePass123!',
      firstName: 'John', // optional
      lastName: 'Doe', // optional
      roleId: 'role-uuid', // optional
      emailVerified: true, // optional
    });
    ```
  </Accordion>

  <Accordion title="List users">
    ```typescript theme={null}
    const { data, error } = await archie.auth.listUsers({
      page: 1, // optional
      limit: 20, // optional
      search: 'john', // optional, searches email and name
      status: 'active', // optional: 'active' | 'locked' | 'unverified'
      orderBy: 'createdAt', // optional
    });
    ```
  </Accordion>

  <Accordion title="Update a user">
    ```typescript theme={null}
    const { data, error } = await archie.auth.updateUser('user-uuid', {
      firstName: 'Jane',
      lastName: 'Smith',
    });
    ```
  </Accordion>

  <Accordion title="Block, unblock, and delete a user">
    ```typescript theme={null}
    await archie.auth.blockUser('user-uuid');
    await archie.auth.unblockUser('user-uuid');
    await archie.auth.deleteUser('user-uuid');
    ```
  </Accordion>
</AccordionGroup>

## Service-role GraphQL

Run GraphQL operations with admin-level permissions.

```typescript theme={null}
// Query
const { data, error } = await archie.graphql.query<{ users: User[] }>(
  '{ users { id email roles } }',
);

// Mutation with variables
const { data: result } = await archie.graphql.mutate(
  `mutation UpdateRole($userId: ID!, $roleId: ID!) {
    assignRole(userId: $userId, roleId: $roleId) { id roles }
  }`,
  { userId: 'user-uuid', roleId: 'admin-role' },
);
```

Both methods use the signature `(gql, variables?, options?)`.

### Impersonate a user

Pass `impersonateUserId` to run an operation as a specific user while keeping service-role access. The SDK adds the `x-impersonate-user` header to the request.

```typescript theme={null}
const { data } = await archie.graphql.query(
  '{ myOrders { id total } }',
  {},
  { impersonateUserId: 'user-uuid' },
);
```

## Service-role REST

Call REST endpoints directly with admin credentials.

```typescript theme={null}
const items = await archie.rest.get<Item[]>('/api/v1/items');
const created = await archie.rest.post<Item>('/api/v1/items', { name: 'New' });
const updated = await archie.rest.put<Item>('/api/v1/items/1', { name: 'Updated' });
const patched = await archie.rest.patch<Item>('/api/v1/items/1', { status: 'active' });
await archie.rest.delete('/api/v1/items/1');
```

REST methods throw an `ArchieError` on non-2xx responses. The error code includes the HTTP status.

```typescript theme={null}
try {
  await archie.rest.get('/api/v1/missing');
} catch (err) {
  // err.code === 'HTTP_404'
  // err.status === 404
}
```

<Note>
  Endpoints that return `204 No Content` or `Content-Length: 0` resolve with `null` data instead of parsing an empty body.
</Note>

## Emit notifications

`archie.notifications.emit()` dispatches a notification through the backend `emitNotification` mutation. `eventKey`, `target`, and `sourceService` are required.

```typescript theme={null}
const { data, error } = await archie.notifications.emit({
  eventKey: 'welcome',
  target: { userIds: ['<uuid>'] }, // or { audience: 'ALL_PROJECT_USERS' }
  sourceService: 'auth', // required: identifies the emitting service
  params: { name: 'Ana' },
  channels: ['email', 'in_app'], // 'email' | 'in_app' | 'slack' | 'sms' | 'whatsapp'
  // optional: action: { text, url }, dedupKey, expiresIn: '7d', broadcastConfirm
});

if (data?.accepted) {
  console.log(data.targetingMode, data.recipientCount, data.fanoutJobId);
}
```

Admin `emit()` always returns `{ data, error }` and never throws. This differs from the browser-side `@archie/js-sdk` notification methods (`list`, `getUnreadCount`, `markAsRead`, `markAllAsRead`), which throw `ArchieError` on failure.

## Middleware and framework integrations

Each integration lives in its own subpath export and verifies the JWT in the `Authorization` header.

<Tabs>
  <Tab title="Express">
    Install Express, then import from `@archie/node-sdk/express`.

    ```bash theme={null}
    npm install express
    ```

    `createArchieMiddleware(client, options?)` verifies the JWT and populates `req.archieUser`.

    ```typescript theme={null}
    import express from 'express';
    import { createAdminClient } from '@archie/node-sdk';
    import { createArchieMiddleware, requireRole } from '@archie/node-sdk/express';

    const archie = createAdminClient({ projectId: '...', apiKey: '...' });
    const app = express();

    // Optional auth: req.archieUser may be undefined
    app.use(createArchieMiddleware(archie, { required: false }));

    app.get('/api/profile', (req, res) => {
      if (!req.archieUser) return res.status(401).json({ error: 'Unauthorized' });
      res.json({ user: req.archieUser });
    });

    // Required auth: returns 401 automatically
    app.use('/api/admin', createArchieMiddleware(archie, { required: true }));

    app.get('/api/admin/dashboard', requireRole('admin'), (req, res) => {
      res.json({ admin: true, user: req.archieUser });
    });
    ```

    | Option | Type | Default | Description |
    | - | - | - | - |
    | `required` | `boolean` | `false` | Return 401 for unauthenticated requests. |
    | `onError` | `function` | None | Custom error handler: `(err, req, res, next) => void`. |

    `requireRole(...roles)` checks that `req.archieUser` has at least one of the given roles and returns 403 if not. Use it after `createArchieMiddleware`.

    ```typescript theme={null}
    app.get('/admin', requireRole('admin'), handler);
    app.get('/editor', requireRole('admin', 'editor'), handler);
    ```

    The middleware augments Express types, so `req.archieUser` is typed as `TokenUser | undefined`.
  </Tab>

  <Tab title="Fastify">
    Install Fastify, then import `archiePlugin` from `@archie/node-sdk/fastify`. The plugin verifies the JWT and decorates `request.archieUser`.

    ```bash theme={null}
    npm install fastify
    ```

    ```typescript theme={null}
    import Fastify from 'fastify';
    import { createAdminClient } from '@archie/node-sdk';
    import { archiePlugin } from '@archie/node-sdk/fastify';

    const archie = createAdminClient({ projectId: '...', apiKey: '...' });
    const fastify = Fastify();

    fastify.register(archiePlugin, {
      client: archie,
      required: false, // set true to enforce auth on all routes
    });

    fastify.get('/api/profile', async (request) => {
      return { user: request.archieUser };
    });
    ```
  </Tab>

  <Tab title="Lambda">
    `withArchieAuth(client, handler)` from `@archie/node-sdk/lambda` wraps a Lambda handler. It verifies the JWT and injects the user into the context.

    ```typescript theme={null}
    import { createAdminClient } from '@archie/node-sdk';
    import { withArchieAuth } from '@archie/node-sdk/lambda';

    const archie = createAdminClient({ projectId: '...', apiKey: '...' });

    export const handler = withArchieAuth(archie, async (event, context) => {
      const { user, client } = context.archie;

      // Use the admin client
      const { data } = await client.auth.listUsers();

      return {
        statusCode: 200,
        body: JSON.stringify({ userId: user.id, totalUsers: data?.total }),
      };
    });
    ```

    The wrapper returns 401 if the `Authorization` header is missing or the token is invalid.
  </Tab>
</Tabs>

## Error handling

Each part of the SDK reports errors in a consistent way:

* **Auth operations** return `{ data, error }`, where `error` is `ArchieError | null`.
* **Verify operations** return `{ user, error }`, where `error` is `AuthError | null`.
* **REST operations** throw `ArchieError` on non-2xx responses.
* **GraphQL operations** return `{ data, error }` with structured GraphQL errors.

| Code | Status | Description |
| - | - | - |
| `AUTH_TOKEN_INVALID` | 401 | Token is missing, malformed, or has invalid signature. |
| `AUTH_TOKEN_EXPIRED` | 401 | Token has expired. |
| `AUTH_INSUFFICIENT_ROLE` | 403 | User lacks a required role. |
| `AUTH_EMAIL_NOT_VERIFIED` | 403 | Email is not verified but was required. |
| `AUTH_JWKS_FETCH_FAILED` | Varies | Failed to fetch JWKS keys. |
| `ADMIN_ERROR` | Varies | Admin operation returned an error. |
| `HTTP_<status>` | Varies | REST request failed with the given HTTP status. |
| `TIMEOUT` | 408 | Request timed out. |
| `NETWORK_ERROR` | 0 | Network-level failure. |

## Retries and timeouts

All HTTP requests go through one engine with configurable retry and timeout.

```typescript theme={null}
const archie = createAdminClient({
  projectId: '...',
  apiKey: '...',
  retries: 3, // retry up to 3 times
  retryDelay: 200, // start with a 200 ms delay
  timeout: 10_000, // 10 s timeout
});
```

The SDK retries on these conditions:

* HTTP status codes `429`, `500`, `502`, `503`, and `504`.
* Network errors, such as DNS failures and refused connections.
* Timeout errors.

Backoff is exponential with jitter: `delay × 2^attempt + random jitter`, capped at 30 seconds. If a `429` response includes a `Retry-After` header, the SDK respects it.

Requests time out after 30 seconds by default. Set `timeout: 0` to disable timeouts. A timed-out request throws an `ArchieError` with `code: 'TIMEOUT'` and `status: 408`.

## Health check

`archie.ping()` sends a `GET /health` request. It returns `true` if the server responds with 2xx within the configured timeout, and `false` on any error.

```typescript theme={null}
const healthy = await archie.ping();
if (!healthy) {
  console.error('Archie API is unreachable');
}
```

## Custom fetch and logging

Pass a custom `fetch` for unit tests with a mocked fetch, HTTP proxies or agents (for example `undici` with a proxy), or transport-level logging.

```typescript theme={null}
const archie = createAdminClient({
  projectId: '...',
  apiKey: '...',
  fetch: myCustomFetch,
});
```

Pass a `logger` to observe request lifecycle events. Any logger works, including pino, winston, and `console`. All methods are optional, so implement only the levels you need.

```typescript theme={null}
import pino from 'pino';
import { createAdminClient } from '@archie/node-sdk';

const logger = pino({ level: 'debug' });

const archie = createAdminClient({
  projectId: '...',
  apiKey: '...',
  logger: {
    debug: (msg, meta) => logger.debug(meta, msg),
    warn: (msg, meta) => logger.warn(meta, msg),
  },
});
```

The SDK logs these events:

| Level | Event | Meta |
| - | - | - |
| `debug` | `Request` | `{ method, url }` |
| `debug` | `Response` | `{ url, status }` |
| `warn` | `Retrying request` | `{ url, attempt, delay }` |
| `warn` | `Rate limited, waiting Retry-After` | `{ url, retryAfter }` |
| `warn` | `Network error, will retry` | `{ url, attempt, error }` |

## Security best practices

* Keep `apiKey` in server-side environment variables or a secrets manager.
* Never import the admin client in code that runs in the browser or in a mobile app.
* Use `requiredRoles` or `requireRole` to restrict sensitive routes, and `requireEmailVerified` when you need verified accounts.
* Use `required: true` on the middleware for routes that must be authenticated.
* Do not log API keys or raw tokens through the `logger` or a custom `fetch`.

## TypeScript

The SDK ships with types. Import them with `import type`.

```typescript theme={null}
import type {
  AdminClientOptions,
  ArchieLogger,
  VerifyTokenOptions,
  VerifyTokenResult,
  TokenUser,
  CreateUserParams,
  ListUsersParams,
  ListUsersResult,
  MiddlewareOptions,
  IAdminAuthModule,
  IAdminGraphQLModule,
  IAdminRestModule,
  AdminResult,
} from '@archie/node-sdk';
```

The SDK also re-exports these types from `@archie/js-sdk`: `User`, `Session`, `GraphQLResponse`, `ArchieError`, `AuthError`, `GraphQLError`, and `NetworkError`.

The `ArchieAdminClient` class and the module classes `AdminAuthModule`, `AdminGraphQLModule`, and `AdminRestModule` are exported for `instanceof` checks, advanced typing, or mocking. `new ArchieAdminClient({ projectId: '...', apiKey: '...' })` is equivalent to `createAdminClient`.

## Resources

<CardGroup cols={2}>
  <Card title="@archie/node-sdk on npm" href="https://www.npmjs.com/package/@archie/node-sdk">
    Package page, versions, and install instructions.
  </Card>

  <Card title="Backend overview" href="/docs/backend">
    Learn about the Backend Console and the APIs your project exposes.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.