Skip to content

Mobile apps — iOS and Android·

apps/ios and apps/android are the native clients. They were moved here from the tinyai-id monorepo on 2026-09-17 with their full git history (git subtree), so git log -- apps/ios goes back to the first commit of the iPhone app.

Both talk to the same backend as the web app through the wire protocol in packages/contracts (SSE chat events, device relay envelopes, notification payloads).

iOS · iPadOS · watchOS · Mac Catalyst (apps/ios)·

  • SwiftUI, Swift 6, project generated by xcodegen from project.yml — Tiny.xcodeproj is committed for convenience but is a build artefact: edit project.yml, run xcodegen generate.
  • Targets: Tiny (app), TinyWidgets, TinyWatch, TinyWatchWidgets, TinyTests, UITests.
  • Deployer-specific values live in Secrets.xcconfig (gitignored):
cd apps/ios
cp Secrets.example.xcconfig Secrets.xcconfig     # DEVELOPMENT_TEAM, MWDAT_*, TINY_BASE_URL
xcodegen generate
open Tiny.xcodeproj

DEVELOPMENT_TEAM is your Apple team id. The two MWDAT_* values are only needed for the Meta Wearables (glasses) integration — leave them empty otherwise; #if canImport(MWDATCore) guards keep the app building. - Device deploy without Xcode UI (what the fleet does):

xcodebuild -project Tiny.xcodeproj -scheme Tiny -destination 'generic/platform=iOS' \
  -derivedDataPath build/device -allowProvisioningUpdates build
xcrun devicectl device install app --device <udid> build/device/Build/Products/Debug-iphoneos/Tiny.app
xcrun devicectl device process launch --device <udid> technology.tiny.app
  • Build number = CURRENT_PROJECT_VERSION in project.yml; scripts/push-ota.sh publishes an OTA manifest the in-app updater compares against.

Android + Wear OS (apps/android)·

  • Kotlin, Jetpack Compose, Gradle Kotlin DSL, modules app and wear.
  • Deployer-specific values come from local.properties (gitignored) or the environment:
cd apps/android
cp local.properties.example local.properties      # sdk.dir, MAPS_API_KEY, MWDAT_*, TINY_BASE_URL
./gradlew :app:assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

MAPS_API_KEY is a Google Maps SDK for Android key — restrict it to the package name and your signing SHA-1 in the Google Cloud console. Empty values build fine; the map and glasses features degrade. - Store listings: fastlane/metadata/android (Play) — screenshots, descriptions, changelogs.

Point the apps at your deployment·

One setting, discovered once — the same rule the CLI follows (docs/CLI.md). Each app is told only the web app's origin, TINY_BASE_URL (your NEXT_PUBLIC_APP_URL). On launch it asks GET <TINY_BASE_URL>/api/health who that deployment is and remembers the answer:

{ "ok": true, "service": "web", "siteName": "acme", "workerUrl": "https://worker.acme.dev", … }
Value Where it comes from Used for
TINY_BASE_URL build setting (Secrets.xcconfig / local.properties or env) every /api/* call, sign-in (/auth/cli), x402 + ERC-8004 URLs, the iOS OTA manifest (/ios/manifest.plist), Android OTA, the host shown in About / onboarding / share exports
worker origin /api/health → workerUrl (what the web app already ships to its own browser bundle as NEXT_PUBLIC_TINY_WORKER_URL), cached per base URL public /community, /profile, /media, /voice/recording/<id>
site name /api/health → siteName Config.siteName / AppConfig.siteName

The answer must say ok:true and service:"web" — anything else (a 404 page, another service) is ignored and the cache is left alone, exactly like probeDeployment in apps/cli/src/config.ts. Until a fork's health route has answered, its worker is assumed to be the base itself, never the upstream operator's worker; only a build whose base is the reference deployment falls back to the reference worker. TINY_WORKER_URL still exists in both files as an optional override for a deployment whose /api/health does not advertise a worker — leave it empty normally.

Unset, TINY_BASE_URL defaults to the reference deployment (https://tiny.technology) so a build from a fresh clone behaves exactly as before. The in-app server override (Settings → developer) still layers on top of it for pointing one device at a local next dev.

iOS — apps/ios/Secrets.xcconfig:

TINY_BASE_URL = https:/$()/tiny.example.com

// starts a comment in xcconfig, so the scheme is written https:/$()/ — the empty $() expands to nothing. project.yml maps the setting into every bundle's Info.plist (TinyBaseURL; app, widgets, watch app, watch widgets — each extension has its own Bundle.main). Config.baseURL reads it; Config.workerURL resolves override → discovered → fallback, and Config.refreshDeployment() (called from TinyApp on every return to the foreground) does the probe — Deployment in Config.swift, cache in the app-group container so the widgets see it too. Verify a build with /usr/libexec/PlistBuddy -c 'Print :TinyBaseURL' build/device/Build/Products/Debug-iphoneos/Tiny.app/Info.plist.

Android — apps/android/local.properties (or the same name in the environment, e.g. in CI):

TINY_BASE_URL=https://tiny.example.com

app/build.gradle.kts and wear/build.gradle.kts turn it into BuildConfig.TINY_BASE_URL; net/AppConfig.kt owns it (baseUrl, workerUrl, siteName, displayHost; attach() + refreshBlocking() from TinyApp.onCreate), net/TinyApi.kt's BASE_URL / WORKER_URL / BASE_HOST are thin getters over it, and the watch's WearChat.kt reads its own BuildConfig. Verify with unzip -p app/build/outputs/apk/debug/app-debug.apk 'classes*.dex' | strings | grep tiny.example.com.

Left for you (not build settings): universal links / associated domains (applinks: in project.yml, needs a paid Apple team), hosting the OTA manifest + .ipa / .apk under TINY_BASE_URL/ios and /android (apps/ios/scripts/push-ota.sh), and any device firmware that allow-lists media hosts (the Sticky's media bridge fetches from the worker host you flash into it, not from the phone).