Communitygithub.com

Swarm-AI-Labs/pie-skills

Agent skills for the Pie ecosystem — pie (Python) and pieui (TypeScript/Next.js) CLIs

¿Qué es pie-skills?

pie-skills is a Claude Code agent skill that agent skills for the Pie ecosystem — pie (Python) and pieui (TypeScript/Next.js) CLIs.

Compatible conClaude CodeCodex CLICursor
npx skills add Swarm-AI-Labs/pie-skills

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

PIE + pieUI — Complete Agent Skill Guide

Full reference for AI agents and developers building projects with the PIE framework. Covers both CLIs, all commands, workflow recipes, envelope policy, and edge cases.


Table of Contents

  1. Architecture
  2. Hard Rules — Never Do Manually
  3. pieui CLI — All Commands
  4. pie CLI — All Commands
  5. Envelope Policy
  6. Workflow Recipes
  7. Edge Cases
  8. Project Setup & .gitignore
  9. check-sync Findings Guide
  10. Previewing & Rendering Cards
  11. Ajax Endpoints, stored, and the pieui 3.0.0 Migration
  12. Express Backend — @swarm.ing/pieui/server
  13. Realtime — Recovery, Link Status, Subscription Sharing

1. Architecture

PIE is a fullstack framework. Frontend and backend are strictly separated, each with its own CLI.

┌─────────────────────────────────────────────┐
│              Platform (pieui.swarm.ing)       │
│  username/my-project/MyCard                  │
│   ├── python/  ← pushed by pie CLI           │
│   └── typescript/  ← pushed by pieui CLI     │
└─────────────────────────────────────────────┘
         ↑ push/pull          ↑ push/pull
┌────────────────┐   ┌────────────────────────┐
│  Backend        │   │  Frontend               │
│  Python/FastAPI │   │  TypeScript/Next.js     │
│  pie CLI        │   │  pieui CLI              │
│  pages/         │   │  piecomponents/         │
│  components/    │   │  app/                   │
└────────────────┘   └────────────────────────┘
LayerLanguageCLIKey directories
FrontendTypeScript / Next.jspieuipiecomponents/, app/
BackendPython / FastAPIpiepages/, pages/components/

CLI invocation:

  • node /path/to/pieui/dist/cli.js <cmd>
  • /path/to/pie/.venv/bin/pie <cmd>

2. Hard Rules — Never Do Manually

These actions must go through the CLI. Doing them manually breaks the registry or platform metadata.

