# Connected App Seam — Multi-App Authoring and Passport

Status: draft for review
Date: 2026-06-14
Related: #2378, #2474, #2486
Builds on: [`source-backed-connected-app-contract.md`](source-backed-connected-app-contract.md), [`l0-l1-standard.md`](l0-l1-standard.md)

## Purpose

The merged Source-Backed Connected App Contract (#2474) defines how *one* pack
authoritatively owns its `_System_ConnectedApp` policy, and it guards
coexistence **defensively**: a non-CRM session stays app-neutral, and a non-CRM
pack cannot claim `salesforce_compat`.

It does not yet state the **positive** design that makes the `connected_app`
seam durable for a second and Nth app, and it does not bind that design to the
L0/L1 Standard's passport rule. This document fills that gap. It is design-only
and adds no new field contract beyond #2474; it constrains *who the seam is for*
and *what proves it is a real seam*.

## The seam is L0; the payload is L1

Per [`l0-l1-standard.md`](l0-l1-standard.md) §1, L0 contains exactly three kinds
of things: the metamodel, engines, and **seams — the registration doors through
which an application enters**. The `connected_app` seam is therefore **L0**:

- the `connected_app` component kind and its component-kind contract
- the source-backed apply / verify / hash / drift machinery
- the source-backed registry entry and validation
- owner-scoped session-issuer-class validation
- the request-scoped OAuth profile-selection hook

What flows **through** the seam is **L1**, authored as data in each app-pack: a
specific client id/secret, redirect URIs, scopes, a pack-owned
`sessionIssuerClass` value, and a product userinfo/token response profile.

Console and cartridge: the slot is L0, the cartridge is L1. CRM is the first
cartridge, **not** the slot. The defect to avoid (the failure mode that closed
#2444) is a Salesforce circuit soldered onto the motherboard.

## Positive second-app authoring path

A second app (for example a Jira-like app) owns its own connected app with
**zero L0 change**:

1. The app ships a `connected_app` component in its own app-pack, with its own
   payload conforming to the #2474 field-set rule (`id`, `consumerKey`,
   `clientType`, `redirectUris`, `scopes`, ...).
2. If it needs product-shaped session behavior, it declares its **own**
   `sessionIssuerClass` value (e.g. `jira_compat`), owned by its own pack. The
   value is pack-owned, never shared with or inherited from CRM.
3. It optionally registers its **own** userinfo/token response profile, selected
   only for sessions issued by its own connected app.
4. L0 installs, verifies, and drift-checks it through the **same** machinery,
   scoped to that pack.
5. No L0 file changes. No `if app == "..."` branch. The platform does not learn
   the second app exists except as installed pack data.

The work of "supporting a second app" is therefore authoring a second cartridge,
never modifying the console.

## Neutrality invariant (litmus)

Per `l0-l1-standard.md` §1: *"if deleting one application's pack changes the
branch taken, the engine is interpreting."*

Applied to this seam: deleting CRM's pack must not change any L0 code path on the
connected_app seam. `salesforce_compat` is a pack-authored **data value**, never
a platform constant; there must be no Salesforce-named branch in L0. The same
holds for any second app's value. If a connected-app code path branches on a
specific app's vocabulary, the seam has leaked back into the engine.

## Passport — the seam's L0 membership criterion

This is the load-bearing requirement. The L0/L1 Standard does not grant a seam
L0 standing on its first consumer alone:

- **T3 (Seam test) passport** — *"a second consumer already landed in-tree and
  running in CI (running code — commitments, roadmap items, and design docs
  count for zero)."*
- **§4.3 (Every seam ships a real consumer)** — *"an in-tree, CI-runnable
  consumer from a non-CRM domain that exercises the seam's exported surface with
  at least one input shape the first consumer cannot produce. A ceremonial toy
  that mirrors the first consumer's wire shape does not qualify."*

Obligations for the `connected_app` seam:

1. The L0 seam PR must include an in-tree, CI-runnable, **non-CRM** consumer.
2. That consumer must exercise at least one input shape CRM cannot produce —
   minimally a connected app owning a non-`salesforce_compat`, non-CRM session
   class, and ideally a second product response profile distinct from CRM's. A
   renamed copy of the CRM client does not satisfy §4.3.
3. The strongest, eventual passport is a **real minimal second app** (the planned
   Jira-like app, #2378) that owns its connected app and a distinct profile
   end-to-end.

### Current state (honest assessment)

#2486 ships `jira-like-sample` / `foreign-client-pack` / `synthetic_session`
tests that prove owner-scoped session-class **acceptance** (same pack) and
**rejection** (foreign pack). This satisfies the **structural** passport: the
seam is exercised by a non-CRM pack producing an input CRM cannot (a non-CRM
session class with a different owner).

It does **not** yet exercise a distinct **product behavior** — there is no
Jira-shaped response profile. The **behavioral** passport (a second app with its
own profile firing end-to-end) is roadmap, owned by #2378. Until it lands, the
synthetic non-CRM pack is the in-CI passport and must be documented as such, not
presented as the final proof.

## Durability properties (must hold as N apps grow)

- **Owner-scoping on every write path** — both source-backed apply and admin/API
  writes are pack-aware; never a host-global "registered class" allowlist
  (reinforces #2474 *Required L0 Changes*).
- **Session-issuer-class values are pack-owned and collision-checked** — two
  packs cannot share or hijack one another's class.
- **Runtime profile selection keys on `(ConnectedAppID, owning sessionIssuerClass)`**
  — never host-global pack presence; no last-writer-wins across product profiles.
- **Fail-closed** (`l0-l1-standard.md` §4.4) — an app that omits a product
  session class gets app-neutral behavior, never another app's.
- **Clean L0 holds no app's connected app row** — every row arrives only via its
  owning pack.

## Acceptance criteria (multi-app, beyond #2474)

- A non-CRM pack authors a connected app plus its own session class and
  installs/verifies green, with zero L0 change.
- A foreign pack claiming another pack's session class is rejected (owner
  mismatch).
- Two product profiles (CRM and non-CRM) coexist; each fires only for its own
  connected-app session; neither overwrites the other.
- Deleting any one app's pack changes no L0 code branch (interpret litmus).
- The in-tree non-CRM consumer produces at least one input shape CRM cannot
  (§4.3).

## Relation to other documents

- [`l0-l1-standard.md`](l0-l1-standard.md): §1 (seam = L0), T3 passport, §4.3
  real-consumer rule, §4.4 fail-closed.
- [`source-backed-connected-app-contract.md`](source-backed-connected-app-contract.md)
  (#2474): the per-pack authoritative ownership / apply / verify contract this
  document builds on.
- #2486: current L0 seam implementation and the synthetic non-CRM passport tests.
- #2378: multi-app binding / installation roadmap; the real second app that
  upgrades the passport from structural to behavioral.

## Residual / out of scope

- The real second app implementation (roadmap, #2378).
- Uninstall, operator-managed secret rotation, and access grants — already
  deferred by #2474.
