RootCX
Docs
Pricing
RootCX/RootCXSource Available
Introduction
What is RootCX?Getting StartedHow it Works
Build
ApplicationAI AgentIntegrationDeploying
Platform
CoreAuthenticationRBACData APISecret VaultJob QueueScheduled Jobs (Crons)Audit LogReal-time LogsChannels
Developers
React SDKBackend & RPCManifest ReferenceREST APICLIClaude CodeSelf-Hosting
DocsDevelopersReact SDK

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.

Point the SDK at your project's API URL (e.g. 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 (receives AuthFormSlotProps with 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;
}
PreviousChannelsNextBackend & RPC

On this page

Installation
useAuth
useAppCollection
useCoreCollection
useAppRecord
usePermissions
PermissionsProvider
AuthGate
Authorized
useCrons
useIntegration
useIdentity
Direct HTTP client
TypeScript types