Portal data
The customer's tables and files — createApiTransport / createPortalData and the PortalData surface (listRows, insertRow, patchRow, deleteRow, uploadFile, fileLink).
Reference · SDK
The portal-data surface is what a customer-facing portal reads and writes: the org's shared tables (orders, services, deliverables, and each running service's own data) and the customer's files. Everything here is scoped by the session token to one signed-in customer.
Setup
Both factories take the same { baseUrl, token } config. createApiTransport is the chat/thread
transport; createPortalData is the tables-and-files surface. Point baseUrl at your same-origin proxy
("") or the gateway, and give token a getter that returns a fresh access token.
import { createApiTransport, createPortalData } from "@holaboss/client";
const config = {
baseUrl: "", // same-origin proxy answers /api/*
token: async () => (await currentAccessToken()) ?? "",
};
export const chat = createApiTransport(config);
export const data = createPortalData(config);PortalData
Tables & rows
listTables(): Promise<PortalTable[]>The tables this customer can see. A table the org hasn't shared is absent, not empty — it answers as if it didn't exist.
listRows(slug: string, limit?: number): Promise<PortalRowPage>Rows plus how many there really are. The server caps a page, so PortalRowPage carries the total —
returning the array alone would let a dashboard present "the most recent 100" as "all of them".
insertRow(slug: string, row: Record<string, unknown>): Promise<string> // → new row id
patchRow(slug: string, rowId: string, patch: Record<string, unknown>): Promise<number>
deleteRow(slug: string, rowId: string): Promise<number>Writes respect the org's schema: insertRow/patchRow accept only the table's writable_fields (pass
an empty string in a patch to clear a field), and deleteRow only removes rows the org marked deletable
and only when nothing depends on them — otherwise it rejects with the reason.
Files
listFiles(): Promise<PortalFile[]>
uploadFile(file: File, message?: string): Promise<{ rowId: string }>
fileLink(rowId: string): Promise<{ url: string; filename: string }>uploadFile sends bytes and the server writes the row (a storage key is never the caller's to
state). fileLink is not a plain URL — the download route needs the customer's token, and an <a href>
navigation carries no Authorization header, so a direct link would 401. Call fileLink to fetch a
signed URL the browser can then follow with no header of its own:
const { url, filename } = await data.fileLink(rowId);
// <a href={url} download={filename}>Download</a>Everything is gated twice: by the org's per-table read/write/delete permissions and by the session token. There's no way to reach a table or a row the signed-in customer isn't allowed to.
Types
PortalTable, PortalRow, PortalRowPage, PortalField, PortalFile, PortalDocument,
PortalDocumentSummary, and the PortalConflict error are all exported from @holaboss/client.
SDK overview
@holaboss/client is a headless SDK for the customer-facing surface of a Holaboss org — portal tables and files, and an employee chat you render yourself.
Chat
Embed a Holaboss employee conversation and render it yourself — createHolaClient, the useChat / useThread hooks, streaming handlers, and the optional <Chat> component.