Use your data from React
The frontend SDK, @app/kit, is how your screens talk to your app: list and change rows, follow changes as they happen, upload a photo, ask the app's AI, or check what the signed-in user may do. You never handle tokens or write fetch calls, and you never have to remember a permission check. Every call goes through your app's data API, so the database decides what each user can see and change.
Two ways in
- The
apphandle. Every custom component in your contract receivesappas a prop. It is the whole SDK narrowed to what a screen may do, and it is what your coding agent uses most. - React hooks. In app code you own in full (an exported or self-hosted app, or pages you write by hand), the same calls are available as hooks:
useAppHandle,useEntityQuery,useRecord,useAggregateandusePlatformSession.
The app handle
import type { AppHandle } from "@app/kit/primitives";
type Props = { app?: AppHandle };
export default function MyJobs({ app }: Props) {
if (!app) return null;
return <p>Hello {app.user.name}, you are signed in as {app.user.role}.</p>;
}| Member | What it does |
|---|---|
user | signedIn, name, email?, role (or null signed out), refs |
me(entity) | The id of my own row in a user table, for example my member row |
query(entity, q?) | A server-side page: where (equality filters), sort, limit, offset. Returns { rows, total } |
get(entity, id) | One row, or null when it does not exist or the user may not read it |
create(entity, values) | Creates a row and returns it. Owner columns are stamped by the server, so pass only the row's own fields |
update(entity, id, patch), remove(entity, id) | Change or delete a row |
transition(entity, id, field, to) | Move a status field along its lifecycle (a plain write to it is refused) |
upload(file) | Uploads a file and returns { url, name }. See Files and payments |
live(entity) | Follow changes to a table as they happen. Returns the unsubscribe function |
can(cap, entity?) | Can this role create, edit or delete? A hint for hiding buttons; the server enforces it either way |
ai.ask, ai.chat | Ask the app's AI. See AI in your app |
auth | Sign in, sign up and sign out. See Sign-in methods |
rows, loading, revision, params, navigate | The newest readable rows held in memory, a counter that moves when a table changes, route params, and navigation |
Read and write
const { rows, total } = await app.query("job", {
where: { status: "open" },
sort: { field: "due_on", dir: "asc" },
limit: 20,
});
const job = await app.create("job", { title: "Fix the boiler", due_on: "2026-11-02" });
await app.update("job", job.id as string, { notes: "Bring a ladder" });
await app.transition("job", job.id as string, "status", "in_progress");If the signed-in user may not do something, the call rejects with a clear message. Catch it and show it; you do not need your own permission logic.
Realtime: a live chat
Call app.live(entity) in an effect and return the unsubscribe it gives you. Every change to the table, from any user or workflow, moves app.revision(entity), so a query that depends on it refetches by itself.
useEffect(() => app.live("message"), [app]);
const [messages, setMessages] = useState<Row[]>([]);
useEffect(() => {
app.query("message", { where: { room_id: roomId }, sort: { field: "created_at" } })
.then((r) => setMessages(r.rows));
}, [app, roomId, app.revision("message")]);Show actions only to people who can use them
{app.can("delete", "job") && (
<button onClick={() => app.remove("job", job.id as string)}>Delete</button>
)}Hooks for app code you own
These are for the React app itself, outside contract components (contract components use the app prop above). Each hook returns { data, loading, refreshing, error, retry } and refetches when the table changes.
import { useEntityQuery, useRecord, useAggregate } from "@app/kit/kits/runtime";
// A page of open jobs, plus the total count
const jobs = useEntityQuery("job", {
filter: [{ field: "status", op: "eq", value: "open" }],
sort: { field: "due_on", dir: "asc" },
limit: 25,
});
// One record (data === null means it does not exist)
const job = useRecord("job", params.id);
// A number for a stat: count, sum, avg, min or max
const overdue = useAggregate("job", {
metric: "count",
filter: [{ field: "due_on", op: "lt", value: today }],
});Filters support eq, neq, in, not_in, lt, lte, gt, gte, contains, is_null, is_not_null and range (a named date window). Totals and aggregates are computed on the server over the whole table, so they stay right however large it grows.
Payments, team and search
The runtime behind the hooks also carries the app's payment, team and full-text search features:
import { useKitRuntime } from "@app/kit/kits/runtime";
const rt = useKitRuntime();
// Payments: "offline" always, "card" once Stripe is connected
const { methods } = await rt.pay!.methods();
await rt.pay!.checkout("order", orderId, methods.includes("card") ? "card" : "offline");
// Team: list, invite and change the app's users (admin session required)
const { members, roles } = await rt.team!.list();
await rt.team!.invite({ email: "sam@acme.com", name: "Sam", password: tempPassword, role: "technician" });
// Full-text search across your tables, scoped to what the user may read
const hits = await rt.search!("boiler"); // [{ entity, row }]Sign-in in your own app shell
usePlatformSession owns the session: sign in, sign up, sign out, password reset and social sign-in. SignInGate shows your app's sign-in screen until there is a session.
import { usePlatformSession, SignInGate } from "@app/kit/kits/platform-auth";
const auth = usePlatformSession({ apiBase: "", projectToken });
const [skipped, setSkipped] = useState(false);
return (
<SignInGate
session={auth.session}
signIn={auth.signIn}
signUp={auth.signUp}
requestReset={auth.requestReset}
startProvider={auth.startProvider}
discovery={auth.discovery}
skipped={skipped}
onSkip={() => setSkipped(true)}
brandName="FieldOps"
>
<App />
</SignInGate>
);
// A "Continue with Google" button anywhere:
<button onClick={() => auth.startProvider("google")}>Continue with Google</button>auth.discovery tells you which sign-in providers are on and whether public sign-up is open, so your screen only shows what works.
When something is not available
In an offline preview there is no server, so upload, ai and payments reject with a message, and reads return demo data. Wrap those calls in try/catch and show a fallback.
Related: Your screens and brand · REST API · Live updates · Access per user