Fly is an AI chat product with two halves:
- A Cloudflare Worker that serves a thin web UI, authenticates users, persists conversations, and brokers model calls through Cloudflare AI Gateway.
- Native iOS and macOS apps (SwiftUI) that talk to the Worker over a small REST surface and additionally run a local on-device "operator" path against Apple Foundation Models.
The product is Fly. The Cloudflare worker name and a handful of physical resources still carry the legacy cloudchat identifier; see CTO review notes for the migration list.
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ iOS / macOS (SwiftUI) │ │ Web (React + Kumo) │
│ CloudChatApp/ Views/ │ │ src/app.tsx │
│ Packages/HarborlineOperator│ │ src/client.tsx │
└──────────────┬──────────────┘ └──────────────┬──────────────┘
│ │
│ REST + WebSocket │ WebSocket via agents/react
▼ ▼
┌─────────────────────────────────────────────────────┐
│ Cloudflare Worker (src/server.ts) │
│ - ChatAgent Durable Object (AIChatAgent) │
│ - /api/native/chat, /api/models, /api/tts/* │
│ - /api/auth/* (better-auth: magic link + Apple) │
└──────┬───────────────┬──────────────┬───────────┬────┘
│ │ │ │
▼ ▼ ▼ ▼
D1 (chats, R2 Vectorize AI binding
users, (uploads) (memory) → AI Gateway
sessions) dynamic routes
All model traffic goes through Cloudflare AI Gateway dynamic routes (text, image, audio, video, embedding) — no direct provider SDK calls. See wrangler.jsonc for the route bindings.
npm install
npm run dev # vite + wrangler dev → http://localhost:5173
npm run db:migrate:local # apply migrations to local D1For the native apps:
xcodegen generate # regenerate CloudChat.xcodeproj from project.yml
open CloudChat.xcodeproj # build "CloudChat iOS" or "CloudChat macOS"The iOS scheme is CloudChat iOS; the macOS scheme is CloudChat macOS. The user-visible product name on both is Fly (PRODUCT_NAME and CFBundleDisplayName in project.yml).
src/
server.ts ChatAgent Durable Object + HTTP routes (large; see review notes)
app.tsx React chat UI (Kumo + Streamdown)
client.tsx React entry
styles.css Tailwind + Kumo styles
migrations/
0001_*.sql D1 schema (chats, uploads, settings, generation_requests, auth tables)
public/ static assets served by Worker
wrangler.jsonc Worker config: bindings, vars, DO migrations
project.yml xcodegen spec for the Apple targets
CloudChatApp/ SwiftUI app sources (entry + model + theme + views)
CloudChatShared/ CloudChatClient (REST actor) + CloudChatModels (Codable types)
CloudChatIntents/ AppIntents shortcuts (Open / New chat / Voice / Run prompt / Save note)
Packages/ HarborlineOperator local Swift package (SharedModels, AIChatCore,
AIOrchestrator, AIMemory, AITools, AppIntentBridge, AutomationBridgeMac)
docs/ privacy, QA smoke tests, deploy, animation, UI references
fastlane/ release automation (Cloudflare bootstrap, build, screenshots, TestFlight)
- Chat with cloud (Worker → AI Gateway) or local (
OperatorRuntimeAdapter→ Foundation Models) routing, toggled bylocalOperatorEnabledin settings. - Projects + threads with archive/restore, starring, forking from any assistant message.
- Attachments: camera, photo library, files, audio, plus a parallel "system prompt context" upload set that travels with the system prompt.
- Voice in two layers: live dictation (
SpeechRecognizer+AVAudioEngine) and TTS playback (/api/tts/gatewaywith localAVSpeechSynthesizerfallback). - Auth via magic link or Sign in with Apple, brokered by the Worker's
better-authmount. - Operator runtime (orchestrator → tool registry → approval manager → audit log) surfaced through
ExecutionRailViewon wide screens. - App Intents / Shortcuts: Open Fly, New chat, Voice prompt, Run prompt in workspace, Save memory note. Bridged into the running app through
AppIntentBridgeStore. - 16 themes (
AppThemeChoice) including Apple system colors and a TweakCN set. Default isfly.
npm run deploy # vite build + wrangler deploy
fastlane bootstrap_cloudflare
fastlane build_ios
fastlane build_macos
fastlane upload_testflightbootstrap_cloudflare checks Wrangler auth, refreshes Worker types, and uploads any secrets listed in CLOUDFLARE_SECRET_NAMES from Doppler or .env.local. The App Store Connect API key is auto-discovered from APP_STORE_CONNECT_API_KEY_PATH, ASC_KEY_PATH, or a local AuthKey*.p8.
See docs/manual-cloudflare-deploy.md and docs/release-checklist.md for step-by-step.
./scripts/qa/smoke-cloudchat.sh app # local app smoke
SKIP_WRANGLER=1 ./scripts/qa/smoke-cloudchat.sh app # skip wrangler dry-rundocs/qa-smoke-tests.md lists the full backend, app, and release smoke checks. UI references for the visual smoke pass live in docs/ui-reference-requirements.md.
The product brand is Fly, but several deployment-level identifiers still read cloudchat. They are intentionally not renamed in this pass because each one carries a real migration cost:
| Identifier | Where | Rename cost |
|---|---|---|
CloudChat.xcodeproj and target names CloudChat iOS / CloudChat macOS |
project.yml, Fastlane |
Renaming the project breaks Xcode history, derived data, and any external CI references. The schemes are user-visible only to developers. |
Bundle IDs com.wplus.cloudchat.ios / com.wplus.cloudchat.macos |
project.yml |
Once the apps ship to TestFlight under these IDs they cannot be changed without re-listing. Coordinate with App Store Connect before flipping. |
Swift symbols CloudChatApp, CloudChatAppModel, CloudChatTheme, CloudChatClient, CloudChatModels, CloudChatRootView etc. |
CloudChatApp/, CloudChatShared/ |
Internal symbols, not user-visible. Renaming touches every source file; do it as a dedicated PR. |
Worker name cloudchat and deployed URL cloudchat.lazee.workers.dev |
wrangler.jsonc, CloudChatConfiguration.defaultWorkerBaseURL |
Renaming the worker creates a new URL and requires updating shipped clients or putting a custom domain in front. Recommend: assign fly.lazee.workers.dev (or a custom domain) and migrate clients before deprecating the old worker. |
D1 database cloudchat (d5f6e560-…) |
wrangler.jsonc |
The physical database is referenced by ID, not name; the name is cosmetic but a true rename means a new D1 + data migration. |
R2 bucket cloudchat-uploads |
wrangler.jsonc |
Bucket renames in R2 require migrating objects. Defer. |
Vectorize index cloudchat-memory |
wrangler.jsonc |
Indexes are immutable on rename; would require rebuilding embeddings. Defer. |
UserDefaults keys cloudchat.appearance / cloudchat.theme / scene key cloudchat.executionRailVisible |
CloudChatAppModel, CloudChatTheme, CloudChatRootView |
Renaming would lose existing preferences across an update. Migrate with a one-time UserDefaults copy if we change keys. |
CloudChatAppModel.swiftis 1,094 lines and currently owns appearance, themes, projects, threads, attachments, model selection, search options, voice, dictation, TTS, sending, errors, sign-in, magic link, Apple sign-in, operator runtime, approvals, audit logs, transient status, App Intents bridge, and system-prompt context. Split along feature boundaries (e.g.ProjectsModel,ChatModel,AuthModel,VoiceModel).src/server.tsis 2,255 lines. SplitChatAgent, route handlers, andbetter-authplumbing into separate modules undersrc/.src/app.tsxis 947 lines. The MCP panel, message rendering, and composer are all colocated; they're each independently testable.SidebarViewhardcodes the user as "Alex Smith". Wire it up to the authenticated user (or aviewervalue loaded from/api/auth/me).OperatorRuntimeAdapteris a struct with stateful in-memory stores (InMemoryStore,InMemoryConversationStore,InMemoryApprovalManager,InMemoryExecutionAuditLog). Today everyrunPromptcall instantiates a fresh orchestrator but reuses the same stores via the struct's properties — this works becauseInMemory*are reference types, but the struct semantics are misleading. Make it anactoror a class to make the lifetime explicit.CloudChatClient.sendreturns the entire response as oneChatMessageinstead of streaming. Worker side already supports streaming viaAIChatAgentover WebSocket; bring streaming to the native path.ChatLayoutconstants and animation tokens are duplicated betweenTheme.swift(CloudChatMotion) and the views. Centralize.
The app uses Cloudflare AI Gateway dynamic routes — never a raw provider model id. Bindings live in wrangler.jsonc under vars.DEFAULT_*_ROUTE. Inside the Worker:
env.AI.run("dynamic/text_gen", { messages }, { gateway: { id: "x" } });Adding a new route is a wrangler.jsonc edit plus a new env.d.ts regen (npm run types).
- Agents SDK documentation
- Cloudflare AI Gateway dynamic routing
docs/architecture/apple-ai-operator.md— local on-device pathdocs/architecture/foundation-models-sdk-notes.md— Apple Foundation Models notesdocs/privacy-and-permissions.md— data flow + permission requests
MIT