Skip to content
Anlyon
Esc
↑↓navigate↵open⌘Jpreview
On this page

Components (beta)

Prebuilt React components for your end users: approve what your agent wants to do, and connect their own accounts, inside your app. A short-lived session token keeps your API key on your server.

@anlyonhq/react gives you the screens Act for your users otherwise has you build by hand: an approval inbox, an inline approval, connect buttons and a connected-accounts list. They follow your shadcn/ui theme, or your own CSS variables.

This package is a beta release. Its API may change before 1.0.

npm install @anlyonhq/react @anlyonhq/sdk

React 18 or 19. No runtime dependencies beyond React.

How it fits together

  • Your server mints a session token for the user who is signed in, with your API key. The key never reaches the browser.
  • The provider fetches the token from your route, keeps it in memory, and fetches a new one before it expires.
  • The components call Anlyon from the browser with that token. It acts for that one user, in one environment.

A session token can:

  • list the user’s connected accounts,
  • create a connect link (it returns only to one of your allowed redirect URLs),
  • disconnect one of the user’s accounts,
  • list the user’s own approvals, and approve or decline them, when your key has end-users:approvals.

Nothing else. It cannot call actions, read secrets, see other users, or confirm a connection. It lasts 15 minutes by default, at most 1 hour, and never past the key that minted it. Revoking that key ends its sessions. Each one is recorded in the audit log as end_user.session_created.

1. Set up the environment

In the console, open Integrations → Connect settings and pick the environment. Add your callback URL to the allowed redirect URLs, for example https://app.example.com/api/anlyon/connected.

In a browser, a session token works only from the origins of these URLs. A request from any other origin is refused with 403 ORIGIN_NOT_ALLOWED, so another website cannot use a token it gets hold of. Outside a browser there is no origin to check: like any bearer token, a copied token works until it expires, for that one user and only for the operations above. Keep the lifetime short. For local work, add your local callback, such as http://localhost:5173/api/anlyon/connected, to a separate environment.

Create an API key with end-users:write, and end-users:approvals for the approval components.

2. The session route

// app/api/anlyon/session/route.ts
import { Client } from '@anlyonhq/sdk';
import { createSessionHandler } from '@anlyonhq/react/server';
import { auth } from '@/lib/auth';

const anlyon = new Client({ apiKey: process.env.ANLYON_API_KEY! });

export const POST = createSessionHandler({
  client: anlyon,
  getEndUserId: async () => (await auth())?.user.id,
});

It answers { token, expiresAt } with Cache-Control: no-store, and 401 when nobody is signed in. The user always comes from your own session, never from the request.

Without the helper, call endUsers.createSession yourself:

const { data } = await anlyon.endUsers.createSession(session.user.id, { ttlSeconds: 600 });
return Response.json({ token: data!.token, expiresAt: data!.expiresAt }, { headers: { 'Cache-Control': 'no-store' } });
session = client.end_users.create_session(user.id, ttl_seconds=600)

3. The connect callback

When a user connects an account, the provider sends them back to your callback URL. Your server confirms the connection for the user who is signed in, so a forwarded link cannot attach someone else’s account. The helper does that, then closes the popup or redirects.

// app/api/anlyon/connected/route.ts
import { createConnectCallbackHandler } from '@anlyonhq/react/server';

export const GET = createConnectCallbackHandler({
  client: anlyon,
  getEndUserId: async () => (await auth())?.user.id,
  redirectTo: '/settings/connections',
});

4. The provider

// app/providers.tsx
'use client';
import { AnlyonProvider } from '@anlyonhq/react';
import '@anlyonhq/react/styles.css';

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <AnlyonProvider
      getToken={() => fetch('/api/anlyon/session', { method: 'POST' }).then((r) => r.json())}
      baseUrl={process.env.NEXT_PUBLIC_ANLYON_BASE_URL} // unset: https://api.anlyon.com
      connect={{ redirectUrl: 'https://app.example.com/api/anlyon/connected' }}
    >
      {children}
    </AnlyonProvider>
  );
}
Prop What it does
getToken Returns { token, expiresAt }, or the token. Called again a minute before expiry, and after the API refuses a token.
baseUrl Defaults to https://api.anlyon.com. Set it for another environment’s API host.
connect { mode, redirectUrl }. mode is popup (default) or redirect. A blocked popup falls back to a redirect.
pollInterval How often open approvals are checked while the page is visible, in ms. Default 30000. 0 turns it off.
locale For amounts and dates. Defaults to the browser’s.

