React SDK
@rootcx/sdk
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.
[!INFO] Runtime URL Point the SDK at your project's API URL (e.g.
https://<your-ref>.rootcx.com). If self-hosting, usehttp://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, 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
login, // (username: string, password: string) => Promise<void>
register, // (data: RegisterInput) => Promise<void>
logout, // () => Promise<void>
} = useAuth();
// src/LoginPage.tsx
import { useAuth } from "@rootcx/sdk";
import { useState } from "react";
export function LoginPage() {
const { login, loading } = useAuth();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await login(username, password);
// On success, user state updates and isAuthenticated becomes true
};
return (
<form onSubmit={handleSubmit}>
<input value={username} onChange={e => setUsername(e.target.value)} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit" disabled={loading}>
{loading ? "Signing in..." : "Sign in"}
</button>
</form>
);
}
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,
});
| Option | Type | Description |
|---|---|---|
where |
Record<string, unknown> |
Filter conditions. |
orderBy |
string |
Column to sort by. |
order |
"asc" | "desc" |
Sort direction. |
limit |
number |
Max records to return. |
offset |
number |
Number of records to skip. |
// src/ContactsList.tsx
import { useAppCollection } from "@rootcx/sdk";
type Contact = {
id: string;
first_name: string;
last_name: string;
email: string;
created_at: string;
};
export function ContactsList() {
const { data, loading, error, create, remove } = useAppCollection<Contact>(
"crm",
"contacts"
);
const addContact = async () => {
await create({
first_name: "Bob",
last_name: "Smith",
email: "bob@example.com",
});
// data array updates automatically
};
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<button onClick={addContact}>Add Contact</button>
<ul>
{data.map(c => (
<li key={c.id}>
{c.first_name} {c.last_name} -- {c.email}
<button onClick={() => remove(c.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
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
// src/ContactDetail.tsx
import { useAppRecord } from "@rootcx/sdk";
export function ContactDetail({ id }: { id: string }) {
const { data, loading, update } = useAppRecord<Contact>(
"crm", "contacts", id
);
if (loading) return <Spinner />;
if (!data) return <div>Not found</div>;
return (
<div>
<h1>{data.first_name} {data.last_name}</h1>
<button onClick={() => update({ email: "new@email.com" })}>
Update email
</button>
</div>
);
}
usePermissions
Check the current user's RBAC permissions for a given app. Use to conditionally show/hide UI elements.
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
loading, // boolean
error, // string | null
refetch, // () => void
} = usePermissions("appId");
import { usePermissions } from "@rootcx/sdk";
export function ContactActions({ contactId }: { contactId: string }) {
const { can } = usePermissions("crm");
return (
<div>
{can("contacts.update") && (
<button>Edit</button>
)}
{can("contacts.delete") && (
<button className="text-red-500">Delete</button>
)}
</div>
);
}
[!WARNING] Client-side only
usePermissionsis for UI convenience only. Never rely on client-side permission checks for security. The Core daemon enforces RBAC on every request server-side.
AuthGate
Pre-built authentication UI. Wrap your app to show a login/registration form when the user is not authenticated:
import { AuthGate } from "@rootcx/sdk";
function App() {
return (
<AuthGate>
<MyProtectedApp />
</AuthGate>
);
}
Authorized
Conditionally render children based on RBAC permissions:
import { Authorized } from "@rootcx/sdk";
<Authorized appId="crm" permission="contacts.delete">
<button>Delete Contact</button>
</Authorized>
useIntegration
Connect to and interact with a bound 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("myApp", "slack");
// Call an action once connected
await call("send_message", { channel: "#general", text: "Hello!" });
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", "password");
// CRUD
const contacts = await client.listRecords("crm", "contacts");
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,
});
TypeScript types
Key types exported from @rootcx/sdk:
// Authenticated user
export interface AuthUser {
id: string;
username: string;
email: string | null;
displayName: string | null;
createdAt: string;
}
// Login response
export interface LoginResponse {
accessToken: string;
refreshToken: string;
expiresIn: number;
user: AuthUser;
}
// Registration input
export interface RegisterInput {
username: string;
password: string;
email?: string;
displayName?: string;
}
// Effective permissions for a user in an app
export interface EffectivePermissions {
roles: string[];
permissions: string[];
}