Appbricx
Docs
PricingStart free

Start here

OverviewGetting startedProjects & editorBring your own key

End-to-end walkthroughs

Lead intakeMulti-user TodoMulti-tenant SaaSReal-time ChatSlack on signup

AI & keys

AI keys — how resolution worksBYOK deep dive

SDK reference

Frontend SDK — @app/sdkBackend SDK — @app/backendUI kit — @app/ui

Integrations

Integrations catalog

Full-stack runtime

Runtime overviewNamed queriesAuth & RLSAuto CRUD REST APIWorkflowsWebhooks & schedulesTopics & CDCData templatesSecrets & env

Ship & own

Publish & domainsExport & GitHubSelf-host & deploy

Developers

Developer guide

Frontend SDK — @app/sdk

@app/sdk is the React runtime every generated Appbricx app is built on. It provides the shell (auth, routing, data, workflows, theming) plus a small vocabulary of data-aware blocks and low-level composites the AI composes screens from. This page is the reference for developers who exported code and want to modify the manifest, add a custom block, or wire up a signature component.

You usually don't need this page. Every project generated by Appbricx already ships with @app/sdk wired up — the manifest, the AppRootmount, the routes, the theme. Read this only if you exported the code and want to hand-edit it, or if you're writing a signature component the AI can drop into a Custom block.

What's inside the package

@app/sdk exports three layers stacked on the same runtime:

  • Runtime providers & hooks — AppRoot, useAuth, useList, useGet, useActions, useNavigate, useWorkflows, useNotifications. Every page reads through these; the platform decides whether they resolve against the in-memory demo store or a real backend (Postgres/PGlite + Auth + Workflow host).
  • Blocks — nine high-level, data-aware React components (DataTable, DataForm, DetailView, KanbanBoard, Chart, Feed, Hero, StatCard, Empty). Each is spec-driven: give it an entity and a shape, it handles loading, empty, error, and theme tokens.
  • Composites & controls — the layout vocabulary the v1 pipeline uses (PageHeader, Rail, CardGrid, MediaCard, ListRow, StatCard, TrendChart, Calendar, FormField, ActionBar) plus the primitive controls (Button, Input, Dialog, Tabs, Select, Table, Badge) re-exported from @app/ui.

A single machine-readable index of every export lives in packages/app-sdk/src/manifest.ts. The appgen agents read it; you can too — it's the fastest way to see what exists.

Installation

pnpm add @app/sdk

# Peer dependencies (18.3+ or 19)
pnpm add react react-dom

Inside a generated project the dep is already declared in package.json and points at the workspace copy. If you vendored the SDK, keep @app/ui alongside it — the controls re-export happens across those two packages.

The renderer

There are two ways to mount an Appbricx app. The v1 pipeline (the current production path) emits real React pages and mounts them via AppRoot with an AppManifest. The v2 Blocks pipeline (dormant, but the runtime ships with it) mounts a spec-only app via a second AppRoot exported from src/renderer.tsx.

v1 — manifest + generated pages

import { AppRoot, type AppManifest } from "@app/sdk";
import { createSupabaseBackend } from "@app/backend";

import manifest from "./manifest";           // the AppManifest below
const backend = createSupabaseBackend({ /* … */ });

export default function App() {
  return <AppRoot manifest={manifest} backend={backend} />;
}

AppRoot composes providers in this order: AuthProvider → DataProvider → WorkflowProvider → AppFrame.AppFrame gates on sign-in (shows SignInScreen when signed out, the app shell when signed in), applies the theme, hydrates document title, and renders the current route wrapped in a per-page error boundary. Any route inside manifest.routes is picked up automatically, plus a built-in /_automations panel for inspecting workflow runs.

With no backend prop, AppRoot runs in demo mode — in-memory store hydrated from manifest.seeds, in-memory auth against manifest.users, workflows executed locally via the samedefineWorkflow runner the server host uses. Same hook contract, so the exact same page code runs in both worlds.

v2 — spec-driven blocks

import { AppRoot } from "@app/sdk/renderer";
import { supabaseBackend } from "@app/backend";
import spec from "./app.json";                // an AppSpec

export default function App() {
  return (
    <AppRoot
      spec={spec}
      backend={supabaseBackend.data}
      auth={supabaseBackend.auth}
      workflows={supabaseBackend.workflows}
      signatures={{ CheckInRitual, StreakBadge }}
    />
  );
}