❌ Never manually✅ Use instead
Create a file in piecomponents/pieui card add ...
Create app/<path>/page.tsxpieui page add <path>
Edit piecomponents/registry.tsUpdated automatically by any pieui card add/remove
Create pages/components/*.pypie card add ...
Create pages/*.pypie page add ...
Add a methods/event key to <PieCard methods={...}>pieui add-event <Card> <event>
Add an IO event to a Python cardpie card add-event <Card> <event>
Delete a piecomponent directorypieui remove <ComponentName>

Other hard rules:

  • "use client" — required at the top of every PIE card TSX file.
  • <button type="button"> — always set; without it the browser submits to /api/process/.
  • is_typed=False — required in every AsyncPage subclass that does not override get_content.
  • Python snake_case → camelCase on frontend. send_label becomes sendLabel. Match in both TS types and component code.
  • After editing web.py (adding a new route) — restart the backend process.
  • Never commit .env, .pie/, .claude/, node_modules/, .next/, __pycache__/, .venv/.

3. pieui CLI — All Commands

3.1 Auth

pieui login

Opens a browser URL for OAuth. On success writes { user_id, api_key, project } to .pie/config.json and appends vars to .env.

When: Before any card remote operation or on first project setup. Note: Run from the project root so config is saved to the correct .pie/config.json.


3.2 Init

pieui init [--out-dir <dir>]

Creates piecomponents/registry.ts. Required once per new frontend project.

FlagDefaultDescription
--out-dir, -o.Base directory for piecomponents
pieui init
pieui init --out-dir packages/app   # monorepo sub-package

3.3 Create project

pieui create <AppName>          # scaffold Next.js app + run pieui init
pieui create-pie-app <AppName>  # blank Next.js template only
pieui create-pieui <AppName>    # alias for create-pie-app

When: Starting a new frontend project from scratch.


3.4 page add

pieui page add <path>

Generates app/<path>/page.tsx with the standard PIE Suspense wrapper.

pieui page add dashboard
pieui page add wallet/send
pieui page add chat/room

When: Every new route. One command per page. No flags.


3.5 card add

pieui card add [type] <ComponentName> [--io] [--ajax]

Scaffolds piecomponents/<ComponentName>/ with index.ts, types/index.ts, ui/<ComponentName>.tsx. Updates piecomponents/registry.ts automatically.

Types:

TypeProps signatureUse when
simple{ data }Card displays data, no children
complex{ data, children }Card wraps other components
simple-container{ data, content }Card has a single content slot
complex-container{ data, content[] }Card has array content slots (default)

Flags:

FlagWhat it adds
--ajaxpathname, depsNames, kwargs fields to the data interface
--ioRealtime / websocket support fields
pieui card add simple ProfileCard
pieui card add complex-container LayoutCard
pieui card add simple LiveTickerCard --io
pieui card add simple ContactFormCard --ajax
pieui card add simple ChatCard --io --ajax

3.6 card remove

pieui remove <ComponentName>

Deletes piecomponents/<ComponentName>/ and removes the entry from registry.ts.

When: Removing a component. Never delete manually.


3.7 list

pieui list [filter] [--src-dir <dir>]

Prints a table of all registered components: Name, Type, Data Type, Lazy, File.

FilterShows
(none)All
simpleSimple only
complexComplex only
simple-containerSimple containers
complex-containerComplex containers
pieui list
pieui list simple
pieui list complex-container --src-dir app

3.8 list-events

pieui list-events <ComponentName> [--src-dir <dir>]

Prints all methods keys declared in <PieCard card="X" methods={...} /> for the component.

When: Before adding a new event (to avoid duplicates); auditing a card's event surface.


3.9 add-event

pieui add-event <ComponentName> <event> [--src-dir <dir>]

Adds a new key with a default handler to <PieCard ... methods={{ <event>: handler }}> in the TSX.

pieui add-event ContactFormCard submit
pieui add-event ChatCard message

When: Adding event handlers to an existing card. Always use this — never edit methods manually.


3.10 postbuild

pieui postbuild [--out-dir <dir>] [--src-dir <dir>] [--append]

Scans piecomponents and generates a manifest JSON for SSR / production builds.

FlagDefaultDescription
--out-dir, -opublicOutput directory for manifest
--src-dir, -ssrcSource directory to scan
--appendoffInclude built-in pieui components in the manifest

When: Part of the production build step (next build).


3.11 card remote push

pieui card remote push <ComponentName>

Uploads piecomponents/<ComponentName>/ to the platform under the typescript/ envelope. Assigns a new revision @N.

When: After implementing or updating a card. Note: Each push creates a new immutable revision. Old revisions remain pullable by @N.


3.12 card remote pull

pieui card remote pull <ComponentName>[@rev]
pieui card remote pull <project>/<ComponentName>[@rev]
pieui card remote pull r/<user>/<ComponentName>

Downloads a component into piecomponents/. Overwrites local files.

FormAccess
MyCardLatest from current project
MyCard@7Specific revision from current project
other-proj/MyCardAnother project of the same user
r/username/MyCardPublic component by any user
pieui card remote pull ProfileCard
pieui card remote pull ProfileCard@3
pieui card remote pull r/someuser/PriceTickerCard

3.13 card remote list

pieui card remote list [--user <U>] [--project <S>]

Lists component names stored on the platform.

Note: --user / --project only works if your API key has access to that project.


3.14 card remote history

pieui card remote history <ComponentName> [--page N] [--per-page N] [--from R] [--to R]

Shows revision history with per-file diffs (git-style). Includes both python/ and typescript/ envelope files.

FlagDefaultDescription
--page1Page number
--per-page10Revisions per page
--fromStart revision number
--toEnd revision number
pieui card remote history ProfileCard
pieui card remote history ProfileCard --from 3 --to 7

When: Auditing what changed between pushes; debugging regressions; verifying both envelopes were pushed.


3.15 card remote public / private

pieui card remote public <ComponentName>   # accessible as r/username/MyCard
pieui card remote private <ComponentName>  # revert to private

3.16 card remote remove

pieui card remote remove <ComponentName>

Deletes the component from the platform (all revisions). Does not touch local files.


4. pie CLI — All Commands

4.1 Auth

pie login

Same OAuth flow as pieui. Writes to .pie/config.json in CWD and appends to .env.

Critical: Run from the backend project root. pie reads .pie/config.json relative to CWD. If env vars PIE_USER_ID, PIE_API_KEY, PIE_PROJECT are set, they take precedence over the config file.


4.2 page add

pie page add <path>

Creates pages/<slug>.py with an AsyncPage subclass.

pie page add dashboard
pie page add wallet/send

When: Every new backend route. Then register the page in web.py and restart the server.


4.3 card add

pie card add <type> <ComponentName> [--io] [--ajax] [--input] [--from REF]

Creates pages/components/<snake_name>.py with a Card dataclass.

pie card add simple ProfileCard
pie card add complex ChatCard

The type is required here. Unlike pieui card add, omitting it is an argparse error — pie's choices are simple, complex, container, complex-container (note container, where pieui says simple-container).

--from REF ports from the frontend: a .ts/.tsx file, a piecomponents dir, a PieMetadata .json, or a bare card name resolved via frontendComponentsDir. Omit it and it auto-resolves by name when a frontend project is configured.

When: Same time as pieui card add — both sides must exist.


4.4 card list

pie card list

Prints a table: Name, Type, Ajax, IO, File path.


4.5 card view

pie card view <ComponentName>

Pretty-prints a card's props table (field name, Python type, default value) plus Ajax / IO / Events flags.

pie card view ProfileCard

When: Quick inspection of a card's contract without opening the file.


4.6 card dump-metadata

pie card dump-metadata <ComponentName> [--out <file.json>]

Outputs full JSON metadata in the Python envelope format:

{
  "python": {
    "name": "MyCard",
    "propsSchema": { "properties": { ... }, "type": "object" },
    "propsCode": "@dataclass\nclass MyCard...",
    "ajaxList": ["pathname"],
    "events": [],
    "eventsPropsCode": {},
    "eventsPropsSchema": {},
    "inputPropsCode": null,
    "inputPropsSchema": null,
    "files": [{ "path": "my_card.py", "content": "..." }],
    "packages": ["pie"]
  }
}
FlagDefaultDescription
--out, -ostdoutWrite JSON to file
pie card dump-metadata ProfileCard
pie card dump-metadata ProfileCard --out /tmp/profile_card_meta.json

Prop types imported from project-local modules are resolved. A prop typed with a value object defined in a sibling module no longer serializes as an opaque schema — resolution is pure AST + filesystem walking under the project root, and nothing is imported at runtime. Two limits: on a name clash the same-file class wins, and import chains are not followed (only the card file's direct dependencies are visible, so a dataclass imported by an imported module stays opaque).


4.7 card check-sync

pie card check-sync [ComponentName]

Compares the Python props schema (local) against the TypeScript props schema (from frontendProjectDir) field by field.

ComponentName is optional — omit it and every card in the components dir is checked. Batch mode prints [pie] {n}/{total} card(s) out of sync: <names> or [pie] All {n} card(s) aligned ✓. Exit 0 = all aligned, 1 = any finding or a card that failed to build/dump.

It shells out to the frontend CLIbun $PIE_CHECK_SYNC_PIEUI_CLI (env override pointing at a local cli.ts/cli.js), then bunx pieui, then npx pieui — and strictly requires a {"typescript": …} envelope back from pieui card dump-metadata. A stale pieui that doesn't emit it fails every card, and with neither bunx nor npx on PATH you get [pie] Neither bunx nor npx found in PATH.

Python int props no longer report a spurious type_mismatch against a TypeScript number: JSON-Schema integer is canonicalized to number before comparing.

Requires frontendProjectDir in .pie/config.json:

{
  "user_id": "...",
  "api_key": "...",
  "project": "...",
  "frontendProjectDir": "/absolute/path/to/frontend-project"
}

When: After editing either side; before pushing to the platform; as part of CI. See section 9 for interpreting output.


4.8 card list-events / card add-event

pie card list-events <ComponentName>    # list IO events (static parse)
pie card add-event <ComponentName> <event_name>  # add event handler stub

4.9 card pull

pie card pull <REF>

Downloads a Python card from the platform into pages/components/.

REF formAccess
MyCardCurrent project
other-proj/MyCardAnother project of the same user
r/username/MyCardPublic component by any user

4.10 card remote push

pie card remote push <ComponentName>

Uploads the Python card to the platform under the python/ envelope.

Known issue: When PIE_USER_ID / PIE_API_KEY / PIE_PROJECT env vars are set, pie searches for config in parent directories instead of CWD. This causes "user_id required" errors even if .pie/config.json exists locally.

# Preferred: run from project root with no conflicting env vars
cd my-api-project && pie card remote push MyCard

# Fallback: pass all credentials explicitly
PIE_USER_ID=username PIE_API_KEY=rp-xxx PIE_PROJECT=my-project \
  pie card remote push MyCard

4.11 web

web takes a module:attribute, not a bare verb:

pie web web:app            # run the FastAPI server
pie web web:app verify     # lint the Web instance
pie web web:app build      # static JSON export

4.12 init

pie init

Sets up Pie in an existing directory (pages/, components/, web.py) and prompts to link a frontend project, stored in .pie/config.json as frontendProjectDir / frontendComponentsDir.

It also scaffolds <frontendComponentsDir>/preview-providers.tsx — the pass-through wrapper the preview harness wraps around every previewed card. This happens on re-runs too, so existing projects pick it up retroactively; an existing file is never overwritten. See section 10.


4.13 self-upgrade

pie self-upgrade [--pm uv|poetry|pip]

Auto-detects the package manager from sys.executable's path (uv tool venv → uv, poetry venv → poetry, else pip), and runs uv tool install --force pieui[cli], poetry update pieui, or <python> -m pip install --upgrade pieui[cli].

The [cli] extra is always forced — a plain pip install pieui yields a CLI that dies on import, so upgrades also repair such installs. It deliberately avoids uv tool upgrade, which would re-resolve the original spec and leave a bare pieui tool broken forever.


4.14 card channels / card emit — realtime

Backend-only; there is no pieui mirror.

pie card channels [web:app] [--live] [--json]
pie card emit <NAME> <EVENT> <CHANNEL> [--data '<json>'] [--web web:app]

channels lists Centrifuge-enabled cards across all pages with their channels — static literals and f-string templates — and --live reconciles them against a running Centrifugo. The composed channel is pie{event}_{name}_{channel}, so always discover a valid CHANNEL with card channels before emitting.


4.15 taskrun

pie taskrun local  <module:attr> <PAGE> <ACTION> [params…]
pie taskrun remote <module:attr> <PAGE> <ACTION> [params…]

remote supports several named backends. The target pseudo-args live in params (they are popped from kwargs, not argparse flags):

pie taskrun remote web:app dashboard refresh --remote=staging
pie taskrun remote web:app dashboard refresh --remote-url=https://api.example.com

.pie/config.json gains a remotes ({name: url}) map and defaultRemote; the legacy single-string remoteUrl still works. Env PIE_REMOTE is a name, PIE_REMOTE_URL a full URL.

Precedence: --remote-url/--url--remote/--remote-name$PIE_REMOTEdefaultRemoteremoteUrl$PIE_REMOTE_URL → interactive prompt (saved back).

Gotchas: an unknown remote name is terminal — it errors listing the available remotes rather than falling back to remoteUrl. Malformed remotes entries are silently dropped. Task files generated before this change still print the old precedence in their summary until regenerated.


4.16 db — Beanie/MongoDB layer

Backend-only by design (MongoDB has no frontend analog), so there is no pieui db.

pie db init | check | status | shell
pie db model add <NAME> [--field NAME:TYPE …] [--collection NAME] [--timestamps] [--index FIELD[,unique] …]
pie db model list | view <NAME> | remove <NAME> | add-field | rename
pie db card add <DOCUMENT> <CARD>          # injects to_ui_<card>_card / list_ui_<card>_cards
pie db card list | remove
pie db index list [MODEL] | sync
pie db pull <COLLECTION> [--as NAME] [--limit 50]
pie db export <COLLECTION> [-o FILE] | import <COLLECTION> --in FILE
pie db seed new <NAME> | run [NAME]
pie db migration new <NAME> | list
pie db migrate [--distance N] | rollback [--distance N]     # 0 = all

Beanie 2.x uses pymongo's AsyncMongoClient (no Motor). db model remove, db import, and db rollback are destructive — require explicit user intent, and remember --distance 0 means all migrations.


4.17 cloudflare — Pyodide Python Worker

Backend-only; no pieui mirror.

pie cloudflare init [--app MODULE:ATTR] [--name WORKER]
pie cloudflare dev [--port 8787]
pie cloudflare deploy

init writes src/worker.py + wrangler.jsonc and symlinks web.py / pages/ into src/ (no copies — single source of truth). Install with pip install 'pieui[cloudflare-worker]' (Python ≥3.12). Deploy needs CLOUDFLARE_API_TOKEN; account-scoped tokens also need CLOUDFLARE_ACCOUNT_ID, otherwise wrangler's /memberships lookup fails with error 9106.


4.18 card show / show-mcp / show-set / show-emit

The whole show* family is backend-only. See section 10 for the full architecture.


5. Envelope Policy

The platform stores each component in two separate envelopes:

username/my-project/MyCard/
  ├── python/my_card.py            ← written by  pie card remote push
  └── typescript/
      ├── piecomponents/MyCard/index.ts
      ├── piecomponents/MyCard/types/index.ts
      └── piecomponents/MyCard/ui/MyCard.tsx  ← written by pieui card remote push

Rules:

  1. pie card remote push writes only python/. It never touches TypeScript files.
  2. pieui card remote push writes only typescript/. It never touches Python files.
  3. pie card pull restores only the Python file locally.
  4. pieui card remote pull restores only the TypeScript files locally.
  5. card remote history shows diffs for both envelopes in a unified revision timeline.
  6. check-sync reads both envelopes and compares their schemas.

A component is fully published only after both pie card remote push AND pieui card remote push have been called.

API key scope: Each project has its own API key. A key for project A cannot read project B even for the same user_id. Log in per-project to get the correct key.


6. Workflow Recipes

Recipe 1 — Create a page with an AJAX card

# Frontend
pieui page add my/route
pieui card add simple MyCard --ajax
# → implement piecomponents/MyCard/types/index.ts and ui/MyCard.tsx

# Backend
pie page add my/route
pie card add simple MyCard
# → implement pages/components/my_card.py and pages/my_route.py
# → register in web.py: "my/route": MyRoutePage()
# → restart backend

The --ajax flag adds pathname, deps_names, kwargs to both scaffolded files, enabling page-navigation callbacks from the card.


Recipe 2 — Create a realtime card with events

# Frontend
pieui card add simple LiveDataCard --io
pieui add-event LiveDataCard update    # adds methods.update handler to TSX

# Backend
pie card add simple LiveDataCard
pie card add-event LiveDataCard update

# Implement both sides, then push
pieui card remote push LiveDataCard
pie card remote push LiveDataCard

Recipe 3 — Verify backend ↔ frontend contract

# 1. Dump Python schema for reference
pie card dump-metadata MyCard --out /tmp/my_card_meta.json

# 2. Run sync check
pie card check-sync MyCard

# 3. Fix any real mismatches, then re-verify
pie card check-sync MyCard

# 4. TypeScript compile check
npx tsc --noEmit

See section 9 to distinguish real bugs from expected differences.


Recipe 4 — Publish a card

# Push TypeScript side
pieui card remote push MyCard    # → MyCard@1

# Push Python side
pie card remote push MyCard      # → MyCard@1

# Verify both envelopes landed
pieui card remote history MyCard
# Confirm diff shows both python/ and typescript/ files

# Optional: make public
pieui card remote public MyCard
# Now accessible as r/username/MyCard by anyone

Recipe 5 — Port a card from the platform into a new project

# Pull TypeScript files
pieui card remote pull r/username/MyCard    # public component
pieui card remote pull other-proj/MyCard    # your other project
pieui card remote pull MyCard@5             # specific revision

# Pull Python file
pie card pull r/username/MyCard
pie card pull other-proj/MyCard

# Register page in web.py if needed, then implement business logic

Recipe 6 — Full project push

# 1. Type check
npx tsc --noEmit

# 2. Sync check all cards
for card in CardA CardB CardC; do
  pie card check-sync $card
done

# 3. Push all frontend cards
for card in CardA CardB CardC; do
  pieui card remote push $card
done

# 4. Push all backend cards
for card in CardA CardB CardC; do
  pie card remote push $card
done

# 5. Commit
git add .
git commit -m "feat: ..."
git push

7. Edge Cases

pie card remote push ignores .pie/config.json when env vars are set

When PIE_USER_ID / PIE_API_KEY / PIE_PROJECT are exported in the shell, pie searches parent directories for config instead of CWD. Result: "user_id required" error even with a valid .pie/config.json in the project root.

Fix: Unset env vars and run from project root, or pass all three explicitly:

PIE_USER_ID=username PIE_API_KEY=rp-xxx PIE_PROJECT=my-project \
  pie card remote push MyCard

check-sync requires frontendProjectDir

[pie] Frontend project path required to run check-sync.

Fix: Add to the backend project's .pie/config.json:

{ "frontendProjectDir": "/absolute/path/to/frontend-project" }

API key 403 on card remote list --project

Each project has a scoped API key. A key for project A cannot access project B, even for the same user.

Fix: Run pieui login / pie login for the target project to get its key.


Stale dev-server bundle after .env change

The Next.js dev server may cache an old SSR bundle after env changes.

Fix: Delete the build cache and restart:

rm -rf .next && bun run dev

Turbopack incompatible with certain native modules

Some npm packages ship native .node binaries that Turbopack cannot bundle.

Fix: Use webpack mode instead:

bun run dev    # ensure next.config uses webpack, not --turbo

is_typed error on AsyncPage

Missing is_typed=False causes a runtime type error on page load.

Fix:

class MyPage(AsyncPage):
    def __init__(self):
        super().__init__(is_typed=False)
        self.fields = UnionCard([MyCard(name="MyCard")])

Form submits to /api/process/ instead of handler

A <button> inside <PieCard> without type="button" is treated as a form submit button.

Fix:

<button type="button" onClick={handler}>Label</button>

Python snake_case props are undefined in TypeScript

PIE converts Python snake_case field names to camelCase when sending to the frontend.

Fix: Always use camelCase in TS interfaces:

// ❌  network_label: string
// ✅  networkLabel: string

8. Project Setup & .gitignore

Monorepo structure (frontend root + backend subdirectory)

my-project/                  ← git root (Next.js frontend)
├── backend/                 ← Python backend (any name)
│   ├── pages/
│   ├── web.py
│   ├── pyproject.toml
│   └── .pie/               ← gitignored
├── piecomponents/
├── app/
├── lib/
├── .env                    ← gitignored
├── .env.example            ← committed (template, no secrets)
└── .pie/                   ← gitignored

If the backend directory has its own .git, remove it before committing to avoid submodule issues:

rm -rf backend/.git
git add backend/

.gitignore

# Frontend
/node_modules
/.next/
*.tsbuildinfo
next-env.d.ts
/coverage

# Backend (adjust directory name as needed)
backend/.venv/
backend/**/__pycache__/
backend/**/*.pyc
backend/.env
backend/.pie/

