Tutorials

Everything you need to get going with Pupa — start with the basics, then learn to use, build, and share a MyApp. Written guides now; video walkthroughs are on the way.

Quick setup

The fastest path to a working Pupa: run the backend on your own machine with Claude Code, reach it over a free Cloudflare tunnel, install the app, pair, and start chatting. About five minutes. Every step has a fuller version in Backend setup below.

The backend runs on your machine and simply connects your own agent harness — like Claude Code — to the Pupa app.

Pupa apppupa-backend
  1. Install the backend and run setup. Make sure the Claude Code CLI is installed and logged in first — the wizard checks for it — then:
    uv tool install "pupa-backend[setup]"
    pupa-backend setup
    In the wizard, enable the Claude Code harness, and when asked “How will your iPhone reach the backend?” pick Cloudflare tunnel and answer no to the domain question — that’s the free quick tunnel, no Cloudflare account needed.
  2. Start the backend. This serves on :8004 and brings the tunnel up:
    pupa-backend run
  3. Install the app. Get Pupa on your iPhone or Mac via TestFlight (email pupa-app-help@proton.me to request an invite), or build it from source in Xcode — see the pupa repo.
  4. Pair, then chat. Mint a code and scan it from the app:
    pupa-backend pair
    In Settings ▸ Backend ▸ Edit, scan the QR. That’s it — open a MyApp and start talking.

MyApp

A MyApp is the thing you make in Pupa. Here’s what one is, and how to use, build, and share it.

What is a MyApp?

A MyApp is an app you and your agent build and use together by talking. Think of it as a box of Lego blocks — a field, a list, a date, a button, a chart — that you snap together into whatever you need: a habit tracker, a calendar, a checklist, a calculator, or a room to hang out in.

You and the agent work on the same footing — anything one of you can do, the other can too. Usually you just describe what you want and the agent assembles the blocks in front of you, but you can also snap them together by hand, and either of you can rearrange them at any time.

  • Chat-driven. You say what you want; the agent snaps the blocks together while you watch and edit the same view.
  • It remembers. A long-lived Memories filesystem keeps what matters to you across sessions and devices.
  • It’s portable. Any MyApp packs into a single .pupa bundle you can hand to anyone — it runs on their host, with their backend.
Use

Use a MyApp

A MyApp is something you live in. You just talk to it, and the agent snaps the blocks into place while you watch and edit the same view.

  1. Open a MyApp from your library — each one blends whatever blocks it needs (trackers, calendars, checklists, charts, rooms) with its own Memories and agent setup.
  2. Type what you want in the side-panel chat: “log today’s run”, “add milk to the list”, “show me this month”.
  3. The blocks rearrange in front of you. Edit any field by hand too — you and the agent share one screen.
  4. It remembers. The long-lived Memories filesystem keeps what matters across sessions and devices.
Read the docs
Build

Build a MyApp

Mould the canvas into the shape you need — no template to pick, just describe it and refine it in place.

  1. Start a new MyApp and tell the agent what it’s for.
  2. Ask for the pieces you need: trackers, calendars, checklists, calculators, charts, rooms — cross-linked into one coherent app.
  3. Iterate by talking. Rename, restructure, add fields; the blocks rearrange live.
  4. Everything persists in the Memories filesystem, so the app grows with you instead of resetting.
Read the docs
Share

Share a MyApp

Export any MyApp as a portable .pupa bundle and hand it to anyone — they run it on their own host, with their own keys.

  1. From Settings ▸ Import & Export, export your MyApp as a .pupa file.
  2. Send it directly to someone, or publish it to a marketplace — a plain HTTPS index.json anyone can host.
  3. On import, Pupa shows a confirm sheet naming the app and its agent prompts before anything installs.
  4. The bundle is inert JSON: it ships structure and intent, never a runtime. It can’t act until the new host grants it capabilities.
Read the docs
Three ways to share a MyApp: publish it to a marketplace, import one from a marketplace, or send it directly device-to-device.
Share directly, device to device — or publish to a marketplace (a plain HTTPS index.json anyone can host) for others to import.

Where your data lives

Pupa has no cloud of its own. Your apps run on your devices, sync through your iCloud, and talk to a backend you run — so your data stays with you, not with us.

  • Your MyApps, memories, and settings live on your device and sync across your iPhone, iPad, and Mac through iCloud, under your own Apple ID. There’s no Pupa account and no Pupa server in the middle — the local copy is the source of truth, and iCloud is just the mirror that keeps your devices in step.
  • Your agent and model credentials never leave your machine. The app pairs to a backend you run, so your API keys and logins sit in that backend’s environment; the paired-device token stays in the iOS Keychain and is excluded from sync.
  • Chat history lives in your backend, not iCloud — kept in the database you point it at, yours to keep or wipe. Give the backend no database and history simply ends with the process.
  • The one external hop is the model you pick. What you send the agent goes to your LLM provider (Anthropic, OpenRouter, …) under your own key — the same trust you already place in that provider, and nowhere else.
A Mac, iPad, and iPhone each showing the same Pupa app, kept in step by iCloud Sync.
Your MyApps, memories, and settings sync across your iPhone, iPad, and Mac through your own iCloud — no Pupa account, no Pupa server in the middle.

Run your own backend

Pupa is user-owned: the app pairs to a backend you run, and your model credentials never leave your machine. Pick a harness or model provider, install the CLI, pair, and you’re live. Full reference lives in the pupa-backend README.

Install the CLI (PyPI)