The v2 renderer walks spec.pages[i].sections[j].blocks[k] and hands each block to the matching component in BLOCK_REGISTRY. A Custom block looks its component up in the signatures map you pass to AppRoot. Every block is wrapped in a BlockBoundary — one bad block never blacks out the page.

The manifest

In v1 the manifest is a TypeScript object of type AppManifest. It declares everything the platform needs to run the app: name, theme, demo users, seed data, navigation, routes, and workflow specs.

import type { AppManifest } from "@app/sdk";
import DashboardPage from "./pages/Dashboard";
import RecipeList from "./pages/RecipeList";
import RecipeDetail from "./pages/RecipeDetail";

const manifest: AppManifest = {
  name: "Cookbook",
  tagline: "Family recipes, always on hand",
  theme: {
    mode: "light",
    fontDisplay: "Fraunces",
    colors: { primary: "hsl(20 90% 55%)", accent: "hsl(210 70% 55%)" },
  },
  navStyle: "side",           // "top" | "side" | "tabs"
  density: "comfortable",
  users: [
    { id: "u1", name: "Nora Blake", email: "nora@demo.app", role: "admin" },
    { id: "u2", name: "Ravi Kumar", email: "ravi@demo.app", role: "user" },
  ],
  seeds: {
    recipes: [
      { id: "r1", title: "Miso soup", cuisine: "Japanese", minutes: 15 },
      { id: "r2", title: "Aloo paratha", cuisine: "Indian", minutes: 45 },
    ],
  },
  nav: [
    { label: "Home", to: "/", icon: "Home" },
    { label: "Recipes", to: "/recipes", icon: "BookOpen" },
  ],
  routes: [
    { path: "/", component: DashboardPage },
    { path: "/recipes", component: RecipeList },
    { path: "/recipes/:id", component: RecipeDetail },
  ],
  workflows: [
    /* defineWorkflow(...) entries — see /docs/workflows */
  ],
};

export default manifest;

The v2 AppSpec is different in shape — pages hold sections which hold blocks, and blocks are data (not components). If you exported a v1 project, ignore v2; if you're on v2, the full Zod schema is in packages/app-sdk/src/spec/schema.ts. In either case the canonical index of what a page may reference is packages/app-sdk/src/manifest.ts — every SDK export has an entry there with props, an example, and when to use it.

The nine blocks

Blocks live in packages/app-sdk/src/blocks/. Each one reads through useList or useGet, renders matched-shape loading and empty states via the shared Loading / EmptyState helpers, and formats values through the shared fmt() table (date/datetime/currency/number/boolean/badge).

Hero

Page-opening banner. Four variants — editorial, gradient, brutalist, photo, minimal — carry different type scales and background treatments. Actions are either a route (to) or a workflow trigger (workflow). Use it as the first block on marketing-flavored screens or the top of a detail page for a visual entity.

Feed

Reverse-chronological view of an entity. Three variants — cards, timeline, activity. Given titleField, an optional subtitleField/bodyField/imageField and a timestampField, it sorts descending and paints the variant. Use it for news feeds, changelogs, comment threads, activity streams.

Chart

Themed recharts chart. type picks bar | line | area | pie; xKey / yKey project the rows into {x, y} points. Uses the theme's --primary hue and a themed ResponsiveContainer at 240px tall. Use it whenever a page needs a one-series chart; use TrendChart / BarBreakdown from the composites layer when you want to precompute your own points.

KanbanBoard

Rows grouped into lanes by one column. Give it groupBy: "status" and either let the block derive lanes from distinct values or pass lanes: ["todo", "doing", "done"] for a fixed order. Each card renders titleField and an optional subtitleField. Use it for task boards, review queues, hiring pipelines.

DataTable

The browse view. Five variants — default, compact, roomy, card, minimal — read the same shape: entity, columns: [{ field, label?, format?, align? }], optional filter/sort/limit, a rowLink pattern like /orders/:id, and an empty block for the zero state. Use it for admin lists and dashboards; use the composite ListRow when you want a consumer-y one-per-row layout instead.

StatCard

One-metric summary. Given entity, a metric (count | sum | avg | min | max), a column, and a format (currency | percent | number), it computes the value client-side. Four variants — default, big, inline, sparkline. Use it as the tiles of a dashboard's top row; put the composite BigStat above them as the hero number.

DataForm