# Secrets & credentials — never commit
.env
.env.*
!.env.example
.pie/

# Misc
.DS_Store
*.pem
npm-debug.log*
.claude/

9. check-sync Findings Guide

pie card check-sync MyCard diffs the Python and TypeScript prop schemas. Not all findings are bugs.

FindingMeaningAction
Python allows null, TS is required+non-nullPython dataclass defaults make fields nullable in JSON Schema; TS correctly marks them requiredNone — backend always sends a value
depsNames / kwargs / flow / pathname in Python, not in TSPIE Card base class internal fieldsNone — framework internals, frontend doesn't use them
integer vs numberPython int → JSON Schema integer; TS numbernumberNone — integers are valid JS numbers
array vs object for List[str] / string[]Schema-generation difference between languagesNone — runtime compatible
Field in TS but not in PythonMissing field in backend dataclassFix — add to pages/components/my_card.py
Field in Python but not in TS (non-framework field)Missing field in TS interfaceFix — add to piecomponents/MyCard/types/index.ts
Completely incompatible types (e.g. string vs number)Real contract mismatchFix — align types on both sides

10. Previewing & Rendering Cards

PIE can render one card in isolation — without wiring it into a full app — for visual review, screenshots, or agent-driven inspection. Three surfaces share one mechanism.

