Spatio Platforms

A platform is a sandboxed micro-frontend you build and ship into Spatio. It renders inside the app using a constrained component kit, reads data through capability-scoped APIs, persists state in workspace-scoped key-value storage, and can expose agent-callable tools. Authors write idiomatic React; the SDK reconciles that tree into a serialized patch stream the host renders with real Spatio UI. Strangers' code never touches the DOM, never sees a token, and only reaches the network you declared.

Three things make a platform: a manifest, a bundle (your built code), and the permissions a user grants at install.

What is a platform

Your code runs in a Web Worker. It composes only SDK primitives (List, Table, LineChart, Form, …) — there is no div, no className, no raw HTML. The worker serializes the tree to the host, which renders each primitive with a native Spatio component. Side effects (storage, provider calls, navigation) are capability RPCs over postMessage; the host enforces the grants the user approved.

This closed surface is the whole point: a serialized tree from a stranger is safe to render in-host.

Quickstart

Terminal
npm install -g @spatio-labs/platform-cli
spatio-platform init "My Platform"
cd my-platform
spatio-platform login
spatio-platform dev

init scaffolds a manifest.json, a src/index.tsx exporting export default function Command(), and a tsconfig.json. dev runs a watch build and serves the bundle locally.

The manifest

manifest.json is the PlatformManifest contract. Required fields are id, name, version, description. The rest declare what your platform contributes and what it needs.

JSON
{
  "id": "spatio-analytics",
  "name": "Spatio Analytics",
  "version": "0.1.0",
  "description": "Revenue + product analytics.",
  "icon": "chart-line",
  "permissions": ["kv:read", "kv:write", "api:read", "providers:execute", "mcp:expose"],
  "network": { "allowedDomains": ["api.stripe.com", "app.posthog.com"] },
  "surfaces": { "main": { "entry": "Command" } },
  "mcpTools": [ /* … */ ]
}
FieldPurpose
permissionsWhat the user grants at install: kv:read / kv:write, api:read / api:write, providers:execute, mcp:expose.
network.allowedDomainsOutbound allowlist for the generic HTTP provider / proxy egress. Exact host or *.suffix. Anything outside the set is rejected.
surfacesWhich Spatio surfaces you contribute (see below).
mcpToolsAgent-operable tools (see MCP tools).

Surfaces

Each surface names an exported Command in your bundle. The worker mounts it for that surface.

JSON
"surfaces": {
  "main":      { "entry": "Command" },
  "sidebar":   { "label": "Analytics", "icon": "chart-line", "entry": "Command" },
  "create":    [{ "id": "report", "title": "New report", "entry": "CreateReport" }],
  "inspector": [{ "id": "details", "label": "Details", "entry": "Inspector" }]
}

main is the full-pane view. sidebar adds a left-nav entry. create entries appear in the global ⌘K create flow. inspector entries render in the right sidebar.

The component kit

Compose only these primitives. Props are a closed, JSON-serializable schema — event handlers are the only function-valued props, and the reconciler converts them to handler tokens on the wire.

  • Collections: List (.Item, .Section, .EmptyView), Grid (.Item, .Section), Table
  • Content: Detail, EmptyState, ConnectPrompt
  • Forms: Form (.TextField, .TextArea, .PasswordField, .Dropdown, .Checkbox, .DatePicker, .TagPicker)
  • Actions: ActionPanel (.Section), Action
  • Board: Board (.Column, .Card)
  • Charts: LineChart, BarChart, AreaChart, PieChart
  • Layout: Stack, Box, Divider, Spacer, ScrollView, Tabs (.Tab)
  • Atoms: StatCard, Badge, Tag, Avatar, Icon, Button, Text
TypeScript
import { render, List, StatCard, Stack } from '@spatio/platform-sdk'

export default function Command() {
  return (
    <Stack direction="vertical" gap={3}>
      <StatCard label="MRR" value="$42,180" delta={{ value: '4.1%', direction: 'up' }} />
      <List navigationTitle="Reports">
        <List.Item id="q3" title="Q3 revenue" subtitle="Updated today" />
        <List.EmptyView title="No reports yet" />
      </List>
    </Stack>
  )
}

render(Command)

A date range picker is composed from two Form.DatePicker fields — there is no dedicated range primitive.

Data: providers + scoped KV

Two data paths, both capability-gated.

Providers — run a provider action with useProviders().execute. The host resolves the connected account + credentials and performs the outbound call. Your worker never sees a token.

TypeScript
import { useProviders, useSpatioAPI } from '@spatio/platform-sdk'

const { execute, getAccounts, list, get } = useProviders()
const series = await execute('stripe', 'mrr_trend', { start, end, currency: 'usd' })

// useSpatioAPI() reaches Spatio's own REST surface (gated by api:read / api:write):
const api = useSpatioAPI()
const notes = await api.get('/v1/notes')

Scoped KVuseStorage is workspace-scoped key-value storage (backed by platform_kv, gated by kv:read / kv:write). Default scope is workspace (shared); pass scope: 'user' to keep a key private to the current user.

TypeScript
import { useStorage } from '@spatio/platform-sdk'

const { value, setValue, remove, loading } = useStorage('range', {
  defaultValue: { start: '2026-01-01', end: '2026-06-01' },
  scope: 'user',
})

MCP tools

