npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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-dom

react 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 dev

pltt 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 staging

The 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-tokens

Validate it before configuring the CLI:

curl https://YOUR-PALETTE-STAGING-URL/api/v1/health

If 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:

  1. Open Palette OS.
  2. Open Settings.
  3. Open Developer.
  4. Create a developer publish token.
  5. 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_xxxxx

The 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_xxxxx

Local Simulator Versus Hosted OS

Use the local simulator for everyday development:

npx --yes @palettelab/cli@latest dev

Use hosted sandbox for full OS behavior:

npx --yes @palettelab/cli@latest dev --sandbox --env staging

For internal sandbox environments where manual approval should not block app testing, run the Palette backend with:

APPSTORE_AUTO_APPROVE_SANDBOX_PREVIEWS=true

When 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?); palette is 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, plus list, create, get, folder, ensureRoom, requireRoomByName, findRoomByName, createFolder, ensureFolder, findFolderByName, resolveFolderPath, findFileByName, requestUpload, confirmUpload, and uploadFile.
  • Storage: StorageClient, upload(file, options), resume(file, options), and uploadToSignedUrl(uploadUrl, file, contentType?).
  • Install config: getInstallConfig(pluginId), updateInstallConfig(pluginId, values).
  • Connections: getConnections(pluginId), startConnection(pluginId, connectionId), disconnectConnection(pluginId, connectionId), and requireConnection(pluginId, connectionId).
  • App-to-app broker: broker.call(target, payload?), broker.emit(target, payload?), servicesProxy(namespaceVersion), events.on(target, handler), events.emit(target, payload?), and BrokerCallError.
  • OS notifications: notifications.push(input) (also palette.notifications.push(input)) — push a persistent notification into the OS notification center.
  • Organization/user: UserClient, OrganizationClient, including current (org settings: name, logo, category, description), updateProfile, listMine, listMembers, getMember, getMemberByEmail, inviteMember, and updateMemberRole.
  • Permissions: hasPermission(ctx, permission), hasAnyPermission(ctx, permissions), hasAllPermissions(ctx, permissions).
  • Translations: normalizePaletteLanguage, translate, usePluginTranslations.
  • OS context: usePlatform().language, usePlatform().setLanguage(), usePlatform().colorMode, and usePlatform().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() and palette.user.updateProfile()
  • palette.organization.current() — the organisation's settings (name, logo_url, company_type category, description, slug), always reflecting the latest values saved in Settings -> Organisation
  • palette.organization.currentId(), currentRole(), listMine(), listMembers(), getMember(), getMemberByEmail(), inviteMember(), and updateMemberRole()
  • palette.dataRooms.list(), create(), get(), folder(), createFolder(), ensureFolder(), findRoomByName(), findFolderByName(), resolveFolderPath(), findFileByName(), and uploadFile()
  • palette.config.get() and palette.config.update(values), with optional plugin ID override
  • palette.connections.list(), connect(), disconnect(), and require()
  • palette.permissions.has(), hasAny(), and hasAll()
  • palette.storage.upload(file, options), resume(file, options), and uploadToSignedUrl()
  • palette.toast.success(), error(), and info()

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, and provides.events.
  • Consumer apps declare exact qualified targets such as hr/v1#approvalChain.get under consumes.services or consumes.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 })
  : null

Frontend 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:

  • usePluginTasks
  • usePluginDataRooms
  • usePluginChat
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/cli provides the pltt command for scaffolding, local dev, validation, packaging, and publishing.
  • palette-sdk is 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.