10.1 How it works

  pie (backend)                          pieui (frontend)
  ┌────────────────────────┐  HTTP GET   ┌───────────────────────────┐
  │ ephemeral Web app      │ ◀────────── │ registry-dev harness       │
  │  /api/content/         │  /api/...   │  (PiePreviewRoot,          │
  │  /api/ajax_content/…   │ ──────────▶ │   no app layout)           │
  │  serves ONE card JSON  │             │  fetches /api/content/,    │
  └────────────────────────┘             │  renders card by name from │
                                         │  piecomponents/registry.ts │
                                         └───────────────────────────┘
  • The backend (pie) serves the card envelope { "card": "<Name>", "data": { … } } at /api/content/, plus a print-and-echo stub for every ajax pathname at /api/ajax_content/<path>.
  • The frontend is the registry-dev harness: a standalone Next app (pieui registry dev) generated under <frontend>/.pie/registry/. It mounts PiePreviewRoot (no app chrome), reads PIE_API_SERVER, fetches /api/content/, and renders the matching component from piecomponents/registry.ts.
  • The card name in the content JSON must be registered on the frontend (created via pieui card add), and the data keys must be camelCase matching the TS props.

10.2 pie card show — interactive preview (human-facing)

Serves one card from an ephemeral backend and opens it in the harness. Blocks until Ctrl+C; always tears down the frontend process.