The provider holds one user’s session. When the signed-in user changes without a page load, remount it with that user’s id as its key, so nothing from the previous user stays on screen.

5. The components

Approval inbox

import { ApprovalInbox } from '@anlyonhq/react';

<ApprovalInbox />

Every approval waiting for this user, newest first. Each row opens to what the agent wants: the action in words, its input, the preview (fields, a diff, the amount), and when it expires. The user approves or declines, with an optional note. Pass status="all" to include answered and expired ones.

The user sees each action’s name and description as you wrote them, so write them for that person, not for your team.

Approvals reach the user when a policy is Decided by: End user. See Act for your users.

One approval

import { ApprovalPrompt } from '@anlyonhq/react';

<ApprovalPrompt approvalId={toolResult.approvalId} onDecided={(decision) => console.log(decision)} />

Use it inline, for example under the chat message whose tool call is waiting.

Both show each state as it is: waiting, sending, approved (with the action’s outcome), declined, expired, and failed with the API’s own message. If someone decided first, the component shows what actually happened.

Connect button

import { ConnectButton } from '@anlyonhq/react';

<ConnectButton integration="github" />

Opens the connect link in a popup. It shows Reconnect GitHub when the connection stopped working, and GitHub connected once it works.

Connected accounts

import { Connections } from '@anlyonhq/react';

<Connections integrations={['github', 'slack', 'notion']} />

The user’s accounts with their status. Reconnect where needed. Disconnect asks first. integrations offers ones the user has not connected yet.

Connect prompt for AI SDK tool results

import { ConnectPrompt } from '@anlyonhq/react';

{message.parts.map((part) =>
  part.type.startsWith('tool-') && part.state === 'output-available' ? (
    <ConnectPrompt key={part.toolCallId} result={part.output} />
  ) : null,
)}

When an @anlyonhq/ai-sdk tool returns connect_required or reconnect_required, it renders a connect card. For any other result it renders nothing.

It takes mode and redirectUrl, which override the provider’s connect, onConnected(integration), called once the account is connected in popup mode (send the user’s message again from there), and name, the integration’s name in its labels.

Hooks

Each component is built on a hook, for your own markup: useApprovals, useApproval, useConnections and useConnect. Errors are AnlyonUIError, with the API’s status, code and message.

Theming

With shadcn/ui

Nothing to do. Each color reads the shadcn token of the same role (--background, --foreground, --primary, --muted, --border, --ring, --destructive, --radius), so the components follow your theme, light and dark. Tokens in OKLCH, hex or rgb() work as they are. If your theme stores bare HSL channels (--background: 0 0% 100%), add class="anlyon-shadcn-hsl" to <html>.

With CSS variables

Set --anlyon-* variables. They win over shadcn tokens.

:root {
  --anlyon-primary: #4f46e5;
  --anlyon-primary-fg: #ffffff;
  --anlyon-radius: 10px;
}

The variables are --anlyon-bg, -fg, -muted, -muted-fg, -accent, -border, -primary, -primary-fg, -destructive, -destructive-fg, -ring, -radius, -success and -warning.

Dark mode follows the system setting. A .dark or [data-theme="dark"] ancestor forces dark, and .light or [data-theme="light"] forces light.

Your own styles

Every component takes className. The bundled rules weigh no more than one class, so yours win. Pass unstyled to drop the bundled styles entirely; the anlyon-* class names and data-state attributes stay for your CSS. The package README lists them.

What stays on your server

  • Your API key, and minting session tokens.
  • Confirming a connection.
  • Calling actions, and everything else a session token cannot do.

If a session token leaks, it works for one user, until it expires, and only for the operations above. Browsers on other websites cannot use it; a script outside a browser can. Revoking the key that minted it ends it at once, and so does erasing that user.

Errors

Code Status Meaning
END_USER_SESSION_REQUIRED 401 No token was sent.
END_USER_SESSION_EXPIRED 401 The token expired. The provider fetches a new one and retries once.
END_USER_SESSION_INVALID 401 The token was altered, or its key was revoked.
END_USER_SESSION_SCOPE 403 The key that minted it lacks end-users:approvals.
ORIGIN_NOT_ALLOWED 403 The page’s origin is not one of the allowed redirect URLs’ origins.
END_USER_SESSION_NOT_ALLOWED 403 A session token was sent to any other route.
END_USER_SESSION_API_KEY_REQUIRED 403 Only an API key can mint a session.

Was this page helpful?