@palettelab/cli
v0.3.37
Published
Developer CLI for building Palette platform plugins — no platform source access required.
Readme
@palettelab/cli
Developer CLI for building plugins for the Palette platform. Works without any access to the platform source — your plugin repo is the only thing you own.
The installed executable is pltt.
Requirements
- Node.js 18+
- Python 3.12+ for local backend simulation
- No Docker is required for normal app development, testing, or publishing.
- Docker Desktop is only for internal
pltt dev --platformparity checks.
Install
You don't have to install globally — use npx:
npx @palettelab/cli <command>
# or after global install
pltt <command>Quick Start: Build And Test A Palette App
Use this flow when a developer wants to create an app, run it locally, then test it inside the real Palette OS without Docker.
npx --yes @palettelab/cli@latest init simple-todo --template database
cd simple-todo
npm install
npx --yes @palettelab/cli@latest devpltt dev runs the local SDK simulator. It is the fastest loop for frontend,
backend, manifest, SDK hooks, and local database checks. It does not require
Docker and does not publish anything to the platform.
When the app is ready to test with real Palette OS services, configure a hosted sandbox environment 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 flow packages the app, uploads it to the configured Palette environment, creates a preview publish, and prints a real OS preview URL. Use that URL to test routing, OS shell behavior, login context, organization context, Data Room APIs, storage, install/review behavior, logs, permissions, and platform APIs.
Staging URL
The staging URL is the base URL of the Palette platform backend/API environment. The CLI appends the API paths itself, for example:
/api/v1/appstore/sign-upload/api/v1/appstore/publish/api/v1/developer/publish-tokens/api/superadmin/publish-tokens
Use the same public origin only if that origin proxies API routes to the
backend. For example, https://apps.pltt.xyz is valid only when these paths are
served by the backend, not by the frontend catch-all route:
/api/v1/*
/api/superadmin/*Validate a staging URL before giving it to developers:
curl https://YOUR-PALETTE-STAGING-URL/api/v1/healthA valid staging URL returns a backend health JSON response. If it returns the
Palette frontend HTML, it is a frontend-only URL and the CLI cannot publish to
it. In that case either use the real backend domain, for example
https://api.your-domain.example, or update the web server/reverse proxy so
/api/v1/* and /api/superadmin/* go to the backend.
Publish Token
Every developer needs a publish token before they can use hosted sandbox or
publish commands. Tokens start with pltt_.
Developers can create their own token after logging in to Palette:
- Open Palette OS in the browser.
- Open Settings.
- Go to Developer.
- Create a developer publish token.
- Copy the token immediately. The raw token is shown only once.
Superadmins can also allocate tokens from the superadmin publish-token section. Use superadmin allocation for service accounts, CI, or developers who should not create their own token.
Do not commit tokens to a repository. Store them through pltt login or an
environment variable.
Configure A Hosted Sandbox
The recommended setup is pltt login, which writes
~/.palette/config.json with file mode 0600:
npx --yes @palettelab/cli@latest login \
--env staging \
--url https://YOUR-PALETTE-STAGING-URL \
--token pltt_xxxxxYou can verify the saved environment with:
cat ~/.palette/config.jsonYou can also use environment variables instead of storing the token:
export PALETTE_STAGING_URL=https://YOUR-PALETTE-STAGING-URL
export PALETTE_STAGING_TOKEN=pltt_xxxxx
npx --yes @palettelab/cli@latest dev --sandbox --env stagingEnvironment variable precedence:
PALETTE_<ENV>_URLandPALETTE_<ENV>_TOKENPALETTE_PUBLISH_TOKENas a token fallback~/.palette/config.json./palette.config.jsonfor repo-local overrides
Test Inside The Real Palette OS Without Docker
Use hosted sandbox for real OS testing:
npx --yes @palettelab/cli@latest dev --sandbox --env stagingThis is the correct flow for internal teams that need real platform features without pushing the app to production approval and without running Docker on the developer machine.
Expected behavior:
- The CLI runs local contract checks.
- The CLI bundles frontend and backend artifacts.
- The CLI uploads the package to the staging Palette environment.
- The platform creates a preview/review publish.
- The CLI prints the preview URL, status command, and logs command.
- The developer opens the preview URL inside the real Palette OS.
For developer sandboxes where manual review should not block testing, the backend environment should run with:
APPSTORE_AUTO_APPROVE_SANDBOX_PREVIEWS=trueWith that backend setting enabled, passing sandbox preview publishes are marked active automatically, so developers can test OS shell, auth, Data Room, organization, install, permission, log, and platform API behavior immediately.
Useful follow-up commands:
npx --yes @palettelab/cli@latest status --env staging
npx --yes @palettelab/cli@latest logs simple-todo --env staging --followLocal-Only Versus OS Testing
Use the right command for the kind of test you need:
| Goal | Command | Docker | Uses real OS services |
|---|---|---:|---:|
| Fast SDK/frontend/backend loop | pltt dev | No | No |
| Real OS preview in hosted cloud sandbox | pltt dev --sandbox --env staging | No | Yes |
| Alias for hosted cloud sandbox | pltt dev --cloud --env staging | No | Yes |
| Internal full local platform parity | pltt dev --platform | Yes | Local platform container |
| Production/review publish | pltt publish --env production | No | Yes |
For normal app developers, Docker is not required. Docker is only needed for
internal platform parity checks with pltt dev --platform.
App-Owned Data, Migrations, And Python Backends
For a detailed Python backend SDK guide with route examples, app-owned data, migrations, Data Rooms, config, secrets, lifecycle hooks, and hosted sandbox testing, see Python Backend SDK Developer Guide.
Palette apps can own their own Python backend, database tables, migrations, and organization-scoped data. The generated database template uses this structure:
my-app/
palette-plugin.json
frontend/src/index.tsx
backend/api/main.py
backend/api/models.py
backend/migrations/env.py
backend/migrations/versions/001_init.pyDeclare database ownership in palette-plugin.json:
{
"capabilities": { "database": true },
"database": {
"schema": "app_my_app",
"migrations": "./backend/migrations"
}
}Define org-scoped models with the backend SDK. Table names must start with the
app prefix (my_app__ for my-app):
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from palette_sdk import OrgScopedTable
class Invoice(OrgScopedTable):
__tablename__ = "my_app__invoices"
id: Mapped[int] = mapped_column(primary_key=True)
customer_name: Mapped[str] = mapped_column(String(255))Use the migration helper in Alembic migrations:
from palette_sdk.db import ensure_org_rls
def upgrade():
op.create_table(...)
ensure_org_rls(op, "my_app__invoices")Use ctx.db for the full database feature set. It is the real async
SQLAlchemy session, scoped by Palette to the app schema and current
organization, so app code can use Core/ORM queries, joins, aggregates,
transactions, bulk operations, and raw text() SQL. Use migrations for schema
changes such as tables, indexes, constraints, and types.
Use ctx.repo(Model) when simple org-safe CRUD is enough:
from fastapi import Depends
from palette_sdk import PluginContext, get_plugin_context
from models import Invoice
@router.get("/invoices")
async def list_invoices(ctx: PluginContext = Depends(get_plugin_context)):
rows = await ctx.repo(Invoice).list(order_by="-id")
return [{"id": r.id, "customer": r.customer_name} for r in rows]
@router.post("/invoices")
async def create_invoice(body: InvoiceIn, ctx: PluginContext = Depends(get_plugin_context)):
invoice = await ctx.repo(Invoice).create(**body.model_dump())
return {"id": invoice.id}Backend SDK features for app-owned data:
PluginRouter,PluginContext, andget_plugin_contextprovide the FastAPI route and request-context surface.PluginContextexposesuser_id,organization_id,org_role,plugin_id,permissions,storage,ctx.db,ctx.data_rooms,ctx.members,ctx.redis,ctx.vector,ctx.config, andctx.logger.ctx.dbis the full scoped SQLAlchemyAsyncSessionfor app-owned database data.ctx.repo(Model)gives org-safe CRUD helpers for app tables.ctx.data_roomsgives backend access to Palette Data Rooms without importing platform internals.ctx.membersgives backend access to current organisation members; it exposes list/get/invite/update-role helpers, but no delete/remove helper.ctx.has_permission("..."),ctx.has_any_permission([...]), andctx.has_all_permissions([...])check declared permissions.ctx.config_value("key")andctx.require_config("key")read app install/config values.ctx.secret("KEY")reads app secrets from config or environment variables.get_config(ctx, key)andrequire_config(ctx, key)are functional config helper forms.require_permission(permission),KNOWN_PERMISSIONS, andis_known_permission(permission)support route and manifest permission checks.ctx.redisgives a Redis-backed, plugin/org-scoped Redis API when"redis"is declared inplatform_services.ctx.vectorgives a Qdrant-backed, plugin/org-scoped vector API when"vector"is declared inplatform_services.ctx.storagegives app/org-scoped file upload helpers when"storage"is declared inplatform_services.LifecycleHookslets apps define install/update/enable/disable/uninstall hooks.OrgScopedTableandPluginBasekeep app data inside the plugin schema model set.plugin_safe_id(...),plugin_schema(...),plugin_table_prefix(...), andensure_org_rls(...)keep database names and row-level security consistent.Eventandsubscribe_event(...)register in-process platform event handlers.sign_webhook(...)andverify_webhook_signature(...)handle HMAC-SHA256 webhook signing checks.ToolDefinitionis the base class for custom agent tools.PluginManifestandload_manifest(...)parse and validatepalette-plugin.json.SuccessResponse,ErrorResponse, andPaginatedResponseare reusable response schemas.route_permission_issues(router, public_routes=None)is the test helper for detecting ungated backend routes.
Python backend Data Room example:
@router.post("/sync-invoices", dependencies=[require_permission("data_rooms:write")])
async def sync_invoices(ctx: PluginContext = Depends(get_plugin_context)):
room = await ctx.data_rooms.ensure_room("Finance")
folder = await ctx.data_rooms.resolve_folder_path(
room["id"],
"Clients/Acme/Invoices",
create=True,
)
file = await ctx.data_rooms.find_file_by_name(
room["id"],
"jan.pdf",
folder_id=folder["id"] if folder else None,
)
content = await ctx.data_rooms.read_file_bytes(file["id"]) if file else None
if folder:
await ctx.data_rooms.upload_file(
room["id"],
"summary.txt",
b"Generated by my app",
folder_id=folder["id"],
content_type="text/plain",
)
return {"room": room, "folder": folder, "bytes": len(content or b"")}Python backend app-storage example:
@router.post("/reports", dependencies=[require_permission("reports:write")])
async def save_report(ctx: PluginContext = Depends(get_plugin_context)):
saved = await ctx.storage.upload_file(
"summary.json",
b'{"ok": true}',
"application/json",
key="reports/summary.json",
)
return savedFrontend apps can use createPaletteClient(platform).storage.upload(file, {
onProgress }). Palette writes every object under the app folder and current
organisation folder, then uses GCS resumable uploads in hosted environments.
The npm @palettelab/sdk package is for frontend JavaScript/React apps.
Python backend code uses palette_sdk, which is embedded in the CLI for
local dev/tests and injected by the hosted Palette runtime.
Lifecycle example:
from palette_sdk import LifecycleHooks, PluginContext
lifecycle = LifecycleHooks()
@lifecycle.on_install
async def seed_defaults(ctx: PluginContext):
await ctx.repo(DefaultSetting).create(name="currency", value="USD")Run local checks before publishing:
npx --yes @palettelab/cli@latest test
npx --yes @palettelab/cli@latest packageThe CLI validates manifest shape, SDK compatibility, frontend bundling, backend imports, backend route permission gates, declared permissions, migration safety, package dependency policy, and backend package size.
Commands
pltt init <name>
Scaffold a new plugin directory from the official template.
pltt init data-explorer
pltt init crm-dashboard --template dashboard
pltt init next-panel --template next
cd data-explorerCreates data-explorer/ with a valid palette-plugin.json, a frontend React entry, and a FastAPI backend entry.
Templates:
dashboardnextagent-toolexternal-servicedatabasefrontend-only
Next-Compatible Frontend Config
Palette native apps publish as a single React module loaded by the OS. They do not run a standalone Next server. When an app needs Next-style frontend config, set the manifest to Next-compatible native mode:
{
"frontend": {
"entry": "./frontend/src/index.tsx",
"sandbox": true,
"framework": "next",
"config": "./frontend/next.config.ts"
}
}Put the config file at frontend/next.config.ts unless you set a custom
frontend.config path. pltt dev, pltt test, pltt package, and
pltt publish load next.config.ts/js/mjs/cjs, apply env values plus
NEXT_PUBLIC_* environment variables to the native bundle, and honor path
aliases from frontend/tsconfig.json.
Supported today: env, NEXT_PUBLIC_*, and TypeScript path aliases. Full Next server features
such as API routes, server components, Next image optimization, middleware, and
multi-file static export are outside this native module mode.
pltt dev
Run a no-Docker local SDK simulator with your plugin mounted live. Run this from inside your plugin directory.
pltt dev
pltt sandbox --env staging
pltt dev --sandbox --env staging
pltt dev --cloud --env staging
pltt dev --platformBy default this starts:
- A small local app shell on the first available port starting at http://localhost:3000
- A local FastAPI backend runner on the first available port starting at http://localhost:8000
- A mock Palette platform context for
usePlatform(), OS language, toasts, org/user data, and authenticated API calls - A plugin-local SQLite database under
.palette/dev/whencapabilities.databaseordatabaseis enabled
Your frontend entry is bundled and watched. Your backend entry is loaded under
/api/v1/plugins/<your-id>/* with a dev PluginContext, so normal SDK calls
work without Docker or platform source.
The simulator also provides the same language fields that Palette OS provides:
language, fallbackLanguage, supportedLanguages, and setLanguage().
Generated frontend templates include app-owned frontend/src/translations.ts
files wired through usePluginTranslations(), so apps can switch when the OS
language changes without storing copy in the platform.
For Python apps with database tables, ctx.db is an async SQLAlchemy session in
local dev. The simulator imports backend/api/models.py when present and creates
tables from palette_sdk.db.PluginBase.metadata. Production installs still use
the declared Alembic migrations and platform-managed Postgres/RLS.
3000 and 8000 are preferred defaults, not hard requirements. If either port is already in use, pltt dev automatically picks the next free port and prints the actual URLs.
pltt dev --sandbox skips Docker and publishes a reviewable preview to a configured Palette sandbox. This is the payment-gateway-style flow: your app uses local SDKs, while real platform services such as login, Data Room, storage, database policies, logs, review, and publish lifecycle run in the hosted sandbox. It defaults to --env staging unless --env or PALETTE_ENV is set, adds a 24-hour preview TTL by default, and prints the preview/status/log commands returned by the platform.
For developer sandboxes where manual approval should not block OS testing, run the sandbox backend with APPSTORE_AUTO_APPROVE_SANDBOX_PREVIEWS=true. Preview publishes are still checked by the automated review gates; passing preview publishes are marked active and synced immediately so backend routes, installs, logs, permissions, and platform APIs can be tested in the OS without Docker.
pltt dev --cloud is kept as an alias for --sandbox.
pltt sandbox --env staging is the same hosted preview flow as
pltt dev --sandbox --env staging.
pltt preview --env staging is also supported as a developer-friendly alias
for the same hosted preview flow.
pltt dev --platform runs the full Docker platform-dev image for internal platform parity testing. App developers should not need it. It pulls ghcr.io/palette-lab/platform-dev:latest and mounts your plugin at /plugins/<your-id>.
Environment variables:
| Name | Default | Purpose |
|---|---|---|
| PALETTE_DEV_IMAGE | ghcr.io/palette-lab/platform-dev:latest | Override the platform image for --platform |
| PALETTE_FRONTEND_PORT | 3000 | Preferred starting host port for the frontend |
| PALETTE_BACKEND_PORT | 8000 | Preferred starting host port for the backend |
| PALETTE_DEV_DATABASE_URL | .palette/dev/<plugin-id>.sqlite3 | Override the local dev database URL |
| APPSTORE_AUTO_APPROVE_SANDBOX_PREVIEWS | false | Backend setting for hosted sandboxes; auto-approve passing preview publishes so developers can test full OS behavior without manual review |
pltt secrets
Palette secrets are declared in palette-plugin.json and resolved through
ctx.secret("NAME").
{
"secrets": {
"OPENAI_API_KEY": { "scope": "install", "required": true },
"STRIPE_SECRET": { "scope": "plugin", "required": true },
"DEBUG_PROBE_URL": { "scope": "dev", "required": false }
},
"platform_services": ["llm", "redis", "storage", "vector"]
}Commands:
pltt secrets init
pltt secrets set STRIPE_SECRET --env staging --value sk_live_...
pltt secrets rotate STRIPE_SECRET --env staging --value sk_live_...
pltt secrets list --env staging
pltt publish --env staging --secrets-file plugin-secrets.envdev secrets live in .palette/.env.local, are loaded by pltt dev, and are
never uploaded. plugin secrets are encrypted by the platform and attached to
the plugin/environment. install secrets are filled by the installing org.
Frontend bundles may only receive public values such as NEXT_PUBLIC_*.
Managed platform services do not require developer Redis/Qdrant keys. Declare them and use scoped SDK clients:
await ctx.redis.set("cart:123", {"items": [1, 2]}, ttl=3600)
cart = await ctx.redis.get("cart:123")
await ctx.vector.upsert_texts(
"products",
[{"id": "p1", "text": "Red cotton shirt", "metadata": {"category": "clothing"}}],
)
matches = await ctx.vector.search("products", query="red shirt", top_k=5)The platform rewrites Redis keys and vector filters with plugin_id and
organization_id, so create/update/delete/list operations cannot reach another
app or org's data.
For provider-level Redis features, use ctx.redis.execute(...). It forwards to
Redis after rewriting key arguments into the plugin/org namespace, and blocks
server/admin commands such as FLUSHDB, CONFIG, KEYS, and cluster
management. For advanced Qdrant features, use ctx.vector.client() with
collection_name(), scoped_filter(), merge_filter(), scoped_payload(),
and scoped_point() so custom calls remain scoped.
Common managed-service commands:
# Redis strings, counters, and key discovery
await ctx.redis.get("key", default=None)
await ctx.redis.set("key", {"json": True}, ttl=600)
await ctx.redis.delete("key1", "key2")
await ctx.redis.exists("key")
await ctx.redis.expire("key", 300)
await ctx.redis.ttl("key")
await ctx.redis.incr("counter")
await ctx.redis.decr("counter")
await ctx.redis.scan(prefix="cache:", limit=100)
# Redis hashes, lists, sets, sorted sets, queues, locks
await ctx.redis.hset("hash", "field", {"value": 1})
await ctx.redis.hgetall("hash")
await ctx.redis.lpush("queue", {"job": 1})
await ctx.redis.lrange("queue", 0, -1)
await ctx.redis.sadd("tags", "red", "blue")
await ctx.redis.smembers("tags")
await ctx.redis.zadd("scores", {"alice": 10})
await ctx.redis.zrange("scores", 0, -1, with_scores=True)
await ctx.redis.enqueue("jobs", {"task": "sync"})
await ctx.redis.dequeue("jobs")
await ctx.redis.lock("invoice:1", token, ttl=30)
await ctx.redis.unlock("invoice:1", token)
# Redis provider-style data-plane calls
await ctx.redis.execute("MSET", "a", "1", "b", "2")
values = await ctx.redis.execute("MGET", "a", "b")
# Vector search
await ctx.vector.upsert_texts("knowledge", [{"id": "doc-1", "text": "Text"}])
await ctx.vector.upsert_vectors("knowledge", [{"id": "vec-1", "vector": [0.1, 0.2]}])
hits = await ctx.vector.search("knowledge", query="invoice policy", top_k=10)
record = await ctx.vector.get("knowledge", "doc-1")
await ctx.vector.delete("knowledge", ["doc-1"])
await ctx.vector.delete_index("knowledge")
stats = await ctx.vector.stats("knowledge")Blocked Redis control-plane commands include FLUSHDB, FLUSHALL, CONFIG,
KEYS, CLUSTER, SCRIPT, EVAL, FUNCTION, and SELECT.
pltt login
Save a Palette sandbox or production environment URL plus token in ~/.palette/config.json with file mode 0600. Environment variables still override the stored token when present.
pltt login --env staging --url https://sandbox.pltt.ai --token <publish-token>
pltt dev --sandbox --env stagingpltt doctor
Check local tooling and common setup problems.
pltt doctorChecks include Node version, port availability, manifest validity, entry files, and frontend bundling.
Docker is intentionally skipped by default:
pltt doctor --platformUse --platform only when validating the internal Docker parity harness.
pltt build
Validate palette-plugin.json and check that all declared entry files exist. Run this before pushing a release.
pltt buildAlso lints plugin migrations for unsafe RLS patterns when database.migrations is declared.
pltt test
Run local contract checks before publishing.
pltt test
pltt test --jsonChecks include manifest validity, SDK/platform compatibility, semver bump detection, forbidden platform imports, frontend bundling and size limits, sandbox bridge smoke, backend dependency installation/import, route permission gates, route permission declarations, migration linting, frontend sandbox policy, and dependency policy for package.json / pyproject.toml.
pltt package
Bundle the plugin into dist/<plugin-id>-<version>.tar.gz without uploading.
pltt package
pltt package --jsonUse this to inspect the publishable artifact before sending it to a Palette environment.
pltt publish
Build and publish the plugin to a Palette appstore environment.
pltt publish --env local
pltt publish --env staging
pltt publish --env production
pltt publish --env production -y
pltt publish --env staging --json
pltt publish --env staging --ttl-hours 24Publishing first runs the same contract checks as pltt test. If preflight passes, it bundles frontend/backend artifacts, uploads them, creates a pending_review publish record, and prints review/preview URLs when the platform returns them.
--ttl-hours sets an expiration on the preview URL. It is mainly used by pltt dev --sandbox, which defaults previews to 24 hours.
Environment config is read from ./palette.config.json, ~/.palette/config.json, or PALETTE_<ENV>_URL plus PALETTE_<ENV>_TOKEN / PALETTE_PUBLISH_TOKEN.
pltt status <publish-id>
Show review status and automated risk report details for a publish.
pltt status 123 --env staging
pltt status --jsonIf no publish ID is provided, the CLI uses .palette/last-publish.json when available.
pltt logs [plugin-id]
Fetch or stream telemetry events for a plugin.
pltt logs my-plugin --env staging
pltt logs --tail 100
pltt logs --follow
pltt logs --jsonIf no plugin ID is provided, the CLI uses the current palette-plugin.json or .palette/last-publish.json.
Global Flags
--jsonemits machine-readable output forpackage,publish,status,logs, andtest.--env <name>selects a configured publish/status/logs environment.-y, --yesskips production publish confirmation.
What gets shipped
@palettelab/cli itself contains only:
bin/pltt.js— entry pointbackend-sdk/— backend SDK files used by local contract tests and simulatorlib/— pure-Node command implementations (no runtime dependencies)platform-dev/docker-compose.yml— compose file for internalpltt dev --platformparity checkstemplate-fallback/— offline fallback forpltt initif git is unavailable
See also
@palettelab/sdkon npm — frontend hooks and typespalette-sdkGitHub Release wheel onsdk-backend-v*— backendPluginRouter+ToolDefinitionsdk/scripts/verify-release-registries.sh— npmjs, GHCR, and backend SDK release verification