pie card show 'ProfileCard(name="p", title="Hi")'
pie card show 'ColCard([ACard(), BCard(a=1)])' --frontend-port 3210 --route /

EXPR is a Python expression that evaluates to a Card. The eval namespace = framework Card subclasses plus every Card subclass in the backend's pages/components/*.py. Any card carrying a pathname gets a print-only ajax stub auto-registered, so ajax cards respond out of the box.

FlagDefaultDescription
--frontend-dirfrontendProjectDir from .pie/config.jsonFrontend project to render with
--frontend-port3000Port for the registry-dev harness
--backend-portauto (free port)Port for the ephemeral backend
--route/Frontend route to open
--pmautodetectPackage manager (bun/pnpm/yarn/npm)
--no-openoffDo not open a browser automatically

10.3 pieui registry dev|build — the harness itself

pieui registry dev --port 3939 --api-server http://127.0.0.1:8000/
pieui registry build --out public/pie-registry
  • registry dev [--port N] [--api-server URL] — runs the standalone PiePreviewRoot harness pointed at a backend's /api/content/. This is what pie card show and the MCP spawn internally; run it by hand only to debug the harness or to drive it from your own backend.
  • registry build [--out DIR] — static-export the harness so pie can serve it directly (disable_serving=False).
  • Generated under <frontend>/.pie/registry/ — a separate Next app with its own .next build cache (see Troubleshooting).

10.4 pie card show-mcp — headless rendering for agents (MCP)

A FastMCP server that renders cards headlessly (JSON / HTML / screenshot) and exposes their ajax — so AI agents can inspect cards without a human browser. Install extras: pip install 'pieui[mcp]' (adds mcp + playwright).

pie card show-mcp                                  # stdio transport (default)
pie card show-mcp --http 9009                      # streamable-HTTP on :9009
pie card show-mcp --frontend-port 3939             # harness port
pie card show-mcp --mirror http://127.0.0.1:8000   # mirror a live `pie card show` backend
pie card show-mcp --no-frontend                    # json + ajax tools only (no browser)
FlagDefaultDescription
--http PORTstdioServe over streamable-HTTP instead of stdio
--frontend-dirconfigFrontend project dir
--frontend-port3000registry-dev harness port
--backend-port PORTautoPin the content backend's port so an external harness can target a stable URL
--mirror URLMirror a running pie card show backend
--no-frontendoffSkip frontend + browser; only render_card(json) + ajax tools

