@palettelab/sdk
v0.1.29
Published
Palette Platform SDK for building plugins and apps
Readme
@palettelab/sdk
Frontend SDK for building Palette plugins and internal apps.
Use this package from plugin frontends created with @palettelab/cli. It provides typed platform context, authenticated API helpers, React hooks for common Palette resources, iframe sandbox bridge helpers, install configuration helpers, and test utilities.
Use @palettelab/sdk only from frontend TypeScript/React code. Python backend
routes should use palette_sdk through PluginContext and ctx.* helpers; the
CLI bundles that backend SDK for local simulation and generated backend apps.
Install
npm install @palettelab/sdk react react-domreact and react-dom are peer dependencies. Plugin bundles should externalize React and @palettelab/sdk; the Palette CLI handles this for standard templates.
Recommended Developer Flow
Create apps with the Palette CLI and use this SDK from the generated frontend.
npx --yes @palettelab/cli@latest init simple-todo --template database
cd simple-todo
npm install
npx --yes @palettelab/cli@latest devpltt dev starts the local SDK simulator. It provides mock platform context,
authenticated API helpers, local backend routing, and a local database when the
app declares database support. This is the fastest loop for building UI,
calling backend routes, and checking SDK behavior.
For real Palette OS testing without Docker, configure a hosted sandbox and run:
npx --yes @palettelab/cli@latest login \
--env staging \
--url https://YOUR-PALETTE-STAGING-URL \
--token pltt_xxxxx
npx --yes @palettelab/cli@latest dev --sandbox --env stagingThe hosted sandbox publishes a preview into the configured Palette environment and returns an OS preview URL. Use that URL when the app must be tested with real login context, organization context, Data Room APIs, storage, install state, review/publish state, logs, permissions, and platform APIs.
Staging URL And Token
The staging URL is the base URL of the Palette backend/API environment used by the CLI. It must serve backend API paths such as:
/api/v1/health
/api/v1/appstore/sign-upload
/api/v1/appstore/publish
/api/v1/developer/publish-tokensValidate it before configuring the CLI:
curl https://YOUR-PALETTE-STAGING-URL/api/v1/healthIf this returns backend health JSON, the URL is correct. If it returns the
frontend app HTML, use the backend domain instead or configure your reverse
proxy so /api/v1/* and /api/superadmin/* are routed to the backend.
Publish tokens start with pltt_. Developers can create one after logging in:
- Open Palette OS.
- Open Settings.
- Open Developer.
- Create a developer publish token.
- Copy it immediately. The raw token is shown only once.
Then save it locally:
npx --yes @palettelab/cli@latest login \
--env staging \
--url https://YOUR-PALETTE-STAGING-URL \
--token pltt_xxxxxThe CLI stores this in ~/.palette/config.json with restricted file
permissions. You can also use environment variables:
export PALETTE_STAGING_URL=https://YOUR-PALETTE-STAGING-URL
export PALETTE_STAGING_TOKEN=pltt_xxxxxLocal Simulator Versus Hosted OS
Use the local simulator for everyday development:
npx --yes @palettelab/cli@latest devUse hosted sandbox for full OS behavior:
npx --yes @palettelab/cli@latest dev --sandbox --env stagingFor internal sandbox environments where manual approval should not block app testing, run the Palette backend with:
APPSTORE_AUTO_APPROVE_SANDBOX_PREVIEWS=trueWhen enabled, passing sandbox preview publishes become active automatically, so the developer can immediately test the app inside the OS preview URL.
Exports
import {
PluginProvider,
usePlatform,
createPaletteClient,
palette,
DataRoomClient,
dataRooms,
StorageClient,
UserClient,
OrganizationClient,
getConnections,
startConnection,
disconnectConnection,
requireConnection,
broker,
servicesProxy,
BrokerCallError,
events,
notifications,
usePluginTranslations,
translate,
normalizePaletteLanguage,
usePluginTasks,
usePluginDataRooms,
usePluginChat,
apiFetch,
apiUpload,
setBaseUrl,
getBaseUrl,
createSandboxBridge,
isSandboxRuntime,
getInstallConfig,
updateInstallConfig,
hasPermission,
hasAnyPermission,
hasAllPermissions,
PaletteApiError,
errorFromResponse,
isPaletteApiError,
createMockPlatformContext,
withPluginProvider,
} from "@palettelab/sdk"Subpath exports are also available:
import { usePlatform } from "@palettelab/sdk/hooks"
import type { PluginManifest } from "@palettelab/sdk/types"
import { PluginProvider } from "@palettelab/sdk/components"Helper Reference
Public frontend helpers exported by @palettelab/sdk:
- Provider/context:
PluginProvider,usePlatform,PlatformCtx. - Client facade:
createPaletteClient(platform?);paletteis the default no-argument client used by generated broker clients. - API:
apiFetch(path, init?),apiUpload(path, file, fieldName?, extraFields?),setBaseUrl(url),getBaseUrl(). - Errors:
PaletteApiError,errorFromResponse(response),isPaletteApiError(error). - Data Rooms:
DataRoomClient,dataRooms, pluslist,create,get,folder,ensureRoom,requireRoomByName,findRoomByName,createFolder,ensureFolder,findFolderByName,resolveFolderPath,findFileByName,requestUpload,confirmUpload, anduploadFile. - Storage:
StorageClient,upload(file, options),resume(file, options), anduploadToSignedUrl(uploadUrl, file, contentType?). - Install config:
getInstallConfig(pluginId),updateInstallConfig(pluginId, values). - Connections:
getConnections(pluginId),startConnection(pluginId, connectionId),disconnectConnection(pluginId, connectionId), andrequireConnection(pluginId, connectionId). - App-to-app broker:
broker.call(target, payload?),broker.emit(target, payload?),servicesProxy(namespaceVersion),events.on(target, handler),events.emit(target, payload?), andBrokerCallError. - OS notifications:
notifications.push(input)(alsopalette.notifications.push(input)) — push a persistent notification into the OS notification center. - Organization/user:
UserClient,OrganizationClient, includingcurrent(org settings: name, logo, category, description),updateProfile,listMine,listMembers,getMember,getMemberByEmail,inviteMember, andupdateMemberRole. - Permissions:
hasPermission(ctx, permission),hasAnyPermission(ctx, permissions),hasAllPermissions(ctx, permissions). - Translations:
normalizePaletteLanguage,translate,usePluginTranslations. - OS context:
usePlatform().language,usePlatform().setLanguage(),usePlatform().colorMode, andusePlatform().setColorMode(). - Hooks:
usePluginTasks,usePluginDataRooms,usePluginChat. - Sandbox:
createSandboxBridge,isSandboxRuntime. - Testing:
createMockPlatformContext,withPluginProvider. - Types:
PluginManifest,PluginComponentProps,PlatformContext,PaletteClient, resource, task, chat, data-room, user, organization, translation, and sandbox bridge types.
Plugin Root
The platform passes plugin runtime context into your root component. Wrap your UI with PluginProvider so hooks can read it.
import { PluginProvider, usePlatform, type PluginComponentProps } from "@palettelab/sdk"
function App() {
const { user, organizationId, pluginId } = usePlatform()
return (
<main>
<h1>{pluginId}</h1>
<p>{user?.email}</p>
<p>Organization: {organizationId}</p>
</main>
)
}
export default function PluginRoot(props: PluginComponentProps) {
return (
<PluginProvider value={props}>
<App />
</PluginProvider>
)
}App Translations And OS Language
Palette OS passes the current global language into every plugin through
usePlatform(). Keep translations in your app repo, then let the SDK choose the
right language.
import { usePluginTranslations, type TranslationResources } from "@palettelab/sdk"
const resources = {
en: { title: "Invoices", greeting: "Hello, {{name}}" },
ko: { title: "청구서", greeting: "안녕하세요, {{name}}님" },
} satisfies TranslationResources
function App() {
const { t, language, setLanguage } = usePluginTranslations(resources)
return (
<main>
<h1>{t("title")}</h1>
<button onClick={() => setLanguage(language === "ko" ? "en" : "ko")}>
{language === "ko" ? "EN" : "KO"}
</button>
</main>
)
}The same context is available in pltt dev, so local development and OS runtime
use the same translation path.
App Color Mode And OS Theme
Palette OS also passes the current light/dark mode into every plugin through
usePlatform(). Apps can read colorMode to update app-owned styling or call
setColorMode() to request an OS-level light/dark change.
import { usePlatform } from "@palettelab/sdk"
function ThemeAwarePanel() {
const { colorMode, setColorMode } = usePlatform()
return (
<section data-color-mode={colorMode}>
<button onClick={() => setColorMode(colorMode === "dark" ? "light" : "dark")}>
Switch to {colorMode === "dark" ? "light" : "dark"}
</button>
</section>
)
}Native React apps re-render when Palette OS changes between light and dark mode.
For non-React code, Palette OS also dispatches a palette:theme-change browser
event with { colorMode: "light" | "dark" } in event.detail.
Next-Compatible Apps
The Palette appstore runtime loads plugin frontends as native React modules. If
an app wants a Next-style config file, set frontend.framework in
palette-plugin.json and place the config at frontend/next.config.ts.
{
"frontend": {
"entry": "./frontend/src/index.tsx",
"sandbox": true,
"framework": "next",
"config": "./frontend/next.config.ts"
}
}The CLI reads that config in pltt dev, pltt test, pltt package, and
pltt publish. In native mode it supports env values from Next config,
NEXT_PUBLIC_* environment variables, and path aliases from
frontend/tsconfig.json.
Palette App Router
For OS-native routed apps, set frontend.framework to palette-app and point
frontend.entry at frontend/app. The CLI scans app-directory UI files and
still publishes one safe browser bundle.
{
"frontend": {
"entry": "./frontend/app",
"sandbox": true,
"framework": "palette-app"
}
}Supported UI files are layout.tsx, page.tsx, loading.tsx, error.tsx,
and not-found.tsx. Static routes, route groups, [id], [...slug], and
[[...slug]] are supported. Use Link, useRouter, usePathname,
useSearchParams, and useParams from @palettelab/sdk, or use the
next/link and next/navigation compatibility imports in palette-app
projects.
Server-side Next features are intentionally not part of this mode. Put APIs, database access, permissions, and secrets in the plugin backend.
Palette Client
Use createPaletteClient() when an app needs common Palette OS services without
remembering raw API routes.
import { createPaletteClient, usePlatform } from "@palettelab/sdk"
function App() {
const platform = usePlatform()
const palette = createPaletteClient(platform)
async function upload(file: File) {
const rooms = await palette.dataRooms.list()
await palette.dataRooms.uploadFile(rooms[0].id, file)
palette.toast.success("Uploaded")
}
return <button onClick={() => palette.user.current()}>Load profile</button>
}Included clients:
palette.user.current()andpalette.user.updateProfile()palette.organization.current()— the organisation's settings (name,logo_url,company_typecategory,description,slug), always reflecting the latest values saved in Settings -> Organisationpalette.organization.currentId(),currentRole(),listMine(),listMembers(),getMember(),getMemberByEmail(),inviteMember(), andupdateMemberRole()palette.dataRooms.list(),create(),get(),folder(),createFolder(),ensureFolder(),findRoomByName(),findFolderByName(),resolveFolderPath(),findFileByName(), anduploadFile()palette.config.get()andpalette.config.update(values), with optional plugin ID overridepalette.connections.list(),connect(),disconnect(), andrequire()palette.permissions.has(),hasAny(), andhasAll()palette.storage.upload(file, options),resume(file, options), anduploadToSignedUrl()palette.toast.success(),error(), andinfo()
These helpers are intentionally thin wrappers over platform APIs. Apps can still
use apiFetch() directly for custom backend routes.
App storage is different from Data Rooms. Use palette.storage for app-owned files that should go directly to the OS-configured storage backend, such as GCS. Use palette.dataRooms only when the file should appear as a user-visible Data Room document.
App storage uploads are scoped by Palette to:
uploads/apps/{app_name}_{plugin_id}/{organisation_slug}_{organisation_id}/{file}Declare "storage" in platform_services, then upload directly from the
browser with resumable chunking and progress:
const palette = createPaletteClient(usePlatform())
await palette.storage.upload(file, {
key: `receipts/${file.name}`,
onProgress: (p) => setPercent(p.percentage),
})Member helpers operate on the active organisation. Use members:read for
listing or looking up members, and members:write for invitations or role
updates. When the runtime provides declared app permissions, these helpers check
those permissions before calling the platform API. Member deletion/removal is
intentionally not exposed through the app SDK.
App-To-App Broker
Frontend apps can call declared broker services and subscribe to declared broker
events through the SDK. The app manifest must list the targets under
consumes.services or consumes.events; Palette checks the manifest and the
org install grant before dispatching.
The model is intentionally contract-first:
- Provider apps declare
provides.namespace,provides.services[].methods, optional JSON schemas, andprovides.events. - Consumer apps declare exact qualified targets such as
hr/v1#approvalChain.getunderconsumes.servicesorconsumes.events. - Installs use a dependency plan so required provider apps can be installed first.
- Org owners/admins can grant or revoke each consumed service/event target after install.
- Every service call and event subscription is checked again at runtime.
import { createPaletteClient, usePlatform } from "@palettelab/sdk"
const palette = createPaletteClient(usePlatform())
const chain = await palette.services("hr/v1").approvalChain.get({ user_id })
const unsubscribe = palette.events.on("hr/v1#hierarchy.updated", (payload) => {
console.log(payload)
})
await palette.events.emit("leave/v1#leave.requested", { user_id, chain })
await palette.events.publishDurable("leave/v1#leave.requested", { user_id, chain })
unsubscribe()Use palette.broker.call("hr/v1#approvalChain.get", payload) for a low-level
call when a generated helper is not available.
pltt services pull generates nested TypeScript clients under
.palette/types/services.ts, so app code can import helpers instead of
managing target strings by hand. Broker failures are surfaced as
MissingDependencyError, CrossAppGrantError, or BrokerCallError.
In Palette native runtime, the SDK calls /api/v1/os-broker/dispatch and
/api/v1/os-broker/events/stream directly with the current app identity. In
iframe sandbox runtime, the SDK sends palette.broker.call,
palette.broker.emit, and palette.broker.subscribe messages to the Palette
host. The host forwards the request through the same broker endpoints, so
sandboxed apps still get the same manifest, install, grant, and schema checks.
Declare the frontend dependency in palette-plugin.json:
{
"consumes": {
"services": [
{
"target": "hr/v1#approvalChain.get",
"required": true,
"reason": "Route leave approvals through HR"
}
],
"events": [
{
"target": "hr/v1#hierarchy.updated",
"required": false,
"reason": "Refresh approval-chain cache"
}
]
}
}Generated service clients use the default exported palette client from the
SDK. That lets generated files work in both app components and shared modules:
import { hr } from "./.palette/types/services"
const chain = await hr.approvalChain.get({ user_id })Data Room Folders By Name
Apps can create or reuse custom Data Room folders by name:
const palette = createPaletteClient(usePlatform())
const room = await palette.dataRooms.ensureRoom("Finance")
const invoices = await palette.dataRooms.ensureFolder(room.id, "Invoices")
await palette.dataRooms.uploadFile(room.id, file, {
folderId: invoices.id,
})Apps can also resolve nested folders:
const folder = await palette.dataRooms.resolveFolderPath(
room.id,
"Clients/Acme/Invoices",
{ create: true },
)To read existing files by name:
const room = await palette.dataRooms.requireRoomByName("Finance")
const folder = await palette.dataRooms.resolveFolderPath(room.id, "Clients/Acme/Invoices")
const file = folder
? await palette.dataRooms.findFileByName(room.id, "jan.pdf", { folderId: folder.id })
: nullFrontend OS Notifications
Use the frontend TypeScript SDK to push a persistent notification into the OS notification center (the bell) for the current signed-in user. Unlike a toast, it survives past the moment, shows your app's icon and name, and opens/focuses your app window when clicked:
import { notifications } from "@palettelab/sdk"
// or: palette.notifications.push(...)
await notifications.push({
title: "Export complete",
body: "Your report is ready to download.",
severity: "success", // "info" | "success" | "warning" | "error"
targetApp: "reports-app", // app icon that owns the unread badge
route: "/exports/123", // optional deep link inside the target app
data: { exportId: 123 }, // arbitrary payload stored with the notification
})targetApp controls which app icon receives the unread badge and which app
opens by default. route is optional; when present it resolves inside the
target app. The source app id is inferred from the URL — pass appId to
override.
Use the frontend SDK helper rather than hand-building platform notification API
calls. Frontend notifications target the calling user and their active
organisation, are delivered live to the OS shell, and show as a toast plus a
notification-center entry. Use the Python backend helper
ctx.notifications.push(...) from palette_sdk for cross-user notifications.
Permissions
Use permission helpers to keep UI actions aligned with backend permission gates.
const palette = createPaletteClient(usePlatform())
const canWrite = palette.permissions.has("resources:write")Backend routes should still enforce permissions with the Python SDK. Frontend permission checks are for UX only.
API Helpers
Use apiFetch for authenticated platform API calls. It includes credentials and performs the platform refresh flow for normal portal runtime.
import { apiFetch } from "@palettelab/sdk"
const res = await apiFetch("/api/v1/tasks")
const tasks = await res.json()Use apiUpload for multipart uploads.
import { apiUpload } from "@palettelab/sdk"
await apiUpload("/api/v1/resources/upload", file)Resource Hooks
The SDK includes hooks for common Palette resources:
usePluginTasksusePluginDataRoomsusePluginChat
import { usePluginTasks } from "@palettelab/sdk"
export function TaskList() {
const { tasks, loading, error, refetch } = usePluginTasks()
if (loading) return <p>Loading...</p>
if (error) return <p>{error}</p>
return (
<ul>
{tasks.map((task) => (
<li key={task.id}>{task.title}</li>
))}
</ul>
)
}Install Configuration
Plugins can read and update their installation configuration through helper APIs.
import { getInstallConfig, updateInstallConfig } from "@palettelab/sdk"
const config = await getInstallConfig("my-plugin")
await updateInstallConfig("my-plugin", { ...config, enabled: true })Plugins can also read and start Palette-managed third-party connections declared
in palette-plugin.json:
import { getConnections, startConnection } from "@palettelab/sdk"
const connections = await getConnections("my-plugin")
await startConnection("my-plugin", "google_calendar")Sandbox Bridge
Appstore plugin frontends run inside an iframe sandbox by default. Use createSandboxBridge when you need to explicitly call host-proxied APIs from sandbox runtime code.
import { createSandboxBridge, isSandboxRuntime } from "@palettelab/sdk"
if (isSandboxRuntime()) {
const bridge = createSandboxBridge()
const result = await bridge.apiFetch("/api/v1/tasks")
const colorMode = bridge.getColorMode()
console.log(result)
}For standard React plugin templates, the runtime wiring is already handled by the platform and CLI.
Testing
Use the SDK test utilities to render plugin components with a mock platform context.
import { createMockPlatformContext, withPluginProvider } from "@palettelab/sdk"
const ctx = createMockPlatformContext({
pluginId: "my-plugin",
organizationId: 1,
})
const Wrapped = withPluginProvider(MyPluginComponent, ctx)Package Contents
The npm package ships compiled CommonJS, ESM, and TypeScript declarations from dist/.
Related Packages
@palettelab/cliprovides theplttcommand for scaffolding, local dev, validation, packaging, and publishing.palette-sdkis the Python backend SDK for FastAPI plugin routes, permissions, tools, DB helpers, RLS, and test helpers.
The returned saved object also includes snake_case aliases (object_path, file_url, content_type, upload_id) so browser and Python backend upload handlers can share response-normalization code.