Declare mcpTools in the manifest to make your platform agent-operable. Each tool surfaces as an action under the single spatio_<platform> MCP tool. There are two execution kinds.

proxy — call an external backend through the generic HTTP provider. {param} placeholders in the URL/query are filled from validated args; the stored credential is injected into the named header.

JSON
{
  "name": "mrr_trend",
  "description": "Monthly recurring revenue over a range. Backed by Stripe.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "start": { "type": "string", "format": "date" },
      "end":   { "type": "string", "format": "date" }
    },
    "required": ["start", "end"]
  },
  "execution": {
    "kind": "proxy",
    "proxy": {
      "providerId": "stripe",
      "method": "GET",
      "url": "https://api.stripe.com/v1/billing/meters/mrr/event_summaries?start={start}&end={end}",
      "authHeader": "Authorization",
      "authFormat": "Bearer {{secret}}"
    }
  },
  "annotations": { "readOnlyHint": true }
}

worker — invoke an action your running worker registered. The host posts an invokeAction; the SDK looks it up in the registry and posts back the result.

TypeScript
import { registerAction } from '@spatio/platform-sdk'

registerAction('refresh', async (params) => {
  // … recompute …
  return { ok: true }
})
JSON
{ "name": "refresh", "description": "Force a reload.",
  "inputSchema": { "type": "object", "properties": {} },
  "execution": { "kind": "worker", "worker": { "action": "refresh" } } }

The proxy host enforces network.allowedDomains, so a tool can only reach hosts you declared.

Network & permissions

Permissions are requested in the manifest and granted by the user at install. They gate the matching capability RPCs at the host boundary:

  • kv:read / kv:writeuseStorage
  • api:read / api:writeuseSpatioAPI
  • providers:executeuseProviders().execute and proxy mcpTools
  • mcp:expose — declaring mcpTools at all

network.allowedDomains is a hard egress allowlist enforced server-side for every proxied call. A platform that omits a host simply cannot reach it.

  1. Create the platform record once: POST /v1/custom-platforms with { name, displayName }. Save the returned id as manifest.platformId.
  2. Publish a version with the CLI (upload + publish, below).
  3. A workspace member installs it from the in-app platform store. The install dialog lists the requested permissions and network.allowedDomains; the user must consent before the platform mounts.
  4. When the platform needs a provider it isn't connected to, it renders a ConnectPrompt; the user connects the account inline.

Grants are per-install and per-workspace. Revoking a permission stops the matching capability immediately.

CLI reference

@spatio-labs/platform-cli ships the spatio-platform binary.

Terminal
spatio-platform login            # OAuth 2.1 PKCE loopback, or SPATIO_API_KEY / pat_ for CI
spatio-platform whoami           # stored identity + owned platforms
spatio-platform logout           # clear ~/.spatio/dev.json
spatio-platform init [name]      # scaffold a project
spatio-platform dev [--port n]   # watch build, serve bundle.js
spatio-platform build            # production bundle + sha256
spatio-platform publish          # upload a version, then publish it as latest
CommandAPI endpoints
loginGET /.well-known/oauth-authorization-server, POST /oauth2/register, GET /oauth2/authorize, POST /oauth2/token
whoamiGET /v1/custom-platforms
publishPOST /v1/custom-platforms/:platformId/versions, then POST /v1/custom-platforms/:platformId/versions/:versionId/publish

Environment: SPATIO_API_URL (default https://api.spatio.app), SPATIO_API_KEY / SPATIO_PAT (CI token), SPATIO_OAUTH_CLIENT_ID (reuse a registered client instead of dynamic registration).

Tutorial: build the analytics platform

The reference platform examples/spatio-analytics is a complete, buildable example. It renders a revenue + product dashboard and exposes mrr_trend (Stripe) and active_users (PostHog) as agent tools.

1. Scaffold and declare. Its manifest.json sets surfaces.main, network.allowedDomains: ["api.stripe.com", "app.posthog.com"], and the two proxy mcpTools with JSON-Schema inputSchemas.

2. Build the dashboard. src/index.tsx composes StatCard (MRR, active users), LineChart (MRR over time), BarChart (active users), Table (MRR rows), Tabs (Revenue / Product), two Form.DatePickers for the range, and EmptyState / ConnectPrompt for the empty + unconnected states.

TypeScript
const { execute, getAccounts } = useProviders()
const { value: range, setValue: setRange } = useStorage('range', { scope: 'user' })

const mrr = await execute('stripe', 'mrr_trend', { start: range.start, end: range.end, currency: 'usd' })

3. Persist the range. useStorage('range', { scope: 'user' }) keeps each viewer's selected window private.

4. Register a worker action. registerAction('refresh', …) lets an agent force a reload.

5. Build, publish, install.

Terminal
cd examples/spatio-analytics
spatio-platform login
spatio-platform build
spatio-platform publish

6. The agent demo. Once installed and connected, ask your agent "what's my MRR trend?" — it calls the mrr_trend action, the host proxies to Stripe injecting the stored credential, and returns the bucketed series. The same path the Revenue tab uses.

See the platform's own README.md for the full walkthrough.

See also

  • Spatio SDK: the TypeScript / Python / Go client SDKs.
  • Spatio MCP: the agent tool surface your mcpTools join.
  • Spatio CLI: the workspace shell client (distinct from spatio-platform).
  • Spatio API: the REST surface behind it all.