Concepts

Projects

How Extentos identifies a project across Android and iOS — the Extentos app id (config.appId) decoupled from the store bundle id, and how the same app id shares one project across platforms.

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 (a reverse-DNS identifier, e.g. com.example.myapp).
  • The Project ID is the Extentos app id you scaffold with (config.appId) — it is decoupled from your store bundle id. It defaults to your bundle id but does not have to equal it.
  • Give both platforms the same Extentos app id and they share one project — two platform chips, one card, zero manual setup. This works even when the store bundle ids differ (Android applicationId ≠ iOS bundle identifier), which is common on apps already shipped to both stores.
  • The id each shipped app actually broadcasts is preserved per row, so the Settings page always shows the truth.

How project identity works

Project identity is the appPackage you pass to generateConnectionModule — recorded in extentos.manifest.json and baked into the app as config.appId (iOS: the EXTENTOS_APP_ID Info.plist key; Android: the appId field of ExtentosConfig). The SDK reports this id for agent config, telemetry, and sounds. It is independent of the store bundle id — you can set it equal to your bundle id (the default expectation) or to any shared value you choose.

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:

  1. Android: rootProject.name from settings.gradle.kts, prefixed as android:<name> (e.g. android:my-app)
  2. iOS: no fallback — anonymous mint, no project grouping

Once generateConnectionModule writes the manifest, future mints use the manifest's appPackage, so the project identity becomes stable.

Two different things are easy to confuse here. Your project identity (projectInstallId, from the appPackage you scaffold with) is a grouping label — it is what puts Android and iOS under one dashboard card, and it is not secret. Your project key (EXTENTOSProjectKey / EXTENTOS_PROJECT_KEY, a pk_… value) is a credential — it authenticates your app to the managed AI gateway and bills usage to your account. This page is about the first one.

Sharing one project across Android + iOS

Give both platforms the same Extentos app id (the appPackage you scaffold with):

  • Scaffold Android with appPackage = com.example.myapp.
  • Scaffold iOS with appPackage = com.example.myapp.

Both apps then broadcast that id to Extentos, so createSimulatorSession from each platform produces the same projectInstallId:

  • 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, and the iOS row joins under the same projectInstallId.
  • Dashboard: ONE project card with two platform chips (Android · iOS).

This holds even when your store bundle ids differ. Because the Extentos id is config.appId and not the store identifier, an app whose Android applicationId is com.co.app and iOS bundle id is com.co.App (different, and locked by the stores) still shares one project — you just give both the same Extentos appPackage. Integrating Extentos is itself a store release, so you pick the shared id at integration time at no extra cost. Matching the appPackage is the mechanism.

Linking a project (recovery path)

The same-id approach above is what you want in almost every case. But if you've already shipped Extentos on both platforms under different, store-locked ids — and can't re-release to unify them — you'd otherwise be stuck with two cards. For that case there's Link, on a project's Settings tab:

  1. On the project you want to be canonical, open Settings → Project identifier → Link project…
  2. Pick the other project. Its install-id is registered as an alias of this project, and its simulators move here.

Unlike a cosmetic relabel, the alias is resolved server-side: the linked platform's shipped app keeps broadcasting its original id, and Extentos maps that id to the canonical project for agent config, sounds, and analytics. The real per-platform id is preserved and shown in Settings. This is the recovery path — matching the appPackage up front is simpler and needs no linking.

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 = the appPackage (config.appId) at mint time. Controls which card a session appears under.
  • platformInstallId — the id the shipped app actually broadcasts, set on insert and never modified. Equals projectInstallId in the normal case; differs only when a separate install-id has been linked to this project. Surfaces in Settings → Project info → per-platform identifiers when it diverges.
  • appLabel — the friendly display name shown on the dashboard card and the project header. Cosmetic, editable from Settings → Project info → Display name.

Per-platform truth in Settings

Because platformInstallId is preserved per row, a project stays honest about the id each platform actually broadcasts. On the project's Settings page:

  • Project ID displays the canonical projectInstallId (the grouping value).
  • Per-platform identifiers appears as a separate section only when at least one platform's platformInstallId differs from the Project ID. It lists each diverging platform with its actual identifier, e.g.:
    • Android: com.example.android-original
    • iOS: 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". Rotating the session ID (URL hijack recovery, a deliberate fresh identity) is a two-step act: deleteSimulatorSession({ sessionId }), then mint again. There is no one-call force-fresh — a silent replace left device registrations pointing at apps that were already gone.

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/sessions/[id]/delete soft-deletes a session — the only per-sim lifecycle action. Rotating an ID is delete, then mint.
  • Schema columns on simulator_sessions: project_install_id, platform_install_id, app_label (plus the rest of the row).