Tenant directories
Tenant directories show tenant records and their users inside your application. Let's provision records and embed a read-only user directory for a customer administrator, scoped to their own tenant.
From records to a working directory
Create the Tenant and TenantUser records from your server. Your Organization API key stays server-side. Keep the returned UUIDs for subsequent updates.
Let's create a customer and one of its users:
import { createTenant, createTenantUser } from "@astralbeam/sdk/api" const auth = { apiKey: organizationApiKey } const tenant = await createTenant({ external_id: "northwind", name: "Northwind Traders" }, auth) await createTenantUser( tenant.id, { external_id: "nancy", name: "Nancy", metadata: { department: "Support" }, }, auth, )These are create operations, not upserts. For repeatable synchronization, look up exact external IDs with
filter[external_id], then create or update using the returned UUIDs. If concurrent creation returns409, look up the existing record. See the API client guide.Authenticate the customer administrator in your host application before minting a token. Your authentication adapter must verify that this user is an administrator of the selected customer and reject unauthorized requests.
Let's connect that host-owned adapter to
/api/astralbeam/token:import { createAstralBeamToken } from "@astralbeam/sdk/server" import { requireCustomerAdmin } from "./auth" export async function POST(request: Request) { const headers = { "Cache-Control": "no-store" } const session = await requireCustomerAdmin(request) try { const token = await createAstralBeamToken({ apiKey: organizationApiKey, tenant: { id: session.tenant.externalId }, user: { id: session.user.externalId, admin: true }, }) return Response.json({ token }, { headers }) } catch { return Response.json({ error: "Token could not be issued" }, { status: 500, headers }) } }requireCustomerAdminandorganizationApiKeybelong to your application, not the SDK. The external IDs must match step 1. Map authentication failures to401or403in your host framework and never return signing errors or credentials. See authentication for token endpoint requirements.Embed the directory. Tenant scope is the default, and the SDK resolves the signed Tenant identity to its persisted record.
Let's render it without installing Tailwind or providing a Query client:
import { AstralBeamTenantUserList } from "@astralbeam/sdk/react" const tokenSource = { url: "/api/astralbeam/token" } export function CustomerUsers() { return <AstralBeamTenantUserList fetchAstralBeamToken={tokenSource} /> }
NOTE: The widget is read-only, but its signed tenant-admin JWT permits reading the Tenant and reading and writing its TenantUsers through the API. Stored admin is informational, not an authorization source.
Mount in React
Use AstralBeamTenantUserList for a tenant's users. AstralBeamTenantList shows Tenant records, limited to the signed tenant by default.
Let's render the current tenant's record in React:
import { AstralBeamTenantList } from "@astralbeam/sdk/react"
;<AstralBeamTenantList fetchAstralBeamToken={{ url: "/api/astralbeam/token" }} />Mount anywhere else
mountAstralBeamTenantUserList takes a target element and options, and returns a handle.
Let's mount the user directory and update its appearance:
import { mountAstralBeamTenantUserList } from "@astralbeam/sdk/client"
const handle = mountAstralBeamTenantUserList(document.getElementById("users")!, {
fetchAstralBeamToken: { url: "/api/astralbeam/token" },
})
handle.update({ colorScheme: "dark" })
handle.unmount()Use mountAstralBeamTenantList from the same entry point for Tenant records. The directory loads lazily with its own bundled React.
Tenant identifiers
Tenant-scoped embeds resolve the Tenant from the signed token automatically. If you supply tenantExternalId, matching is exact and case-sensitive, with whitespace preserved. tenantId is the internal UUID and takes precedence if both options are supplied. A missing external ID shows an empty state, never another Tenant's records. View options never grant authorization.
Account changes and lifecycle
Unmount before sign-out or a user or tenant switch, even if the token endpoint URL stays the same. Render the new directory only after the host session is ready.
Let's make the authenticated identity control the React lifecycle:
import { AstralBeamTenantUserList } from "@astralbeam/sdk/react"
const tokenSource = { url: "/api/astralbeam/token" }
export function CustomerUsers({ accountId }: { accountId: string | null }) {
if (accountId === null) return null
return <AstralBeamTenantUserList key={accountId} fetchAstralBeamToken={tokenSource} />
}Use an accountId that changes with either the signed-in user or tenant, and set it to null during transitions. Vanilla hosts call unmount() instead.
- React props and vanilla
update()apply live options. Inline token-source objects and callbacks do not clear the view. - React refs and vanilla handles expose
refresh()to reload queries andreset()to clear state and immediately reacquire authentication. reset()requires a ready host session. It is not a pause or sign-out operation.refresh()reloads data and may reuse the cached JWT. Token-source updates apply on the next token acquisition. Usereset()to reacquire authentication with a ready host session. API URL changes clear authentication and cached rows.- Changing scope or a pinned tenant clears the view. Changing page size restarts pagination while preserving filters.
Host callbacks
Let's connect user row actions and terminal request failures to your application:
<AstralBeamTenantUserList
fetchAstralBeamToken={{ url: "/api/astralbeam/token" }}
onTenantUserSelect={(user) => openUserDetails(user)}
onError={(error) => reportDirectoryError(error)}
/>openUserDetails and reportDirectoryError are host functions. Providing onTenantSelect or onTenantUserSelect adds an Open action. Clicking the name still expands metadata. Callbacks receive API records with snake_case field names.
onError reports each failed request after the automatic authentication retry finishes. It includes initial and background authentication failures, excludes cancellations and identity-change resets, and preserves the widget's error UI. Shared authentication failures report once, including when several requests are waiting. Use isAstralBeamApiError from /api to inspect HTTP status and structured problem details. Multiple failed requests can report separately, so deduplicate host notifications if needed. A 403 is a permission failure, not necessarily an expired session.
onTenantChange observes actual organization-scope picker changes, returning a Tenant record or null when cleared. It does not fire for initial or pinned values and does not make the picker controlled.
Options and behavior
Directories share chat's theme and colorScheme options. Reuse the same theme object across widgets. Removing overrides restores the SDK palette without resetting the directory. See Theming.
Because directories render inside a Shadow DOM, host styles and React className only affect the outer container. Let's pass application-owned CSS through customCss to customize the contents:
<AstralBeamTenantList
fetchAstralBeamToken={{ url: "/api/astralbeam/token" }}
customCss={`
[data-slot="directory"] { gap: 1.5rem; }
[data-slot="directory-toolbar"] { padding: 0.5rem; }
[data-slot="table"] { font-size: 0.75rem; }
`}
/>customCss is ordinary CSS, not uncompiled Tailwind classes. It applies only inside this widget. Changing or removing it updates styles without resetting filters, pagination, or authentication. Use handle.update({ customCss }) with vanilla mounts, or change the React prop. Pass only trusted application CSS, never user-supplied content.
The directory styling slots are directory, directory-header, directory-toolbar, directory-page-controls, directory-tenant-picker, directory-tenant-label, directory-empty, directory-page, directory-pagination, and directory-avatar. Shared control slots include input, input-group, native-select, button, combobox-content, alert, table-container, table, table-head, and table-cell. Select them with [data-slot="..."]. Prefer theme for colors and fonts, and customCss for layout, spacing, focus treatment, and component sizing.
| Option | Default | Purpose |
|---|---|---|
fetchAstralBeamToken | Required | Endpoint { url, ...RequestInit } or function returning { token }. Unlike chat, directories have no default token source. |
apiUrl | https://app.astralbeam.ai/api | AstralBeam API base. Set your deployment's /api URL when self-hosting. |
scope | "tenant" | Tenant view, or "organization" with an organization-management JWT. |
tenantId, tenantExternalId | Signed tenant in tenant scope | Pin a Tenant by internal UUID or exact external ID. Internal ID takes precedence. |
pageSize | 20 | Initial page size, one of 20, 50, or 100. |
title | "Tenants" or "Tenant users" | Header and accessible region name. |
showHeader | true | Show the directory heading and tenant context. |
customCss | None | Trusted CSS inside the widget's Shadow DOM. Omit to retain SDK styles. |
showAdmin | false | User directory only. Show the stored admin column and filter, without changing permissions. |
colorScheme, theme | "system", SDK palette | Chat-compatible appearance options. |
onTenantSelect | None | Tenant directory's Open action. |
onTenantUserSelect | None | User directory's Open action. |
onTenantChange | None | User directory's organization-scope picker changes. |
onError | None | Failed requests after authentication retry, excluding cancellations. |
- Previous/Next follow server cursors, with no estimated totals or client-side sorting.
- Search is debounced and matches literal text in names or external IDs. Collections use server-side search, while an explicitly selected single Tenant is filtered locally. Filters reset the current page.
- Hiding stored admin fields removes any active admin filter.
- Rows show external IDs under ID, names, metadata previews, and creation dates. Expand a name for full metadata. Internal UUIDs are not displayed.
- Tokens refresh once on HTTP
401. Other failures offer explicit retry.
NOTE: For uncommon internal-management use cases, both components also support scope="organization" with an organization-management token. The user directory then offers a searchable Tenant picker. Set tenantId or tenantExternalId to pin one Tenant, or when using that token with tenant scope. Tokens retain their delegated permissions even though the UI is read-only.
Troubleshooting
- An empty directory can mean no persisted records or no search matches. Tokens never create or synchronize records. Check provisioning first, then clear filters.
- A
403means the token lacks permission. Tenant views require signed tenant-admin authority. - A missing tenant shows an empty state. Check the token and record's external IDs for exact spelling and case.
- Stored admin controls are hidden intentionally. Enable
showAdminonly when the attribute is useful to your audience.