MCP tools (the server name is chosen at registration, e.g. mcp__pie-show-<proj>__* — match on the tool suffix, not a fixed server name):

ToolPurpose
list_cards(source?, contains?)Start here — don't guess names. source = all | backend | frontend; a frontend_only bucket lists cards with no backend class (render those with a content dict). The first call can take ~20s: it imports every backend component module.
describe_card(card_name)Data shape without reading source: backend dataclass fields + the frontend <Name>Data interface + nested helpers, plus a render_with hint (expression vs content dict).
render_card(card?, format)Render a card. card accepts a Python expression, a content-JSON string, or a content dict. format = json (echo content) | html | screenshot. No arg → re-render the current source.
list_ajax()List ajax pathnames on the current card.
call_ajax(pathname, data?)POST to a running ajax endpoint and return its content.
emit_event(method, card_name, payload?)Push pie{method}_{card_name} to the harness Mitt bus. Returns the number of event streams that received it — 0 means nothing is connected. Needs the card rendered with useMittSupport and a pieui carrying the preview-events bridge.
get_submissions(since?)Ajax submissions the previewed card POSTed back, id-ordered; pass since = highest id already seen. Bounded at 128 entries.
attach(base_url) / detach()Mirror / stop mirroring a running pie card show backend.
harness(action?, port?, api_server?)Manage the human-facing harness: status | start | stop | restart. Note the default port 3940, distinct from --frontend-port's 3000.
doctor()Run this first when anything looks wrong — checks the frontend link, Tailwind resolution, public/ serving, the preview-events bridge, backend/harness liveness, and stale processes. Every failing check carries a fix.
cleanup_orphans(scope?)Kill stale show-mcp processes, never itself. orphaned (default) spares instances serving other projects; all clears every other instance.
status()pid, backend url/liveness, active source, subscriber and submission counts, browser state.

Notes:

  • describe_card parses the frontend side with regex, not a TypeScript parser — fine for the flat fields, unions, and arrays pie cards declare, but generics, mapped types, and inline nested interfaces parse poorly. It reads <frontend>/piecomponents/<CardName>/types/index.ts.
  • format=json only echoes the content envelope (no browser needed). html / screenshot need the running harness, a linked frontend (pie init), and Playwright.
  • Prefer the expression form for card. A { … } JSON string is frequently coerced to a dict by the MCP arg layer and rejected — pass an expression like ProfileCard(name="p", …) instead. The eval namespace includes any Card subclass in the backend's pages/components/.
  • Register it as an MCP server (Claude Code / Cursor / Codex) with a small launcher that pins the backend project and pie on PYTHONPATH:
    #!/usr/bin/env bash
    cd /path/to/backend-project
    PYTHONPATH=/path/to/pie exec ./.venv/bin/python -m pie card show-mcp --frontend-port 3939 "$@"
    

10.5 Driving a running show backend — show-set / show-emit

Two thin HTTP clients against a running show backend's loopback control API. No MCP, no stdio — useful from a shell script or a non-MCP agent.

pie card show-set <CONTENT|-> [--api-server URL]   # {card, data} JSON → /api/preview/content
pie card show-emit <METHOD> <CARD_NAME> [--payload '<json>'] [--api-server URL]

show-set takes a content JSON file path or - for stdin. show-emit publishes a pie{method}_{cardName} event via /api/preview/emit. Both print the JSON response.

Backend discovery uses .pie/show-backend.json — written by the backend at startup as {"url": …, "pid": …}not .pie/config.json. Without it: "no running show backend found — pass --api-server, or start pie card show-mcp in this project first".

Security: both write routes are loopback-only. A client that isn't 127.0.0.1 / ::1 / localhost gets 403 {"error": "preview control is loopback-only"}, and this holds even under show-mcp --http. The read routes (/api/preview/events, /api/preview/submissions) stay open.

Note the preview stack does not use Centrifugo: show-mcp fans events out in-process over SSE with a bounded replay buffer, so a briefly disconnected harness catches up via Last-Event-ID without double-firing.


10.6 Preview providers

Cards that depend on app context (a theme, a wallet, a store) crash when rendered bare. The harness therefore wraps every previewed card in a preview provider:

// <componentsDir>/preview-providers.tsx   ('use client')
export default function PreviewProviders({ children }: { children: ReactNode }) {
  return <>{children}</>
}

This is a convention, not a config key — the path is <componentsDir>/preview-providers.tsx (PIE_COMPONENTS_DIR, default piecomponents). Both pie init and pieui init scaffold the pass-through version and never overwrite an existing file; add your real providers (or mocks) inside it.

Discovery is a bare existence check at scaffold time on each registry dev|build run, so adding or removing the file requires a harness restart. A preview error boundary sits outside the provider and pattern-matches missing-context errors to print an "add the provider to …" hint.


10.7 Showing the harness in an agent's side panel

