React SDK
@rootcx/sdk v0.10.0
Official React hooks for interacting with the RootCX runtime from your frontend applications.
Installation
$ npm install @rootcx/sdk
$ pnpm add @rootcx/sdk
$ yarn add @rootcx/sdk
The SDK requires React 18+ and TypeScript 5+ (recommended). It is ESM-only and works with Next.js, Vite, and other modern bundlers.
https://<your-ref>.rootcx.com). If self-hosting, use http://localhost:9100.// src/main.tsx
import { RuntimeProvider } from "@rootcx/sdk";
import App from "./App";
createRoot(document.getElementById("root")!).render(
<RuntimeProvider baseUrl="https://<your-ref>.rootcx.com">
<App />
</RuntimeProvider>
);
useAuth
Manage authentication state -- login, registration, logout, OIDC, and the current user.
import { useAuth } from "@rootcx/sdk";
const {
user, // AuthUser | null -- current user, null if not authenticated
isAuthenticated, // boolean
loading, // boolean -- true while checking session or performing auth
authMode, // AuthMode | null -- server auth configuration
login, // (email: string, password: string) => Promise<void>
register, // (data: RegisterInput) => Promise<void>
logout, // () => Promise<void>
oidcLogin, // (providerId: string) => Promise<void>
} = useAuth();
The authMode object contains { authRequired, setupRequired, passwordLoginEnabled, providers } where providers is an array of OidcProvider ({ id, displayName }). Use it to show OIDC login buttons.
The oidcLogin method redirects the browser to the OIDC provider's authorization page. After authentication, the user is redirected back and tokens are consumed automatically from the URL.
// src/LoginPage.tsx
import { useAuth } from "@rootcx/sdk";
import { useState } from "react";
export function LoginPage() {
const { login, oidcLogin, authMode, loading } = useAuth();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
return (
<div>
{authMode?.passwordLoginEnabled && (
<form onSubmit={async (e) => { e.preventDefault(); await login(email, password); }}>
<input value={email} onChange={e => setEmail(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit" disabled={loading}>Sign in</button>
</form>
)}
{authMode?.providers.map(p => (
<button key={p.id} onClick={() => oidcLogin(p.id)}>
Sign in with {p.displayName}
</button>
))}
</div>
);
}
useAppCollection
Fetch and mutate a collection of records for a given entity. Data is loaded on mount and re-fetched after mutations.
import { useAppCollection } from "@rootcx/sdk";
const {
data, // T[] -- array of records
total, // number -- total record count
loading, // boolean
error, // string | null
create, // (data: Record<string, unknown>) => Promise<T>
bulkCreate, // (data: Record<string, unknown>[]) => Promise<T[]>
update, // (id: string, data: Record<string, unknown>) => Promise<T>
remove, // (id: string) => Promise<void>
refetch, // () => void -- manually re-fetch
} = useAppCollection<T>("appId", "entityName", query?);
The optional query parameter accepts a QueryOptions object to filter and sort results:
useAppCollection<Contact>("crm", "contacts", {
where: { stage: "lead" },
orderBy: "created_at",
order: "desc",
limit: 50,
offset: 0,
});
QueryOptions
| Option | Type | Description |
|---|---|---|
where |
WhereClause |
Filter conditions using MongoDB-style DSL. |
orderBy |
string |
Column to sort by. |
order |
"asc" | "desc" |
Sort direction. |
limit |
number |
Max records to return. |
offset |
number |
Number of records to skip. |
linked |
boolean | string[] |
Resolve entity_link references. |
Where operators
| Operator | Description | Example |
|---|---|---|
$eq |
Equal | { status: { $eq: "active" } } or { status: "active" } |
$ne |
Not equal | { status: { $ne: "archived" } } |
$gt, $gte |
Greater than (or equal) | { amount: { $gt: 100 } } |
$lt, $lte |
Less than (or equal) | { amount: { $lt: 1000 } } |
$like, $ilike |
Pattern match (case-sensitive / insensitive) | { name: { $ilike: "%acme%" } } |
$in, $nin |
In / not in array | { status: { $in: ["active", "lead"] } } |
$contains |
Array contains | { tags: { $contains: "vip" } } |
$isNull |
Is null check | { email: { $isNull: true } } |
Combine with $and, $or, $not:
where: {
$or: [
{ stage: "lead" },
{ amount: { $gt: 10000 } }
]
}
useCoreCollection
Read-only access to core system collections (currently users).
import { useCoreCollection } from "@rootcx/sdk";
const {
data, // T[]
loading, // boolean
error, // string | null
refetch, // () => void
} = useCoreCollection<User>("users");
No create/update/delete operations — core collections are managed by the platform.
useAppRecord
Fetch and manage a single record by ID.
import { useAppRecord } from "@rootcx/sdk";
const {
data, // T | null
loading, // boolean
error, // string | null
refetch, // () => void -- manually re-fetch
update, // (data: Record<string, unknown>) => Promise<T>
remove, // () => Promise<void>
} = useAppRecord<T>("appId", "entityName", recordId); // recordId: string | null
If recordId is null, nothing is fetched and loading is immediately false.
usePermissions
Check the current user's RBAC permissions. Permission keys are global (not scoped per-app).
import { usePermissions } from "@rootcx/sdk";
const {
roles, // string[] -- resolved roles for the current user
permissions, // string[] -- flat list of permission strings
can, // (permission: string) => boolean -- supports wildcards
loading, // boolean
error, // string | null
refetch, // () => void
} = usePermissions();
The can() method supports the same wildcard matching as the server: * matches everything, app:crm:* matches any app:crm: prefix.
import { usePermissions } from "@rootcx/sdk";
export function ContactActions({ contactId }: { contactId: string }) {
const { can } = usePermissions();
return (
<div>
{can("app:crm:contacts.update") && <button>Edit</button>}
{can("app:crm:contacts.delete") && <button className="text-red-500">Delete</button>}
</div>
);
}
usePermissions is for UI convenience only. Never rely on client-side permission checks for security. The Core daemon enforces RBAC on every request server-side.PermissionsProvider
Wraps a subtree to share a single permissions fetch across multiple <Authorized> components and usePermissionsContext() calls.
import { PermissionsProvider, Authorized } from "@rootcx/sdk";
function App() {
return (
<PermissionsProvider>
<Authorized permission="app:crm:contacts.read">
<ContactsList />
</Authorized>
<Authorized permission="app:crm:contacts.create">
<NewContactButton />
</Authorized>
</PermissionsProvider>
);
}
AuthGate
Pre-built authentication UI. Wrap your app to show a login/registration form when the user is not authenticated. Supports OIDC providers automatically.
import { AuthGate } from "@rootcx/sdk";
function App() {
return (
<AuthGate appTitle="My App">
{({ user, logout }) => (
<div>
<p>Welcome, {user.displayName}</p>
<button onClick={logout}>Logout</button>
<MyProtectedApp />
</div>
)}
</AuthGate>
);
}
AuthGate accepts:
appTitle— shown in the login form header.renderLoading— custom loading component.renderForm— custom form component (receivesAuthFormSlotPropswith mode, error, submitting, onSubmit, providers, passwordLoginEnabled, onOidcLogin).children— render function receiving{ user, logout }.
Authorized
Conditionally render children based on RBAC permissions:
import { Authorized } from "@rootcx/sdk";
<Authorized permission="app:crm:contacts.delete" fallback={<span>No access</span>}>
<button>Delete Contact</button>
</Authorized>
useCrons
Create, manage, and trigger scheduled jobs from your frontend.
import { useCrons } from "@rootcx/sdk";
const {
data, // CronSchedule[] -- all crons for this app
loading, // boolean
error, // string | null
refetch, // () => void
create, // (input: CreateCronInput) => Promise<CronSchedule>
update, // (id: string, patch: UpdateCronInput) => Promise<CronSchedule>
remove, // (id: string) => Promise<void>
trigger, // (id: string) => Promise<{ msgId: number }> -- fire immediately
} = useCrons("appId");
Create a cron that runs every morning at 9 AM:
await create({
name: "daily-check",
schedule: "0 9 * * *",
payload: { campaignId: "abc123" },
});
The schedule accepts standard 5-field cron expressions (0 9 * * *) or a seconds interval (10 seconds, 1-59). Use $ in the day-of-month field for the last day of the month. All times are GMT unless timezone is set.
The overlapPolicy controls what happens when a cron fires while the previous job is still queued: "skip" (default) drops duplicates, "queue" enqueues anyway.
// Pause a cron
await update(cronId, { enabled: false });
// Resume with a new schedule
await update(cronId, { enabled: true, schedule: "*/5 * * * *" });
// Fire once right now (for testing)
await trigger(cronId);
// Delete
await remove(cronId);
Cron jobs are delivered to the backend's onJob handler. See Scheduled Jobs for the full flow.
useIntegration
Connect to and interact with an integration's actions:
import { useIntegration } from "@rootcx/sdk";
const {
connected, // boolean -- whether the integration is authenticated
loading, // boolean
connect, // () => Promise<{ type: string; schema?: Record<string, unknown> } | void>
submitCredentials, // (credentials: Record<string, string>) => Promise<void>
disconnect, // () => Promise<void>
call, // (action: string, input?: Record<string, unknown>) => Promise<unknown>
} = useIntegration("slack");
// Call an action once connected
await call("send_message", { channel: "#general", text: "Hello!" });
The connect() method returns either void (for redirect-based OAuth — opens a popup and polls for completion) or { type: "credentials", schema } (for manual credential entry — pass collected values to submitCredentials).
useIdentity
Query federated identity records across multiple apps:
import { useIdentity } from "@rootcx/sdk";
const {
data, // IdentityRecord<T>[] -- records with _source metadata
total, // number
loading, // boolean
error, // string | null
refetch, // () => void
} = useIdentity<Contact>("contact", {
where: { email: { $ilike: "%acme%" } },
});
Each record includes _source: { app: string; entity: string } indicating its origin.
Direct HTTP client
For use cases not covered by the hooks, import the underlying HTTP client directly:
import { RuntimeClient } from "@rootcx/sdk";
const client = new RuntimeClient({ baseUrl: "https://<your-ref>.rootcx.com" });
// Authenticate
await client.login("alice@example.com", "password");
// CRUD
const { data, total } = await client.queryRecords("crm", "contacts", {
where: { stage: "lead" },
limit: 10,
});
const created = await client.createRecord("crm", "contacts", {
first_name: "Alice",
email: "alice@example.com",
});
// RPC
const result = await client.rpc("crm", "sendWelcomeEmail", {
contactId: created.id,
});
// Roles & permissions
const roles = await client.listRoles();
await client.createRole({ name: "viewer", permissions: ["app:crm:contacts.read"] });
await client.assignRole(userId, "viewer");
const perms = await client.getPermissions(); // current user
// Core collections (read-only)
const users = client.core().collection("users");
// Integrations
await client.callIntegration("slack", "send_message", { channel: "#general", text: "Hello!" });
// Jobs
await client.enqueueJob("crm", { action: "sync" });
const jobs = await client.listJobs("crm");
// Crons
const crons = await client.listCrons("crm");
const cron = await client.createCron("crm", { name: "nightly-sync", schedule: "0 0 * * *" });
await client.updateCron("crm", cron.id, { enabled: false });
await client.triggerCron("crm", cron.id);
await client.deleteCron("crm", cron.id);
TypeScript types
Key types exported from @rootcx/sdk:
// Authenticated user
interface AuthUser {
id: string;
email: string;
displayName: string | null;
createdAt: string;
}
// Auth mode (server configuration)
interface AuthMode {
authRequired: boolean;
setupRequired: boolean;
passwordLoginEnabled: boolean;
providers: OidcProvider[];
}
interface OidcProvider {
id: string;
displayName: string;
}
// Login response
interface LoginResponse {
accessToken: string;
refreshToken: string;
expiresIn: number;
user: AuthUser;
}
// Registration input
interface RegisterInput {
email: string;
password: string;
displayName?: string;
}
// Effective permissions
interface EffectivePermissions {
roles: string[];
permissions: string[];
}
// Role definition
interface RoleDefinition {
name: string;
description: string | null;
inherits: string[];
permissions: string[];
}
// Role assignment
interface RoleAssignment {
userId: string;
role: string;
assignedAt: string;
}
// Permission declaration
interface PermissionDeclaration {
key: string;
description: string;
}
// Query types
type WhereOperator = "$eq" | "$ne" | "$gt" | "$gte" | "$lt" | "$lte"
| "$like" | "$ilike" | "$in" | "$nin" | "$contains" | "$isNull";
interface QueryOptions {
where?: WhereClause;
orderBy?: string;
order?: "asc" | "desc";
limit?: number;
offset?: number;
linked?: boolean | string[];
}
interface QueryResult<T> {
data: T[];
total: number;
}
// Identity record (federated)
type IdentityRecord<T> = T & { _source: { app: string; entity: string } };
// Integration
interface IntegrationSummary {
id: string;
name: string;
version: string;
description: string;
actions: ActionDefinition[];
configSchema: Record<string, unknown>;
webhooks: string[];
}
// Job
interface Job {
msg_id: number;
app_id: string;
payload: Record<string, unknown>;
user_id: string | null;
read_ct: number;
enqueued_at: string;
}
// Cron
type OverlapPolicy = "skip" | "queue";
interface CronSchedule {
id: string;
appId: string;
name: string;
schedule: string;
timezone: string | null;
payload: Record<string, unknown>;
overlapPolicy: OverlapPolicy;
enabled: boolean;
pgJobId: number | null;
createdBy: string | null;
createdAt: string;
updatedAt: string;
}
interface CreateCronInput {
name: string;
schedule: string;
timezone?: string;
payload?: Record<string, unknown>;
overlapPolicy?: OverlapPolicy;
}
interface UpdateCronInput {
schedule?: string;
payload?: Record<string, unknown>;
overlapPolicy?: OverlapPolicy;
enabled?: boolean;
}