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
npm install -g @spatio-labs/platform-cli
spatio-platform init "My Platform"
cd my-platform
spatio-platform login
spatio-platform devinit 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.
{
"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": [ /* … */ ]
}| Field | Purpose |
|---|---|
permissions | What the user grants at install: kv:read / kv:write, api:read / api:write, providers:execute, mcp:expose. |
network.allowedDomains | Outbound allowlist for the generic HTTP provider / proxy egress. Exact host or *.suffix. Anything outside the set is rejected. |
surfaces | Which Spatio surfaces you contribute (see below). |
mcpTools | Agent-operable tools (see MCP tools). |
Surfaces
Each surface names an exported Command in your bundle. The worker mounts it for that surface.
"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
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.
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 KV — useStorage 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.
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.
{
"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.
import { registerAction } from '@spatio/platform-sdk'
registerAction('refresh', async (params) => {
// … recompute …
return { ok: true }
}){ "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:write—useStorageapi:read/api:write—useSpatioAPIproviders:execute—useProviders().executeandproxymcpToolsmcp:expose— declaringmcpToolsat 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.
Install & consent
- Create the platform record once:
POST /v1/custom-platformswith{ name, displayName }. Save the returned id asmanifest.platformId. - Publish a version with the CLI (upload + publish, below).
- A workspace member installs it from the in-app platform store. The install dialog lists the requested
permissionsandnetwork.allowedDomains; the user must consent before the platform mounts. - 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.
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| Command | API endpoints |
|---|---|
login | GET /.well-known/oauth-authorization-server, POST /oauth2/register, GET /oauth2/authorize, POST /oauth2/token |
whoami | GET /v1/custom-platforms |
publish | POST /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.
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.
cd examples/spatio-analytics
spatio-platform login
spatio-platform build
spatio-platform publish6. 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
mcpToolsjoin. - Spatio CLI: the workspace shell client (distinct from
spatio-platform). - Spatio API: the REST surface behind it all.