To display the live harness inside a host with a web-preview panel (e.g. Claude Code's preview_*):

  1. Set the card via render_card first, so the backend serves it at /api/content/.
  2. Find the backend port — the show-mcp process's listening port whose /api/content/ returns your card.
  3. Launch the harness as a managed preview server. Preview tools won't reuse an externally-started server, and launch configs may lack a cwd field — so use a shell wrapper and an auto-assigned port:
    {
      "name": "pie-registry",
      "runtimeExecutable": "bash",
      "runtimeArgs": ["-lc", "cd <frontend> && exec pieui registry dev --port \"$PORT\" --api-server http://127.0.0.1:<backend>/"],
      "autoPort": true
    }
    

10.8 Troubleshooting

SymptomCauseFix
Persistent Parsing CSS source code failed / stale build error in the preview after the source is already fixedThe harness keeps its own Turbopack/Next cache at <frontend>/.pie/registry/.next; clearing the main app .next does nothingrm -rf <frontend>/.pie/registry/.next, then restart the harness
pieui registry … prints the general help / acts like an unknown commandThe project-local node_modules/.bin/pieui predates registry; the harness resolves project-local firstUse the global pieui (e.g. ~/.bun/bin/pieui) or run pieui self-upgrade
Preview shows the full app or a loading splash instead of the bare cardThe harness never started (often the missing registry command above) and something fell back to a plain next dev on that portConfirm a pieui registry dev process is actually serving the port
render_card rejects the card with a dict/validation errorA { … } JSON string was coerced to a dictPass a card expression instead
html / screenshot returns "needs a linked frontend"No Playwright, or no linked frontendpip install 'pieui[mcp]' and pie init to link the frontend
Card renders blank / "unknown card"Name not in frontend registry.ts, or data keys are snake_casepieui card add <Name>; use camelCase data keys
Anything at all looks wrong with the previewRun the MCP doctor tool first; every failing check carries its own fix
emit_event returns 0 subscribersNo harness connected, or the installed pieui lacks the preview-events bridgeStart/restart the harness; doctor flags a missing bridge
show-mcp respawn fails on a pinned --backend-portOrphaned processes squatting the portcleanup_orphans (scope orphaned spares sibling projects)
Card crashes with a missing-context / provider errorIt needs app context the bare harness doesn't provideAdd the provider to <componentsDir>/preview-providers.tsx, then restart the harness
Unstyled preview on a Tailwind v3 projectHarness PostCSS config missing, or a tailwind.config.ts the harness cannot re-exportIt is automatic on the next registry dev; for .ts configs make the content globs absolute yourself (the CLI warns)
Preview hits the wrong backend despite .env.localThe CLI exports PIE_API_SERVER as '', and @next/env only fills missing vars, so the empty value winsPass --api-server explicitly
registry build --out DIR wrote nothing to DIR--out is currently log-line onlyRead the export from .pie/registry/out

Heads up: killing/clearing a preview harness restarts a next dev. If the user runs their own dev server on that port, confirm before replacing it.


11. Ajax Endpoints, stored, and the pieui 3.0.0 Migration

11.1 deps_names auto-derivation (backend)

register_ajax returns an AjaxEndpoint — a str subclass equal to the pathname that also carries the handler. When a card field holds one, Card.generate() overwrites that endpoint's deps key from the handler signature:

  • plain params → DOM deps
  • Dep / Source subclasses (Sid, LocalStorage, …) → sourced deps
  • Kwarg / ExtraKwarg → excluded
  • ParsedPieField / RawPieField are Dep subclasses (DOM-only) controlling server-side parsing

A plain-string pathname keeps the card's manual deps_names. The decision is per field, so one card can mix an auto-derived pathname with a manual plain-string search_pathname.

AjaxEndpoint is a copy-leaf — it shares the handler reference instead of deep-copying it, so cards holding endpoints are safe to deepcopy / pickle.

11.2 Multiple endpoints per card, paired by field name

A card may carry several endpoints. Pairing across the stack is purely by field-name prefix — no negotiation, no schema, no ordering.

Backend card fieldWire keyFrontend endpoint key
pathnamedepsNamesajaxSubmits.default
search_pathnamesearchDepsNamesajaxSubmits.search
filter_pathnamefilterDepsNamesajaxSubmits.filter
foo (no pathname suffix)fooDepsNamesajaxSubmits.foo

Declare several by giving the dataclass several <prefix>_pathname fields assigned from register_ajax. The frontend discovers them by scanning data keys and reading the sibling <x>DepsNames / <x>Kwargs (missing siblings default to [] / {}).

Gotchas on both sides:

  • Backend: deps keys are assigned after the field loop, so a manually declared search_deps_names field is silently clobbered whenever the paired endpoint field holds an AjaxEndpoint. Use a plain-string pathname if you need manual deps.
  • Frontend: the generated *Data interface still declares only the primary triple — named endpoints must be hand-added to the TS interface; codegen documents them in a comment only.
  • useAjaxSubmits memoizes on JSON.stringify(data), so non-serializable values in data mis-memoize.
  • pieui metadata flags a card ajax: true on a loose /\b\w*(?:Pathname|DepsNames)\b/ regex, which false-positives on unrelated identifiers ending in Pathname.

11.3 The stored prop accepts a thunk

stored?: TStored | (() => TStored)   // PieCardProps
stored:  TStored | (() => TStored)   // all four InputPie*ComponentProps
  • Plain value → unchanged: a hidden <input name={data.name} value={JSON.stringify(stored)}>.
  • Functionno hidden input; a resolver is registered under data.name and unregistered on unmount, so the submit path reads the current value at submit time.

Gotchas: the resolver map is keyed by data.name globally — two mounted cards sharing a name clobber each other. And because this widens a required prop type, consumer code doing const s: TStored = props.stored stops typechecking.

For native / global-form submits, submitGlobalForm() first flushes resolvers into #piedata_global_form as hidden data-pie-stored="1" inputs (previous ones are removed, so re-submits replace rather than accumulate) — because HTMLFormElement.submit() fires no submit/formdata event. Async resolvers and File values are skipped: a native submit cannot await, and a hidden input cannot carry a File. The ajax path is unaffected and still resolves both.

11.4 Migrating to pieui 3.0.0

The pieui CLI surface did not change in 3.x. All three breaking changes are in the runtime:

  1. Generated ajax scaffolds switched useAjaxSubmituseAjaxSubmits: call sites move from ajaxSubmit(extra) to ajaxSubmits.default(extra), and the scaffold's data destructure no longer pulls pathname / depsNames / kwargs. Older hand-edited components still compile (useAjaxSubmit remains exported), but newly generated cards have a different shape.
  2. PieNativeRoot takes PieRootProps, not PieBaseRootProps. It is now a full fetching root: it fetches UIConfig from /api/content{pathname}{search} itself, mounts its own QueryClientProvider, and provides the Mitt/SocketIO/Centrifuge contexts. Callers passing a static uiConfig must switch to location + config (plus optional queryClient / fallback / piecache / onError / onNavigate / queryOptions). It appends __pieroot=<Platform.OS> (ios/android) to the query string — the native counterpart of telegram / max — so a backend can branch on it.
  3. stored type widened (§11.3).

12. Express Backend — @swarm.ing/pieui/server

A TypeScript/Express mirror of the Python pie FastAPI runtime. It is an alternative for the HTTP/UIConfig serving layer only, and a partial one — it covers pie's route surface and nothing else in the ecosystem: no CLI, no db layer, no hub, no serverless, no pie card show.

import { Web, AsyncPage, UnionCard, HiddenCard } from '@swarm.ing/pieui/server'

class Home extends AsyncPage {
  constructor() { super(true); this.fields = new UnionCard({ content: [new HiddenCard({ name: 'email' })] }) }
  async getContent(ctx) { return this.fields!.fill(ctx) }
  async process(data) { return `/welcome?e=${data.email}` }   // string → 303
}
const web = new Web({ '': new Home(), home: '' /* string value = alias */ }, { enableCors: true })
web.getApp().listen(8000)

The page map is keyed by pathname-without-leading-slash; a string value is an alias to another key.

Routes (prefixed by adminSubdomain, default /):

MethodPathNotes
GETapi/content/{pathname}UIConfig JSON; 404 {error:'page not found'}
POSTapi/process/{pathname}string return → 303 redirect; non-string → 204
POST / GETapi/ajax_content/{pathname}POST body via form2dict+aggregate; GET from query
GETapi/support/:namea bare boolean body (true/false), not an object — pie parity
GET/api/centrifuge/gen_token{ token } HS256; not prefixed by adminSubdomain (also parity)

Cards bind 1:1 to the frontend *Data types via a generic auto-constructor, so subclasses need no constructor, and field names are emitted verbatim — the old camelCase transform was removed because *Data interfaces are already camelCase:

export class HiddenCard extends InputCard<HiddenCardData> {}
export class UnionCard  extends Card<UnionCardData & { content: Card[] }> {}

Gotchas a developer will hit:

  • Peer deps express, jsonwebtoken, cookie-parser are declared optional, but the imports are static — importing any symbol from /server, even just Card, pulls in all three and fails with Cannot find package 'express'. The optionality only spares frontend-only consumers of the root entry; this is not graceful degradation.
  • Props are Partial<D>, so required *Data fields are not enforced — it is a compile-time type parameter only, with no runtime validation or codegen.
  • Ajax handlers from all pages are flattened into one global map at getApp() time: the URL tail selects the handler, the page is never consulted, same-pathname registrations across pages silently collide, and registerAjax after getApp() is invisible.
  • The request body is not merged into ctx (only cookies listed in cookieKeys plus query params), despite the PageContext docstring claiming otherwise.
  • page.emit() throws 'no publisher configured' unless useCentrifugeSupport + centrifugeUrl + centrifugeApiKey are all set. No Socket.IO server is hosteduseSocketioSupport is only a flag reported by /api/support/socketio.
  • No auth of any kind: no login, session store, middleware, or route protection. JWT appears only in gen_token, TTL hardcoded to 1h. With no centrifugeSubFn and no cookieKeys, every anonymous client gets the same sub, collapsing per-user channel scoping.
  • Not covered vs pie: static/SPA serving, a Socket.IO server, typed pages (isTyped is stored but never read; InputCard.parse() is never invoked by any route), non-string process returns, compiled-events endpoints, broker/cache hooks, and OpenAPI generation. Only 5 built-in cards vs ~11 frontend components. cookieOptions and assetsPath are declared but never read.

13. Realtime — Recovery, Link Status, Subscription Sharing

Message recovery is delegated to centrifuge-js; there is no client-side history fetching and no epoch/offset tracking. Every subscription is created { recoverable: true, positioned: true }, and recovered publications arrive through the normal publication handler. On resubscribe after an unrecoverable gap:

onResync?: (info: { channel: string; reason: 'gap' }) => void   // PieCard prop

A channel the server has no history for only logs a warning. Channel names are pie${methodName}_${data.name}_${centrifugeChannel} — one per method key. The server side (Centrifugo history_size / history_ttl / force_recovery) is a documented prerequisite the client does not enforce.

Link statusPieConfig callbacks plus matching hooks (useOnLinkLost() / useOnLinkRestored()):

type PieLinkSource = 'centrifuge' | 'socketio'
onLinkLost?:     (source: PieLinkSource, detail?: unknown) => void
onLinkRestored?: (source: PieLinkSource) => void

The initial connect is suppressed and repeated same-direction transitions collapse, so lost fires once and only after a real prior connection. No provider needs mounting by hand — every root wires the contexts internally; the developer surface is PieRootProps.config.

Subscription sharing hazard: cards sharing data.name + centrifugeChannel + method key share a single Subscription, and ownership is tracked by a createdHere flag rather than a refcount. If the creating card unmounts first while a reusing card is still mounted, the shared subscription is torn down under the survivor, which then silently stops receiving events. Subscription options are also ignored on reuse — the first creator's win.

Backend note: the pie preview stack does not use Centrifugo at all (see §10.5), and pie card channels / pie card emit (§4.14) are the backend-only tools for discovering and firing real channels.

Do not document as features: lazy realtime transports and event coalescing. Both exist only as planning documents — there is no useTransports hook and no coalesceEvents prop, and getSocket / getCentrifuge remain synchronous.

Skills relacionados