The create / edit form. mode: "create" | "edit"; in edit mode recordIdFrom: ":id" pulls the row by URL param. Fields declare a kind — text | number | textarea | select | boolean — plus required, placeholder, hint, and, for selects, options. onSaved.navigate redirects after success; onSaved.toast flashes a confirmation. Three variants — default, inline, sectioned. Use it whenever the AI wants a whole form emitted from spec instead of hand-coded fields.

DetailView

Read-mostly page for one row. Given entity, recordIdFrom, and grouped sections: [{ title, fields: [{ field, label?, format? }] }], it renders a titled card per section and a dl of label/value pairs. actions can be { kind: "navigate" | "edit" | "workflow" | "remove", label, target } — a workflow action calls useRuntime().workflows.trigger(...) with the record id automatically.

Empty

A themed empty state as a stand-alone block. title, optional body, optional lucide icon, optional action: { label, to }. Use it when a page's whole zero state is one warm sentence and one button — every block ships its own empty state inline, but sometimes the page itself is empty.

The registry & shared helpers

blocks/registry.tsx maps every block name to its component; the renderer looks up by b.block and handsb.props through. blocks/shared.tsx is the tiny toolkit every block uses: BlockBoundary, Loading (with kind: table | cards | detail | form | stat), EmptyState, fmt(), labelize(), and Icon — a lucide-react wrapper so blocks (and signatures) never import lucide directly.

Signatures — the escape hatch

A signatureis a small custom React component the AI (or you) writes to carry the app's one memorable moment: a check-in ritual, a sticky-note detail card, a receipt-printer confirmation. It uses the same hooks blocks use (useList, useGet, useActions, useNavigate, plus Icon, Skeleton from the shared toolkit). It just paints differently.

Signatures live at src/signatures/<Name>.tsx, default-export one React component, and are referenced by name from a Custom block. You register them with <AppRoot signatures={...} />.

// src/signatures/CheckInRitual.tsx
import * as React from "react";
import { useList, useActions, useAuth } from "@app/sdk";

export default function CheckInRitual(props: {
  entity?: string;
}): React.ReactElement {
  const entity = props.entity ?? "check_ins";
  const { user } = useAuth();
  const today = new Date().toISOString().slice(0, 10);
  const { data } = useList(entity, {
    filter: [
      { field: "user_id", op: "eq", value: user.id },
      { field: "date", op: "eq", value: today },
    ],
    limit: 1,
  });
  const actions = useActions(entity);
  const done = data.length > 0;

  return (
    <button
      disabled={done}
      onClick={() => actions.create({ user_id: user.id, date: today })}
      className="rounded-full bg-primary text-primary-foreground px-6 py-3 disabled:opacity-40"
    >
      {done ? "Checked in for today" : "Check in"}
    </button>
  );
}
// Wire signatures into the app
import { AppRoot, buildSignatureRegistry } from "@app/sdk";
import CheckInRitual from "./signatures/CheckInRitual";

// Or, glob every file:
// const modules = import.meta.glob("./signatures/*.tsx", { eager: true });
// const signatures = buildSignatureRegistry(modules);

<AppRoot
  manifest={manifest}
  signatures={{ CheckInRitual }}
/>
Contract. Signatures may only import from "react", "@app/sdk", or relative paths. No fetch, no dangerouslySetInnerHTML, no raw hex colors — use theme token classes (bg-primary, text-foreground, border-border). Stay under ~200 LOC. The pipeline's static check enforces all of this before the file is emitted; the full rule text is exported as SIGNATURE_CONTRACT from @app/sdk.

Runtime hooks

The runtime is what makes generated pages boring in the good way: every page uses the same tiny hook surface, and the platform decides where the values come from.

useAuth() — the signed-in user

import { useAuth } from "@app/sdk";

function AdminOnly({ children }: { children: React.ReactNode }) {
  const { user } = useAuth();
  if (user.role !== "admin") return null;
  return <>{children}</>;
}

useAuth() returns { user, users, switchUser, signOut }. Pages only render when signed in, so user is always present. useAuthGate() is the wider surface (adds signedIn, signIn, signInWithEmail, signUp, resolving) and is what the sign-in screen uses — regular pages should stick to useAuth.

useList / useGet / useActions — data

import { useList, useGet, useActions, useParams, useNavigate } from "@app/sdk";

