@palettelab/sdk
v0.1.16
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.
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,
DataRoomClient,
StorageClient,
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?). - 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). - Organization/user:
UserClient,OrganizationClient, includingcurrent,updateProfile,listMine,listMembers,getMember,getMemberByEmail,inviteMember, andupdateMemberRole. - Permissions:
hasPermission(ctx, permission),hasAnyPermission(ctx, permissions),hasAllPermissions(ctx, permissions). - Translations:
normalizePaletteLanguage,translate,usePluginTranslations. - 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.
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.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.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 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.
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 })
: nullPermissions
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 })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")
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.
