Installation
npm install @raystack/frontier
Peer Dependencies
The React SDK requires the following peer dependencies:{
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@raystack/apsara": ">=0.30.0"
}
Quick Start
Wrap your app with FrontierProvider
The
FrontierProvider component provides authentication context and React Query setup:import { FrontierProvider } from '@raystack/frontier/react';
function App() {
return (
<FrontierProvider
config={{
endpoint: 'http://localhost:8080',
connectEndpoint: '/frontier-connect',
redirectLogin: window.location.origin + '/login',
redirectSignup: window.location.origin + '/signup',
redirectMagicLinkVerify: window.location.origin + '/verify',
callbackUrl: window.location.origin + '/callback'
}}
>
<YourApp />
</FrontierProvider>
);
}
Use the useFrontier hook
Access user data, organizations, and billing information:
import { useFrontier } from '@raystack/frontier/react';
function Profile() {
const { user, activeOrganization, isUserLoading } = useFrontier();
if (isUserLoading) return <div>Loading...</div>;
if (!user) return <div>Please log in</div>;
return (
<div>
<h1>Welcome, {user.name}</h1>
<p>Email: {user.email}</p>
<p>Organization: {activeOrganization?.name}</p>
</div>
);
}
Configuration
FrontierProvider Props
TheFrontierProvider accepts the following configuration:
interface FrontierClientOptions {
endpoint: string; // Frontier API endpoint
connectEndpoint?: string; // Connect RPC endpoint (default: '/frontier-connect')
redirectLogin?: string; // Login redirect URL
redirectSignup?: string; // Signup redirect URL
redirectMagicLinkVerify?: string; // Magic link verification URL
callbackUrl?: string; // OAuth callback URL
dateFormat?: string; // Date format (default: 'DD MMM YYYY, hh:mm A')
shortDateFormat?: string; // Short date format
billing?: {
supportEmail?: string; // Billing support email
successUrl?: string; // Payment success redirect
cancelUrl?: string; // Payment cancel redirect
hideDecimals?: boolean; // Hide decimal amounts
cancelAfterTrial?: boolean; // Cancel subscription after trial
showPerMonthPrice?: boolean; // Show per-month pricing
tokenProductId?: string; // Token product ID
basePlan?: BasePlan; // Base/free plan configuration
};
customization?: {
terminology?: { // Customize entity names
organization?: { singular: string; plural: string };
project?: { singular: string; plural: string };
team?: { singular: string; plural: string };
member?: { singular: string; plural: string };
user?: { singular: string; plural: string };
appName?: string;
};
messages?: { // Custom messages
billing?: { plan_change?: Record<string, string> };
general?: Record<string, string>;
};
};
}
Custom Headers
Add custom headers to all API requests:<FrontierProvider
config={{...}}
customHeaders={{
'X-Custom-Header': 'value',
'Authorization': () => `Bearer ${getToken()}` // Dynamic headers
}}
>
{children}
</FrontierProvider>
Theming
Customize the visual theme using Apsara’s theme provider:<FrontierProvider
config={{...}}
theme={{
theme: 'dark',
accentColor: 'blue'
}}
>
{children}
</FrontierProvider>
Core Hooks
useFrontier
The main hook for accessing Frontier context:import { useFrontier } from '@raystack/frontier/react';
function MyComponent() {
const {
// User data
user, // Current user object
isUserLoading, // User loading state
// Organizations
organizations, // All user's organizations
activeOrganization, // Currently selected organization
setActiveOrganization, // Set active organization
isActiveOrganizationLoading, // Organization loading state
// Groups/Teams
groups, // User's groups
// Billing
billingAccount, // Active billing account
isBillingAccountLoading, // Billing account loading state
paymentMethod, // Default payment method
activeSubscription, // Active subscription
trialSubscription, // Trial subscription
activePlan, // Active plan details
trialPlan, // Trial plan details
allPlans, // All available plans
fetchActiveSubscription, // Refetch subscription
// Organization details
organizationKyc, // KYC information
isOrganizationKycLoading, // KYC loading state
billingDetails, // Billing details
// Configuration
config, // Frontier configuration
// Session metadata
sessionMetadata, // Browser, IP, location info
} = useFrontier();
return <div>...</div>;
}
usePermissions
Check user permissions for resources:import { usePermissions } from '@raystack/frontier/react';
function ResourceActions({ resourceId }) {
const { permissions, isFetching } = usePermissions([
{ permission: 'read', resource: resourceId },
{ permission: 'write', resource: resourceId },
{ permission: 'delete', resource: resourceId }
]);
return (
<div>
{permissions['read'] && <button>View</button>}
{permissions['write'] && <button>Edit</button>}
{permissions['delete'] && <button>Delete</button>}
</div>
);
}
useTokens
Manage billing token balance:import { useTokens } from '@raystack/frontier/react';
function TokenBalance() {
const { tokenBalance, isTokensLoading, fetchTokenBalance } = useTokens();
return (
<div>
<p>Balance: {tokenBalance.toString()} tokens</p>
<button onClick={fetchTokenBalance}>Refresh</button>
</div>
);
}
useOrganizationMembers
Fetch and manage organization members:import { useOrganizationMembers } from '@raystack/frontier/react';
function MembersList() {
const {
members, // Array of members
memberRoles, // Member role mappings
roles, // Available roles
isFetching, // Loading state
refetch, // Refetch function
error // Error state
} = useOrganizationMembers({
showInvitations: true // Include pending invitations
});
return (
<ul>
{members.map(member => (
<li key={member.id}>
{member.name} - {member.email}
{member.invited && <span>(Pending)</span>}
</li>
))}
</ul>
);
}
usePreferences
Access user preferences:import { usePreferences } from '@raystack/frontier/react';
function Settings() {
const preferences = usePreferences();
return <div>Timezone: {preferences.timezone}</div>;
}
useBillingPermission
Check billing-related permissions:import { useBillingPermission } from '@raystack/frontier/react';
function BillingSettings() {
const { canManageBilling, isLoading } = useBillingPermission();
if (!canManageBilling) return <div>Access denied</div>;
return <div>Billing settings...</div>;
}
Pre-built Components
Authentication Components
- SignIn
- SignUp
- MagicLink
- MagicLinkVerify
import { SignIn } from '@raystack/frontier/react';
<SignIn
logo={<img src="/logo.png" />}
title="Welcome back"
excludes={['google']} // Exclude specific OAuth providers
footer={true}
/>
import { SignUp } from '@raystack/frontier/react';
<SignUp
logo={<img src="/logo.png" />}
title="Create your account"
excludes={['github']}
footer={true}
/>
import { MagicLink } from '@raystack/frontier/react';
<MagicLink
title="Sign in with email"
description="We'll send you a magic link"
/>
import { MagicLinkVerify } from '@raystack/frontier/react';
<MagicLinkVerify />
Organization Components
- CreateOrganization
- OrganizationProfile
import { CreateOrganization } from '@raystack/frontier/react';
<CreateOrganization
title="Create a new organization"
description="Organizations are shared environments where teams collaborate"
/>
import { OrganizationProfile } from '@raystack/frontier/react';
<OrganizationProfile />
Other Components
import {
AvatarUpload, // Avatar upload with cropping
Container, // Layout container
Header, // Page header
PageHeader, // Common page header
Layout, // Main layout wrapper
Subscribe, // Subscription management
Updates, // User updates/notifications
Window // Modal window
} from '@raystack/frontier/react';
Connect Query Hooks
Direct access to Connect Query hooks for advanced use cases:import {
useQuery,
useMutation,
useInfiniteQuery,
FrontierServiceQueries,
create
} from '@raystack/frontier/hooks';
import {
ListProjectsRequestSchema
} from '@raystack/proton/frontier';
function ProjectsList({ orgId }) {
const { data, isLoading } = useQuery(
FrontierServiceQueries.listProjects,
create(ListProjectsRequestSchema, { orgId })
);
return (
<ul>
{data?.projects?.map(project => (
<li key={project.id}>{project.name}</li>
))}
</ul>
);
}
Mutations
import { useMutation, FrontierServiceQueries, create } from '@raystack/frontier/hooks';
import { UpdateUserRequestSchema } from '@raystack/proton/frontier';
function UpdateProfile() {
const { mutateAsync: updateUser, isPending } = useMutation(
FrontierServiceQueries.updateCurrentUser
);
const handleUpdate = async () => {
await updateUser(
create(UpdateUserRequestSchema, {
body: { name: 'New Name' }
})
);
};
return <button onClick={handleUpdate} disabled={isPending}>Update</button>;
}
Utilities
Timestamp Utilities
Convert Protocol Buffer timestamps:import {
timestampToDate,
timestampToDayjs,
isNullTimestamp
} from '@raystack/frontier/react';
const date = timestampToDate(user.createdAt);
const dayjs = timestampToDayjs(user.updatedAt);
const isNull = isNullTimestamp(user.deletedAt);
Terminology
Access customized terminology:import { useTerminology } from '@raystack/frontier/react';
function Header() {
const t = useTerminology();
return <h1>Create a new {t.organization.singular}</h1>;
}
Advanced Usage
Custom Transport
Provide a custom Connect transport:import { createConnectTransport } from '@connectrpc/connect-web';
import { TransportProvider } from '@connectrpc/connect-query';
const transport = createConnectTransport({
baseUrl: 'https://api.example.com',
interceptors: [/* custom interceptors */]
});
<TransportProvider transport={transport}>
<App />
</TransportProvider>
Query Client Configuration
Access and configure the React Query client:import { queryClient } from '@raystack/frontier/react';
// Configure default options
queryClient.setDefaultOptions({
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
retry: 3
}
});
// Invalidate queries
queryClient.invalidateQueries({ queryKey: ['organizations'] });
Polling
Use the polling hook for real-time updates:import { useConnectQueryPolling } from '@raystack/frontier/react';
import { FrontierServiceQueries } from '@raystack/frontier/hooks';
function RealtimeData() {
const { data } = useConnectQueryPolling(
FrontierServiceQueries.getUser,
{ id: 'user-123' },
{
interval: 5000, // Poll every 5 seconds
enabled: true
}
);
return <div>{data?.user?.name}</div>;
}
TypeScript Support
The React SDK is fully typed. Import types from the appropriate packages:import type {
User,
Organization,
Project,
Subscription,
Plan,
BillingAccount,
PaymentMethod
} from '@raystack/proton/frontier';
import type {
FrontierClientOptions,
FrontierClientBillingOptions,
FrontierClientCustomizationOptions
} from '@raystack/frontier/react';
Error Handling
import { ConnectError, Code } from '@raystack/frontier/hooks';
function MyComponent() {
const { data, error } = useQuery(...);
if (error) {
if (error instanceof ConnectError) {
switch (error.code) {
case Code.Unauthenticated:
return <div>Please log in</div>;
case Code.PermissionDenied:
return <div>Access denied</div>;
case Code.NotFound:
return <div>Not found</div>;
default:
return <div>Error: {error.message}</div>;
}
}
}
return <div>...</div>;
}
Best Practices
Single FrontierProvider Instance
Single FrontierProvider Instance
Always use a single
FrontierProvider at the root of your application. Multiple providers will cause errors.// Good
<FrontierProvider config={{...}}>
<App />
</FrontierProvider>
// Bad - multiple providers
<FrontierProvider config={{...}}>
<FrontierProvider config={{...}}>
<App />
</FrontierProvider>
</FrontierProvider>
Conditional Hook Usage
Conditional Hook Usage
Use the
enabled option to conditionally execute queries:const { activeOrganization } = useFrontier();
const { data } = useQuery(
FrontierServiceQueries.listProjects,
{ orgId: activeOrganization?.id },
{ enabled: !!activeOrganization?.id } // Only run when org is selected
);
Optimistic Updates
Optimistic Updates
Use optimistic updates for better UX:
const queryClient = useQueryClient();
const { mutateAsync } = useMutation(...);
const handleUpdate = async (data) => {
// Optimistically update UI
queryClient.setQueryData(['user'], data);
try {
await mutateAsync(data);
} catch (error) {
// Rollback on error
queryClient.invalidateQueries({ queryKey: ['user'] });
}
};
Lazy Loading
Lazy Loading
Load Frontier components lazily for better performance:
import { lazy, Suspense } from 'react';
const SignIn = lazy(() => import('@raystack/frontier/react').then(m => ({ default: m.SignIn })));
function LoginPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<SignIn />
</Suspense>
);
}
Examples
Complete Authentication Flow
import { FrontierProvider, useFrontier, SignIn } from '@raystack/frontier/react';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
function App() {
return (
<FrontierProvider
config={{
endpoint: process.env.REACT_APP_FRONTIER_ENDPOINT,
redirectLogin: '/login',
redirectSignup: '/signup'
}}
>
<Router />
</FrontierProvider>
);
}
function Router() {
const { user, isUserLoading } = useFrontier();
const navigate = useNavigate();
useEffect(() => {
if (!isUserLoading && !user) {
navigate('/login');
}
}, [user, isUserLoading, navigate]);
if (isUserLoading) return <div>Loading...</div>;
return user ? <Dashboard /> : <LoginPage />;
}
function LoginPage() {
return <SignIn title="Welcome to MyApp" />;
}
function Dashboard() {
const { user, activeOrganization } = useFrontier();
return (
<div>
<h1>Welcome, {user?.name}</h1>
<p>Organization: {activeOrganization?.name}</p>
</div>
);
}
Multi-tenant Organization Switcher
import { useFrontier } from '@raystack/frontier/react';
function OrganizationSwitcher() {
const { organizations, activeOrganization, setActiveOrganization } = useFrontier();
return (
<select
value={activeOrganization?.id}
onChange={(e) => {
const org = organizations.find(o => o.id === e.target.value);
setActiveOrganization(org);
}}
>
{organizations.map(org => (
<option key={org.id} value={org.id}>
{org.name}
</option>
))}
</select>
);
}
Subscription Management
import { useFrontier } from '@raystack/frontier/react';
import { useMutation, FrontierServiceQueries } from '@raystack/frontier/hooks';
function SubscriptionManager() {
const {
activeSubscription,
allPlans,
billingAccount
} = useFrontier();
const { mutateAsync: createCheckout } = useMutation(
FrontierServiceQueries.createCheckout
);
const handleUpgrade = async (planId: string) => {
const result = await createCheckout({
billingId: billingAccount?.id,
body: {
planId,
successUrl: window.location.origin + '/success',
cancelUrl: window.location.origin + '/cancel'
}
});
if (result.checkoutSession?.checkoutUrl) {
window.location.href = result.checkoutSession.checkoutUrl;
}
};
return (
<div>
<h2>Current Plan: {activeSubscription?.planId}</h2>
<div>
{allPlans.map(plan => (
<div key={plan.id}>
<h3>{plan.title}</h3>
<p>{plan.description}</p>
<button onClick={() => handleUpgrade(plan.id)}>
Upgrade
</button>
</div>
))}
</div>
</div>
);
}
Next Steps
JavaScript SDK
Learn about the core JavaScript SDK
API Reference
Explore the complete API documentation
Authentication Guide
Deep dive into authentication
Billing Guide
Set up billing and subscriptions