Build an Application
You describe what you want to your AI coding agent. It generates the code. You review it and deploy. But to review effectively, and to ask for the right things, you need to understand how a RootCX application is structured.
This page explains every piece of an internal tool and how it connects to the Core. Each section shows you the concept with an example, then links to the full reference.
Structure of an application
my_crm/
├── manifest.json # Data model + permissions + config (required)
├── backend/
│ └── index.ts # Custom logic (optional)
└── src/
└── App.tsx # User interface (optional)
The manifest is the only required piece. It tells the Core what data your tool manages, who can access it, what webhooks it receives, what crons it runs, and what actions it exposes. The Core reads it and creates everything: database tables, CRUD APIs, permission policies, webhook URLs, cron schedules.
The frontend is a React app that uses @rootcx/sdk to interact with the Core's Data API. It gets served at /apps/{appId}/.
The backend is TypeScript code that runs as an isolated Bun process supervised by the Core. It handles custom logic: RPC methods, background jobs, scheduled tasks, webhook handlers, and direct database access.
Your AI coding agent generates all three. You review and deploy with rootcx deploy.
The manifest
The manifest.json is the single source of truth for your application. Everything the Core does for you is derived from it.
Top-level fields
| Field | Required | Description |
|---|---|---|
appId |
Yes | Unique identifier. Lowercase snake_case. Becomes the PostgreSQL schema name. Immutable after first deploy. |
name |
Yes | Display name shown in the dashboard. |
version |
No | Semantic version (default: "0.0.1"). |
description |
No | Short description. |
type |
No | "app" (default), "integration", or "agent". |
icon |
No | Icon identifier. |
dataContract |
No | Array of entity definitions. |
permissions |
No | Permission key declarations. |
actions |
No | Named RPC operations discoverable by AI agents. |
webhooks |
No | Inbound webhook definitions. |
crons |
No | Declarative cron schedules. |
public |
No | Public access surface (no auth required). |
configSchema |
No | JSON Schema for app-level configuration. |
instructions |
No | Usage instructions surfaced to AI agents via list_integrations. |
trigger |
No | Auto-invoke an agent on entity changes in another app. |
Full reference: Manifest Reference.
Data model
An entity is a PostgreSQL table. You declare them in the dataContract array:
{
"appId": "sales_crm",
"name": "Sales CRM",
"dataContract": [
{
"entityName": "contacts",
"fields": [
{ "name": "first_name", "type": "text", "required": true },
{ "name": "last_name", "type": "text", "required": true },
{ "name": "email", "type": "text" },
{ "name": "phone", "type": "text" },
{ "name": "company_id", "type": "entity_link", "references": { "entity": "companies", "field": "id" } },
{ "name": "assigned_to", "type": "entity_link", "references": { "entity": "core:users", "field": "id" } },
{ "name": "stage", "type": "text", "enum_values": ["lead", "qualified", "proposal", "won", "lost"], "default_value": "lead" }
]
}
]
}
Every entity gets id, created_at, and updated_at automatically. When you deploy, the Core creates the PostgreSQL table and generates CRUD API endpoints.
Field types
| Type | What it stores |
|---|---|
text |
Variable-length string |
number |
64-bit floating-point number |
boolean |
True/false |
date |
Calendar date (YYYY-MM-DD) |
timestamp |
Date and time in UTC |
json |
Arbitrary structured data |
uuid |
UUID value |
file |
Uploaded file reference (max 50 MB) |
entity_link |
Foreign key to another entity |
[text] |
Array of strings |
[number] |
Array of numbers |
Field options
| Option | Description |
|---|---|
required |
NOT NULL constraint. |
default_value |
Default on insert ("draft", 0, false, {"key": "val"}). |
enum_values |
Restrict to specific values (generates a CHECK constraint). |
is_primary_key |
Custom primary key. |
on_delete |
For entity_link: "cascade", "restrict", or "set_null". Default depends on whether the field is required. |
Relationships
Use entity_link to create foreign keys. Reference entities in the same app, or core:users for platform users:
{ "name": "contact_id", "type": "entity_link", "references": { "entity": "contacts", "field": "id" } },
{ "name": "assigned_to", "type": "entity_link", "references": { "entity": "core:users", "field": "id" } }
Identity federation
If multiple apps manage the same kind of entity (contacts in a CRM, contacts in support), federate them:
{
"entityName": "contacts",
"identityKind": "contact",
"identityKey": "email",
"fields": [...]
}
The SDK's useIdentity hook queries across all apps that share the same identityKind.
Full reference: Data API.
Permissions
Declare permission keys in the manifest to define what actions exist:
"permissions": {
"permissions": [
{ "key": "contacts.create", "description": "Create contacts" },
{ "key": "contacts.read", "description": "View contacts" },
{ "key": "contacts.update", "description": "Edit contacts" },
{ "key": "contacts.delete", "description": "Delete contacts" }
]
}
On deploy, keys are prefixed with app:{appId}: (e.g., app:sales_crm:contacts.create). Assign them to roles, roles to users. The Core enforces on every request before any SQL runs.
If you do not declare permissions, the Core auto-generates CRUD keys from your entities.
Full reference: RBAC.
The backend
The backend runs as an isolated Bun process. The Core supervises it, restarts it on crash, routes RPC calls to it, and delivers jobs. Your AI coding agent writes all backend code. Here is what the backend can do:
| Capability | Description |
|---|---|
| RPC methods | Custom endpoints your frontend or other apps can call. 30-second timeout. |
| Background jobs | Durable queue. Jobs survive restarts, retried automatically. |
| Scheduled jobs | Recurring crons (every minute, every 10 seconds, daily at 9 AM). |
| Webhook handlers | Receive HTTP callbacks from external services (Stripe, GitHub, etc.). |
| Direct database access | PostgreSQL connection string available at startup. |
| Secrets | Injected as environment variables from the encrypted vault. |
| Collection operations | Read/write entities directly via IPC (no HTTP round-trip). |
| File storage | Upload files via IPC nonce URLs. |
| Custom events | Emit named events consumed by other parts of the system. |
The backend receives caller context on every RPC call: userId, email, and authToken (the JWT bearer token you can use for Core API calls from your handler).
Full reference: Backend Development.
Webhooks
Declare webhooks in the manifest to receive HTTP callbacks from external services:
{
"webhooks": [
{ "name": "stripe", "method": "onStripePayment" },
{ "name": "github", "method": "onGithubPush" }
]
}
On deploy, the Core generates a unique 64-character token per webhook. External services POST to /api/v1/hooks/{token}. The Core routes to your backend with headers, parsed body, and raw body (base64, for signature verification).
Tokens are stable across re-deploys. Removing a webhook from the manifest deletes it.
Full reference: Webhooks.
Scheduled jobs (crons)
Declare crons in the manifest for recurring work:
{
"crons": [
{
"name": "daily-check",
"schedule": "0 9 * * *",
"method": "onDailyCheck",
"payload": { "task": "check_replies" },
"overlapPolicy": "skip"
}
]
}
Schedule formats: standard 5-field cron (0 9 * * *), seconds interval (10 seconds), or $ for last day of month. All times GMT unless you set timezone.
Crons can also be created at runtime via the API or the SDK's useCrons hook.
Full reference: Scheduled Jobs.
Actions
Actions make your backend's RPC methods discoverable by AI agents and integrations:
"actions": [
{
"id": "pipeline",
"name": "Pipeline Summary",
"description": "Returns deal counts per stage",
"inputSchema": { "type": "object", "properties": { "period": { "type": "string" } } },
"outputSchema": { "type": "object", "properties": { "counts": { "type": "object" } } }
}
]
AI agents discover actions via the list_actions tool and call them via call_action, if they have permission app:{appId}:action:{actionId}.
Public access
By default, all endpoints require authentication. To expose specific RPCs or collections without auth (public forms, status pages, embeds):
{
"public": {
"rpcs": [
{ "name": "submitForm", "scope": [] },
{ "name": "checkStatus", "scope": ["orderId"] }
],
"collections": ["public_announcements"]
}
}
File uploads
Add a file field to any entity. Max 50 MB per file. Supported formats: CSV, JSON, XML, TXT, PDF, PNG, JPG, JPEG, GIF, WebP, SVG, XLSX, XLS, DOC, DOCX, ZIP, GZ, Parquet.
Upload endpoint: POST /api/v1/apps/{appId}/upload (multipart/form-data).
The frontend
The frontend uses @rootcx/sdk (React hooks) to interact with the Core. Served at /apps/{appId}/.
| Hook | Purpose |
|---|---|
useAuth() |
Login, registration, logout, OIDC SSO, current user |
useAppCollection(appId, entity, query?) |
List, filter, create, update, delete records |
useAppRecord(appId, entity, id) |
Single record CRUD |
usePermissions() |
Check RBAC permissions client-side |
useCrons(appId) |
Manage scheduled jobs |
useIntegration(id) |
Connect and call integration actions |
useIdentity(kind, query?) |
Federated query across apps |
useCoreCollection(entity) |
Read-only access to platform data (users) |
| Component | Purpose |
|---|---|
AuthGate |
Pre-built login/registration UI with OIDC support |
Authorized |
Render children conditionally based on RBAC permissions |
PermissionsProvider |
Share permissions fetch across multiple components |
Full reference: React SDK.
Evolving your application
Tell your AI coding agent what to change. It updates the manifest. You deploy again. The Core diffs the manifest against the live database and applies only what changed. No migration files to manage.
id, created_at, and updated_at are protected.appId is immutable. It becomes the PostgreSQL schema name. Changing it after first deploy requires manual data migration.Full reference: Manifest Reference - Schema Sync.
Deploy
rootcx deploy
The CLI handles dependency install, frontend build, manifest sync, backend deployment, and static asset publishing. Idempotent: deploy as many times as you want.
Next steps
- Getting Started to deploy your first internal tool in five minutes.
- Build an AI Agent to deploy agents on top of your fleet.
- For developers: Manifest Reference, Backend Development, React SDK.