Install pupa-backend from PyPI with uv — it puts the CLI on your PATH in an isolated environment and is fast. Pin the version to match your app:

uv tool install "pupa-backend[setup]"          # latest

Run the setup wizard once, then start the server:

pupa-backend setup    # interactive — writes ~/.pupa-backend/config.yml
pupa-backend run      # serves on http://localhost:8004

Prefer it always-on? Install it as a background service (logs go to ~/.pupa-backend/logs/).

pupa-backend service-install   # install and start the background service
pupa-backend status            # is it running?
pupa-backend logs              # tail the service log
pupa-backend service-stop      # stop and remove it

Credentials live in your shell env (see the backend’s .env.example), never in the config file.

Option A · Claude Code (subscription)

Claude Code is a harness — a self-contained agent loop the backend runs on your existing Claude subscription instead of metered API tokens. Install the Claude Code CLI so the claude binary is on your PATH, then enable the harness in ~/.pupa-backend/config.yml:

harnesses:
  claude_code:
    enabled: true

Every enabled harness is mounted separately, so the app shows a Claude Code option in its per-connection harness picker — no app update needed. It uses Claude Code’s native host tools, gated by the permission controls the app renders.

Option B · OpenRouter (300+ models)

Prefer metered API access or non-Claude models? Point the backend at OpenRouter, an OpenAI-compatible endpoint. Set these in your shell env:

LLM_PROVIDER=openai_compatible
LLM_BASE_URL=https://openrouter.ai/api/v1
LLM_API_KEY=sk-or-...
LLM_MODEL=anthropic/claude-sonnet-4.6

Or set just OPENROUTER_API_KEY=sk-or-... to unlock the curated in-app model picker (GLM, Qwen, MiniMax, Kimi, DeepSeek) — the client chooses the model per request, no LLM_MODEL needed.

Connection channels

Pupa apppupa-backendCloudflareTailscaleLocalhost

Cloudflare tunnel — a public HTTPS URL to your backend, reachable from anywhere with no ports to open.

“How will your iPhone reach the backend?” is the question in pupa-backend setup that picks your connection channel. It’s saved as connectivity in config.yml; re-run setup any time to change it. Three options, quietest to most reachable:

  • Localhost — same machine only. The app talks to http://localhost:8004 and nothing leaves your computer. Ideal when the macOS app runs on the same Mac as the backend: no tunnel, no exposure.
  • Cloudflare tunnel — a public HTTPS URL to your backend with no ports to open. The free quick tunnel needs no domain and no account (the backend brings it up on run), but its URL changes on every restart, so you re-pair each time. Own a domain on Cloudflare? Choose “I have a domain” for a named tunnel with a stable URL — pair once, never again.
  • Tailscale — a private mesh VPN across your own devices (free). You get a stable MagicDNS name and traffic stays private instead of being published to the internet; the wizard generates a self-signed TLS cert for it. Recommended for everyday remote use — needs Tailscale installed and running on both the backend host and your phone.

Whichever you pick, pairing is the same flow below — the channel just decides which URL the QR code embeds.

Pair the app

With the backend running, mint a pairing code and scan it from your device:

pupa-backend pair     # prints a QR pairing code

In the app, go to Settings ▸ Backend ▸ Edit and scan the QR (or paste the code). The app defaults to http://localhost:8004/; point it at a hosted instance from the same screen. Pairing is one-time — after that the app reconnects on its own.

Screen-share

Screen-share lets the agent see your screen as live WebRTC video. It’s optional and macOS-only, and the sidecar is built from source — so use the one-line source installer instead of the PyPI package (it clones the repo and compiles the sidecar):

curl -fsSL https://raw.githubusercontent.com/pupa-app/pupa-backend/main/install.sh | bash

Enable the broker in config.yml (or PUPA_SCREENSHARE=1), then start publishing from the same Mac:

screenshare: true     # in ~/.pupa-backend/config.yml
pupa-backend screenshare

Requires macOS 14+ and the Xcode toolchain (the first run compiles the Swift sidecar). The broker stays off by default and on hosted multi-tenant deployments.

Deploy to the cloud (Railway)

Don’t want to keep a laptop running? The repo ships a ready-to-deploy Railway configrailway.json and a Dockerfile at the root, plus a baked cloud-config.yml with multi-tenant-safe defaults. You just fork it, point Railway at your fork, add a database, and set a few secrets:

  1. Fork pupa-backend on GitHub.
  2. On Railway: New Project ▸ Deploy from GitHub repo → pick your fork. Railway auto-detects railway.json + Dockerfile and builds the image. The first deploy fails until env vars are set — that’s expected.
  3. Add the Postgres plugin (New ▸ Database ▸ PostgreSQL). Railway injects the connection string, which the backend auto-parses into its checkpointer + store.
  4. Set the service Variables, then redeploy:
ANTHROPIC_API_KEY=sk-ant-...              # or OPENROUTER_API_KEY=sk-or-...
PUPA_API_KEY=<random>                     # openssl rand -hex 32 — bootstrap pairing key
DATABASE_URL=${{Postgres.DATABASE_URL}}      # reference the Postgres plugin

The safety posture (shell off, screen-share off, no MCP servers) is baked into the image — don’t override it on a public deployment. Railway redeploys on every push, so your fork stays in sync. Once the deploy is green, pair against your https://<svc>.up.railway.app URL.

Optional: add OPENROUTER_API_KEY, AWS_*, or LANGFUSE_* to unlock more models and tracing. Full walkthrough in docs/deploy.md.