Custom components & branding
Blocks are the fast path. When an app needs its own look or interaction (a swipe deck, a chat thread, a profile editor), your agent writes the React component itself and puts it in the contract. It can also set an exact brand colour and replace the sign-in screen.
Code components
Declare a component in components with "kind": "code" and place it on a page with a custom layout node:
"components": [{
"name": "SwipeStack",
"kind": "code",
"purpose": "Discovery deck: one profile at a time, like / pass",
"props": { "title": { "type": "text" } },
"sample": { "title": "Discover" },
"source": "<the whole TSX file>"
}],
"pages": [{
"name": "discover", "route": "/discover", "title": "Discover", "personas": ["member"],
"layout": { "kind": "custom", "custom": "SwipeStack", "props": { "title": "Discover" } }
}]| Key | Meaning |
|---|---|
name | PascalCase. The default export must use it |
kind | "code" for a component with source |
purpose | One line, for whoever edits it next |
props | The declared props and their types |
sample | Props the render gate uses |
source | The whole TSX file, up to 40 KB |
A custom node can be the whole page layout or sit inside a stack or grid next to blocks. Prop values are literals, or bindings such as { "$each": { "entity": "member" } }, which gives a records prop its rows. The component ships as src/components/<Name>.tsx and stays in the contract, so get_contract returns it with its source.
The app prop
Every component has one default export and receives app: AppHandle next to its declared props. Import the type and declare it as optional (it is always set at runtime; the render gate passes a stub with no data).
import type { AppHandle } from "@app/kit/primitives";
type SwipeStackProps = { title?: string; app?: AppHandle };
export default function SwipeStack(props: SwipeStackProps) { ... }| Member | What it does |
|---|---|
user | signedIn, name, email?, role (persona id or null), refs (my row id per persona table) |
me(entity) | My own row id in a persona table |
params | Route params (:id pages: params.id) |
rows(entity), loading(entity) | The working set (newest ≤ 200 readable rows); re-renders as rows change |
revision(entity) | Changes when the entity changes; use as a useEffect dependency |
query(entity, q?), get(entity, id) | Server-side filtered, sorted page (where, sort, limit, offset) with a total; one row |
create, update, remove | Writes. Owner columns are stamped by the server |
transition(entity, id, field, to) | Advance a lifecycle enum field |
upload(file) | Upload a file; store the returned url in an image or attachment field |
live(entity) | Follow live changes; call in useEffect and return the unsubscribe |
navigate(route) | Go to another page |
can(cap, entity?) | UI hint for create / edit / delete; the server enforces it either way |
auth | Sign-in, sign-up and sign-out (below) |
Every call goes through the app's data API, so row-level security decides what the signed-in user may do, whatever the component tries. Never pass owner columns (user_id, created_by). For a ref that means "me" (sender_id, swiper_id), declare it as the entity's actor_ref so the server stamps it.
Gates
New or changed source runs through four gates on apply. Run them yourself first with check_component (MCP) or POST /dev/v1/components/check. It takes name, source and sample, needs no project, changes nothing, and answers in seconds.
- static — imports only from
react,@app/kit/primitivesandlucide-react(icons from the icon set). No fetch, XMLHttpRequest, WebSocket, eval, dynamic import, storage, cookies,dangerouslySetInnerHTML,<script>,<iframe>or<form>(use<button onClick>). - types — strict TypeScript against the real kit types.
- compile.
- render — a sandboxed server render with the sample props and with empty props. Guard every array and show an empty state; no
Math.randomorDate.nowduring render.
// pass
{ "ok": true, "passed": ["static", "types", "compile", "render"] }
// fail
{ "ok": false, "stage": "types", "errors": ["..."] }Styling
Use Tailwind utility classes, including arbitrary values like h-[62vh], aspect-[3/4] or -rotate-6. Appbricx compiles exactly the classes each component names when it is applied. Theme tokens (bg-primary, bg-card, text-foreground, text-muted-foreground, border-border) follow the app theme and the brand colour; prefer them over literal colours. style works too. Design for a phone first (about 390×844).
Full-screen pages
Add "chrome": "none"to a page and it renders without the app's header and navigation. Your component owns the whole screen and draws its own header and tab bar. Navigate with app.navigate("/matches"); verify counts those links when it checks that every page is reachable. Keep the role's other pages reachable from your own nav, and add a Sign out button yourself (app.auth.signOut()).
{ "op": "set_page", "page": "discover", "chrome": "none" } // full screen
{ "op": "set_page", "page": "discover", "chrome": null } // framed againBrand colour
"app": { "name": "Hive", "tagline": "Make the first move", "brand_color": "#FFC629" }- The exact colour becomes the primary colour over any theme: buttons, links, focus rings and the platform's own sign-in screen, in light and dark mode.
- Text on it is near-black (
#111111) or white, whichever has the higher WCAG contrast ratio. Yellow gets near-black text; navy gets white. - It is rendered into the preview's server-side styles, so it applies before any app code runs, and compiled into the app's CSS.
- Accepts
#RGBor#RRGGBB. Change it with{ "op": "set_app", "brand_color": "#FFC629" };""removes it.
Custom sign-in screen
Write a code component and name it in the top-level authsection. It replaces the platform's sign-in and sign-up screen.
"auth": { "screen": "HiveLogin" }The component gets app.auth:
| Member | What it does |
|---|---|
mode, setMode(mode) | Which form to show: login, signup or forgot |
canSignUp | Public sign-up is open for this app |
signIn(email, password) | Sign in |
signUp({ name, email, password }) | Create an account |
requestReset(email) | Send a password reset |
skip?() | Continue without signing in, when the app allows browsing |
signOut() | Sign out. This is the only verb that works inside the app; the others reject there |
verify and screenshot sign in through them. If auth.screennames a component that doesn't exist, the check drops it with a fix. If your component throws, the platform's screen shows instead, so users are never locked out.A minimal sketch (run it through check_component):
import { useState } from "react";
import type { AppHandle } from "@app/kit/primitives";
type HiveLoginProps = { headline?: string; app?: AppHandle };
export default function HiveLogin(props: HiveLoginProps) {
const app = props.app;
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const submit = async () => {
if (!app) return;
setError(null);
try {
await app.auth.signIn(email, password);
} catch (e) {
setError(e instanceof Error ? e.message : "Sign-in failed");
}
};
return (
<div className="min-h-screen bg-background px-6 flex flex-col justify-center">
<h1 className="text-3xl font-bold text-foreground">{props.headline ?? "Welcome back"}</h1>
<label htmlFor="email" className="mt-8 text-sm text-muted-foreground">Email</label>
<input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)}
className="mt-1 rounded-full border border-border bg-card px-4 py-3" />
<label htmlFor="password" className="mt-4 text-sm text-muted-foreground">Password</label>
<input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)}
className="mt-1 rounded-full border border-border bg-card px-4 py-3" />
{error ? <p className="mt-3 text-sm text-destructive">{error}</p> : null}
<button type="button" onClick={() => void submit()}
className="mt-6 rounded-full bg-primary py-3 font-semibold text-primary-foreground">
Sign in
</button>
{app?.auth.canSignUp ? (
<button type="button" onClick={() => app.auth.setMode("signup")} className="mt-3 text-sm text-foreground">
Create account
</button>
) : null}
</div>
);
}A real screen also renders the signup and forgot modes. Change the section later with { "op": "set_app", "auth": { "screen": "HiveLogin" } } (auth: null goes back to the platform screen).
Changing components later
set_componentadds or replaces a whole component by name.edit_componentchanges part of the source:{ "op": "edit_component", "name": "ChatThread", "edits": [{ "find": "text-sm", "replace": "text-base" }] }. Eachfindmust occur exactly once; add surrounding text to make it unique. Edited source is gated again on apply.set_appchangestheme,direction,nav_style,density,brand_colorandauthwithout resending the contract.
After any visual change, call screenshot on each screen (and as: "guest" for the sign-in screen) before reporting it done.
Next
- App contract — pages, patch ops
- Before-write rules
- MCP server reference —
screenshotandcheck_component