Overview
Build a native iOS app that lets users control their Meticulous espresso machine directly — with no Pi, no server, and no Docker stack required. Users download the app, connect to the same Wi-Fi as their machine, and are up and running.
This issue tracks everything from #252 onward. #252 (MachineService abstraction) is a prerequisite — the iOS work should not begin until that is in place and the web app is sufficiently mature.
Why This Is Possible
The Meticulous machine exposes a full native API on port 80 of the local network:
- Socket.IO (
/socket.io/) — real-time telemetry: sensors, weight, pressure, flow, state
- REST (
/api/v1/) — full profile CRUD, shot history, machine commands, settings
The current MeticAI Python server is a translation and orchestration layer on top of these. An iOS app on the same Wi-Fi can bypass it entirely.
Current: Phone → MeticAI server (Pi) → MQTT bridge → Socket.IO → Machine
iOS app: Phone → HTTP REST + Socket.IO → Machine (directly)
Architecture
┌──────────────────────────────────────┐
│ Existing React web app (unchanged) │
│ │
│ useMachineService() │
│ ↓ │
│ MachineService interface │
│ ↓ ↓ │
│ MeticAIAdapter MachineDirectAdapter ← new, in this issue
│ (Docker/web) (iOS) │
└──────────────────────────────────────┘
↓ ↓
MeticAI server meticulous-{id}.local
(Pi, unchanged) HTTP REST + Socket.IO
The React codebase is shared. Only the adapter changes per deployment target.
Part 1: MachineDirectAdapter
Implement MachineDirectAdapter against the machine's native API.
Machine Discovery
The machine's mDNS name is randomised per device (e.g. meticulous-a3f7.local). Discovery options:
- mDNS browse — use a Capacitor plugin to browse Bonjour services on the LAN (preferred, zero friction)
- QR code — the machine already exposes
GET /api/v1/wifi/config/qr.png; scan it at first launch
- Manual IP entry — fallback, already supported via config.json mechanism
Note: the existing install script already solves mDNS discovery — port that logic.
Command Mapping
| MachineService method |
Machine native call |
start() |
GET /api/v1/action/start |
stop() |
GET /api/v1/action/stop |
abort() |
GET /api/v1/action/abort |
tare() |
GET /api/v1/action/tare |
preheat() |
GET /api/v1/action/preheat |
purge() |
GET /api/v1/action/purge |
loadProfile(id) |
GET /api/v1/profile/load/{id} |
listProfiles() |
GET /api/v1/profile/list?full=true |
getHistory() |
POST /api/v1/history |
subscribe(listener) |
Socket.IO status + sensors events |
Telemetry (Socket.IO)
Connect to http://meticulous-{id}.local/socket.io/. Subscribe to status and sensors events, map to the existing MachineState shape used by the web app.
AI Profile Generation
Currently handled server-side (Python calls Gemini). In this adapter:
- User supplies their own Gemini API key (entered in app settings)
- Image analysis and profile generation are called directly from the app
- The existing server-side logic is the reference implementation
Part 2: Capacitor Setup
apps/
├── web/ # React source (unchanged)
└── ios/ # NEW: Capacitor Xcode project
├── App/
│ ├── App/ # Swift app entry
│ └── public/ # Symlink → ../../web/dist
└── capacitor.config.ts
capacitor.config.ts
const config: CapacitorConfig = {
appId: 'com.meticai.app',
appName: 'MeticAI',
webDir: '../web/dist',
server: {
// In dev: point to vite dev server
// In prod: use bundled web app
},
}
Info.plist additions
<key>NSLocalNetworkUsageDescription</key>
<string>MeticAI needs local network access to communicate with your Meticulous espresso machine.</string>
<key>NSBonjourServices</key>
<array>
<string>_meticulous._tcp</string>
</array>
Build script (apps/ios/scripts/build.sh)
#!/usr/bin/env bash
# 1. Build web app
cd ../web && bun run build
# 2. Sync to Capacitor
cd ../ios && npx cap sync ios
# 3. Open Xcode (or use xcodebuild for CI)
npx cap open ios
Part 3: App Store Preparation
Part 4: Features Scope
In scope (v1.0 iOS)
- Machine discovery (mDNS + manual IP fallback)
- Live telemetry view
- Profile list and selection
- Shot start / stop / abort / tare / preheat
- Shot history browser
- AI profile generation (user's own Gemini key)
- Pour over mode
- Settings (machine IP, Gemini API key, preferences)
Out of scope (post-v1.0)
- Scheduling / recurring shots (server feature, no direct machine equivalent)
- Home Assistant / MQTT integration (server feature)
- Remote access via Tailscale (server feature — users who want remote access run the Pi stack)
- Android
What Stays On The Pi (Docker)
The Pi stack is not replaced — it continues to serve:
- Home Assistant integration (MQTT)
- Remote access via Tailscale
- Advanced scheduling
- Web browser access
The iOS app is an alternative client for users who don't want to (or can't) run a Pi.
Dependencies
#252 MachineService abstraction layer (hard prerequisite)
- Capacitor 7.x (
@capacitor/core, @capacitor/ios, @capacitor/camera)
- Capacitor community network plugin for mDNS browsing (or native Swift plugin)
- Apple Developer Program membership
- Xcode 16+
Acceptance Criteria
Overview
Build a native iOS app that lets users control their Meticulous espresso machine directly — with no Pi, no server, and no Docker stack required. Users download the app, connect to the same Wi-Fi as their machine, and are up and running.
This issue tracks everything from #252 onward. #252 (MachineService abstraction) is a prerequisite — the iOS work should not begin until that is in place and the web app is sufficiently mature.
Why This Is Possible
The Meticulous machine exposes a full native API on port 80 of the local network:
/socket.io/) — real-time telemetry: sensors, weight, pressure, flow, state/api/v1/) — full profile CRUD, shot history, machine commands, settingsThe current MeticAI Python server is a translation and orchestration layer on top of these. An iOS app on the same Wi-Fi can bypass it entirely.
Architecture
The React codebase is shared. Only the adapter changes per deployment target.
Part 1: MachineDirectAdapter
Implement
MachineDirectAdapteragainst the machine's native API.Machine Discovery
The machine's mDNS name is randomised per device (e.g.
meticulous-a3f7.local). Discovery options:GET /api/v1/wifi/config/qr.png; scan it at first launchNote: the existing install script already solves mDNS discovery — port that logic.
Command Mapping
start()GET /api/v1/action/startstop()GET /api/v1/action/stopabort()GET /api/v1/action/aborttare()GET /api/v1/action/tarepreheat()GET /api/v1/action/preheatpurge()GET /api/v1/action/purgeloadProfile(id)GET /api/v1/profile/load/{id}listProfiles()GET /api/v1/profile/list?full=truegetHistory()POST /api/v1/historysubscribe(listener)status+sensorseventsTelemetry (Socket.IO)
Connect to
http://meticulous-{id}.local/socket.io/. Subscribe tostatusandsensorsevents, map to the existingMachineStateshape used by the web app.AI Profile Generation
Currently handled server-side (Python calls Gemini). In this adapter:
Part 2: Capacitor Setup
capacitor.config.ts
Info.plist additions
Build script (
apps/ios/scripts/build.sh)Part 3: App Store Preparation
com.meticai.app)PrivacyInfo.xcprivacy) — local network, no trackingPart 4: Features Scope
In scope (v1.0 iOS)
Out of scope (post-v1.0)
What Stays On The Pi (Docker)
The Pi stack is not replaced — it continues to serve:
The iOS app is an alternative client for users who don't want to (or can't) run a Pi.
Dependencies
#252MachineService abstraction layer (hard prerequisite)@capacitor/core,@capacitor/ios,@capacitor/camera)Acceptance Criteria
MachineDirectAdapterimplements the fullMachineServiceinterface