# Pororoca OTA complete setup runbook Pororoca OTA delivers signed JSON UI documents that render as native SwiftUI or Jetpack Compose UI. It does not deliver executable code. Native code owns state, actions, billing, authentication, navigation, data access, analytics, and all side effects. Production API and dashboard: https://pororoca-ota.fly.dev Human docs: https://pororoca-ota.fly.dev/docs ## Completion definition A correct integration: 1. Pins a Pororoca source revision. 2. Ships a compiled embedded document as the final fallback. 3. Uses one stable random app-owned install ID. 4. Stores only the runtime token and public Ed25519 key in mobile configuration. 5. Rejects invalid signatures, file hashes, unsafe paths, malformed documents, and unsupported host capabilities. 6. Maps named document actions to compiled native handlers. 7. Retains verified content for offline startup. 8. Reports applied and failure signals only when those events happen. 9. Validates, signs, and publishes platform-specific immutable bundles. 10. Starts production rollout at 10% and observes health before widening. ## Credential model POROROCA_API_URL=https://pororoca-ota.fly.dev is public and belongs in CI and MCP configuration. POROROCA_APP_SLUG is a non-secret stable app identifier used by CI, MCP, and the mobile app. POROROCA_API_TOKEN is a delivery-scoped token. It can publish, change rollout, and roll back. Put it only in CI or the MCP process environment. Never put it in mobile source, app bundles, prompts, logs, or build artifacts. POROROCA_PRIVATE_KEY is the base64-encoded 32-byte Ed25519 signing private key. Put it only in the CI secret store or a secured local development environment. Never send it to the Pororoca server or MCP tool and never put it in a mobile app. POROROCA_PUBLIC_KEY is the matching base64-encoded 32-byte Ed25519 public key. It is safe to include in CI and the mobile app and is used to verify signed bundles. POROROCA_RUNTIME_TOKEN is an app-scoped, publishable mobile identifier. It can resolve only that app's updates and report untrusted runtime events, but cannot inspect status, publish, change rollout, or roll back. Mobile credentials remain extractable, so it must never be replaced by the app-scoped delivery token, which expires after 90 days. Tokens are displayed once when created. Save them immediately. Revoke exposed tokens in Dashboard. ## Account and app setup 1. Create or sign into the Pororoca account. 2. Confirm Pororoca Pro is active. 3. Create an app workspace and record its slug. 4. Create one delivery token for CI/MCP. 5. Create one separate runtime token for the native app. 6. Generate the signing key pair once. 7. Back up the private key in the CI secret store. 8. Add the public key to each supported mobile app target. ## Publisher model The publisher is the `pororoca` Swift executable in the repository's `ios` package. It consumes a directory containing schema-v1 JSON documents. It does not compile an arbitrary customer app or discover SwiftUI views dynamically. Authoring choices: - Start from a JSON fixture and edit it. - Author schema-v1 JSON directly. - Use the Pororoca Swift DSL in a small executable or test, then write `DocumentCodec.encode(document)` into `.pororoca/documents`. Example workspace: .pororoca/ documents/ # one or more JSON input documents assets/ # optional referenced assets keys/ # private.key must not be committed update/ # immutable signed output bundle Generate keys: cd /path/to/pororoca-ota/ios swift run pororoca keys generate --output .pororoca/keys Validate iOS documents: swift run pororoca validate .pororoca/documents --platform ios Validate Android documents: swift run pororoca validate .pororoca/documents --platform android Export and sign iOS: swift run pororoca export .pororoca/documents \ --assets .pororoca/assets \ --output .pororoca/update \ --update-id UNIQUE_IMMUTABLE_ID \ --message "Human-readable change" \ --git-sha GIT_SHA \ --private-key .pororoca/keys/private.key Omit `--assets` when unused. Pass `--platform android` for Android export. Export refuses to replace an existing output directory. Use `--force` only in a disposable CI workspace. Publish: POROROCA_API_TOKEN=pororoca_live_... swift run pororoca publish .pororoca/update \ --server https://pororoca-ota.fly.dev \ --app APP_SLUG \ --channel production \ --rollout 10 \ --public-key .pororoca/keys/public.key The CLI verifies the signed bundle against the public key before upload. iOS and Android require separate platform bundles. Update IDs are immutable and unique within an app. CI must pin the repository revision and Swift toolchain, require review for production publish, use secret variables, and retain the signed bundle as an incident-review artifact. ## SwiftUI integration Requirements: iOS 17 or newer. Add the repository's `ios` directory as a pinned local Swift package and link the `Pororoca` product. Run `swift test` in the package before adopting a revision. Create a stable install ID in UserDefaults. Use a random app-owned UUID and reuse it across launches. Do not use an advertising identifier. Decode the public key from base64 and require exactly 32 bytes. Create UpdateStore under Application Support. Pass the public key and a host-capability entry for every updatable screen. A good capability source is the compiled embedded document's `requires` value. Create UpdateClient with: - baseURL: https://pororoca-ota.fly.dev - apiToken: runtime token - app: app slug - channel: production unless intentionally using another channel - installID: stable app-owned ID - store: configured UpdateStore At launch: 1. Read `pendingUpdateID()`. 2. Call `prepareForLaunch()` before showing the updatable screen. 3. For `.update`, use `update.documentURL(for: SCREEN_NAME)` as `.file(url)`. 4. If the update has no expected screen, use `.embedded(EmbeddedScreen.document)`. 5. For `.embedded`, use the same embedded document. Build ScreenHost with the embedded document's HostCapabilities, design-token resolver, localization resolver, and native slot registry if required. Render OTAScreen with DocumentSource, StateSnapshot or StateBox, ScreenHost, and an exhaustive native action handler. Unknown actions should be visible in diagnostics and never invoke arbitrary behavior. Only after the newly promoted screen reaches a real healthy render point: if activeUpdateID == pendingID, let activeUpdateID { try await store.markLaunchSuccessful() try await client.record(event: "applied", updateID: activeUpdateID) } Run `checkForUpdate()` in a background Task. A download stages content for the next launch; it does not replace the active screen in the current session. Keep current or embedded content on network failure. UpdateStore retains pending, current, and previous bundles. It verifies from network and disk. If a promoted launch exits before the good marker, the next launch marks that update bad and restores previous, then embedded if needed. Reference implementation: `examples/PaywallSpike`. SwiftUI integration tests must cover embedded offline launch, valid signed update, wrong-key rejection, unsupported-capability rejection, native action routing, good marker, and failed-launch recovery. ## Jetpack Compose integration Requirements: Android API 26+, Java 17, Compose, Kotlin serialization, coroutines, and Tink. Include the repository's pinned `android/sdk` module and depend on its project from the app. Run: cd /path/to/pororoca-ota/android ./gradlew :sdk:testDebugUnitTest :app:assembleDebug Create a stable install ID in app-owned SharedPreferences. Use one random UUID and reuse it across process restarts. Decode the public key from base64 and require exactly 32 bytes. Create PororocaClient with server URL, runtime token, app slug, production channel, stable install ID, and public key. Call `checkForUpdate()` from a coroutine, not the main thread. The client verifies the Ed25519 signature, key ID, SHA-256 hashes, byte counts, safe paths, Android platform, and strict document decoding before returning a PororocaRemoteUpdate. Persist `update.documentBytes[SCREEN_NAME]` atomically inside app files storage. Write a pending file and move it with ATOMIC_MOVE plus REPLACE_EXISTING. If no network update is returned, decode the retained file. If it is absent or invalid, decode a compiled embedded document. Render PororocaScreen with PororocaHost. Provide the host localization string map and an exhaustive action handler that calls existing Kotlin functions. After the verified document renders, call: client.record(event = "applied", updateId = update.id) When catching a presentation failure, report `render_error` with the screen name in metadata. Current Compose nodes: vstack, hstack, zstack, box, spacer, scroll, lazycolumn, text, divider, progress, button. Text can be literal or host-localized. Buttons emit a named action. Full Swift expression/modifier parity is not available. Android early access uses host-managed retained storage. Managed current/previous crash recovery is not yet part of the Android SDK. The compiled embedded document is mandatory. Android integration tests must cover airplane-mode fallback, valid signed update, wrong-key rejection, malformed-document rejection, atomic retained bytes, stable install identity, and every native action. ## Resolve API smoke test curl -i \ -H "Authorization: Bearer $POROROCA_RUNTIME_TOKEN" \ "https://pororoca-ota.fly.dev/api/v1/apps/APP_SLUG/channels/production/resolve?install_id=docs-smoke-0001" HTTP 200 means this install receives a signed update. HTTP 204 means no eligible update; at a staged rollout it may be outside the cohort. HTTP 401 means the token is missing, revoked, or wrong for the app. HTTP 404 usually means the app slug or channel is wrong. ## MCP setup for Codex Requirements: Node 22 or newer and a delivery token. codex mcp add pororoca \ --env POROROCA_API_URL=https://pororoca-ota.fly.dev \ --env POROROCA_API_TOKEN=pororoca_live_... \ -- npx -y @pororoca-ota/mcp-server codex mcp list Restart the MCP client if necessary. In Codex TUI use `/mcp` to inspect configured tools. The package is `@pororoca-ota/mcp-server` on npm. The MCP server uses stdio, so stdout is reserved for protocol messages. MCP tools: - `pororoca_channel_status(app, channel)` is read-only and reports delivery health. - `pororoca_publish(app, channel, external_id, label, platform, manifest, files, rollout_percentage)` publishes an already-signed bundle. - `pororoca_set_rollout(app, channel, percentage)` sets the current rollout from 0 through 100. - `pororoca_rollback(app, channel)` restores the previous known-good update for the next device check. Safe MCP sequence: 1. Read channel status and summarize baseline. 2. Validate and sign outside MCP. Never provide the private key to MCP. 3. Show exact app, channel, update ID, and rollout percentage. 4. Receive explicit human approval. 5. Publish at 10%. 6. Read status again and inspect downloaded, applied, reverted, and render-error signals. 7. Receive approval before widening. 8. Widen through 25%, 50%, and 100%, observing between changes. 9. On regression, receive approval to roll back, then confirm status again. ## Production operations Default rollout: status, publish 10%, observe, 25%, observe, 50%, observe, 100%. Interpret events as a funnel. Eligible means cohort selection. Downloaded means verified and staged. Applied means the host reached its healthy render point. Render_error means the host caught a presentation failure. Reverted means the host recovered away from the update. Compare rates instead of raw counts because adoption requires eligible users to open the app and check. Incident sequence: 1. Capture status, update ID, rollout, and symptoms. 2. Stop automation that widens rollout. 3. Roll back the exact app and channel. 4. Confirm status points to the previous update. 5. Verify a real device resolves and renders known-good content. 6. Create a new immutable update ID for the fix. Rollback affects the next device update check. It is not a remote process kill. Ship through the App Store or Play Store when compiled native behavior is broken. Credential incident response: - Leaked delivery token: revoke immediately, replace in CI/MCP, inspect release history. - Leaked runtime token: revoke and replace in the next mobile build. - Lost private key: create a pair and ship the new public key in a normal mobile release before publishing with it. - Compromised private key: stop publishing, rotate, and distrust later bundles signed with it. ## Agent authorization boundary An agent may inspect code, add the SDK, create documents, run local validation, run tests, and prepare a signed bundle when the repository owner has provided the required local secrets through approved configuration. An agent must stop and request explicit human approval before publishing, changing rollout, rolling back, revoking credentials, or changing a paid subscription. Approval must name the exact app, channel, update ID when applicable, and percentage when applicable. ## Copy-paste agent task Read https://pororoca-ota.fly.dev/llms-full.txt and the target repository's existing build, state, navigation, design-token, localization, and test patterns. Integrate Pororoca OTA for SwiftUI or Jetpack Compose on one named screen. Reuse native state and action handlers. Keep a compiled embedded fallback. Use a stable app-owned install ID. Put only the runtime token and Ed25519 public key in mobile configuration. Never put the delivery token or private signing key in the mobile app, prompt, logs, artifacts, or source control. Create one representative document. Test signature rejection, offline fallback, rendering, and native actions. Run existing platform checks. Report exact files changed and early-access limitations. Do not publish, change rollout, revoke credentials, or roll back production without showing the exact target and receiving explicit approval. ## Known early-access limits - SDKs and MCP are source-distributed and must be revision-pinned. - Android retained storage is host-managed. - Android supports a smaller node/modifier/expression surface than SwiftUI. - Documents cannot add native executable behavior, permissions, entitlements, or dependencies. - Native store releases remain required for code changes and failures in embedded behavior.