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.xcodeprojis committed for convenience but is a build artefact: editproject.yml, runxcodegen 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_VERSIONinproject.yml;scripts/push-ota.shpublishes an OTA manifest the in-app updater compares against.
Android + Wear OS (apps/android)·
- Kotlin, Jetpack Compose, Gradle Kotlin DSL, modules
appandwear. - 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:
| 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:
// 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):
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).