# Build and deploy a Podda app

Podda apps start private and are available only to named members. For invite-only and link apps, the app URL is never permission. An owner can separately confirm public mode, where the app URL opens to anonymous guests with shared data and file access.

## Recommended workflow

Author every file as text (HTML, JS, CSS, JSON, SVG — prefer SVG and CSS over binary images so the whole app stays text). Directories are just slashes in paths ("styles/main.css"); nothing creates folders.

**Most apps deploy in one call:** create_app, then one stage_app_files call carrying every file plus the publish block (activate true, expected_active_version_id — explicit null for the first activation). One call, app live. The minimal podda.json — this plus your files is a complete deploy; see get_docs(manifest) only for capabilities, a server module, or network expectations:

```json
{
  "schemaVersion": 2,
  "name": "Weekend plans",
  "client": { "entrypoint": "index.html" },
  "sdkVersion": "2",
  "capabilities": {}
}
```

Apps that store shared state import the SDK exactly as `import { init } from "/__podda/v2/sdk.js"` then `const app = await init()`, and declare the capability (for example `"capabilities": { "kv": ["browser"] }`). The SDK exposes app.user, app.session, app.members.list(), app.kv.list({ prefix? }), app.kv.get(key), app.kv.set(key, value, { ifVersion?, idempotencyKey }), app.kv.delete(key, { ifVersion?, idempotencyKey }), app.files, app.notifications, app.notify, app.ai.generate, and app.errors.capture — that list is complete for authoring; read get_docs(sdk) only for error semantics or the bundled-package variant.

**Using a third-party library?** Read get_docs(dependencies) first. A shell or code-execution tool in your tool list is the deciding signal: it means download the library into your bundle and deliver everything by ZIP PUT (below) — assume egress works until one probe fails. Only without a shell tool, or after that one failed probe, have Podda download it for you with a stage_app_files content_url entry. Never retype library source into a tool argument or hand-write a replacement for a well-known library.

**Apps of any size:** call stage_app_files with files (content_utf8 entries) and a fresh idempotency key. Omit upload_id on the first call; it opens a one-hour session and returns one. Repeat with the returned upload_id until every file is staged, in any order — podda.json is just another file; re-staged paths are replaced, and append: true continues an oversized single file across calls. Include the publish block (activate, expected_active_version_id, provenance) on the final call to publish immediately — most apps deploy in one stage_app_files call this way — or publish later with deploy_version action=publish. The response reports status awaiting_files (naming what is still missing) until podda.json and its client entrypoint are both staged; only you know when every file is staged, so publishing is the finish signal. The publish response is the verification: activation_status "activated" with deployment "live" means the revision validated, deployed, and is serving at app_url — hand the owner that URL and stop. Read get_app or read_app_logs only when something in the response looks wrong.

## Updating an existing app

Call edit_app_source instead of resending source: exact-string edits [{ path, old_string, new_string, replace_all? }] applied to the active revision (or base_version_id), plus add_files to add or replace whole files and remove_paths to delete. old_string must match exactly once unless replace_all; whitespace must match exactly, so call read_app_source first whenever your copy of the source might be stale. Pass manifest only when it changes. Publishing is atomic — a failed edit publishes nothing — and creates a new immutable revision with compare-and-swap activation; the edit response is the verification, so no follow-up reads are needed on success. edit_app_source with upload_id fixes files in an open staged session instead, publishing nothing.

## Binary files and large archives

Choose the delivery method yourself from what the environment supports; never ask the user to pick one or to change network settings. The decision starts from your tool list, not from a guess: a shell or code-execution tool being available means use the ZIP PUT below for any zip-worthy bundle (a vendored library, many files, or binaries) — do not fall back to staging just because egress is unverified; assume it works until an attempt fails. If an egress attempt fails, probe at most once, then fall back to stage_app_files (or a content_url entry for a library) without asking. Small text apps skip all of this — one stage_app_files call.

The PUT step below needs a shell. Without one — or after a failed egress probe — action=fetch_source and action=upload_chunk work from any client, and a library file vendors with a stage_app_files content_url entry — see get_docs(dependencies).

When an app genuinely needs binary files, or a shell built a large archive that would take many staging calls: ZIP the directory with podda.json at the root, compute the exact byte length and SHA-256, call deploy_version action=prepare_upload, PUT the ZIP to the returned upload_url (an api.podda.app URL) with every required_headers value exactly as returned, then publish with the upload_id. The credential is single-purpose and expires after ten minutes. If the PUT fails with a network, DNS, or egress error, deliver anyway over this connection, cheapest path first. Most apps are text plus at most a library: abandon the ZIP and send the files with stage_app_files, using a content_url entry for the library — one call, no base64, and the bytes Podda fetches are the bytes the CDN served. Only when the app genuinely needs binary bytes that no URL can supply, use action=fetch_source (a hosted https URL or a data:application/zip;base64 URI) or action=upload_chunk (sequential base64 chunks at recommended_chunk_bytes, about 12 KB raw, with per-chunk SHA-256; a model retyping printed base64 corrupts bytes, so emit chunks programmatically). On Claude specifically you may also mention — without blocking on it — that adding api.podda.app to Claude's network allowlist once enables the direct PUT for future deploys. If every delivery path fails in this client, suggest the user try Podda from Claude Code, the Claude app, or Codex.

Every mutation needs a fresh idempotency key for a fresh intent. Reuse the same key only to replay the exact same request.