function OverdueInvoices() {
  const { data, isLoading, isEmpty } = useList("invoices", {
    filter: [{ field: "status", op: "eq", value: "overdue" }],
    sort: { field: "due_date", dir: "asc" },
    limit: 20,
  });
  if (isLoading) return <p>Loading…</p>;
  if (isEmpty) return <p>All caught up.</p>;
  return <ul>{data.map((i) => <li key={i.id}>{String(i.number)}</li>)}</ul>;
}

function InvoiceDetail() {
  const { id } = useParams();
  const { data: invoice, isLoading } = useGet("invoices", id);
  const invoices = useActions("invoices");
  const navigate = useNavigate();
  // …
  const markPaid = () => {
    invoices.update(id!, { status: "paid" });
    navigate("/invoices");
  };
}

useList(entity, query) returns { data, isLoading, isEmpty, isError }. The query shape is the same everywhere: an array of { field, op, value } filters (with ops eq / neq / in / lt / lte / gt / gte / contains), an optional sort, and an optional limit. In demo mode this filters the in-memory seed rows; in backend mode it turns into a real SQL query on your project database.

useActions(entity) returns { create, update, remove, isPending }. Writes are optimistic — the local store updates immediately and the change feed keeps every reader in sync.

useWorkflows / useNotifications

import { useWorkflows, useNotifications } from "@app/sdk";

function ReminderButton({ requestId }: { requestId: string }) {
  const wf = useWorkflows();
  return (
    <button onClick={() => wf.trigger("nudge-approver", { request_id: requestId })}>
      Send reminder
    </button>
  );
}

function InboxBadge() {
  const { unread } = useNotifications();
  return unread > 0 ? <span className="badge">{unread}</span> : null;
}

useWorkflows() exposes { specs, runs, trigger, runNow, simulateWebhook, markAllRead }. In backend mode trigger enqueues on the server host and runs streams from the database; in local mode the same workflow module executes in-page against the demo store. useNotifications()is role-filtered for the signed-in user and drives the shell's bell — only use it for a dedicated inbox surface.

Router — useNavigate / useParams / usePath / Link

import { Link, useNavigate, useParams, usePath } from "@app/sdk";

function BackToList() {
  return <Link to="/recipes">All recipes</Link>;
}

function RecipeEditor() {
  const { id } = useParams();      // route "/recipes/:id/edit"
  const path = usePath();          // current path string
  const navigate = useNavigate();
  const cancel = () => navigate(-1); // useBack() is the tidier alternative
}

The router is hash-based (URLs look like #/recipes/r1) so the whole app runs from a single static index.htmlwithout a server-side rewrite. Link to="/…" renders a plain anchor with the leading # handled for you. useBack() is a nicer navigate(-1).

Toaster + toast()

import { toast } from "@app/sdk";
toast("Saved", { kind: "success", description: "Invoice #1042 marked paid" });

The Toaster is mounted by AppRoot. Call toast(title, opts) from anywhere; kind is default | success | error.

The shell & theme

AppShell is the chrome — top or side navigation, dark/ light toggle, notifications bell, the current-user chip. It reads nav, navStyle, and density from the manifest and hosts each route inside a padded content area. You don't mount it directly; AppRoot does.

applyTheme(theme, mode) writes CSS variables (--primary, --background, --foreground, --muted, --border, --ring, plus --font-display) onto :root and toggles the dark class. Blocks and composites reference those variables via Tailwind arbitrary-value classes (bg-[hsl(var(--card))], text-[hsl(var(--muted-foreground))]), so swapping the theme repaints everything without touching any component.

The backend seam

@app/sdk defines only the interfaces — PlatformBackend, DataBackend, AuthBackend, WorkflowBackend. The concrete implementation lives in @app/backend (the Appbricx runtime) or a vendor package. Generated pages never import backends directly; you wire one into AppRoot and every hook follows the same contract.

If you want to plug in a different persistence layer for an exported project — say, plain fetch against your own REST API — implement the four interfaces from runtime/backend.ts and pass the resulting object as backend to AppRoot.

Related

For developers · Runtime overview · Workflows · Export your project · Named queries

Appbricx

Full-stack AI app builder for teams. Hosted cloud or private deploy into your account — multi-tenant, sandboxed, credit-metered.

Product

How it worksDemoCapabilitiesPricingPrivate cloudBYOKFAQ

Resources

DocumentationBlogFor freelancersFor agenciesSupport

Company

ContactPrivate cloud / agencyPrivacyTerms

© 2026 Appbricx. All rights reserved.

TermsPrivacyCookiesAcceptable Use