Cross-Tenant Identity
Steward supports multi-tenant authentication. A single user identity can belong to multiple apps (tenants), each with its own agents, policies, and configuration. The user keeps the same wallet and credentials across all apps.
How It Works
When a user signs up through any auth method, Steward creates a user record tied to their email or wallet address. This user can then join additional tenants without creating new accounts.
User (user@example.com)
├── Tenant: "trading-app" → role: admin
├── Tenant: "nft-gallery" → role: member
└── Tenant: "defi-dashboard" → role: member
The JWT access token includes a tenantId claim that scopes all API operations. Switching tenants refreshes the JWT with a new tenant context.
Join Modes
Each tenant controls how new users can join:
| Mode | Description |
|---|
open | Anyone can join by calling joinTenant(id) |
invite | Users must be invited by a tenant admin |
closed | No new members allowed |
SDK Usage
List Tenants
const auth = new StewardAuth({
baseUrl: "https://api.steward.fi",
storage: localStorage,
});
// After signing in...
const tenants = await auth.listTenants();
console.log(tenants);
// [
// { tenantId: "trading-app", tenantName: "Trading App", role: "admin", joinedAt: "..." },
// { tenantId: "nft-gallery", tenantName: "NFT Gallery", role: "member", joinedAt: "..." },
// ]
Switch Tenant
Switching tenants refreshes the session JWT with a new tenantId claim:
const session = await auth.switchTenant("nft-gallery");
if (session) {
console.log("Now in:", session.tenantId); // "nft-gallery"
} else {
console.log("Switch failed, user may need to re-authenticate");
}
Join a Tenant
const membership = await auth.joinTenant("defi-dashboard");
console.log(membership);
// { tenantId: "defi-dashboard", tenantName: "DeFi Dashboard", role: "member", joinedAt: "..." }
Leave a Tenant
await auth.leaveTenant("defi-dashboard");
Users cannot leave their personal/default tenant. Attempting to leave the tenant they signed up with will throw an error.
React Usage
Tenant Picker
The <StewardTenantPicker> component renders a dropdown or list for switching between apps:
import { StewardTenantPicker } from "@stwd/react";
// Dropdown variant (compact, click to expand)
<StewardTenantPicker
variant="dropdown"
onSwitch={(tenantId) => console.log("Switched to:", tenantId)}
/>
// List variant (always visible, good for settings pages)
<StewardTenantPicker variant="list" />
The component shows the active tenant, lists all memberships, and handles switching via auth.switchTenant(). It auto-fetches tenant memberships when the user signs in.
The <StewardUserButton> can include an inline tenant switcher:
import { StewardUserButton } from "@stwd/react";
<StewardUserButton
showTenantSwitcher // adds tenant switching inside the dropdown
showWallet // show wallet address
onSignOut={() => router.push("/login")}
/>
Login with Tenant Context
Pass a tenantId to <StewardLogin> to scope authentication to a specific app:
<StewardLogin
tenantId="trading-app"
showGoogle
showPasskey
title="Sign in to Trading App"
/>
API Endpoints
| Endpoint | Method | Description |
|---|
/user/me/tenants | GET | List all tenants the current user belongs to |
/user/me/tenants/{tenantId}/join | POST | Join an open tenant |
/user/me/tenants/{tenantId}/leave | DELETE | Leave a tenant |
/auth/refresh | POST | Refresh with { refreshToken, tenantId } to switch tenant |
Data Isolation
Each tenant is a fully isolated environment:
- Agents are scoped to a tenant. Agent IDs are unique within a tenant.
- Policies are set per-agent, per-tenant.
- Secrets and routes are tenant-scoped.
- Transaction history is per-tenant.
The user’s wallet address is shared across tenants, but all operational data is isolated.