Projects
How Extentos models projects and identifies them across Android and iOS — the matching reverse-DNS convention, the auto-join behavior at mint time, the Merge action when identifiers diverge, and what's stored on each session row.
A project in Extentos is one app — your Android version and your iOS version live under the same project. The dashboard at extentos.com/projects groups them automatically, and the agent's createSimulatorSession calls flow into the right project without you doing anything special — if you follow one convention. This page explains exactly how project identity works, what makes two platforms join under one project, what to do when they don't, and what each piece of stored data means.
TL;DR
- Each project has a Project ID (your reverse-DNS package name, e.g.
com.example.myapp). - The Project ID is derived from your app's source code, not invented by Extentos:
- Android:
applicationIdinapp/build.gradle.kts - iOS: bundle identifier in your Xcode project
- Android:
- When the Android
applicationIdand iOS bundle identifier match (the standard reverse-DNS convention —com.example.myappon both), minting a simulator from each platform automatically joins the same project. Two platform chips, one card. Zero manual setup. - When they don't match, two separate projects appear. Fix by aligning the identifiers in your source, or by using the dashboard's Merge action.
- The original platform identifier is preserved per row even after a Merge, so the Settings page always shows the truth.
How project identity works
When the agent calls createSimulatorSession for one of your projects, the MCP server reads the project's extentos.manifest.json and extracts the appPackage field:
- Android:
appPackage= theapplicationIdGradle compiles into your APK. Set inapp/build.gradle.ktsunderdefaultConfig. - iOS:
appPackage= the bundle identifier compiled into your.app. Set in your Xcode project's General tab → Identity → Bundle Identifier (or in the project's pbxproj asPRODUCT_BUNDLE_IDENTIFIER).
This value is sent to the backend as projectInstallId. The backend's get-or-create logic looks for an existing saved sim under (your account, this projectInstallId, this platform); if one exists, it's returned; otherwise a new one is minted. The dashboard groups all of an account's sessions by projectInstallId — one card per unique value.
If the project doesn't have a manifest yet (you've called createSimulatorSession before generateConnectionModule), the MCP server falls back to:
- Android:
rootProject.namefromsettings.gradle.kts, prefixed asandroid:<name>(e.g.android:my-app) - iOS: no fallback — anonymous mint, no project grouping
Once generateConnectionModule writes the manifest, future mints use the manifest's appPackage and the project keys become stable.
The matching reverse-DNS convention (and why it matters)
Most professional projects use the same reverse-DNS identifier for both Android and iOS:
- Android
applicationId:com.example.myapp - iOS bundle identifier:
com.example.myapp
This is the standard that App Store and Google Play both nudge developers toward (transferability across stores, brand consistency, deep-linking). Most React Native, Expo, Flutter, Capacitor, and Tauri toolchains enforce it by default.
When the identifiers match, createSimulatorSession from each platform produces the same projectInstallId, so:
- First mint (e.g. Android): a new project row is created with
projectInstallId = com.example.myapp. - Second mint (e.g. iOS): the get-or-create lookup finds the existing project key, and the iOS row joins under the same
projectInstallId. - Dashboard: shows ONE project card with two platform chips (Android · iOS).
No Merge action needed. No manual linking. No URL surgery. This is the happy path and it's how the system is supposed to feel — invisible.
When the identifiers don't match
Sometimes Android applicationId and iOS bundle identifier diverge. Common causes:
- Different reverse-DNS chosen per platform (rare in pro projects, more common in hobby projects or projects that grew organically per-platform).
- One project had no manifest yet — Android fell back to
android:<rootProject.name>while iOS used the real bundle ID. - The Android and iOS apps were authored in different teams or eras with no coordination on identifiers.
When the identifiers don't match, two separate projects appear on the dashboard. Two cards, each with one platform chip.
Fix option 1: align the identifiers (best long-term)
Edit one platform's reverse-DNS to match the other:
- Android: change
applicationIdinapp/build.gradle.kts:android { defaultConfig { applicationId = "com.example.myapp" // match iOS bundle ID } } - iOS: change the bundle identifier in your Xcode project (Target → General → Identity → Bundle Identifier).
Then re-run generateConnectionModule to regenerate the manifest, and mint a fresh sim. The new mint joins the existing project (or creates a new one with the unified ID — whichever already exists). Same root cause permanently fixed.
Caveat: changing applicationId after a Play Store release is a breaking change for installed users (it's a new app from the store's perspective). Don't do this lightly if your app is already shipped.
Fix option 2: dashboard Merge action
If you can't or don't want to change the identifiers, use the Merge action on the dashboard:
- Open the source project's Overview at
extentos.com/projects/<id>. - Navigate to the Settings tab (sidebar) → scroll to Danger Zone → click Merge….
- Pick the target project from the dropdown. The modal previews which platforms will move and surfaces any platform conflicts (e.g. both source and target already have a live Android sim — must Reset or Delete one of the duplicates first).
- Confirm. The merge updates the source's
projectInstallIdto the target's value across every live row in a single atomic database update. Live WebSocket sessions are unaffected (the sessionId stays stable; only the project grouping changes).
After the merge, the source project disappears from the dashboard and its sessions appear under the target.
What's stored on each session row
There are three identifier fields on every session row, each with a distinct role:
projectInstallId— the dashboard grouping key. Equals the source-derivedappPackageat mint time. Updated to the target's value during a Merge. This is what controls which card a session appears under.platformInstallId— the original platform identifier, set on insert and never modified. EqualsprojectInstallIdat mint time. After a Merge, this preserves the row's original AndroidapplicationId/ iOS bundle identifier even thoughprojectInstallIdhas been rewritten. Surfaces in Settings → Project info → Per-platform identifiers when it diverges from the project ID.appLabel— the friendly display name shown on the dashboard card and the project header. Cosmetic, editable from Settings → Project info → Display name. Defaults to the source-derived value at mint time (Android:rootProject.name; iOS:appPackage).
The first two are derived from your source code; the third is yours to edit.
After a Merge: per-platform truth in Settings
Because platformInstallId is preserved per row, even a heavily-merged project stays honest about its underlying platform identifiers. On the project's Settings page:
- Project ID displays the post-merge
projectInstallId(the canonical grouping value). - Per-platform identifiers appears as a separate section only when at least one platform's
platformInstallIddiffers from the Project ID. It lists each diverging platform with its actual identifier, e.g.:Android: com.example.android-originaliOS: com.example.ios-original
If your identifiers match (the happy path), this section is hidden — the project ID alone is the complete picture.
Editing the project name
The Project ID is read-only on the dashboard — your app's source is the source of truth. Editing the dashboard's stored projectInstallId would just create a mismatch with the next mint (which derives the value from the manifest again).
The display name (appLabel) is editable on the Settings tab. Saving it propagates atomically across every platform row of the project, so Android and iOS rows display the same label.
What the agent does
createSimulatorSession is get-or-create under the hood. The first call from a project mints; subsequent calls return the existing saved sim with status: "resumed". When you want to rotate the session ID (URL hijack recovery, clean-slate test), pass resetFresh: true — the existing session is archived and a new one is minted with the same project key.
When the agent mints from a second platform with a matching identifier, the second-platform row simply joins the existing project — no special tool call, no flag, no agent prompt required. The agent just calls createSimulatorSession for the iOS or Android project as normal.
Reference
- Settings UI lives at extentos.com/projects/[id]/settings.
- Backend endpoints:
PATCH /api/projects/[projectId]updates the display name across all rows.POST /api/projects/[sourceId]/mergeruns the merge.POST /api/sessions/[id]/resetrotates a single session ID.POST /api/sessions/[id]/deletesoft-deletes a session (7-day undo grace).
- Schema columns on
simulator_sessions:project_install_id,platform_install_id,app_label(plus the rest of the row).
Protocol overview
The Extentos V1 wire format that decouples the SDK from vendor-specific glasses APIs.
Capabilities
The Extentos capability vocabulary — vendor-agnostic SDK primitives (audio.transcriptions, audio.recordDiscrete, audio.speak, camera.capturePhoto, camera.videoFrames, hardware events) that your handler code subscribes to. How abstract capabilities translate to platform-specific calls on iOS and Android, how permissions derive automatically, how validation negotiates against per-vendor manifests, and why a shared vocabulary plus a standard transport interface is what makes the same code run across Meta Ray-Ban, Mentra G1, Android XR, and future smart-glasses vendors.