# Decision records

https://exo.now/docs/adr

> Every architecture decision record, rendered directly from docs/adr.



The pages in this section are compiled straight from the Markdown in
[`docs/adr/`](../../../docs/adr/0001-reversible-content-backend.md). The ADR files are the single
source of truth: they carry no docs-specific frontmatter, and their title, status, and date are
read from each record's own heading and bullet list. Links between records resolve to docs pages,
and links to other repository files point at the file on `staging`.

When records conflict, the newest accepted ADR wins. For example, ADR 0003 (Convex Auth v2)
superseded ADR 0002 (Clerk identity boundary), ADR 0012 records that Clerk is fully removed, and
ADR 0013 (EXO is mission control; apps pick components) supersedes ADR 0011 (forge, Bases and Ships).

<AdrIndex />

<Callout title="Not yet on staging" type="info">
  ADRs 0005–0007 were accepted on `main` on 2026-09-27 and will appear here automatically once
  they reach `staging`. Until then, read them on `main`:
  [0005 EXO Verify](https://github.com/Interspace-Ventures/exo/blob/main/docs/adr/0005-exo-verify-verification-service.md) ·
  [0006 Exo Analyze](https://github.com/Interspace-Ventures/exo/blob/main/docs/adr/0006-exo-analyze-product-analytics.md) ·
  [0007 Exo Deploy (was exo-mobilize)](https://github.com/Interspace-Ventures/exo/blob/main/docs/adr/0007-exo-mobilize-mobile-release-toolkit.md).
  Summaries are on the [platform services](/docs/platform) pages.
</Callout>

## Writing a new ADR [#writing-a-new-adr]

1. Add `docs/adr/NNNN-short-slug.md` with a `# ADR NNNN: Title` heading and `- Status:` and
   `- Date:` bullets, following the existing records.
2. Open it through the normal canary → `staging` path. The docs pick it up at build time with no
   further change.


---

# Maintaining these docs

https://exo.now/docs/contributing

> How the Exo docs are built, where the content lives, and how to add or change a page.



## Framework: Fumadocs [#framework-fumadocs]

The docs are built with [Fumadocs](https://fumadocs.dev) (MIT), a documentation framework native
to the Next.js App Router. They are served by the existing Exo Next.js app at `/docs`, so they
share its build, Docker image, Railway service, and staging → production path. There is no second
deployment.

| Piece                | Location                                                                            |
| -------------------- | ----------------------------------------------------------------------------------- |
| Authored pages (MDX) | `content/docs/**`                                                                   |
| Sidebar order        | `meta.json` in each folder of `content/docs`                                        |
| Decision records     | `docs/adr/*.md`, rendered in place under `/docs/adr/*`                              |
| Roadmap board        | `roadmap/roadmap.json`, rendered on `/docs/roadmap`                                 |
| Source wiring        | `src/lib/docs/source.ts` (Fumadocs MDX macro API, no codegen step)                  |
| Route and layout     | `src/app/(docs)/`, a root layout separate from the landing page                     |
| Exo theme            | `src/app/(docs)/docs.css`, which maps Fumadocs tokens onto the canonical Exo tokens |
| MDX components       | `src/components/Docs/`                                                              |
| Search               | `GET /api/docs-search`, a static index over every page including the ADRs           |

## Adding or changing a page [#adding-or-changing-a-page]

1. Add or edit an `.mdx` file under `content/docs/`. Frontmatter needs `title` and should include
   a one-sentence `description`.
2. List new pages in the folder's `meta.json` to control their position in the sidebar.
3. Link to other pages or repository files with **relative paths**, for example
   `../../../docs/ai-routing.md`. Links to docs pages become in-site links, and links to other repo
   files open on `staging`, so the same link works in the repository browser.
4. Cite the source for every fact. If the repository does not record something, write **TBD**.
5. Merge to `staging` and review the page at [https://staging.exo.now/docs](https://staging.exo.now/docs). Docs are only ever reviewed on Staging.

## Components available in MDX [#components-available-in-mdx]

| Component            | Use                                                                                     |
| -------------------- | --------------------------------------------------------------------------------------- |
| `<Callout>`          | Notes, warnings, and known inconsistencies between sources                              |
| `<Cards>` / `<Card>` | Section overviews                                                                       |
| `<Steps>` / `<Step>` | Ordered procedures                                                                      |
| `<Tabs>` / `<Tab>`   | Alternatives, such as staging versus production values                                  |
| `<StackFacts>`       | The platform panel on each stack page, read from `src/platform/integrations/catalog.ts` |
| `<AdrIndex>`         | The list of decision records                                                            |
| `<RoadmapBoard>`     | The roadmap board                                                                       |

## Design rules [#design-rules]

The docs follow the [Structured Liquidity design standard](../../docs/design-standard.md). The
theme introduces **no new colors, radii, or fonts**: `docs.css` points Fumadocs' semantic tokens at
the Exo tokens defined in `src/app/(frontend)/globals.css`, and the navigation uses the Exo layer
mark from `src/components/Logo`. Light is the default and dark is the alternate, sharing the
landing page's theme preference.

## Checks [#checks]

`pnpm check` covers the docs: `tsc` type-checks the MDX wiring, `pnpm lint` runs ESLint plus the
Structured Liquidity and visual-vocabulary gates, and `pnpm build` compiles every page statically. A broken
MDX file fails the build.


---

# Overview

https://exo.now/docs

> What Exo is, what it owns, and how these docs are organized.



EXO is mission control for launching apps. Its products are **components**: Exo ID, Exo Key, Exo Deploy,
EXO Verify, Exo Analyze, Exo Rank, Exo Inform and the other services in the
[platform](./platform/index.mdx) and [stack](./stack/index.mdx) docs. People build **apps**, and
each app picks the components it needs. It is modular: no app uses everything. EXO is infrastructure, not
a general-purpose application generator and not a CMS. Its reusable core is schemas, access
controls, provider adapters, provisioning, deployment contracts, evidence, and documentation
([architecture](../../docs/architecture.md),
[ADR 0013](../../docs/adr/0013-exo-is-mission-control-apps-pick-components.md)).

Exo lives at [exo.now](https://exo.now). Its shared staging environment is
`https://staging.exo.now`, and every managed app stages under it at `staging.exo.now/{app}`.

## The product hierarchy [#the-product-hierarchy]

EXO is mission control for launching apps. Its components are its products. People build apps,
and each app picks its components.
Construct, one of those apps, publishes Surfaces, and Surfaces carry Cards.

| Term             | Meaning                                                                                                                                                                                                                          |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Organization** | Ownership and collaboration: a person or a team. Legacy Payload `tenants` records are migration input, not a synonym for anything here.                                                                                          |
| **Account**      | A walled-off space on EXO with its own users, data, secrets, environments and audit trail. Anything that needs walled-off money or data (for example Foundation's donor money) gets a separate account, not a separate platform. |
| **Component**    | One EXO product an app can pick, declared in the app's `project.yaml` as a top-level `components` list. Components are either core or specialty (see below).                                                                     |
| **App**          | What people build on EXO. It picks the components it needs and owns its persistent runtime state, privileged workflows and operating responsibility. The artwork shows an app as a rocket ship; the prose always says app.       |
| **Starter kit**  | A saved set of components that new apps start from. Each app still declares its own components.                                                                                                                                  |
| **Version**      | One iteration of an app. **Launch** means a version reaching Production; Staging is the launch pad and EXO Verify is the preflight.                                                                                              |
| **Surface**      | An interaction layer for users, published by Construct: a web surface (website), a mobile surface (PWA), a phone surface (iOS or Android app), or any combination. Surfaces are **published**, not launched.                     |
| **Card**         | A UI unit that sits on a Surface and gives it functionality. Cards inherit the Surface's identity and release boundary, and are published with it.                                                                               |
| **Page**         | A routable content unit inside a web Surface, owned by Construct. Exo sees Pages only as typed resources for grants, routing, deployment evidence, and aggregate policy.                                                         |
| **UI component** | A reusable implementation or UI unit inside an app's code, with no identity responsibility. Not the same thing as an EXO component.                                                                                              |

### Core components and specialty components [#core-components-and-specialty-components]

**Core components** are the ones most apps need:

* `source`: code and its history (Entire); recommended, but an app may opt out
* `identity`: sign-in for the app's users (Exo ID); an app with no users who sign in sets
  `signIn: false` and skips it
* `preflight`: checks before a launch (EXO Verify Level 1)
* `runtime`: where the app runs
* `secrets`: keys and credentials
* `evidence`: the record of what was checked and launched
* `data`: the app's database (Convex)

**Specialty components** are the ones only some apps need (not every app needs comms and support,
or model routing through Exo Rank):

* `comms`: email, SMS and messaging (Bird)
* `payments`
* `models`: AI model routing (Exo Rank)
* `support`: help for the app's users (Exo Ask, proposed)
* `analytics`: product analytics (Exo Analyze)
* `surfaces`: screens published through Construct

The control plane (Exo, driven through Hark, with Hermes as the open-source option; see the
[stack](./stack/control-plane.mdx)) is recommended in the same way as source control, and an app
may opt out of it too.

EXO Verify requires preflight, runtime, secrets and evidence, plus identity unless `signIn` is
false. Source is recommended, not required. Data is core because almost every app uses it, but an app without a database
is still valid. A **starter kit** is a saved set of components, so a family of similar apps can
start from the same picks.

### Surface, app, or account? [#surface-app-or-account]

* **A Surface in Construct** when it is mostly screens over Construct's components.
* **Its own app** when it has its own backend and data.
* **Its own account on EXO** only when it needs walled-off money or data, for example regulated
  money or a different owner.

Applied: Hadron is an app in the Interspace account. Foundation's donor money lives in its own
account on EXO, while its landing page stays a Surface in Construct. If Hadron is sold, each
customer gets their own account on EXO and their own Hadron app, the same pattern as Parlour's
Construct.

## Five responsibilities, kept separate [#five-responsibilities-kept-separate]

Exo is easiest to understand as five connected responsibilities rather than one large admin
panel. They are deliberately separate: a green build is not a deployment, a provider deployment is
not a review, and a completed issue is not a Production approval.

| Responsibility | Question it answers                                                    | Primary interface               | Source of truth                                         |
| -------------- | ---------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------- |
| Work           | What are we trying to change, and who owns the next action?            | Exo portfolio                   | Exo work item                                           |
| Product        | What app, account, environment, grant, and release state do we manage? | Exo control plane               | Convex records and versioned contracts                  |
| Source         | What exact code and agent context produced the result?                 | Entire                          | Commit, branch, session, checkpoint, and tag            |
| Runtime        | What is configured and running now?                                    | Railway or another OCI provider | Live provider state and secrets                         |
| Evidence       | Is the declared version healthy, reviewable, and approved?             | Exo portfolio                   | Provider observations, manifests, checks, and approvals |

## What Exo owns, and what it does not [#what-exo-owns-and-what-it-does-not]

* **Exo owns** app registration, accounts, organizations and access, environments, provider provisioning,
  deploy/release/rollback state, runtime-dependency metadata, policy and security controls, audit
  evidence, orchestration, health, and bounded usage and cost aggregates
  ([ADR 0003](../../docs/adr/0003-convex-first-control-plane.md)).
* **Construct owns** Page and website editing, Puck, publishing, Page authorization, and
  authenticated Surfaces.
* **Independent apps own** their domain records. Exo provisions and observes their resources and
  accepts only bounded operational observations. It does not centralize their prompts, content,
  user data, datasets, or financial ledgers.

## Platform services [#platform-services]

Exo provides its services as components that each app picks, so apps skip basic plumbing:

* **[Exo Key](./platform/exo-key.mdx)**: the single identity provider and OAuth client. Apps are
  relying parties.
* **[EXO Verify](./platform/exo-verify.mdx)**: code verification as a service, with a standard
  verify contract, sandboxed runs, live staging checks, and AI review.
* **[Exo Analyze](./platform/exo-analyze.mdx)**: Exo-owned product analytics, from visits through
  cost to serve.
* **[Exo Deploy](./platform/exo-deploy.mdx)**: one release path for every surface (web on Railway, mobile on Expo).
* **[Exo Rank](./platform/exo-router.mdx)** (code and URLs still say router): Exo's smart model
  routing, one metered AI egress. Apps never hold provider keys.
* **Exo Inform**: docs and READMEs that agents and people can read (`/llms.txt`, the docs MCP tool
  and `exo docs` CLI); offering it to every app is planned
  ([roadmap](../../roadmap/items/exo-inform.md)).

## How these docs are organized [#how-these-docs-are-organized]

<Cards>
  <Card title="Tenets" href="/docs/tenets" description="The four principles every decision is checked against." />

  <Card title="The stack" href="/docs/stack" description="Twelve dimensions: platform, boundary, replaceability, cost, and status." />

  <Card title="Cost and unit economics" href="/docs/stack/economics" description="Every cost figure recorded in the ADRs, in one place." />

  <Card title="Platform services" href="/docs/platform" description="The components apps pick: Exo Key, EXO Verify, Exo Analyze, Exo Deploy, Exo Rank and more." />

  <Card title="How work ships" href="/docs/shipping" description="Canary branch → Staging → Release Candidate → Production." />

  <Card title="Decision records" href="/docs/adr" description="The ADRs, rendered straight from docs/adr." />

  <Card title="Roadmap" href="/docs/roadmap" description="Rendered from roadmap/items." />
</Cards>

<Callout title="Conventions">
  Every statement here is drawn from the repository: `docs/`, `docs/adr/`, `roadmap/`, and the
  code. Each page links to its sources. Anything the repository does not record is marked
  **TBD**, not estimated. When a source document and these pages disagree, the source document
  (and the newest accepted ADR) wins, and the page should be fixed.
</Callout>


---

# Exo Analyze

https://exo.now/docs/platform/exo-analyze

> Exo-owned product analytics — one funnel from visit to margin, with PostHog as a swappable adapter.



Source: ADR 0006, accepted (owner-approved) on 2026-09-27. It is on `main` and not yet on
`staging`: [read it on main](https://github.com/Interspace-Ventures/exo/blob/main/docs/adr/0006-exo-analyze-product-analytics.md).
Roadmap: [product-analytics](../../../roadmap/items/product-analytics.md).

## Why [#why]

RevenueCat and Stripe only report payments. Without visits, sign-up conversion, and cost to serve,
Exo cannot compute per-App unit economics or margin. Letting each App wire its own analytics
platform would repeat the integration per App and scatter the data.

## Decision [#decision]

* **Ingestion and schema live in Exo's Convex backend**, partitioned per App and per environment.
  Apps send events to one Exo endpoint through a thin SDK (web and Expo).
* **Exo stores the funnel end to end:**

  ```text
  visits → sign-ups (Exo Key) → paid (RevenueCat / Stripe webhooks)
         → cost to serve (exoAiBrokerJobs + exo-verify + infra)
  ```

  That join gives per-App acquisition, conversion, revenue per user, and margin.
* **PostHog sits behind Exo as a swappable adapter** for dashboards and session replay. Plausible
  and GA4 remain possible adapters.
* **Apps never integrate an analytics platform directly.**

## Unit economics [#unit-economics]

* Today's event volume fits PostHog's free tier (\~1M events/month). The incremental cost is Convex
  storage and function usage for the event table, expected to be a few dollars per month.
* Each App's dashboard reports **cost per active user** and **gross margin per paying user**.
* Beyond the free tier, Exo can sample or aggregate before forwarding to the adapter, while keeping
  full-fidelity funnel counts in Convex.

## Alternatives considered [#alternatives-considered]

| Option                             | Why not                                                                                          |
| ---------------------------------- | ------------------------------------------------------------------------------------------------ |
| Per-App PostHog, Plausible, or GA4 | No cross-App funnel, no join to Exo Key or Exo Router cost, and vendor lock-in in every codebase |
| PostHog as system of record        | Ties the revenue and cost-to-serve joins to a vendor schema                                      |
| RevenueCat and Stripe only         | The status quo: payments without the top of the funnel or cost                                   |

## Consequences [#consequences]

* Exo ships and versions the analytics SDK, and Exo Deploy bakes it into the mobile template.
* Event schema changes are platform decisions, reviewed like any other Exo contract.
* Privacy and consent handling (cookie banners, data deletion) is centralized in Exo.

## Status [#status]

Accepted; planned. Event schema, SDK, and endpoint: **TBD**.


---

# Exo Deploy

https://exo.now/docs/platform/exo-deploy

> One release path for every surface. Web on Railway, mobile on Expo (config, signing, TestFlight and store listings, Exo Key native sign-in, Exo Analyze built in).



Source: ADR 0007, accepted (owner-approved) on 2026-09-27. It is on `main` and not yet on
`staging`: [read it on main](https://github.com/Interspace-Ventures/exo/blob/main/docs/adr/0007-exo-deploy-mobile-release-toolkit.md).
Roadmap: [mobile-delivery-pipeline](../../../roadmap/items/mobile-delivery-pipeline.md).

**Renamed 2026-10-02:** Exo Deploy became **Exo Deploy** and now covers every surface (web on
Railway, mobile on Expo, pages and embedded app surfaces). ADR 0007 keeps its original filename.
RevenueCat moved to [Payments](../stack/payments.mdx).

## Why [#why]

Shipping to iOS and Android involves the same setup every time: app config, signing, store
listings, TestFlight, native sign-in redirects, and analytics. Exo's stack is TypeScript, React,
Convex, and Exo Key, and most code is written by agents. Universe already ships on Expo and is the
reference.

## Decision [#decision]

* **Expo is the standard mobile framework** for Exo Apps. Expo is an MIT-licensed framework on
  React Native.
* **EAS is optional and swappable** for build, submit, and OTA updates. Start on EAS Free, move to
  Starter when several Apps ship, and run an own Mac builder (`eas build --local` or fastlane) only
  at high volume.
* **Exo Deploy owns the release template:** app config, signing, TestFlight and store listings,
  Exo Key sign-in via a native redirect scheme (for example `universe://auth/callback`), and the
  Exo Analyze SDK built in.
* **The PWA-required-for-mobile gate** in the repository contract stays.

## Unit economics [#unit-economics]

| Stage                   | Fixed cost             | Marginal cost                                 |
| ----------------------- | ---------------------- | --------------------------------------------- |
| Apple Developer Program | $99/year (required)    | —                                             |
| EAS Free                | $0                     | Capped at 15 + 15 builds/month, OTA to 1K MAU |
| EAS Starter             | $19/month              | Usage beyond the $45 build credit             |
| Own Mac builder         | Hardware or hosted Mac | \~$0 per build; pays off only at high volume  |

At current volume the toolkit costs **\~$8/month** (Apple fee amortized). Move up a tier only when
build counts or OTA MAU exceed the current tier.

## Alternatives considered [#alternatives-considered]

| Option                         | Why not                                                         |
| ------------------------------ | --------------------------------------------------------------- |
| Bare React Native              | Same runtime without Expo's build, config, and OTA tooling      |
| Flutter                        | Dart rewrite, no official Convex client, OTA only via Shorebird |
| Capacitor / Ionic              | Webview feel                                                    |
| Kotlin / Compose Multiplatform | Kotlin rewrite                                                  |
| Lynx (ByteDance)               | Too young                                                       |
| Native Swift + Kotlin          | Two codebases                                                   |

## Consequences [#consequences]

* New mobile Apps start from the Exo Deploy template.
* EAS is a service choice, not a dependency: the template keeps a local-build path.
* Each App registers its native redirect scheme with Exo Key during onboarding.

## Status [#status]

Accepted; planned. Template location: **TBD**.


---

# Exo Key

https://exo.now/docs/platform/exo-key

> Exo is the only OAuth client and identity provider; Apps are relying parties that receive app-scoped RS256 tokens.



Sources: [ADR 0004](../../../docs/adr/0004-exo-id-central-identity-provider.md) and the
[Exo Key integration guide](../../../docs/exo-id.md). The guide is the integration reference; this
page summarizes it.

> **Naming:** Exo Key was called Exo ID until 2026-09-29. Technical identifiers, including the
> `/exo-id` endpoint paths, the `exo-id` ADR filename and the code, keep the old name until the
> code rename lands.

**Exo is the only OAuth client** registered with Google, GitHub, and (later) Apple. An App never
mounts Convex Auth or OAuth provider components, never holds `AUTH_PRIVATE_KEY`, `AUTH_JWKS`, or
provider client secrets, and never touches a provider console. &#x2A;*Onboarding an App means adding it
to `convex/lib/exoIdApps.ts`**, and that is the whole provider-side setup.

## Endpoints (staging) [#endpoints-staging]

| Purpose             | URL                                                                          |
| ------------------- | ---------------------------------------------------------------------------- |
| Issuer (`iss`)      | `https://frugal-eel-930.convex.site/exo-id`                                  |
| Authorize (browser) | `https://staging.exo.now/id/authorize`                                       |
| Token               | `POST https://frugal-eel-930.convex.site/exo-id/v1/token`                    |
| Revoke              | `POST https://frugal-eel-930.convex.site/exo-id/v1/revoke`                   |
| JWKS                | `https://frugal-eel-930.convex.site/exo-id/.well-known/jwks.json`            |
| Discovery           | `https://frugal-eel-930.convex.site/exo-id/.well-known/openid-configuration` |

Read the endpoints from the discovery document instead of hard-coding them. Production endpoints:
**TBD** (not recorded in the guide).

## Flow: authorization code + PKCE (S256) [#flow-authorization-code--pkce-s256]

<Steps>
  <Step>
    The App generates a `code_verifier`, its S256 `code_challenge`, and an opaque `state`.
  </Step>

  <Step>
    It redirects to `/id/authorize?app=…&redirect_uri=…&state=…&code_challenge=…&code_challenge_method=S256`.
    Exo validates the `app` and `redirect_uri` pair server-side. An invalid pair renders an error page
    and never redirects.
  </Step>

  <Step>
    After sign-in, Exo mints a one-time code (valid for 60 seconds, single use, stored only as a
    SHA-256 hash) and redirects back with `code` and `state`.
  </Step>

  <Step>
    The App checks `state` and exchanges the code at the token endpoint. It refreshes before
    `expires_in`; refresh tokens rotate. Replaying the previous token within 30 seconds returns the
    same new token; any other reuse of a rotated token signs the whole session out.
  </Step>

  <Step>
    Sign-out posts the refresh token to the revoke endpoint, which always returns 200.
  </Step>
</Steps>

## Tokens [#tokens]

* **Access token:** RS256 JWT, 900 seconds. Claims: `iss`, `aud` (the App name), `sub` (the Exo
  user id, stable across Apps), `email`, `email_verified`, `name`, `iat`, and `exp`.
* **Refresh token:** opaque, 30 days, rotating.
* **Errors:** HTTP 400 with `invalid_request`, `invalid_grant`, or `unsupported_grant_type`. Any
  redemption attempt consumes the code. Too many token requests return HTTP 429 with
  `Retry-After`.

Relying Apps verify tokens in their own Convex backend with a `customJwt` provider that points at
the Exo Key issuer and JWKS. `ctx.auth.getUserIdentity()` then returns the Exo user id, email, and
name.

## Registered Apps [#registered-apps]

| App           | Redirect URIs (summary)                                                 |
| ------------- | ----------------------------------------------------------------------- |
| `observatory` | staging subpath, `observatory.bio`                                      |
| `construct`   | `staging.construct.page`, `construct.page` (server-side cookie session) |
| `rampart`     | staging subpath, `rampart.fit`                                          |
| `dropship`    | staging subpath, `dropship.wtf`                                         |
| `universe`    | staging subpath, native `universe://auth/callback`                      |

Redirect URIs match **exactly**, with no prefix, wildcard, query, or trailing-slash tolerance.
Origins control CORS on the token and revoke endpoints.

## Boundaries [#boundaries]

* **Authorization stays in the App (v1).** Exo Key proves who the person is. Allowlists, roles, and
  tenant grants are enforced by the App on every request.
* **Separate signing keys.** Exo Key signs with `EXO_ID_PRIVATE_KEY` and `EXO_ID_JWKS`, independent
  of Exo's own session keys. Rotation publishes the new key alongside the old one, waits at least
  the JWKS cache time (5 minutes) plus the access-token lifetime, then drops the old key.
* **Hard dependency.** Exo's Convex deployment is on the sign-in path for the whole portfolio.
  Its availability and key rotation are platform responsibilities.

## Cost notes [#cost-notes]

No license fee (Convex Auth is open source). Marginal Convex usage: **TBD**. The decision to leave
Clerk cited its per-MAU pricing
([ADR 0003](../../../docs/adr/0003-convex-auth-v2-identity-plane.md)).

## Status [#status]

Live on staging for the five registered Apps. Clerk retirement is done: EXO carries no Clerk code,
package, or variable, and its own sign-in and staging gateway run on Exo Key
([ADR 0012](../../../docs/adr/0012-clerk-removed-convex-auth-everywhere.md),
[roadmap: convex-auth](../../../roadmap/items/convex-auth.md)). Centralized grants are future
work; staging tester grants are declared in `EXO_STAGING_GRANTS`.


---

# Exo Router

https://exo.now/docs/platform/exo-router

> Exo's smart model routing — one metered AI egress where an assessor scores each task, the utility function weighs capability against cost, and the Convex AI Gateway executes. Apps never hold provider keys.



Sources: [AI routing](../../../docs/ai-routing.md), `convex/aiBroker.ts`, `convex/ai/*`,
[services/arch-router](../../../services/arch-router/README.md), and the roadmap item
[ai-gateway-jev-routing](../../../roadmap/items/ai-gateway-jev-routing.md).

**Exo Router** is the name for Exo's own smart model routing: the whole path from an App's prompt
to a metered model call. Older code and docs call the HTTP half the "broker" (`convex/aiBroker.ts`,
`EXO_AI_BROKER_TOKEN`, `exoAiBrokerJobs`); those technical names are unchanged.

## Two pieces [#two-pieces]

### The endpoint (`POST /v1/ai/generate`) [#the-endpoint-post-v1aigenerate]

An HTTP action on the Exo Convex deployment. Construct, and later every Exo App, posts generation
jobs here instead of holding provider credentials. Exo Router owns model routing, budgets, and
provider keys, and records every job for unit economics.

* **Auth:** an Exo Key token (`exo_tk_…`) issued per tenant, optionally bound to one app, with
  scopes (`ai`, `usage:read`) and a 90-day default expiry. Only the token's SHA-256 hash is stored.
  The legacy shared token (`EXO_AI_BROKER_TOKEN`) still works and bills to the `legacy-broker`
  tenant. Phase 1 of ADR 0008 ([proposal](https://github.com/Interspace-Ventures/exo/pull/145)); see
  [Metering and usage](#metering-and-usage).
* **Job types:** `page.draft` only, today.
* **Provider:** with `EXO_AI_MODEL` set, jobs go to the Convex AI Gateway, which holds the provider
  credentials. Without it, a deterministic local composer (`exo-local-composer`) serves the
  request so the loop works end to end.
* **Failure policy:** gateway failures fail the job loudly. A silent fallback would hide a spend or
  configuration problem.
* **Metering:** every job writes `exoAiBrokerJobs` (job type, status, provider, model, source
  count, duration, input and output tokens), plus tenant, vendor cost, markup, and price.

### The router (`convex/ai/router.ts`) [#the-router-convexairouterts]

```text
surface (help desk / chat)
        │
        ▼
convex/ai/router.ts  →  assessor (Arch-Router or Jev)
        │  utility = intelligence^w / blended cost
        ▼
convex/ai/chat.ts  →  Convex AI Gateway (OpenAI-compatible, service-token auth)
        │
        ▼
chosen model from aiModelPool
```

* **Fast path:** jobs that already know their category and difficulty (every broker job type:
  `page.draft` is writing, `classify.choice` extraction, `design.review` visual-review) skip the
  assessor; the value score still picks from the live pool. Decisions are memoised for 10 minutes
  per pool version, and every call still writes its own `aiRoutingLog` row. Routing adds a few
  milliseconds before the model call starts.
* **Assessors** (`EXO_AI_ASSESSOR`, for prompts without a caller category): `jev` = TypeSafe's Jev
  through the Convex AI Gateway Decisions API (same gateway token, 600 ms hard timeout), `arch` =
  self-hosted Arch-Router (`ARCH_ROUTER_URL`), `none` = pure utility at difficulty weight 0.5 with
  every decision flagged `fallback: true`. Unset defaults to `arch` when `ARCH_ROUTER_URL` is set.
* **Guardrail:** `utility = intelligence^w / blendedCost^(1-w)` per pool model, where `w` is the
  assessed difficulty (0–1) and `blendedCost` blends input and output prices 3:1. The assessor's choice gets a confidence-scaled bonus
  (`1 + 0.25 × confidence`), utility keeps it honest against cost, and the highest score wins.
* **Hot-swappable pool:** `aiModelPool` rows carry intelligence (0–100) and per-million-token
  costs. Edits take effect on the next request, with no redeploy.
* **Audit:** every decision lands in `aiRoutingLog`: surface, task, chosen model, utility,
  assessor provider, raw assessor response, the fallback flag, decision latency, and memo hits.
* **Surfaces today:** apps reach the router only through the broker endpoint
  `POST /v1/ai/generate` (decisions log as `surface: "broker:<jobType>"`). `ai/chat:chat` (routed)
  and `ai/chat:chatWithModel` (pinned) are internal actions for trusted server-side Exo code; they
  are not callable from clients.
* **Endpoint jobs:** with `EXO_AI_MODEL=auto` (or a per-request `routing: "auto"`), `/v1/ai/generate`
  jobs go through the router, and every job records its tier, routing-log id, and estimated cost
  in `exoAiBrokerJobs`. Unset `EXO_AI_MODEL` keeps the $0 local composer.

## What is Arch-Router? [#what-is-arch-router]

**Arch-Router** is a small open-weights model from Katanemo (Arch-Router-1.5B, 1.5 billion
parameters) built for one job: reading a prompt and choosing which route, and so which model,
should handle it. It does not answer the prompt itself.

In Exo Router it is the difficulty assessor. Exo runs it on its own Railway service from
`services/arch-router/`, served by llama.cpp as an OpenAI-compatible endpoint
(`POST /v1/chat/completions`). It is CPU-only, runs comfortably in \~1 GB RAM, and needs no
external account, so there is no per-call cost. The router sends it the task plus one route per
pool model and it answers `{"route": "<model>"}`. Arch-Router does not score difficulty directly,
so on `staging` the router derives `w` from the model it reaches for (that model's intelligence ÷
100, in `convex/ai/assessors.ts`), then applies the utility guardrail before picking a model. It replaced the
dormant Jev integration when TypeSafe closed signups.

## Model benchmarks [#model-benchmarks]

The catalog names **Artificial Analysis** as the source of independent intelligence, speed, and
price benchmarks for scoring models. The code does not use it yet: the five seeded pool models
(GPT-5 mini, Claude Haiku 4.5, Gemini 2.5 Flash, Claude Sonnet 4.5, GPT-5) carry intelligence
estimates of 72–94 that `seedPool` describes as "ours and meant to be calibrated from aiRoutingLog
over time". How and when Artificial Analysis scores feed `aiModelPool`: **TBD**.

## Privacy boundary [#privacy-boundary]

Exo may receive aggregate quality, latency, token, cost, reliability, and rollback findings. It
must not receive prompts, responses, conversation content, user identifiers, provider keys, or
application source ([Convex framework](../../../docs/convex-framework.md)).

## Metering and usage [#metering-and-usage]

Every AI call is attributed to the tenant that owns the calling token. If the token is bound to
an app, that app is recorded and the app the request claims is ignored.

| Field              | Meaning                                                                   |
| ------------------ | ------------------------------------------------------------------------- |
| `vendorCostMicros` | What Exo pays upstream: the gateway-reported cost, or pool price × tokens |
| `markupBps`        | The tenant's markup in basis points (default 2000 = 20%)                  |
| `priceMicros`      | What the tenant is charged: vendor cost × (1 + markup), rounded up        |

* **Ledger:** each metered call appends one `usageLedger` row (tenant, dimension `ai`, app,
  model, tokens, cost, price). A running monthly total per tenant makes spend checks a single read.
* **Usage API:** `GET /v1/usage?month=YYYY-MM` with a `usage:read` token returns the tenant's
  events, tokens, vendor cost and price for the month, broken down by app and model.
* **Spend cap:** a tenant can have a hard monthly AI cap. Once spend reaches it, calls return
  HTTP 402 (`spend_cap_exceeded`) until the cap is raised or the month rolls over.
* **Issuing tokens (phase 1):** an Exo operator mints and revokes tokens with the deployment key
  (`npx convex run exoKeyTokens:mint`, `exoKeyTokens:revoke`, `exoKeyTokens:setBilling`). The
  plaintext token is shown once. Self-serve issuing comes with the Exo Key account UI.

## Cost notes [#cost-notes]

* The assessor has no per-call cost.
* Vendor cost and price are recorded per call and per tenant (see above). Fixed costs (Convex
  plan, Arch-Router hosting) are not yet allocated per tenant: **TBD**.

## Status [#status]

* **Live:** the `/v1/ai/generate` endpoint, used by Construct.
* **Built:** router, assessors, routing log, and model pool.
* **Live on staging:** Exo Key tokens, per-tenant metering, the usage ledger, `/v1/usage`, and
  monthly spend caps (ADR 0008 phase 1).
* **TBD:** Artificial Analysis benchmarks in the model pool.


---

# EXO Verify

https://exo.now/docs/platform/exo-verify

> Code verification as a platform service — a verify contract, sandboxed runs, live staging checks, and an AI review pass.



Source: ADR 0005, accepted (owner-approved) on 2026-09-27. It is on `main` and not yet on
`staging`: [read it on main](https://github.com/Interspace-Ventures/exo/blob/main/docs/adr/0005-exo-verify-verification-service.md).

## Why [#why]

Each App carries ad-hoc unit tests, and staging failures slip through. The motivating example: the
AI router passed all unit tests and review but fell back 100% of the time on staging, because a
fix never merged. Only a live behavioral check against the deployed environment catches that class
of failure.

Two constraints shape the design:

* The owner is moving from GitHub to Entire, so GitHub Actions and GitHub-app bots are out.
* Most commits are made by agents (\~300/month across the portfolio), so per-seat pricing is the
  wrong unit.

## Four layers [#four-layers]

<Steps>
  <Step>
    ### Standard verify contract [#standard-verify-contract]

    Every repository declares `verify` commands (`typecheck`, `lint`, `test`, `smoke`) in
    `project.yaml`. Exo runs what is declared, and a missing command is a visible gap, not a silent
    pass.
  </Step>

  <Step>
    ### Sandboxed runs on Railway workers [#sandboxed-runs-on-railway-workers]

    The preferred trigger is an Entire webhook, if Entire exposes one. The fallbacks are an
    `exo verify` CLI that agents run before pushing, then Exo polling. Inside the sandbox, an agent may
    also write and run targeted tests for the change.
  </Step>

  <Step>
    ### Post-deploy live checks against staging [#post-deploy-live-checks-against-staging]

    Health, auth (Exo Key JWKS and OAuth callbacks), and two or three declared user journeys per App,
    driven by Playwright or a browser agent.
  </Step>

  <Step>
    ### AI review pass [#ai-review-pass]

    An open-source engine (in the style of PR-Agent or Kodus) or Exo's own prompts. Every model call
    goes through [Exo Router](./exo-router.mdx), so cost is logged per App and the router keeps easy
    diffs on cheap models.
  </Step>
</Steps>

Results are stored in Convex as release gates, attached to the Entire trail, and gate the
staging → production promotion.

## Unit economics [#unit-economics]

| Option               | Pricing                                                                                     | Est. monthly at \~300 agent commits |
| -------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------- |
| **EXO Verify (own)** | \~$0.007 compute per 5-min 2 vCPU / 2 GB Railway run + $0.01–0.10 model tokens per run      | **\~$6–30**                         |
| Greptile Pro         | $30/seat/mo, 50 credits/seat, $1 per extra credit; non-GitHub/GitLab forges Enterprise-only | \~$300–900                          |
| CodeRabbit           | $24–30/user; CLI free at 3 reviews/hr on local git                                          | Per seat; CLI usable as an interim  |
| Graphite             | $40/user                                                                                    | Per seat, GitHub-bound              |

Cost per verified commit is roughly **$0.02–0.10**, recorded per App so that it shows up in each
App's cost to serve.

## Consequences [#consequences]

* Every App gains a `verify` block in `project.yaml`, and onboarding includes declaring its two or
  three golden journeys.
* Third-party review engines start out better tuned. Exo tracks catch rate against false positives per
  layer and will revisit buying the review layer if the gap persists.
* Live checks need a staging-only sign-in path for bots (seeded Exo Key test identities), because
  browser agents cannot pass Google's bot detection.
* **Open question:** does Entire expose webhooks or a checks API?

## Levels [#levels]

EXO Verify groups its checks into levels. **Level 1** is the baseline every managed App runs (it was
called the base test tier until 2026-10-12). Higher levels are reserved for later and not built.

* **Level 1 checks**: `exo-verify contract` (managed-repository contract), `exo-verify static` (the
  repo's own typecheck, lint, and UI-system scripts), `exo-verify playtest` (Playwright on phone and
  desktop: page loads, console errors, accessibility, overflow, images, screenshots), and
  `exo-verify review --level 1` (PR-Agent on the diff). `exo-verify all` runs them in order.
* **Config**: `exo-verify.config.json` in the App repo, with `"level": 1` (the default). Reports go
  to `.exo-verify/reports/`.
* **Any runner**: the same command runs on a laptop, in the pre-push hook, in a Railway build, in
  Entire, or in a thin GitHub Actions wrapper. No source host is the source of truth.
* **Package**: `@interspace/exo-verify` (one CLI for the contract and Level 1). Apps vendor the
  tarball from `pnpm verify:pack`. The vendored single file from `exo-verify vendor` has only the
  contract commands, because Level 1 needs Playwright and axe.

## Status [#status]

Built in part on `staging` (2026-10-01). What runs today:

* **Contract + CLI** (`packages/exo-verify`, zero dependencies): a `verify:` block in `project.yaml`
  (typecheck, lint, a list of test commands, build, smoke checks per env, `mode: enforce|report`).
  `exo-verify run [--gate] [--json <path>]` and `exo-verify smoke --env staging|production`. A missing
  command counts as a **gap**, never a pass. Only enforce mode exits non-zero.
* **Results**: `POST /v1/verify/runs` (service token) stores rows in the `verifyRuns` Convex table.
  `GET /v1/verify/status` returns the latest status for each app.
* **Build gates**: Exo's and Observatory's Railway builds run `exo-verify run --gate` in enforce mode. Construct
  (12 existing lint errors) and Rampart (tests assume a root base path) run it in report mode.
  Apps use a vendored copy (`scripts/exo-verify.mjs`) from `exo-verify vendor`.
* **Live checks**: a Convex cron runs every 15 minutes. It makes HTTP GETs to each app's declared
  smoke routes on staging and production. For drift, it records whether production's
  `/api/health` commit differs from staging's and since when. It also checks production domains
  over DNS-over-HTTPS.
* **Pre-push hook**: `exo-verify install` turns on a versioned `.githooks/` for one worktree. The
  hook runs the gate and then hands off to the existing `.git/hooks` (Entire). To skip once, set
  `EXO_VERIFY_SKIP=1`. The skip is always written to a local log. It is also posted as a `skip` run
  (best effort) when `EXO_VERIFY_URL` and `EXO_VERIFY_TOKEN` are set.
* **AI review**: `exo-verify review --base <ref>` runs PR-Agent (MIT, pinned `pr-agent==0.46.0`,
  plain-diff mode) through Exo Router's OpenAI-compatible `POST /v1/chat/completions`, which logs
  per-app tokens and cost in `exoAiBrokerJobs`. **Off by default**: the Convex AI gateway is not
  enabled for the team, so the endpoint returns 503 `exo_router_unavailable`, records the failed
  job, and the CLI prints `review skipped: <reason>`.

Measured cost per run:

| Check                                                                                                      | Time measured                                                                                   | Cost                                                                                    |
| ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| Exo Railway build gate (typecheck 15.4s, lint 12.7s, Vitest 14.0s)                                         | 42 s added to each deploy                                                                       | No new service. If billed at Railway's 2 vCPU / 2 GB list rate: about $0.001 per deploy |
| App gates on Railway (Observatory: typecheck 8.7s, lint 9.7s, tests 1.1s) and locally (Construct, Rampart) | 20 s on Railway for Observatory; 7–9 s locally                                                  | About $0.0005 per deploy at the same rates                                              |
| Live sweep (4 apps, both envs)                                                                             | One Convex action plus about 40 GETs (smoke, health, and DNS-over-HTTPS; Exo alone declares 14) | About 96 sweeps a day, well inside the Convex plan's function-call quota                |
| AI review                                                                                                  | Not measured: the gateway is not enabled                                                        | Recorded per app once it is                                                             |

Not built yet: browser journeys (Playwright, Apache-2.0), Entire triggers, and sandboxed Railway
workers. Vitest (MIT) is the test runner for Exo itself.


---

# Platform services

https://exo.now/docs/platform

> EXO's components. Each app picks the ones it needs, so apps skip basic plumbing.



These services are EXO's components. EXO is mission control for launching apps: people build apps
on EXO, and each app picks the components it needs. It is modular, and no app uses everything. An
app declares its components in `project.yaml` as a top-level `components` list ([ADR 0013](../../../docs/adr/0013-exo-is-mission-control-apps-pick-components.md)),
and EXO Verify Level 1 checks that each declared component is wired. Core components are the
ones most apps need; specialty components are the ones only some apps need
([overview](../index.mdx#core-components-and-specialty-components)). A **starter kit** is a saved set
of components that new apps can start from. Each service follows the same
pattern: Exo owns the platform relationship, keys, and schema, and apps call one Exo contract.
Swapping the platform behind a service is an Exo change, not an app change
([tenet 3](../tenets.mdx#3-integrated-not-locked-in)).

| Service                                     | Dimension              | Decision                                                               | Status                             | Platform behind it                                                        |
| ------------------------------------------- | ---------------------- | ---------------------------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------- |
| [Exo Key](./exo-key.mdx)                    | Identity               | [ADR 0004](../../../docs/adr/0004-exo-id-central-identity-provider.md) | Live on staging                    | Convex Auth v2                                                            |
| [EXO Verify](./exo-verify.mdx)              | Continuous integration | ADR 0005 (on `main`)                                                   | Accepted; not built                | Vitest, Playwright, Railway workers, open-source review engine            |
| [Exo Analyze](./exo-analyze.mdx)            | Analytics              | ADR 0006 (on `main`)                                                   | Accepted; planned                  | PostHog adapter                                                           |
| [Exo Deploy](./exo-deploy.mdx)              | Deploy                 | ADR 0007 (on `main`)                                                   | Web live; mobile accepted, planned | Railway; Expo; EAS optional                                               |
| [Exo Rank](./exo-router.mdx) (code: router) | AI                     | [AI routing](../../../docs/ai-routing.md)                              | Endpoint live; routing built       | Convex AI Gateway, Arch-Router; Artificial Analysis TBD; TypeSafe dormant |

## How they join up [#how-they-join-up]

The services are designed to compose into one per-App cost-to-serve record:

* **Exo Key** supplies the sign-up step of the Exo Analyze funnel.
* **Exo Rank** records every model call in `exoAiBrokerJobs`.
* **EXO Verify** sends its AI review calls through Exo Rank and records its cost per App.
* **Exo Deploy** bakes the Exo Analyze SDK and Exo Key native sign-in into the mobile template.
* **Exo Analyze** joins visits, sign-ups, payments, and cost to serve into per-App unit economics.

See [cost and unit economics](../stack/economics.mdx) for which of these joins exist today.


---

# Roadmap

https://exo.now/docs/roadmap

> What Exo is building now, next, and later, rendered from roadmap/items.



The roadmap lives in the repository as one Markdown file per item in
[`roadmap/items/`](../../roadmap/README.md). `scripts/build-roadmap.mjs` compiles those files into
`roadmap/roadmap.json`, and the board below renders that file, so this page never restates roadmap
content by hand. Items link to their Entire trail for execution evidence.

To change the roadmap, edit or add an item file, run `node scripts/build-roadmap.mjs` (it fails
loudly on invalid fields), and commit both the Markdown and the regenerated `roadmap.json`, as
described in the [roadmap README](../../roadmap/README.md).

<RoadmapBoard />

<Callout title="Staging vs. main" type="info">
  This board reflects the `staging` copy of `roadmap/`. The `main` branch also carries an
  `exo-verify` item and updated items that accompany ADRs 0005–0007. They will appear here once
  the branches are reconciled.
</Callout>


---

# Approval boundaries

https://exo.now/docs/shipping/approvals

> The consequential actions that never follow implicitly from a validated commit, a green check, or a merged review.



Source: [How Exo builds and ships](../../../docs/build-and-deploy.md#approval-boundaries).

The following **never** follow implicitly from a validated commit, staging integration, a green
check, a merged change review, a completed Exo item, or a `project.yaml` declaration:

* creating or replacing a Release Candidate;
* Production promotion or a public release;
* domain or DNS changes;
* adding or upgrading a paid service, or changing billing controls;
* database or persistent-data migration;
* credential creation, rotation, or provider-account linking; and
* external email or other communication.

These boundaries are deliberate. Routine code should move quickly to private Staging, and material
business and production actions should stay unmistakably human decisions. This is the
[fourth tenet](../tenets.mdx#4-automated-not-unsupervised) in practice.

## Related gates [#related-gates]

| Gate                                   | What it checks                                                                                                           | Source                                                      |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
| projects.dev credential and spend gate | Environment match, credential inventory (names only), recent spend, a configured global spend limit that is not exceeded | [Repository contract](../../../docs/repository-contract.md) |
| `pnpm check:ui-system`                 | Structured Liquidity foundation and visual vocabulary                                                                    | [Design standard](../../../docs/design-standard.md)         |
| Staging evidence                       | Exact commit, `/api/health`, access boundary, environment label, browser pass                                            | [Environments](./environments.mdx)                          |


---

# Environments

https://exo.now/docs/shipping/environments

> Preview, Staging, Release Candidate, and Production — one purpose, one artifact, and one approval boundary each.



Source: [managed environment lifecycle](../../../docs/environment-lifecycle.md). These are
deployment stages, not product lifecycle labels. A project can be in private beta while running a
production environment, and setting `project.lifecycle` to `stable` never authorizes a deployment.

## Canonical names [#canonical-names]

| Stage             | Persistent   | Default Git source                 | Release channel |
| ----------------- | ------------ | ---------------------------------- | --------------- |
| Preview           | No, optional | Remote-canary or pull-request head | Internal        |
| Staging           | Yes          | `staging`                          | Internal        |
| Release Candidate | Yes          | `release`                          | Candidate       |
| Production        | Yes          | `main`                             | Stable          |

Human-readable names follow the pattern `{Project} · Staging`. Provider names and generated URLs
are separate operational metadata.

## Preview (optional) [#preview-optional]

An ephemeral, server-backed environment for the exact pushed canary or pull-request head. It
inherits staging configuration, uses isolated data when mutations are possible, and is removed
when the pull request closes. It must never inherit production customer data or credentials.

## Staging [#staging]

The persistent integration environment, showing the combined state of upcoming work.

* **Source:** `staging`. Agents never develop directly on it; they integrate validated commits from
  canary branches.
* **Deployment:** every update automatically deploys after the projects.dev credential and spend
  gate passes. No owner approval sits between the branch update and the deployment.
* **Access:** private by default, routed by the [staging topology](./staging.mdx).
* **Optional services:** integrations such as outbound email may stay disabled when
  staging-specific credentials are absent. Health and read-only routes must keep working.

## Release Candidate [#release-candidate]

The production-like environment for the exact version proposed for the next release. It accepts
fixes only after the candidate is cut, promotes the same verified build onward, and applies to any
release worth freezing, including patches. Creating or replacing it needs explicit owner
direction.

## Production [#production]

The live environment. `stable` is its release channel label, not a name for the environment. It
deploys only the approved Release Candidate artifact after explicit owner approval on the
`release` → `main` pull request.

## Promotion rules [#promotion-rules]

1. No stage implies approval for the next stage.
2. The deployed commit, build checksum, configuration class, and approval evidence are recorded at
   every promotion.
3. Secrets and persistent data are isolated by stage. Production values never flow backward.
4. Rollback promotes a previously verified artifact or reverts through the normal approval path.
5. Provider state is observed separately from `project.yaml`. Drift does not rewrite the contract.
6. A provider capability is not configured until its environment, URL, access boundary, and
   lifecycle behavior are verified live.
7. Every deployment needs fresh projects.dev evidence: credential inventory checked, billing
   visible, a spend limit configured, and no limit exceeded.
8. Staging integration is not an approval gate. Domain, billing, destructive data, credential, and
   production migration actions keep their own approvals.
9. Ordinary staging code delivery is automatic. Changing the provider binding, credentials,
   domain, billing configuration, or persistent data is a separately authorized mutation.
10. Apps stage under `staging.exo.now` and Construct Pages under `staging.construct.page`. An App's
    production domain never owns its staging hostname.


---

# How work ships

https://exo.now/docs/shipping

> The one promotion path every Exo-managed App uses — canary branch → Staging → Release Candidate → Production.



This section summarizes the canonical operating guide,
[How Exo builds and ships](../../../docs/build-and-deploy.md). That guide stays authoritative, and
specialist contracts must not describe a different path.

```text
prompt → validated prompt-linked commit → Staging → Release Candidate → Production (Stable)
             remote canary └→ optional PR Preview ┘
```

## The path in one minute [#the-path-in-one-minute]

| Stage             | Plain-language meaning                                           | Git source                                      | Who moves it forward                        | Required evidence                                                     |
| ----------------- | ---------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------- |
| Plan              | Decide what outcome matters and who owns the next action.        | Exo portfolio item                              | Product owner or operator                   | Scope, owner, priority, and acceptance criteria                       |
| Build             | Make one coherent change without disturbing shared environments. | Short-lived remote canary branch from `staging` | Developer or agent                          | Pushed prompt-linked commit and relevant checks                       |
| Staging           | Show the integrated change in a private, realistic environment.  | `staging`                                       | Automatic after validation                  | Deployed commit, passing gate, health, access, and browser review     |
| Release Candidate | Freeze the exact version proposed for release.                   | `release`                                       | Owner explicitly directs candidate creation | Reviewed staging evidence, manifest, migration and rollback readiness |
| Production        | Put the approved candidate in front of its intended audience.    | `main`, channel `stable`                        | Owner explicitly approves the release       | Identical artifact, approval record, health, and live behavior        |

Preview is optional and temporary. It never replaces Staging or shortens the release path. A local
checkout is an implementation tool, never a review surface.

## Every target, one path [#every-target-one-path]

Every environment has one purpose, one artifact, and an unmistakable human approval boundary.
Responsive websites, PWAs, and iPhone apps share one versioned delivery manifest, release
identity, approval model, and evidence contract, while keeping platform-specific configuration
([cross-platform delivery](../../../docs/platform-delivery.md)).

* **Web** runs this path on [Railway](../stack/deploy.mdx). The Staging → Release Candidate →
  Production path is live for web.
* **Mobile** apps follow the same path through Exo Deploy on Expo, from TestFlight to the app
  stores ([Deploy](../stack/deploy.mdx)). Exo Deploy is accepted (ADR 0007) and planned.
* **Delivery targets are typed.** Shared release identity and evidence live at the Exo layer, and
  provider specifics stay inside target adapters.
* **Drift never rewrites the contract.** Provider observations report whether the live target
  agrees with the declared manifest
  ([environment lifecycle](../../../docs/environment-lifecycle.md)).

<Steps>
  <Step>
    ### Contributor loop [#contributor-loop]

    1. Start from the latest `staging`, confirm `entire status`, and create a short-lived remote
       canary branch.
    2. Make one coherent, prompt-linked change, and push checkpoints promptly.
    3. Run `pnpm check` while working: TypeScript, lint with the UI-system gates, and
       repository-safe integration tests.
    4. Run `pnpm verify` before integration: it repeats the checks and creates the production build.
    5. Open a change review, keep the Entire session context, and integrate into `staging`. Ordinary
       staging integration needs no owner approval.
    6. Verify the staging deployment: exact commit, deployment result, `/api/health`, private access
       boundary, environment label, and browser behavior.
  </Step>

  <Step>
    ### Release loop [#release-loop]

    1. The owner reviews the staging outcome and explicitly directs creation of the Release
       Candidate.
    2. Freeze the reviewed commit on `release` and promote the identical artifact.
    3. Verify migrations, rollback, release notes, health, authorization, and user-visible behavior.
    4. Present the [approval packet](./releases.mdx#approval-packet).
    5. Only explicit approval of the `release` → `main` release authorizes Production.
    6. Verify the deployed commit or checksum, health, and live behavior before marking the release
       complete.
  </Step>
</Steps>

## Commands by intent [#commands-by-intent]

| Intent                                 | Command                   | Changes external state?                         |
| -------------------------------------- | ------------------------- | ----------------------------------------------- |
| Run the app locally                    | `pnpm dev`                | No provider change                              |
| Check an implementation                | `pnpm check`              | No                                              |
| Verify an integration candidate        | `pnpm verify`             | No                                              |
| Inspect credential and spend readiness | `pnpm delivery:gate`      | Read-only evidence only                         |
| Inspect provider resources and health  | `pnpm services:status`    | Read-only evidence only                         |
| Prepare a service change               | `pnpm services:plan-add`  | Creates a non-mutating local plan               |
| Apply an approved service plan         | `pnpm services:apply-add` | Yes; exact approval and execution gate required |

## In this section [#in-this-section]

<Cards>
  <Card title="Environments" href="/docs/shipping/environments" description="Preview, Staging, Release Candidate, and Production, and the promotion rules between them." />

  <Card title="Staging topology" href="/docs/shipping/staging" description="Apps under staging.exo.now; Pages under staging.construct.page." />

  <Card title="Releases and changelogs" href="/docs/shipping/releases" description="Stable N, internal versions, and the approval packet." />

  <Card title="Approval boundaries" href="/docs/shipping/approvals" description="What never follows implicitly from a green build." />
</Cards>


---

# Releases and changelogs

https://exo.now/docs/shipping/releases

> Every deployment gets an internal version and, when stable, a tenant-specific public "Stable N" release.



Source: [tenant releases and changelogs](../../../docs/releases.md).

## Two version identities [#two-version-identities]

| Identifier       | Example        | Audience                 | Purpose                                                    |
| ---------------- | -------------- | ------------------------ | ---------------------------------------------------------- |
| Internal version | `2.4.0+abc123` | Operators and developers | Precisely identifies the build, commit, or migration state |
| Public release   | `Stable 18`    | Site users and clients   | A simple, memorable indication of the stable version       |

Public numbers are independent for each tenant. The first stable release is `Stable 1`, and each
later one increments by one. Drafts, previews, and internal releases don't consume a number, and a
published number is never reused. **Stable N** is the default human-facing name in product emails,
changelogs, dashboards, and updates. The exact technical version stays available as secondary
evidence.

## Publishing workflow [#publishing-workflow]

1. Create a release draft for the tenant and record its internal version.
2. Add a title, summary, highlights, and optional detailed notes.
3. Link the prompt-linked commits, change review, Entire checkpoint, deployment, changelog, and
   roadmap item.
4. Review the release against the staging build.
5. Send the approval packet to an authorized owner.
6. After explicit owner approval on the release pull request, merge the candidate into `main` and
   publish it on the `stable` channel.
7. Exo assigns the next public release number and publication date.
8. Verify the live version, tenant health checks, and changelog entry.
9. Send the configured changelog email, first to the owner-preview address, and keep non-secret
   delivery evidence.

Publishing a changelog entry records a release. It does not bypass the release pull-request gate
or deploy code by itself.

## Approval packet [#approval-packet]

```text
Changed
- ...

Removed
- Nothing.

Pressure-test / concerns
- No specific concerns.

Validation
- ...

Deployment impact
- ...

Rollback
- ...
```

Call out data migrations, authentication or authorization changes, destructive operations,
provider changes, and tenant-specific behavior explicitly. Don't invent a concern when there is
none. Approval to merge code is not approval to deploy it unless the owner explicitly grants both.

## Cadence [#cadence]

* Integrate task-sized commits into staging as soon as they pass validation.
* Consolidate reviewed staging commits into a release pull request instead of approval-gating
  every staging update.
* Don't let approved work sit unreleased without a stated reason.
* Keep staging on the same commit and configuration intended for production.
* Never report a release as live until the deployed version and health checks are verified.

## Public changelog [#public-changelog]

Off by default. When a tenant enables it, the built-in renderer exposes `/changelog`, and external
renderers can read `GET /api/content/:tenant/changelog`, which returns only published, public,
stable releases.


---

# Staging topology

https://exo.now/docs/shipping/staging

> Exo owns staging for Apps; Construct owns staging for Pages. Review routes follow ownership, not production hostnames.



Source: [Exo staging topology](../../../docs/staging-topology.md).

## Canonical routes [#canonical-routes]

| Resource       | Canonical staging route                 | Example                              |
| -------------- | --------------------------------------- | ------------------------------------ |
| Exo platform   | `https://staging.exo.now`               | The Exo control plane                |
| App            | `https://staging.exo.now/{app}`         | `https://staging.exo.now/rampart`    |
| Construct Page | `https://staging.construct.page/{page}` | `https://staging.construct.page/dan` |

Construct is an App, so it stages at `staging.exo.now/construct`. It is also the Page platform,
so the Pages it manages stage on `staging.construct.page`.

## Routing and authorization [#routing-and-authorization]

The canonical route is a gateway address. A provider-generated hostname is an operational
coordinate and a source of deployment evidence, never the review URL.

1. The shared origin authenticates the session.
2. The gateway resolves the first path segment to a declared App or Construct Page.
3. The server validates an active, environment-specific Exo grant for that resource.
4. The gateway forwards only to the declared provider target and keeps the canonical browser URL.

Apps share one non-production identity plane without every signed-in user getting access to every
App. Health checks and signed webhooks keep narrow, documented exceptions.

<Callout title="Identity" type="info">
  Exo Key (Exo ID) authenticates the shared staging origin; Clerk is removed
  ([ADR 0012](../../../docs/adr/0012-clerk-removed-convex-auth-everywhere.md)). The gateway checks
  an Exo ID session and an exact staging grant before it opens an app path
  ([staging access](../../../docs/staging-access.md)).
</Callout>

## Adoption inventory [#adoption-inventory]

The last verified observations recorded in the topology doc:

| Resource                | Route                           | Status                                                                      |
| ----------------------- | ------------------------------- | --------------------------------------------------------------------------- |
| Exo                     | `staging.exo.now`               | Verified live                                                               |
| Construct               | `staging.exo.now/construct`     | Verified live (Construct commit `875ac2e`)                                  |
| Rampart                 | `staging.exo.now/rampart`       | Verified live; removal of `staging.rampart.fit` is a separate domain action |
| Observatory             | `staging.exo.now/observatory`   | Verified live                                                               |
| Cosmograph              | `staging.exo.now/cosmograph`    | Verified live                                                               |
| Vector                  | `staging.exo.now/vector`        | Provisioning required                                                       |
| Universe                | `staging.exo.now/universe`      | Provisioning required                                                       |
| Dropship                | `staging.exo.now/dropship`      | Verified live                                                               |
| Construct-managed sites | `staging.construct.page/{page}` | Per-Page migration                                                          |

A route is never marked live from configuration alone. It needs provider health, exact-commit
evidence, and a browser pass.

## Per-resource migration checklist [#per-resource-migration-checklist]

1. Classify the resource as `platform`, `app`, or `construct-page`, and reserve its slug.
2. Record the current provider service, source branch, provider URL, and verified commit.
3. Configure gateway routing for the target path without changing Production.
4. Configure the non-production authorized origin and the resource-specific Exo grant.
5. Verify unauthenticated denial, authorized success, cross-resource denial, return paths, health,
   noindex headers, and the exact deployed commit.
6. Review the canonical URL in a browser.
7. Remove the old app-domain staging route, or make it redirect-only.
8. Attach evidence to the Exo work and release records.


---

# 12 · Artificial intelligence

https://exo.now/docs/stack/ai

> Apps send prompts to Exo Router, Exo's smart model routing. It scores how hard each task is and picks the least costly capable model through the Convex AI Gateway.



<StackFacts dimension="12 · Artificial intelligence" platforms="[&#x22;convex-ai-gateway&#x22;, &#x22;artificial-analysis&#x22;, &#x22;arch-router&#x22;]" status="Exo Router endpoint live on Convex; difficulty assessor and model pool built; Artificial Analysis benchmarks and per-App cost attribution TBD." />

## What it is [#what-it-is]

**Exo Router** is Exo's own smart model routing. Apps send prompts to Exo Router and never hold
provider keys. It scores how hard each task is, weighs model capability against cost, and sends
the call to the chosen model through the Convex AI Gateway. Every job is metered. In short: **an
assessor decides, and the gateway executes** ([AI routing](../../../docs/ai-routing.md)). The
design is on the [Exo Router](../platform/exo-router.mdx) page.

## Platform [#platform]

* **Convex AI Gateway** is the OpenAI-compatible egress with service-token auth. Convex holds the
  provider credentials.
* **Artificial Analysis** is named in the catalog as the source of independent intelligence,
  speed, and price benchmarks used to score models. It is **not wired into the code yet**: the
  `aiModelPool` intelligence scores (0–100) are Exo's own seed estimates in
  `convex/ai/router.ts`, "meant to be calibrated from aiRoutingLog over time". The benchmark
  import is **TBD**.
* **Arch-Router** (Katanemo Arch-Router-1.5B, open weights) is the difficulty assessor,
  self-hosted on Railway via llama.cpp from `services/arch-router/`. See
  [What is Arch-Router?](../platform/exo-router.mdx#what-is-arch-router).
* **TypeSafe** (Jev, System One) is a dormant alternate assessor: TypeSafe closed new signups
  (verified 2026-09-23).

## Boundary and replaceability [#boundary-and-replaceability]

* **Hot-swappable model pool.** `aiModelPool` is a table, so pool changes are data edits, not
  deploys.
* **Pluggable assessors.** The first configured assessor wins (`ARCH_ROUTER_URL`, then
  `TYPESAFE_API_KEY`). With neither configured, Exo Router uses pure utility and flags every
  decision `fallback: true`.
* **No silent fallback.** Gateway failures fail the job and are recorded.
* **Aggregate findings only.** Exo may receive aggregate quality, latency, token, cost, and
  reliability findings from Apps. It never receives prompts, responses, user identifiers, or
  provider keys ([Convex framework](../../../docs/convex-framework.md)).

## Cost notes [#cost-notes]

* **Assessor:** no per-call cost; CPU-only, \~1 GB RAM on Railway.
* **Routing policy:** on `staging`, `convex/ai/router.ts` computes
  `utility = intelligence^w / blendedCost`, where `w` is the assessed difficulty (0–1) and
  `blendedCost` is the 3:1 input:output blend of the pool row's per-million-token prices. Open
  PR [#115](https://github.com/Interspace-Ventures/exo/pull/115) (against `main`) changes it to
  `intelligence^w / blendedCost^(1-w)`. Easy tasks let cost dominate, and hard tasks weight
  intelligence.
* **Metering:** each pool row carries input and output cost per million tokens (USD).
  `exoAiBrokerJobs` records tokens and duration per job. On `staging` the endpoint does not yet
  compute `costMicrosUsd` and rows carry no App identifier, so per-App AI spend is **TBD**.

## Status [#status]

* **Live:** the Exo Router HTTP endpoint on the Exo Convex deployment (`POST /v1/ai/generate`),
  used by Construct. With `EXO_AI_MODEL` unset, a deterministic local composer serves requests so
  the loop works without keys.
* **Built:** `convex/ai/router.ts`, `assessors.ts`, and `chat.ts` with the routing log.
* **In progress:** sending `/v1/ai/generate` jobs through the router (PR #115), and per-App cost
  attribution
  ([roadmap: ai-gateway-jev-routing](../../../roadmap/items/ai-gateway-jev-routing.md)).
* **TBD:** Artificial Analysis benchmark import into `aiModelPool`.


---

# 04 · App state & storage

https://exo.now/docs/stack/app-state

> Convex is the live backend for every app: data, functions, real-time state, and files. Object storage only when a workload justifies it.



<StackFacts dimension="04 · App state & storage" platforms="[&#x22;convex&#x22;, &#x22;convex-storage&#x22;]" status="App state: Live. Exo's platform data is in Convex; exceptions need an ADR-level justification. Storage: Convex file storage for small blobs; large media on S3-compatible storage (Garage) per the infrastructure split." />

## What it is [#what-it-is]

Convex is the default persistence, transaction, function, and reactive-state layer for every state
Exo owns ([ADR 0003](../../../docs/adr/0003-convex-first-control-plane.md)). It holds tenants,
content, roadmap, billing, releases, delivery targets, observations, and identity, and serves the
live queries behind dashboards and boards ([infrastructure](../../../docs/infrastructure.md)).

### Storage [#storage]

Media covers uploaded files, images, and attachments. Construct's Convex deployment owns Page
media metadata, checksums, lifecycle, and references. Exo uses Convex file storage for help-desk
attachments and similar small blobs ([infrastructure](../../../docs/infrastructure.md)).

## Platform [#platform]

**Convex.** Exo runs two deployments, `frugal-eel-930` (staging) and `befitting-bullfrog-619`
(production) ([`convex/README.md`](../../../convex/README.md)).

### Storage [#storage-1]

**Convex Storage** for small files and for the authoritative metadata. Large media libraries stay
on S3-compatible object storage, which is "Garage today" according to the infrastructure split.

## Boundary and replaceability [#boundary-and-replaceability]

* **Exceptions are explicit.** An exception to Convex needs a concrete workload constraint, a named
  authority, reconciliation and rollback rules, and an exit path. Exo runs no other
  application database.
* **Derived indexes stay derived.** Specialized analytical or search indexes may live outside
  Convex, but they must be rebuildable and never the sole authority.
* **Function discipline.** Every query and mutation validates arguments and returns, derives
  identity server-side, enforces the resource boundary, and bounds result size. Privileged
  functions are internal, and retryable mutations use atomic idempotency receipts
  ([architecture](../../../docs/architecture.md)).
* **No secrets in tables.** Convex stores only non-secret provider coordinates and the names of
  runtime environment variables.
* **Exit.** Tenant exports use versioned JSON Schema and newline-delimited JSON with stable IDs
  ([portability](../../../docs/portability.md)).

### Storage [#storage-2]

* Large binary objects never go in Convex documents. Blob bytes may use Convex file storage or an
  external object store when size, transformation, retention, egress, or interoperability
  requirements justify it. **Convex stays authoritative** for ownership, checksum, metadata,
  lifecycle, and the opaque object reference
  ([ADR 0003](../../../docs/adr/0003-convex-first-control-plane.md)).
* Because references are opaque, moving bytes between stores is a reconciliation job, not a schema
  change.
* Exports include original media files, metadata, alt text, checksums, and a media manifest
  ([portability](../../../docs/portability.md)).

## Cost notes [#cost-notes]

* Convex plan and usage: **TBD**.
* ADR 0006 expects Exo Analyze's event table to add "a few dollars per month" of Convex storage
  and function usage at current portfolio scale.

### Storage [#storage-3]

Convex storage and object-storage costs: **TBD**. The ADR names egress as one of the valid reasons
to move bytes out of Convex.

## Status [#status]

* **Done:** Exo's data layer moved from Payload/PostgreSQL to Convex
  ([exo-convex-backend](../../../roadmap/items/exo-convex-backend.md)).
* **Retired:** Payload and its PostgreSQL code path (removed from Exo and Construct, October
  2026; the old databases are kept untouched until their data is exported), Clerk as identity
  (fully removed, [ADR 0012](../../../docs/adr/0012-clerk-removed-convex-auth-everywhere.md)), and
  bespoke API services ([infrastructure](../../../docs/infrastructure.md)).

### Storage [#storage-4]

* **Live:** Convex file storage for small blobs.
* **TBD:** which Apps currently keep large originals on Garage, and the measured constraint for
  each (the ADR requires one).


---

# 08 · Communications & support

https://exo.now/docs/stack/communications

> One Exo communications layer sends email, text, and voice through Bird; customer records live in Twenty, an open-source CRM.



<StackFacts dimension="08 · Communications & support" platforms="[&#x22;bird&#x22;, &#x22;twenty&#x22;]" status="Communications: Planned (roadmap horizon: now). Resend is still the required default until the migration lands. CRM: TBD — named standard in the catalog; no integration is documented in the repository yet." />

## What it is [#what-it-is]

One Exo communications layer sends email, SMS, WhatsApp, Apple Messages, and voice. It is
consent-aware, runs server-side, uses per-app sending domains, and stays separate from product
data. Apps never hold provider keys. They call Exo's send function with their tenant ID
([roadmap: bird-communications](../../../roadmap/items/bird-communications.md)).

### CRM [#crm]

Customer context stays in the CRM, while forms and signals synchronize through replaceable,
consent-aware adapters (the stack description on exo.now).

## Platform [#platform]

**Bird** will be the single communications provider, replacing Resend and any one-off messaging
integrations.

### CRM [#crm-1]

**Twenty**, an open-source CRM. Its catalog posture is **optional adapter**, and the **App** is the
authority: customer relationships belong to the App, not to Exo.

## Boundary and replaceability [#boundary-and-replaceability]

* **Exo owns the infrastructure:** the Bird account, keys, DNS verification, templates, analytics,
  and suppression lists.
* **Vendor-neutral subdomains.** Each App sends from its own custom domain (for example
  `mail.2daysearly.com` or `inbox.2daysearly.com`) for recipient trust and per-domain reputation.
  Names are never `bird.`, so a provider swap never touches DNS.
* **Exo speaks as Exo** from an Exo-owned subdomain (`ops.exo.now` or `notify.exo.now`) for
  billing, operations, and agent notifications.
* **Adapter shape.** A Convex `communications` layer (an `outboundMessages` table plus per-channel
  `send` actions), with the same adapter shape as the billing providers.

### CRM [#crm-2]

* The catalog describes the boundary as "open-source customer context synchronized through
  replaceable intake adapters". The adapter contract itself is **TBD**.
* Related intake that exists today: Exo's own contact inquiries are stored in the Convex `contactInquiries` table, and the portfolio
  dashboard counts inbound volume over 30 days
  ([revenue monitoring](../../../docs/revenue-monitoring.md)).
* Direct customer contact data is excluded from Exo's revenue ledger
  ([commerce contract](../../../docs/commerce-contract.md)).

## Cost notes [#cost-notes]

Bird per-channel pricing: **TBD**. The roadmap notes that SMS, WhatsApp, and voice "land free on
the same layer — no new vendor later", which means no added vendor, not no usage cost.

### CRM [#crm-3]

Twenty hosting (self-hosted or cloud) and cost: **TBD**.

## Status [#status]

* **Planned**, priority high, horizon now, trail `IV-170`.
* **Scope:** migrate Resend call sites (Construct waitlist, contact, and email library; the
  Interspace inbound route; the Observatory changelog email; Exo templates), add inbound email via
  Bird mailboxes, and add domain onboarding with warm-up sequencing to tenant provisioning.
* **Until then:** Resend remains the required default email provider for active managed projects,
  and outbound email fails closed unless explicitly enabled
  ([repository contract](../../../docs/repository-contract.md),
  [environment lifecycle](../../../docs/environment-lifecycle.md)).

### CRM [#crm-4]

**TBD.** The repository names Twenty as the standard, but no adapter, ADR, or roadmap item exists
yet. Adding one is a next step.


---

# 10 · Continuous integration

https://exo.now/docs/stack/continuous-integration

> EXO Verify type-checks and tests every change before it deploys and checks live Staging and Production routes every 15 minutes; Vitest runs the code checks. Playwright browser journeys on Staging are planned.



<StackFacts dimension="10 · Continuous integration" platforms="[&#x22;vitest&#x22;, &#x22;playwright&#x22;]" via="EXO Verify" status="EXO Verify partly built on staging (2026-10-01): contract, CLI, build gates, 15-min live checks, pre-push hook; AI review waits on the AI gateway." />

## What it is [#what-it-is]

Continuous integration means **code checks and AI evals**. The goal: every change is type-checked,
tested, and clicked through in a real browser on Staging before anyone approves it (the stack
description on exo.now). Today the type checks, tests, and live route checks run; the
browser click-through is planned. A release is healthy, reviewable, and approved, not merely green in a provider
dashboard. This dimension is about verifying code, not identity; sign-in lives under
[Identity](./identity.mdx). The service design is on the [EXO Verify](../platform/exo-verify.mdx)
page.

## Platform [#platform]

* **Vitest** (open source) runs typed unit and integration checks. In this repository,
  `pnpm test:int` runs Vitest with `vitest.config.mts`.
* **Playwright** (open source) drives browser checks against Staging. In this repository,
  `pnpm test:e2e` runs Playwright with `playwright.config.ts`.

The rest of EXO Verify is first-party: a verify contract, Railway sandbox workers, live checks,
and an AI review pass whose model calls go through [Exo Router](../platform/exo-router.mdx).

## Boundary and replaceability [#boundary-and-replaceability]

* The contract is the command list each repository declares (`typecheck`, `lint`, `test`,
  `smoke`). Exo runs what is declared, and a missing command is a visible gap, not a silent pass.
* The AI review layer uses an open-source engine or Exo's own prompts. Third-party review
  (Greptile Enterprise) stays a later option if Exo's review quality falls short.
* No GitHub Actions or GitHub-app bots: the owner is leaving GitHub.
* The AI eval format (what an eval declares, how it is scored, and where results are stored):
  **TBD**. ADR 0005 records AI review and agent-written targeted tests, not a separate eval
  contract.

## Cost notes [#cost-notes]

From ADR 0005 (2026-09-27), at \~300 agent commits per month:

| Option               | Pricing                                                                                            | Est. monthly           |
| -------------------- | -------------------------------------------------------------------------------------------------- | ---------------------- |
| **EXO Verify (own)** | \~$0.007 compute per 5-min 2 vCPU / 2 GB Railway run + $0.01–0.10 model tokens per run             | **\~$6–30**            |
| Greptile Pro         | $30/seat/month, 50 credits/seat, $1 per extra credit; non-GitHub/GitLab forges are Enterprise-only | \~$300–900             |
| CodeRabbit           | $24–30/user; CLI free at 3 reviews/hour                                                            | Per seat               |
| Graphite             | $40/user                                                                                           | Per seat, GitHub-bound |

Cost per verified commit is roughly **$0.02–0.10**, recorded per App so it appears in that App's
cost to serve. Vitest and Playwright are open source.

## Status [#status]

* **Today in this repo:** `pnpm check` (legacy-backend freeze, `tsc`, ESLint plus the UI-system
  gates, and Vitest integration tests), `pnpm verify` (adds the production build), and Playwright
  e2e via `pnpm test:e2e`.
* **Built on `staging` (2026-10-01):** the EXO Verify contract, CLI, and results store; Railway
  build gates (Exo and Observatory enforce, Construct and Rampart report); 15-minute live
  smoke, drift, and domain checks; the pre-push hook; and the PR-Agent review pass through
  Exo Router. Review is off until the Convex AI gateway is enabled. Measured: the Exo gate adds
  42 s to each deploy. See [EXO Verify](/docs/platform/exo-verify#status).
* **Open source parts:** PR-Agent (MIT), Vitest (MIT), Playwright (Apache-2.0; browser journeys not
  wired yet).
* **Open question:** does Entire expose webhooks or a checks API for triggering runs?


---

# 01 · Control plane

https://exo.now/docs/stack/control-plane

> Exo itself — the portfolio control plane for App registration, environments, releases, and evidence — with work run through Hark, and Hermes as the open-source option.



<StackFacts dimension="01 · Control plane" firstParty="[&#x22;Exo&#x22;]" platforms="[&#x22;hark&#x22;, &#x22;hermes&#x22;]" openSource="hermes" status="Exo is live on Convex (staging and production deployments); Payload removed (October 2026). Hark is the primary way work is driven; Hermes is the open-source option. Integration contract for both: TBD." />

## What it is [#what-it-is]

Exo is the application platform and control plane. It owns projects, organizations and access,
environments, provider provisioning, deploy/release/rollback state, runtime-dependency metadata,
policy and security controls, audit evidence, orchestration, health, and bounded usage and cost
aggregates ([ADR 0003](../../../docs/adr/0003-convex-first-control-plane.md)).

Priorities, dependencies, release gates, and verified delivery share one accountable operating
record. The public Exo shell explains the platform without exposing App data. The authenticated
workspace is the internal operator surface.

On exo.now the dimension reads: "Plans, releases, and approvals for every app live in one record.
Work runs through Hark, with Hermes as the open-source option. Recommended, not required."

The control plane is recommended but optional, like source control: an app may opt out of it
([overview](../index.mdx#core-components-and-specialty-components)).

## Platform [#platform]

* **Exo** is first-party. Its state lives in [Convex](./app-state.mdx), its web app runs on
  [Railway](./deploy.mdx), and its source lives in [Entire](./source.mdx).
* **Hark** is the primary: the agent operators drive Exo work through, from plans and changes to
  reviews and approvals (integration catalog: ecosystem standard, authority Exo).
* **Hermes** (agents) is the open-source option: the open-source agent runtime from Nous Research
  (integration catalog: ecosystem standard, authority Exo). Every layer names an open-source option;
  this is the control plane's.
* **FluidVoice** is an optional, open-source, on-device dictation tool an operator can use to drive
  work by voice. It is not part of the control plane's primary stack (integration catalog:
  optional adapter).

| Environment | Convex deployment        | Web                       |
| ----------- | ------------------------ | ------------------------- |
| Staging     | `frugal-eel-930`         | `https://staging.exo.now` |
| Production  | `befitting-bullfrog-619` | `https://exo.now`         |

Source: [`convex/README.md`](../../../convex/README.md) and
`src/platform/integrations/catalog.ts`.

## Boundary and replaceability [#boundary-and-replaceability]

* **Authority rules.** Every record has exactly one authority during every cutover phase. External
  providers stay authoritative for live deployment, billing, source, and health facts. Exo owns
  portfolio and release coordination, Entire owns source and execution provenance, Construct owns
  Pages, and independent Apps own their domain data (ADR 0003).
* **Agents and voice are inputs, not authorities.** Hark (or Hermes, the open-source option) is how
  operators drive work; the accountable record stays in Exo. How Hark and Hermes sessions connect
  to Exo records is not documented in the repository yet: **TBD**.
* **No Payload.** Payload and its PostgreSQL adapter are gone from the code. The old Postgres
  database is kept, untouched, only until its records are exported to their new owners.
* **Exit.** Every tenant can obtain independently usable content, media, design, source, and
  deployment artifacts ([portability](../../../docs/portability.md)).

## Cost notes [#cost-notes]

* Convex and Railway plan costs for the control plane: **TBD**.
* Hark operating cost: **TBD**. Hermes and FluidVoice are open source (per the catalog); their
  operating cost: **TBD**.
* Exo stores bounded usage and cost aggregates for Apps, not their ledgers. See
  [cost and unit economics](./economics.mdx).

## Status [#status]

* **Done:** Convex backend for the platform, deployed to staging and production
  ([roadmap: exo-convex-backend](../../../roadmap/items/exo-convex-backend.md)).
* **In progress:** roadmap as repo files with a public board
  ([roadmap-as-repo](../../../roadmap/items/roadmap-as-repo.md)).
* **Data export:** where each legacy Postgres record family goes is described in
  [ADR 0003](../../../docs/adr/0003-convex-first-control-plane.md) and the
  [legacy authority map](../../../docs/exo-legacy-authority-map.md).
* **Hark and Hermes:** named in the catalog and on exo.now (Hark as the primary, Hermes as the
  open-source option). No ADR or roadmap item records their rollout: **TBD**.


---

# 07 · Deploy

https://exo.now/docs/stack/deploy

> Exo Deploy ships every surface. Web apps and services run on Railway, and iOS and Android apps ship with Expo.



<StackFacts dimension="07 · Deploy" platforms="[&#x22;railway&#x22;, &#x22;expo&#x22;]" via="Exo Deploy" status="Web live on Railway. Mobile toolkit accepted (ADR 0007, 2026-09-27) and planned; Universe already ships on Expo." />

## What it is [#what-it-is]

The deploy dimension answers: how does each surface reach users, and what is running now?
**Exo Deploy** owns the release path for every surface:

| Surface                         | Platform             | Path                                                               |
| ------------------------------- | -------------------- | ------------------------------------------------------------------ |
| Web apps and services           | Railway              | `staging` auto-deploys; a release PR to `main` promotes production |
| Mobile apps (iOS, Android)      | Expo                 | TestFlight, then the App Store and Google Play                     |
| Pages and embedded app surfaces | Construct on Railway | Same path as web                                                   |

Every surface follows the same Staging → Release Candidate → Production path, described in
[How work ships](../shipping/index.mdx). The service design is on the
[Exo Deploy](../platform/exo-deploy.mdx) page. In-app purchases live under
[Payments](./payments.mdx), not here.

## Platform [#platform]

* **Railway** runs Next.js hosting for the Exo web app, cron workers that reach external systems
  (Entire trail sync, provider health sweeps, revenue snapshot pulls), and Valkey for rate limits
  and short-lived caches ([infrastructure](../../../docs/infrastructure.md)). The Exo web image is
  a standard `Dockerfile` (`node:22-bookworm-slim`, pnpm 11.15, `pnpm build`, port 3000,
  `output: 'standalone'`). The self-hosted Arch-Router assessor is a second Railway service built
  from `services/arch-router/`.
* **Expo**, an MIT-licensed framework on React Native, is the standard mobile framework for Exo
  apps. **EAS** is optional and swappable for build, submit, and OTA updates (ADR 0007).

## Boundary and replaceability [#boundary-and-replaceability]

* **Web: the contract is an OCI image:** a standard `Dockerfile`, an HTTP port, and environment
  variables. Railway and Coolify are the initial adapters, and the contract requires neither
  ([architecture](../../../docs/architecture.md),
  [cross-platform delivery](../../../docs/platform-delivery.md)).
* Railway holds runtime secrets. Exo stores only the names of environment variables.
* Leaving the managed runtime includes a documented handoff of the container contract, health
  checks, variable names, volumes, scheduled jobs, domains, and rollback
  ([portability](../../../docs/portability.md)).
* A provider-generated hostname is deployment evidence, never the review URL
  ([staging topology](../../../docs/staging-topology.md)).
* **Mobile:** EAS is a service choice, not a dependency. The Exo Deploy mobile template keeps a
  local-build path (`eas build --local` or fastlane). The repository contract keeps a
  **PWA-required-for-mobile** gate, and each app registers its native redirect scheme with Exo Key
  during onboarding.

## Cost notes [#cost-notes]

Web:

* One recorded unit cost: **\~$0.007 per 5-minute run on a 2 vCPU / 2 GB Railway worker**
  (ADR 0005, used to price EXO Verify).
* The Arch-Router service is CPU-only and runs comfortably in \~1 GB RAM, with no per-call cost
  ([arch-router](../../../services/arch-router/README.md)).
* Monthly Railway spend for Exo and the portfolio: **TBD**. Every deployment must first pass the
  projects.dev credential and spend gate.

Mobile (ADR 0007, 2026-09-27):

| Stage                   | Fixed cost             | Marginal cost                                             |
| ----------------------- | ---------------------- | --------------------------------------------------------- |
| Apple Developer Program | $99/year (required)    | —                                                         |
| EAS Free                | $0                     | Capped at 15 iOS + 15 Android builds/month, OTA to 1K MAU |
| EAS Starter             | $19/month              | Usage beyond the $45 build credit                         |
| Own Mac builder         | Hardware or hosted Mac | \~$0 per build; pays off only at high volume              |

At current volume the mobile toolkit costs about **$8/month** (Apple fee amortized).

## Status [#status]

* **Live:** Exo web on Railway, with staging at `staging.exo.now` and production at `exo.now`.
* **Planned:** the Exo Deploy mobile template
  ([roadmap: mobile-delivery-pipeline](../../../roadmap/items/mobile-delivery-pipeline.md)).
  Template location: **TBD**.
* **Reference:** Universe already ships on Expo, registered with Exo Key using the native
  redirect `universe://auth/callback`.
* **Evidence rule:** a push proves only that Git received the commit, and a provider success proves
  only that a deployment job finished. Staging is current only after the deployed commit,
  `/api/health`, access boundary, and browser behavior are verified
  ([build and deploy](../../../docs/build-and-deploy.md)).


---

# 06 · Design system

https://exo.now/docs/stack/design-system

> Every Exo app uses shadcn/ui components with the Structured Liquidity theme; websites and pages are built and published in Construct.



<StackFacts dimension="06 · Design system" platforms="[&#x22;shadcn&#x22;, &#x22;structured-liquidity&#x22;, &#x22;construct&#x22;]" status="Design system: Live. Enforced in this repository by pnpm check:ui-system, which runs inside pnpm lint. Pages: Construct is live on staging at staging.exo.now/construct; Pages stage at staging.construct.page." />

## What it is [#what-it-is]

Every app uses the same open-source components, styled with the Structured Liquidity theme (the
stack description on exo.now). The Structured Liquidity foundation is mandatory for every
Exo-managed application, Exo-owned interface, management surface, and first-party template
([design standard](../../../docs/design-standard.md)).

### Pages [#pages]

Landing pages, marketing sites, and comparison pages are written, laid out, and published in
Construct, Exo's content management system for websites. **Construct owns presentation and page
organization; Exo owns function** (identity, deployment, access, data, and
[Exo Router](../platform/exo-router.mdx)). Components and theming are a separate dimension:
[Design system](./design-system.mdx).

## Platform [#platform]

* **shadcn/ui** is the open component foundation. Managed frontend work starts from the
  repository's shadcn configuration (`components.json`, `new-york` baseline) and composes
  primitives from `src/components/ui` instead of raw interactive controls.
* **Structured Liquidity** is the shared theme and tokens every Exo app and Construct page uses.
  Its three dimensions are Neobrutalist structure, Liquid motion, and Scientific modern clarity,
  and Construct is its canonical first-party expression.

### Pages [#pages-1]

* **Construct** is a first-party Interspace App and the Page platform (catalog layer: CMS,
  authority Construct).
* **Puck** is open-source visual page composition inside Construct.

## Boundary and replaceability [#boundary-and-replaceability]

* **One foundation, many expressions.** Tenants may replace palette, typography, imagery, and
  content density through their documented token and brand contracts, but they must keep the
  Structured Liquidity and shadcn component foundation and the platform accessibility,
  responsiveness, performance, and semantic-control rules.
* **Controlled visual vocabulary.** No more than two actual typefaces mapped to display, body, and
  detail roles, seven semantic size tiers, four weight tiers, and semantic color roles instead of
  component-local values. Exo-owned surfaces keep `--radius: 0px`.
* **Design exports as tokens.** Design is part of the tenant export
  ([portability](../../../docs/portability.md)).

### Pages [#pages-2]

* Construct owns Page and website editing, Puck, publishing, Page authorization, and authenticated
  Surfaces, on its own Convex-native model, with no Payload
  ([ADR 0003](../../../docs/adr/0003-convex-first-control-plane.md)).
* Exo knows Pages only as typed resources for grants, routing, deployment evidence, and aggregate
  policy ([architecture](../../../docs/architecture.md)).
* Portability: the tenant export includes Puck data plus the component contract and migrations
  needed to interpret it, and a framework-neutral static HTML fallback for published pages
  ([portability](../../../docs/portability.md)).

## Cost notes [#cost-notes]

shadcn/ui is open source. Structured Liquidity is first-party. No cost figure is recorded: **TBD**.

### Pages [#pages-3]

Construct operating cost per site: **TBD**. Puck is open source. Construct's AI generation goes
through [Exo Router](../platform/exo-router.mdx) (endpoint `POST /v1/ai/generate`), so its model
spend is recorded in `exoAiBrokerJobs`.

## Status [#status]

* **Live and enforced:** `node scripts/check-structured-liquidity-ui.mjs` checks the shadcn
  configuration, required primitives, and foundation tokens, and
  `node scripts/check-visual-vocabulary.mjs` ratchets inherited raw font, size, weight, and color
  declarations against a baseline. Both run through `pnpm check:ui-system` inside `pnpm lint`.
* These docs use the same tokens: `src/app/(docs)/docs.css` maps Fumadocs tokens onto the Exo
  tokens ([maintaining these docs](../contributing.mdx)).

### Pages [#pages-4]

* **Live:** Construct App at `https://staging.exo.now/construct`, verified at Construct commit
  `875ac2e`. Construct-managed Pages stage at `https://staging.construct.page/{page}`, migrating
  Page by Page ([staging topology](../../../docs/staging-topology.md)).


---

# Cost and unit economics

https://exo.now/docs/stack/economics

> Every cost figure recorded in the ADRs and docs, the unit-economics model Exo is building toward, and what is still TBD.



This page collects every cost figure the repository records. Figures are quoted from their source
with the date of the decision. Nothing here is a new estimate, and where the repository records no
figure, the cell says **TBD**.

## Recorded figures [#recorded-figures]

| Dimension              | Item                                      | Figure                                                                                                                                        | Source                                                                                            |
| ---------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Continuous integration | EXO Verify compute                        | \~$0.007 per 5-minute run on a 2 vCPU / 2 GB Railway worker                                                                                   | ADR 0005 (2026-09-27)                                                                             |
| Continuous integration | EXO Verify model tokens                   | $0.01–0.10 per run                                                                                                                            | ADR 0005                                                                                          |
| Continuous integration | EXO Verify per verified commit            | \~$0.02–0.10                                                                                                                                  | ADR 0005                                                                                          |
| Continuous integration | EXO Verify monthly at \~300 agent commits | \~$6–30                                                                                                                                       | ADR 0005                                                                                          |
| Continuous integration | Greptile Pro (alternative)                | $30/seat/month, 50 credits/seat, $1 per extra credit; est. \~$300–900/month at the same volume                                                | ADR 0005                                                                                          |
| Continuous integration | CodeRabbit (alternative)                  | $24–30/user; CLI free at 3 reviews/hour on local git                                                                                          | ADR 0005                                                                                          |
| Continuous integration | Graphite (alternative)                    | $40/user                                                                                                                                      | ADR 0005                                                                                          |
| Analytics              | PostHog free tier                         | \~1M events/month                                                                                                                             | ADR 0006 (2026-09-27)                                                                             |
| Observability          | Exo event table on Convex                 | "a few dollars per month" expected                                                                                                            | ADR 0006                                                                                          |
| Deploy (mobile)        | Apple Developer Program                   | $99/year (required)                                                                                                                           | ADR 0007 (2026-09-27)                                                                             |
| Deploy (mobile)        | EAS Free                                  | $0; 15 iOS + 15 Android builds/month, OTA updates to 1K MAU                                                                                   | ADR 0007                                                                                          |
| Deploy (mobile)        | EAS Starter                               | $19/month plus usage beyond the included $45 build credit                                                                                     | ADR 0007                                                                                          |
| Deploy (mobile)        | Own Mac builder                           | Hardware or hosted Mac; \~$0 per build; pays off only at high volume                                                                          | ADR 0007                                                                                          |
| Deploy (mobile)        | Exo Deploy at current volume              | \~$8/month (Apple fee amortized)                                                                                                              | ADR 0007                                                                                          |
| AI                     | Arch-Router assessor (Exo Router)         | No per-call cost; self-hosted, CPU-only, \~1 GB RAM                                                                                           | [ai-routing](../../../docs/ai-routing.md), [arch-router](../../../services/arch-router/README.md) |
| Identity               | Clerk (removed)                           | Charged per MAU (reason for leaving; no figure recorded). Removed from EXO 2026-10-12; the account and subscription are the owner's to cancel | [ADR 0012](../../../docs/adr/0012-clerk-removed-convex-auth-everywhere.md)                        |
| Portability            | Exit                                      | No punitive exit fee; direct infrastructure egress may be passed through                                                                      | [portability](../../../docs/portability.md)                                                       |

<Callout title="Where ADRs 0005–0007 live" type="info">
  ADRs 0005, 0006, and 0007 were accepted on `main` on 2026-09-27 and are not yet on `staging`.
  They will render under [Decision records](/docs/adr) once the branches are reconciled. Until
  then, read them on `main`:
  [0005](https://github.com/Interspace-Ventures/exo/blob/main/docs/adr/0005-exo-verify-verification-service.md),
  [0006](https://github.com/Interspace-Ventures/exo/blob/main/docs/adr/0006-exo-analyze-product-analytics.md),
  [0007](https://github.com/Interspace-Ventures/exo/blob/main/docs/adr/0007-exo-mobilize-mobile-release-toolkit.md).
</Callout>

## Not yet recorded (TBD) [#not-yet-recorded-tbd]

The repository does not record plan or monthly figures for these. Treat each one as **TBD**
until someone adds an ADR or doc:

* Convex plan and usage (control plane, identity, app state, media) for both deployments
* Railway monthly spend for the Exo web services and the `arch-router` service
* Entire (source host) pricing
* Construct operating cost per site
* Bird per-message pricing for email, SMS, and voice
* Twenty hosting (self-hosted or cloud)
* Stripe and RevenueCat fees at portfolio volume
* AI model spend per App (see the Exo Router caveat below)
* Hark operating cost, and Hermes and FluidVoice operating cost
* Artificial Analysis benchmark access (not wired into `aiModelPool` yet)

## The unit-economics model [#the-unit-economics-model]

Exo Analyze (ADR 0006) defines the funnel Exo stores end to end, so that every App has computable
unit economics:

```text
visits → sign-ups (Exo Key) → paid (Stripe / RevenueCat webhooks) → cost to serve
                                                                   (exoAiBrokerJobs + exo-verify + infra)
```

Each App's dashboard is meant to report **cost per active user** and **gross margin per paying
user**, so a feature is judged on cost per unit and margin, not revenue alone.

### What is measured today [#what-is-measured-today]

* **AI jobs.** Every Exo Router job (`POST /v1/ai/generate`) writes a row to `exoAiBrokerJobs` with `jobType`, `status`,
  `provider`, `model`, `sourceCount`, `durationMs`, `inputTokens`, and `outputTokens`
  (`convex/schema.ts`). The table has a `costMicrosUsd` column, but on `staging` the endpoint does
  not compute it yet (the response returns `0`), and rows carry no App identifier. Per-App AI cost
  is therefore **TBD**; token counts are the current unit.
* **Routing.** `aiRoutingLog` records the chosen model, its utility score, the assessor, and a
  `fallback` flag for every routed decision. `aiModelPool` holds per-model input and output cost
  per million tokens (USD) and a 0–100 intelligence estimate.
* **Revenue.** Stripe and RevenueCat events land in an idempotent, normalized ledger with cached
  metric snapshots. Currencies are never summed silently, and unobserved metrics display as
  unknown, never zero ([revenue monitoring](../../../docs/revenue-monitoring.md)).

## Spend governance [#spend-governance]

* **projects.dev spend gate.** Before each deployment, Exo verifies the matching environment,
  reads the credential inventory without revealing values, inspects recent spend, and confirms a
  global spend limit exists and has not been exceeded. Free tiers are preferred, and any new or
  upgraded paid plan needs explicit owner approval
  ([repository contract](../../../docs/repository-contract.md)).
* **Buy vs. build at agent volume.** Third-party tools are priced per run at agent commit volume
  (hundreds per month), not per seat. That is why EXO Verify was built rather than bought.


---

# 02 · Development framework

https://exo.now/docs/stack/framework

> Every Exo app is written in strict TypeScript with React, on Next.js for the web and Expo for mobile. One opinionated path.



<StackFacts dimension="02 · Development framework" platforms="[&#x22;nextjs&#x22;, &#x22;typescript&#x22;, &#x22;react&#x22;]" status="Proposed (2026-10-02), pending team confirmation. Exo itself already runs on it; the repository contract still allows other frameworks until this is confirmed." />

## What it is [#what-it-is]

The framework dimension answers: what is every app written in? One answer for every app:
**TypeScript, React, and Next.js** for the web, and the same TypeScript and React on Expo for
mobile ([Deploy](./deploy.mdx)). There is no second path: no Svelte, Remix, Vue, or Angular.

One path means code, components, agents, and people move between apps without relearning a
stack, and every Exo service (EXO Verify, Exo Analyze, Exo Key, the design system) is built and
tested for exactly one target.

## Platform [#platform]

* **Next.js** (MIT, Vercel) for web apps: routing, server rendering, and API routes. Exo runs on
  Next.js 16.2 and React 19.2.
* **TypeScript** (Apache-2.0, Microsoft), strict mode, for all application code. JavaScript
  application sources are disabled (already required by the repository contract).
* **React** (MIT, Meta) for UI, shared by web (Next.js) and mobile (Expo / React Native).

All three are open source. Developers use them directly. Exo does not wrap or rename them.

## Boundary and replaceability [#boundary-and-replaceability]

* The framework is a convention, not a hosted service, so there is no account, key, or bill.
* Next.js runs on any OCI host: Exo apps build to a standard container
  ([Deploy](./deploy.mdx)) and do not depend on Vercel.
* Replacing the framework is a portfolio-wide migration, not a per-app choice. That is the point
  of an opinionated stack.
* **Open change:** the [repository contract](../../../docs/repository-contract.md) currently says
  "Application framework choice remains independent". It changes to Next.js + TypeScript + React
  once the team confirms this page.

## Cost notes [#cost-notes]

* License cost: **$0** (MIT and Apache-2.0).
* The cost of one path is the alternatives given up: a team that prefers Svelte or Remix must
  build outside Exo.

## Status [#status]

* **Live:** Exo, Construct, and the portfolio web apps already use Next.js, TypeScript, and React.
* **Proposed:** making it mandatory for every Exo app (pending team confirmation, 2026-10-02).


---

# 05 · Identity

https://exo.now/docs/stack/identity

> Exo Key on Convex Auth v2 — Exo is the only OAuth client and identity provider; Apps are relying parties.



<StackFacts dimension="05 · Identity" platforms="[&#x22;convex-auth&#x22;]" via="Exo Key" status="Exo Key live on staging for registered Apps; Clerk fully removed from EXO (ADR 0012)." />

## What it is [#what-it-is]

One sign-in for every property, with explicit App and Page scopes instead of a separate account
per property. Exo's Convex deployment mounts Convex Auth v2 (core plus Google and GitHub OAuth;
Apple when added) and holds the only provider registrations. It issues **app-scoped** RS256 access
tokens that Apps verify ([ADR 0004](../../../docs/adr/0004-exo-id-central-identity-provider.md)).

The full contract is on the [Exo Key](../platform/exo-key.mdx) service page.

## Platform [#platform]

**Convex Auth v2**, which is open source and native to the Convex database
([roadmap: convex-auth](../../../roadmap/items/convex-auth.md)). It runs as a pinned alpha
(`@convex-dev/auth` `2.0.0-alpha.2` in `package.json`). Per the Convex team, v2 is stable enough
to deploy now and reaches GA by Q4
([ADR 0003](../../../docs/adr/0003-convex-auth-v2-identity-plane.md)).

## Boundary and replaceability [#boundary-and-replaceability]

* **Apps never touch providers.** Apps don't mount auth components, don't hold `AUTH_*` or
  provider secrets, and never register with Google, GitHub, or Apple. Onboarding an App is a
  reviewed code change to `convex/lib/exoIdApps.ts`.
* **Authentication and authorization are separate.** Exo proves identity (`sub`, `email`, `name`).
  Authorization (roles, tenant grants) stays with the App in v1.
* **Upgrade containment.** Convex Auth upgrades are contained to `convex/auth.ts`,
  `convex/auth.config.ts`, `convex/convex.config.ts`, and the client provider.
* **Portability.** A person has one Exo user id across Apps, and Apps key their records on it.
  Identity exports omit credentials.
* **Consequence.** Exo's Convex deployment is a hard dependency for sign-in across the portfolio.
  Its availability and key rotation are platform responsibilities.

## Cost notes [#cost-notes]

* The move off Clerk was partly economic: Clerk "charges per MAU and makes a proprietary vendor the
  default login surface" (ADR 0003). Clerk's actual spend: **TBD**. EXO no longer uses Clerk; the
  account and subscription are the owner's to cancel.
* Convex Auth has no license fee. Its marginal cost is Convex function and storage usage:
  **TBD**.

## Status [#status]

* **Live on staging:** authorize page `https://staging.exo.now/id/authorize`, and token, revoke,
  JWKS, and discovery endpoints on `frugal-eel-930.convex.site/exo-id`.
* **Registered Apps:** `observatory`, `construct`, `rampart`, `dropship`, and `universe`, plus
  `exo-staging` (EXO's own browser session) and the headless `exo` API audience
  ([Exo Key guide](../../../docs/exo-id.md)).
* **Done:** Clerk is fully removed from EXO
  ([ADR 0012](../../../docs/adr/0012-clerk-removed-convex-auth-everywhere.md)). EXO's own `/sign-in`
  and the `staging.exo.now/{app}` gateway sign in through Exo Key as the `exo-staging` relying
  party, and staging access needs an exact non-production grant (`EXO_STAGING_GRANTS`).
* **In progress:** Apps outside EXO that still carry their own Clerk code move to Exo Key in their
  repositories ([convex-auth](../../../roadmap/items/convex-auth.md)).
* **Pending:** a version-bump pass before Convex Auth v2 GA (Q4).


---

# The stack

https://exo.now/docs/stack

> Twelve named dimensions, each with a preferred platform, a boundary, and a replacement path.



Exo names every dimension of an App's stack, the preferred platform for it, and the boundary that
keeps that platform replaceable. The table mirrors the stack on [exo.now](https://exo.now), whose
source of truth is the `dimensions` list in `src/components/ExoShell/index.tsx`. The platform
facts on each page are rendered from the integration catalog in
`src/platform/integrations/catalog.ts`, and a docs test checks every page against the landing
page's numbers, names, and platforms, so the landing page, the catalog, and these docs cannot drift
apart.

There are twelve dimensions. On 2026-10-05, Media moved into App state & storage, the CMS into
Design system (as pages), and CRM into Communications & support; Analytics is now Observability.

| #  | Dimension                                              | Exo platform              | Infrastructure                                        |
| -- | ------------------------------------------------------ | ------------------------- | ----------------------------------------------------- |
| 01 | [Control plane](./control-plane.mdx)                   | Exo                       | Hark (primary) + Hermes (open-source option)          |
| 02 | [Development framework](./framework.mdx)               | Exo                       | Next.js + TypeScript + React (proposed)               |
| 03 | [Source control](./source.mdx)                         | Exo                       | Entire                                                |
| 04 | [App state & storage](./app-state.mdx)                 | Exo                       | Convex + Convex Storage                               |
| 05 | [Identity](./identity.mdx)                             | Exo Key                   | Convex Auth                                           |
| 06 | [Design system](./design-system.mdx)                   | Exo Design                | shadcn/ui + Structured Liquidity + Construct (pages)  |
| 07 | [Deploy](./deploy.mdx)                                 | Exo Deploy (Exo Deploy)   | Railway (web) + Expo (mobile)                         |
| 08 | [Communications & support](./communications.mdx)       | Exo Broadcast             | Bird + Twenty (CRM)                                   |
| 09 | [Payments](./payments.mdx)                             | Exo Pay                   | Stripe + RevenueCat                                   |
| 10 | [Continuous integration](./continuous-integration.mdx) | EXO Verify (EXO Verify)   | Vitest + Playwright                                   |
| 11 | [Observability](./observability.mdx)                   | Exo Analyze (Exo Analyze) | PostHog + RevenueCat                                  |
| 12 | [Artificial intelligence](./ai.mdx)                    | Exo Router (Exo Router)   | Convex AI Gateway + Artificial Analysis + Arch-Router |

Where a layer's primary platform is not open source, its page names an **open-source option**
(the `openSource` field on the landing page's dimension and on the page's facts panel). Today the
control plane names Hermes; the other layers are filled in as their options are confirmed.

Source control and the control plane are recommended but optional: an app may opt out of either.

Each platform's catalog posture (core standard, ecosystem standard, optional adapter, or retiring
compatibility) and authority are shown in the panel at the top of its dimension page.

## How to read a dimension page [#how-to-read-a-dimension-page]

Each page answers the same five questions:

1. **What it is.** The job this dimension does for an App.
2. **Platform.** The platform, its catalog posture, and which system is authoritative.
3. **Boundary and replaceability.** What sits behind an Exo-owned contract, and what a swap would
   touch.
4. **Cost notes.** Only figures recorded in the ADRs and docs. Everything else is **TBD**. The
   [cost and unit economics](./economics.mdx) page collects every figure in one place.
5. **Status.** What is live, what is in progress, and what is only planned, with the evidence.

## Rule of thumb: who runs it, who remembers it [#rule-of-thumb-who-runs-it-who-remembers-it]

> Railway runs services; Convex holds state. If a piece of infrastructure primarily *runs*
> something, it belongs on Railway. If it primarily *remembers* something, it belongs in Convex.
> — [Infrastructure split](../../../docs/infrastructure.md)

## Retiring platforms [#retiring-platforms]

These stay in the catalog as `compatibility` entries until migration completes. They are not
choices for new work.

| Platform             | Replaced by            | Why                                                                                                                                                               |
| -------------------- | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Clerk                | Exo Key on Convex Auth | Per-MAU pricing and a second identity system beside the data; fully removed ([ADR 0012](../../../docs/adr/0012-clerk-removed-convex-auth-everywhere.md))          |
| Resend               | Bird                   | One communications layer for email, SMS, and voice ([roadmap](../../../roadmap/items/bird-communications.md))                                                     |
| GitHub               | Entire                 | Entire is the primary source host; GitHub is a compatibility mirror                                                                                               |
| Payload + PostgreSQL | Convex                 | Removed from the code (October 2026); the old database is kept only until its data is exported ([ADR 0003](../../../docs/adr/0003-convex-first-control-plane.md)) |


---

# 11 · Observability

https://exo.now/docs/stack/observability

> Exo Analyze keeps visits, sign-ups, conversion, and cost to serve in one Exo event layer; PostHog is a swappable dashboard adapter.



<StackFacts dimension="11 · Observability" platforms="[&#x22;posthog&#x22;, &#x22;revenuecat&#x22;]" via="Exo Analyze" status="Accepted (ADR 0006, 2026-09-27); planned." />

Renamed from Analytics (2026-10-05): the same Exo Analyze layer, named for what it does.

## What it is [#what-it-is]

Visits, sign-ups, conversion, and cost to serve land in one Exo event layer beside payments.
Dashboards are an adapter, so the provider can change without touching an App. The service design
is on the [Exo Analyze](../platform/exo-analyze.mdx) page.

## Platform [#platform]

* **PostHog** provides dashboards and session replay, sitting behind Exo as a swappable adapter.
  Plausible and GA4 remain possible adapters.
* **RevenueCat** supplies the paid step of the funnel for app-store purchases, beside Stripe for
  web payments (ADR 0006). Revenue events land in the ledger described on
  [Payments](./payments.mdx).

## Boundary and replaceability [#boundary-and-replaceability]

* Ingestion and schema live in Exo's Convex backend, partitioned per App and per environment. Apps
  send events to one Exo endpoint through a thin SDK (web and Expo).
* **Apps never integrate an analytics platform directly.** Changing platforms is an Exo adapter change.
* Event schema changes are platform decisions. Privacy and consent (cookie banners, data deletion)
  are centralized in Exo.

## Cost notes [#cost-notes]

From ADR 0006:

* At today's portfolio scale, event volume fits PostHog's free tier (\~1M events/month).
* The incremental cost is Convex storage and function usage for the event table, expected to be a
  few dollars per month.
* If volume outgrows the free tier, Exo can sample or aggregate before forwarding to the adapter
  while keeping full-fidelity funnel counts in Convex.

## Status [#status]

* **Planned** ([roadmap: product-analytics](../../../roadmap/items/product-analytics.md)).
* ADR 0006 is on `main` and is not yet on `staging`
  ([read on main](https://github.com/Interspace-Ventures/exo/blob/main/docs/adr/0006-exo-analyze-product-analytics.md)).


---

# 09 · Payments

https://exo.now/docs/stack/payments

> Stripe for web payments and RevenueCat for in-app purchases, normalized into one commercial record without making Exo a payment processor.



<StackFacts dimension="09 · Payments" platforms="[&#x22;stripe&#x22;, &#x22;revenuecat&#x22;]" status="Ingestion boundary and dashboard built; enabling real data per tenant needs human-approved provider configuration." />

## What it is [#what-it-is]

Payments and subscriptions are normalized into one commercial record and isolated from the
portable product core. Exo gives platform admins one private portfolio view while checkout,
entitlements, and provider-native subscription state stay with the tenant App
([revenue monitoring](../../../docs/revenue-monitoring.md)).

## Platform [#platform]

* **Stripe** is the payment, refund, dispute, and settlement source. It is Merchant of Record only
  for transactions through Stripe Managed Payments.
* **RevenueCat** is the app-store purchase and cross-platform entitlement source.

Both are **optional adapters** with the **App** as authority. A tenant that doesn't monetize
declares `commerce.profile: none` and stays fully conformant
([commerce contract](../../../docs/commerce-contract.md)).

## Boundary and replaceability [#boundary-and-replaceability]

* **Opt-in profiles:** `none`, `web-stripe` (Stripe Checkout and the Customer Portal own the web
  billing experience), `mobile-revenuecat`, and `hybrid`. `web-clerk-stripe` is accepted only in
  legacy schema 1.8 declarations.
* **Default governance for a monetized App:** `governanceModel: exo-managed`,
  `stripePaymentMode: managed-payments`, `merchantOfRecord: stripe`. This is a target policy, not
  evidence that billing is live.
* **What Exo stores:** non-secret provider coordinates, normalized events, source definitions,
  freshness, and reconciliation status. It never stores secrets, raw payment methods, full
  webhook payloads, or direct customer contact data.
* **Webhooks:** one URL per tenant and environment
  (`POST /api/billing/webhooks/{stripe|revenuecat}/:tenant?environment=staging`). HMAC
  verification uses a five-minute replay window, and tenant-scoped event keys make retries
  idempotent.
* **Commercial entitlements are not access grants.** An entitlement answers what a customer
  bought. An Exo Key grant answers whether an operator may access a resource.

<Callout title="Clerk Billing is gone" type="info">
  Clerk and Clerk Billing are removed from EXO
  ([ADR 0012](../../../docs/adr/0012-clerk-removed-convex-auth-everywhere.md)). Web billing is
  Stripe-only: Stripe Checkout and the Customer Portal own plans and subscriptions, and accounts
  key on the Exo ID user.
</Callout>

## Cost notes [#cost-notes]

* Stripe and RevenueCat fees at portfolio volume: **TBD**.
* **Reporting rules that protect the numbers:** currency totals are never converted or summed
  silently. Portfolio totals include only snapshots explicitly marked `include`. A RevenueCat
  cross-platform aggregate is excluded until overlap with Stripe is reconciled. Metrics a source
  cannot establish (such as MRR) show as unknown, never zero. Snapshots older than the freshness
  target (36 hours in the scaffold) are marked stale.

## Status [#status]

* **Built:** schema, secure ingestion boundary, and portfolio dashboard.
* **Pending per tenant:** product and plan selection, scoped RevenueCat V2 keys, webhook
  destinations, runtime secrets, and test events. Production activation is a separate approval.


---

# 03 · Source control

https://exo.now/docs/stack/source

> Entire is the primary Git-compatible source host and keeps agent sessions with source history.



<StackFacts dimension="03 · Source control" platforms="[&#x22;entire&#x22;]" status="Primary source host. GitHub remains a compatibility mirror during migration." />

## What it is [#what-it-is]

The source dimension answers: what exact code and agent context produced this result? Entire is
the primary source host and implementation-context authority for every managed project. It captures
repositories, branches, sessions, checkpoints, source, and review context
([build and deploy](../../../docs/build-and-deploy.md)).

## Platform [#platform]

**Entire** ([entire.io](https://entire.io)). Work is tracked as Entire trails. For example, the
roadmap's platform items link to trail `IV-170`. `IV-###` identifiers persist from the retired
work register.

## Boundary and replaceability [#boundary-and-replaceability]

* The contract is a plain Git repository with **no host-only runtime dependency**
  ([architecture](../../../docs/architecture.md)). Switching source hosts moves Git history and loses
  only the captured session context.
* GitHub may mirror source and change objects for ecosystem compatibility, but it must not become
  a second workflow (AGENTS.md). Linear is retired and must not be updated.
* Exo owns portfolio intent and release state. Entire owns source and execution provenance
  ([ADR 0003](../../../docs/adr/0003-convex-first-control-plane.md)).

<Callout title="Known inconsistency" type="warn">
  [How Exo builds and ships](../../../docs/build-and-deploy.md) still describes GitHub Actions
  running `pnpm check` and `pnpm verify`. The accepted EXO Verify decision (ADR 0005, on `main`)
  rejects GitHub Actions for verification because the owner is leaving GitHub. Until EXO Verify
  ships, treat the Actions description as legacy.
</Callout>

## Cost notes [#cost-notes]

Entire pricing: **TBD** (not recorded in the repository).

## Status [#status]

* **Live:** Entire is the primary remote for this repository; GitHub is the mirror.
* **Open question (ADR 0005):** does Entire expose webhooks or a checks API? The answer decides
  how EXO Verify is triggered. The CLI and polling fallbacks keep the design unblocked either way.


---

# Tenets

https://exo.now/docs/tenets

> The four principles behind every Exo decision, and the concrete rules that enforce them.



The four tenets appear on [exo.now](https://exo.now). This page pairs each one with the
repository rules that turn it into behavior.

## 1. Opinionated, not restrictive [#1-opinionated-not-restrictive]

> A clear operating model and reusable primitives—then enough room for every property to remain
> unmistakably itself.

What it means in practice:

* **One design foundation, many expressions.** The Structured Liquidity foundation is mandatory
  for every Exo-managed application: shadcn primitives, square geometry, a controlled visual
  vocabulary, and visible focus. Tenants still choose their own palette, typography, imagery, and
  content density through documented tokens
  ([design standard](../../docs/design-standard.md)). `pnpm check:ui-system` enforces the
  foundation, and it runs inside `pnpm lint`.
* **One promotion path.** Every managed App uses the same Preview → Staging → Release Candidate →
  Production order ([How work ships](./shipping/index.mdx)).
* **One named standard per dimension.** Each of the [twelve stack dimensions](./stack/index.mdx)
  names a preferred platform and the boundary it sits behind.

## 2. Open by preference [#2-open-by-preference]

> We start with inspectable, open-source foundations. Proprietary services must earn their place
> and stay behind a replaceable boundary.

What it means in practice:

* Open-source defaults where they exist: Convex Auth for identity, Expo (MIT) for mobile,
  Arch-Router (open weights, self-hosted) for Exo Router's difficulty scoring, Vitest and
  Playwright for continuous integration, Puck for composition, shadcn/ui for the design system, and Twenty for CRM.
* "Proprietary managed services may be used operationally, but a self-hosted path must remain
  documented" ([architecture](../../docs/architecture.md)).
* For any framework or platform choice, state the open-source status and license up front, and keep
  the free framework separate from the paid hosted service. For example, Expo is MIT and EAS is an
  optional paid service ([Exo Deploy](./platform/exo-deploy.mdx)).

## 3. Integrated, not locked in [#3-integrated-not-locked-in]

> Deeply integrated, never welded shut. Content exports as schema + NDJSON, media as files +
> checksums, design as tokens, routes with SEO and redirects, delivery as manifests, plus a
> deployable static archive. Leaving should be boring, complete, and documented.

What it means in practice:

* **Portability is a product contract.** Export is a normal authenticated action. It uses a
  documented, versioned format and needs no Exo account to read. There is no punitive exit fee;
  direct egress costs may be passed through
  ([portability](../../docs/portability.md)).
* **Adapters, not integrations.** Platforms sit behind Exo-owned adapters so that swapping one is an
  Exo change, not an App change. Examples: PostHog behind Exo Analyze, Bird behind the
  communications layer, and the Convex AI Gateway behind Exo Router.
* **Vendor-neutral names.** Sending subdomains are `mail.` and `inbox.`, never `bird.`, so a
  provider swap never touches DNS
  ([Bird roadmap item](../../roadmap/items/bird-communications.md)).

## 4. Automated, not unsupervised [#4-automated-not-unsupervised]

> Machines observe, compare, and prepare. Humans approve consequential production, billing,
> domain, and data changes.

What it means in practice:

* **Staging is automatic; consequential changes are not.** Validated commits deploy to Staging
  without an approval step. Creating a Release Candidate, promoting to Production, changing DNS,
  paid plans, data migrations, credentials, and external communications each keep an explicit
  human approval gate ([approval boundaries](./shipping/approvals.mdx)).
* **Failures are loud.** Exo Router fails a job, and records the failure, rather than falling
  back silently, because a silent fallback would hide a spend or configuration problem
  ([Exo Router](./platform/exo-router.mdx)). Routing decisions made without an assessor are flagged
  `fallback: true`.
* **Evidence over claims.** Nothing is called live until the deployed commit, health, access
  boundary, and browser behavior are verified.


---

# 0001: Reversible content backend

https://exo.now/docs/adr/0001-reversible-content-backend

> Status: superseded by ADR 0003 · Date: 2026-07-18



# ADR 0001: Reversible content backend [#adr-0001-reversible-content-backend]

* Status: superseded by [ADR 0003](0003-convex-first-control-plane.md)
* Date: 2026-07-18

> Historical decision only. Do not use this ADR to justify new Payload, PostgreSQL, or Puck work
> in EXO. ADR 0003 makes Convex the default authority for EXO-owned state and assigns Page editing
> and Puck to Construct.

## Decision [#decision]

Exo will use Payload with PostgreSQL, hosted initially by Neon, as its canonical content system. Neon is an operational provider rather than a domain dependency.

Convex remains an intentional future option. The current choice can be revisited after feedback from the Convex team or once Exo has workloads where Convex's reactive state model materially improves the product.

## Why this is not a configuration toggle [#why-this-is-not-a-configuration-toggle]

Payload currently has official adapters for PostgreSQL, MongoDB, and SQLite. Convex exposes a different execution, query, identity, and realtime model. A full switch therefore requires either a production-grade Payload database adapter or a replacement CMS persistence implementation.

Exo must make that migration controlled and testable, but must not describe it as effortless until those missing components exist.

## Portability requirements [#portability-requirements]

1. Application rendering reads canonical DTOs through `ContentRepository`.
2. Provider-specific code stays under `src/platform/content/<provider>` or Payload configuration and migrations.
3. Canonical records use UUIDv7 identifiers. Provider-native identifiers are mapping details.
4. No feature may require raw SQL outside migrations or a provider adapter.
5. Tenant exports use a versioned, newline-delimited JSON format with media manifests and checksums.
6. Imports are idempotent and preserve canonical IDs, timestamps, slugs, relationships, and publication state.
7. Tenant isolation, drafts, versions, access rules, and referential integrity must pass the same contract suite for every backend.
8. A change-data or event interface must be introduced before adding a second live backend; ad hoc dual writes are prohibited.
9. Production cutover requires reconciliation counts, relationship validation, media verification, and a documented rollback window.

## Convex activation gates [#convex-activation-gates]

A Convex implementation may become canonical only after it supports:

* all `ContentRepository` behavior;
* Payload-equivalent editorial writes or a replacement admin workflow;
* tenant-scoped authorization on every query and mutation;
* drafts, publishing, versions, redirects, forms, and media references;
* complete export and restore;
* migration rehearsal against a production-shaped tenant;
* operational backup, observability, and incident procedures.

## Near-term use of Convex [#near-term-use-of-convex]

Convex may be added earlier as an optional realtime service for presence, deployment progress, preview state, notifications, or activity feeds. Durable CMS records remain in the canonical content backend until the activation gates are met.


---

# 0002: Clerk identity and Exo authorization boundary

https://exo.now/docs/adr/0002-clerk-identity-boundary

> Status: superseded by 0003-convex-auth-v2-identity-plane.md · Date: 2026-08-26



# ADR 0002: Clerk identity and Exo authorization boundary [#adr-0002-clerk-identity-and-exo-authorization-boundary]

* Status: superseded by [0003-convex-auth-v2-identity-plane.md](0003-convex-auth-v2-identity-plane.md)
* Date: 2026-08-26

## Context [#context]

Exo needs strong multi-tenant identity, organization membership, MFA, passkeys, session
revocation, and future enterprise SSO. It must also avoid making a proprietary identity provider
the canonical owner of tenant authorization or data identity.

## Decision [#decision]

Use Clerk as the target authentication provider for an environment-specific Exo identity
plane. Exo-owned Apps share that plane so a person does not need a product-specific identity for
each repository or domain. Map one Clerk user to one Exo ID within the plane and authenticate
Clerk sessions through a Payload custom strategy or the App's Exo authorization adapter.

Exo remains responsible for authorization. Resource-local grants carry canonical scopes,
roles, explicit capabilities, validity, and revocation state. Clerk Organizations may mirror coarse
Workspace membership, but Organization membership is neither the canonical resource graph nor
sufficient authorization.

Production and non-production use isolated identity planes. A separate identity plane for a client
application's customers is allowed only when the application has an independently owned user
population, legal boundary, or identity adapter. Another Exo-owned App does not qualify merely
because it has a different repository or domain.

## Consequences [#consequences]

* Privileged users receive Clerk MFA, session management, recovery, and Organization context.
* Exo-owned Apps no longer provision separate Clerk identities per product.
* Exo must implement and test a custom Payload authentication strategy.
* Clerk webhooks are lifecycle synchronization, not synchronous authorization.
* The platform must maintain Exo identities, environment-specific external mappings,
  resource grants, account-linking safeguards, and audited revocation.
* Claims remain compact; resource authorization stays server-side.
* Privileged actions require recent factor verification and an audit event.
* Identity exports omit credentials and retain portable canonical user and membership IDs.
* A future provider adapter or self-hosted identity implementation remains possible.

## Migration gate [#migration-gate]

Payload local authentication remains active until Clerk staging proves:

1. account linking without duplicate or hijacked users;
2. production/non-production identity-plane isolation;
3. App, Construct, and Construct Page grant enforcement;
4. MFA and step-up flows;
5. cross-App session, grant-revocation, and membership-removal behavior;
6. webhook replay and outage behavior;
7. recovery and break-glass access;
8. rollback to the previous admin login path.

Disabling Payload's local strategy requires a separate approved production migration.


---

# 0003: Convex Auth v2 as the shared identity plane

https://exo.now/docs/adr/0003-convex-auth-v2-identity-plane

> Status: accepted · Date: 2026-09-23



# ADR 0003: Convex Auth v2 as the shared identity plane [#adr-0003-convex-auth-v2-as-the-shared-identity-plane]

* Status: accepted
* Date: 2026-09-23
* Supersedes: [0002-clerk-identity-boundary.md](0002-clerk-identity-boundary.md)
* Amended by: [0004-exo-id-central-identity-provider.md](0004-exo-id-central-identity-provider.md) — Exo is the sole OAuth client and identity provider; apps are relying parties.

## Context [#context]

ADR 0002 chose Clerk as the authentication provider for the Exo identity plane, with Exo
retaining authorization. Since then, two things changed:

1. Convex Auth reached a v2 architecture (components: a core that mints RS256 JWTs and owns
   sessions, plus provider components for password/username, OAuth, passkeys, email). Per the
   Convex team it is stable enough for deployment now and reaches GA by Q4; Interspace product
   scale stays small until then, so preview risk is acceptable.
2. Clerk charges per MAU and makes a proprietary vendor the default login surface for every
   Exo-owned app, while all of our app state already lives in Convex. Keeping identity in
   the same system that owns the data removes a vendor, a webhook surface, and a whole
   identity-mapping layer.

Rampart shipped Convex Auth v2 with Google and Apple OAuth in September 2026 and is the
reference implementation for the platform pattern.

## Decision [#decision]

Convex Auth v2 is the shared identity plane for every Interspace application. Exo-owned
apps use it for authentication; Exo retains authorization exactly as before (resource-local
grants, roles, capabilities, validity, revocation).

The platform pattern, proven in Rampart:

* ~~Mount the auth **core component** (`httpPrefix: "/auth"`, `AUTH_PRIVATE_KEY`/`AUTH_JWKS`
  env) plus whichever **provider components** the app needs (password/username, OAuth
  Google/Apple, passkeys) in the app's `convex/convex.config.ts`.~~ &#x2A;*Superseded by [ADR 0004](0004-exo-id-central-identity-provider.md):** only Exo mounts the
  core and provider components; apps verify Exo ID tokens with `customJwt` and mount none.
* `convex/auth.config.ts` verifies core JWTs as `customJwt` against the deployment JWKS at
  `${CONVEX_SITE_URL}/auth/.well-known/jwks.json`.
* Each provider wires `setup*` with `attachUserCallbacks({ createUser })`; the app owns its
  `users` table and the profile mapping. Provider components own accounts/sessions.
* Clients use `@convex-dev/auth/react` (`ConvexAuthProvider` with
  `api: { refreshSession, signOut }`) and the provider hooks
  (`useSignInWithGoogle`, `useSignInWithApple`, password helpers). Route protection is
  client-side (`Authenticated`/`Unauthenticated`); v2 ships no Next.js middleware helper.
* Backend identity is always derived server-side (`getAuthUserId` or
  `ctx.auth.getUserIdentity().tokenIdentifier`); never accept a caller-supplied owner ID.

A separate identity plane for a client application's customers remains allowed under the same
conditions as ADR 0002 (independently owned user population, legal boundary, or identity
adapter) — implemented with Convex Auth v2, not Clerk.

## Consequences [#consequences]

* Clerk is no longer the platform authentication provider. Clerk dependencies are removed from
  each app as it migrates; apps with existing Clerk user populations (Construct, Observatory)
  need a per-app user-mapping or re-login plan before cutover.
* ~~OAuth provider approval is platform-level: the approved catalog (Google, GitHub; Apple per-app)
  is defined in `docs/exo-auth-contract.md` in Construct and mounted uniformly by every Exo app.
  A provider added for one app is added to the catalog and swept into every app.~~ &#x2A;*Superseded by [ADR 0004](0004-exo-id-central-identity-provider.md):** the
  provider catalog (Google, GitHub; Apple when added) is mounted once, at Exo. There is no
  per-app Apple or any other per-app provider.
* ~~OAuth client registration (Google Cloud Console, GitHub, Apple Developer) is per-provider platform
  setup, recorded in the app's env contract; Convex Auth does not broker those registrations.~~ &#x2A;*Superseded by [ADR 0004](0004-exo-id-central-identity-provider.md):**
  Exo holds the only OAuth client registrations; apps never touch Google Cloud Console, GitHub
  OAuth settings, or Apple Developer.
* v2 is pre-GA: pin the alpha version per app, track the `reboot` branch, and budget a
  version-bump pass before Q4. Upgrades are contained to `convex/auth.ts`,
  `convex/auth.config.ts`, `convex/convex.config.ts`, and the client provider.
* The ADR 0002 migration gate is retired with it; identity verification moves to per-app
  auth/isolation tests plus the Convex Auth component contract tests.
* Payload local authentication in EXO itself remains the admin fallback until the Convex-native
  frontend ships; this ADR governs the app identity plane, not Payload's internal admin login.


---

# 0003: Convex-first EXO control plane

https://exo.now/docs/adr/0003-convex-first-control-plane

> Status: accepted · Date: 2026-09-14



# ADR 0003: Convex-first EXO control plane [#adr-0003-convex-first-exo-control-plane]

* Status: accepted
* Date: 2026-09-14
* Historical work reference: IV-173 (retired)
* Supersedes: [ADR 0001](0001-reversible-content-backend.md)

## Decision [#decision]

EXO is the application platform and control plane. It owns projects, organizations and access,
environments, provider provisioning, deploy/release/rollback state, runtime-dependency metadata,
policy and security controls, audit evidence, orchestration, health, and bounded usage and cost
aggregates.

Convex is the default persistence, transaction, function, and reactive-state layer for every state
EXO owns. An exception requires a concrete workload constraint, a named authority, reconciliation
and rollback rules, and an exit path. An inherited Payload or PostgreSQL implementation is not a
constraint.

Construct owns Page and website editing, Puck, publishing, Page authorization, and authenticated
Surfaces such as Interspace/ION. Construct persists those records against its own Convex-native
model. Payload is not moved into Construct by default; a future external-CMS adapter requires a
demonstrated integration need and remains optional rather than authoritative.

Independent Apps own their domain records. EXO provisions and observes their resources and accepts
only bounded operational observations. It does not centralize their prompts, content, user data,
scientific datasets, financial ledgers, or other domain records.

## Legacy freeze [#legacy-freeze]

Payload, PostgreSQL, and Puck in this repository are migration sources and rollback material only.
No new EXO capability may add a Payload collection, Payload/Puck dependency, or Puck implementation
file. `pnpm check:legacy-backend-freeze` enforces the checked-in baseline. Removing items from that
baseline is allowed; extending it requires changing this ADR and explicit architecture review.

Bug fixes needed to export, reconcile, secure, or retire the legacy path are allowed when they do
not increase its product scope. The normal approval gates still apply to data writes, provider
changes, Release Candidate, Production, domains, billing, and resource deletion.

## Authority rules [#authority-rules]

The collection-by-collection disposition is maintained in the
[EXO legacy authority and migration map](../exo-legacy-authority-map.md). A record has exactly one
authority during every cutover phase:

1. External providers remain authoritative for live deployment, billing, source, and health facts.
2. EXO owns portfolio and release coordination; Entire owns source and execution provenance.
3. Clerk authenticates identities; EXO Convex owns resource grants and authorization state.
4. EXO Convex owns control-plane intent, orchestration state, bounded observations, and audit
   evidence.
5. Construct Convex owns Pages, content, revisions, Puck documents, publishing, and Page/Surface
   access.
6. Independent Apps own domain data and expose only versioned contracts and bounded observations to
   EXO.

## SQL, analytical index, and object-storage exceptions [#sql-analytical-index-and-object-storage-exceptions]

SQL is not retained merely because a legacy schema exists. A SQL exception must identify a workload
Convex cannot reasonably satisfy, such as a required external protocol or specialized query shape,
and must document synchronization and failure behavior. No current EXO control-plane collection has
such an approved exception.

Large binary objects do not belong in Convex documents. Blob bytes may stay in Convex file storage
or an external object store when size, transformation, retention, egress, or interoperability
requirements justify it. Convex remains authoritative for ownership, checksum, metadata, lifecycle,
and the opaque object reference.

Specialized analytical or search indexes may remain external when their workload requires them.
They are derived and rebuildable, never the sole authority. EXO stores only bounded aggregate state
and freshness/reconciliation evidence.

## Cutover shape [#cutover-shape]

The migration is staged and reversible:

1. freeze the legacy feature surface and inventory every record family;
2. widen Convex schemas, validators, indexes, auth checks, receipts, and bounded audit logs;
3. export deterministic, secret-free, versioned source bundles with counts and hashes;
4. dry-run transforms and reconcile identifiers, relationships, constraints, and totals;
5. import through idempotent receipts into isolated Staging after the normal data-write approval;
6. shadow-compare reads without making two systems authoritative;
7. cut Staging reads and writes to Convex, then prove auth, isolation, index, idempotency, audit,
   retention, rollback, and recovery parity;
8. separately approve any Production migration and retain the frozen source for the rollback window;
9. remove EXO Payload, PostgreSQL, and Puck code and dependencies; then retire provider resources
   only through their explicit approval gates.

Rollback restores the last single authority and replays only receipt-backed operations. Ad hoc
dual writes are prohibited.


---

# 0004: Exo ID — Exo is the central identity provider; apps are relying parties

https://exo.now/docs/adr/0004-exo-id-central-identity-provider

> Status: accepted · Date: 2026-09-25



# ADR 0004: Exo ID — Exo is the central identity provider; apps are relying parties [#adr-0004-exo-id--exo-is-the-central-identity-provider-apps-are-relying-parties]

* Status: accepted
* Date: 2026-09-25
* Amends: [0003-convex-auth-v2-identity-plane.md](0003-convex-auth-v2-identity-plane.md)

## Context [#context]

ADR 0003 made Convex Auth v2 the shared identity plane but described it as a per-app pattern:
every app mounted its own auth core and OAuth provider components and registered its own OAuth
clients with Google, GitHub, and Apple. That gives each person a separate identity per app,
spreads provider secrets and callback registrations across deployments, and forces every app
team into Google Cloud Console and Apple Developer.

Owner directive (2026-09-25): "I'd love for you to do anything related to sign-on to exist at the
Exo level. There shouldn't be multiple apps that need access to the cloud console — Exo is the
platform provisioning IDs to apps like Construct or Observatory."

## Decision [#decision]

**Exo is the sole OAuth client and identity provider for Interspace apps ("Exo ID").** Apps
(Observatory, Construct, and every future app) are relying parties.

* Exo's Convex deployment mounts Convex Auth v2 (core + Google/GitHub OAuth; Apple when added)
  and holds the only provider registrations and callback URLs
  (`<exo-convex-site>/oauth/{google,github}/callback`).
* Exo issues **app-scoped** RS256 access tokens from a dedicated Exo ID signing key
  (`EXO_ID_PRIVATE_KEY`/`EXO_ID_JWKS`), separate from Exo's own session keys.
* Apps verify Exo ID JWTs with a `customJwt` provider in their `convex/auth.config.ts`:
  `issuer: ${EXO_SITE}/exo-id`, `applicationID: <app>`,
  `jwks: ${EXO_SITE}/exo-id/.well-known/jwks.json`, `algorithm: 'RS256'`.
* Apps **never** mount the auth core or OAuth provider components, never hold `AUTH_*` or
  provider client secrets, and never register with Google, GitHub, or Apple.
* Onboarding an app is a code change to Exo's registry (`convex/lib/exoIdApps.ts`), reviewed in
  this repo.
* Authorization (who may use which app, roles, tenant grants) stays with the app in v1. Exo
  proves identity and includes the user's email; it does not block issuance on app allowlists.

## Contract (v1) [#contract-v1]

Issuer: `ISSUER = ${CONVEX_SITE_URL}/exo-id` (staging:
`https://frugal-eel-930.convex.site/exo-id`).

Registry (`convex/lib/exoIdApps.ts`): per app, `redirectUris` (exact match) and `origins` (CORS).

1. **Authorize page** (Exo web): `GET /id/authorize?app&redirect_uri&state&code_challenge&code_challenge_method=S256`.
   Exo validates app and redirect URI server-side; invalid requests render an error and never
   redirect. Unauthenticated visitors sign in to Exo (Google/GitHub via Convex Auth v2, returning
   to the same URL). Authenticated visitors trigger mutation `exoId.authorize`, then
   `location.replace(redirect_uri?code&state)`.
2. **`exoId.authorize`** (mutation): requires an authenticated Exo user; validates app,
   redirect URI, and an S256 challenge; stores only `sha256(code)` in `exoIdCodes` with a
   60-second expiry; codes are single-use.
3. **`POST ${ISSUER}/v1/token`** (JSON):
   * `authorization_code`: `{app, code, redirect_uri, code_verifier}` — code must exist, be
     unused and unexpired, match app and redirect URI, and satisfy PKCE S256. Any redemption
     attempt consumes the code.
   * `refresh_token`: `{app, refresh_token}` — rotates the refresh token (hashes in
     `exoIdSessions`, 30-day lifetime, 30-second grace for the previous token).
   * 200: `{access_token, token_type: 'Bearer', expires_in, refresh_token, user: {id, email, name}}`.
   * 400: `{error: 'invalid_request' | 'invalid_grant' | 'unsupported_grant_type'}`.
   * CORS: preflight allowed for any registered origin; POST responses echo the Origin only
     when it belongs to the body's app.
   * Access token: RS256, header `kid` from `EXO_ID_JWKS`; claims `iss`, `aud=<app>`,
     `sub=<exo user id>`, `email`, `email_verified` (boolean), `name`, `iat`, `exp = iat + 900`.
4. **`POST ${ISSUER}/v1/revoke`** `{app, refresh_token}` → always 200.
5. **`GET ${ISSUER}/.well-known/jwks.json`** → public keys only, `Cache-Control: public, max-age=300`, CORS `*`.
6. **`GET ${ISSUER}/.well-known/openid-configuration`** → `issuer`, `jwks_uri`, `token_endpoint`,
   `authorization_endpoint` (from deployment env `EXO_ID_AUTHORIZE_URL`).

Integration guide: [docs/exo-id.md](../exo-id.md).

## Consequences [#consequences]

* ADR 0003's per-app provider mounting, per-app OAuth registration, and "Apple per-app" bullets
  are superseded (marked in 0003). The rest of 0003 (Convex Auth v2 as the technology, alpha
  pinning, server-side identity derivation) stands.
* Apps that already mounted their own Convex Auth components (e.g. Observatory, Rampart,
  Construct's in-flight work) migrate to `customJwt` verification of Exo ID tokens and remove
  their provider components and secrets.
* Exo's Convex deployment becomes a hard dependency for sign-in across the portfolio; its
  availability and key rotation are platform responsibilities.
* A person has one Exo user id (`sub`) across apps; apps key their own records on it.
* Exo's Clerk-backed `/sign-in` stays in place until Construct leaves Clerk; Exo ID lives at
  `/id/authorize` and does not depend on Clerk.


---

# 0005: exo-verify — code verification as a platform service

https://exo.now/docs/adr/0005-exo-verify-verification-service

> Status: accepted (owner-approved 2026-09-27) · Date: 2026-09-27



# ADR 0005: exo-verify — code verification as a platform service [#adr-0005-exo-verify--code-verification-as-a-platform-service]

* Status: accepted (owner-approved 2026-09-27)
* Date: 2026-09-27
* Roadmap: [`exo-verify`](../../roadmap/items/exo-verify.md)

## Context [#context]

Exo provisions shared platform services to every app so apps do not spend time on basic
plumbing. Verification is not one of them yet: each app carries ad-hoc unit tests, and
staging failures slip through. Example: the AI router passed all unit tests and review but
fell back 100% of the time on staging, because a fix never merged. Only a live behavioral
check against the deployed environment catches that class of failure.

Two constraints shape the design. The owner is moving off GitHub to Entire as the forge, so
GitHub Actions and GitHub-app PR bots are out. And most commits are made by agents
(\~300/month across the portfolio), so per-seat vendor pricing is the wrong unit.

## Decision [#decision]

Exo provides verification as a shared service, **exo-verify**, in four layers:

1. **Standard verify contract.** Every repo declares `verify` commands (`typecheck`, `lint`,
   `test`, `smoke`) in `project.yaml`. Exo runs what is declared; a missing command is a
   visible gap, not a silent pass.
2. **Sandboxed runs on Railway workers** (not GitHub Actions). Trigger order of preference:
   an Entire webhook if Entire exposes one; otherwise an `exo verify` CLI that agents run
   before pushing; otherwise Exo polling. In the sandbox an agent may also write targeted
   tests for the change and run them (TREX-style runtime validation).
3. **Post-deploy live checks against staging:** health, auth (Exo ID JWKS, OAuth callbacks),
   and 2–3 declared user journeys per app driven by Playwright or a browser agent.
4. **AI review pass** using an open-source engine (PR-Agent / Kodus-style) or our own
   prompts. All model calls go through the Exo AI broker, so cost is logged per app in
   `exoAiBrokerJobs` and the router keeps easy diffs on cheap models.

Results are stored in Convex as release gates, attached to the Entire trail, and gate
staging → production promotion.

## Unit economics [#unit-economics]

| Option               | Pricing                                                                                                                                        | Est. monthly at \~300 agent commits |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- |
| **exo-verify (own)** | \~$0.007 compute per 5-min 2 vCPU / 2 GB Railway run + $0.01–0.10 model tokens per run                                                         | **\~$6–30**                         |
| Greptile Pro         | $30/seat/mo, 50 credits/seat, $1/extra credit; TREX runtime validation (beta) costs more credits; non-GitHub/GitLab forges are Enterprise-only | \~$300–900                          |
| CodeRabbit           | $24–30/user; CLI free at 3 reviews/hr on local git                                                                                             | per seat; CLI usable as interim     |
| Graphite             | $40/user                                                                                                                                       | per seat, GitHub-bound              |

Cost per verified commit for the own service is roughly $0.02–0.10, recorded per app so it
shows up in each app's cost-to-serve.

## Alternatives considered [#alternatives-considered]

* **Greptile** — strongest vendor review and has runtime validation, but priced per seat plus
  credits and requires Enterprise for a non-GitHub/GitLab forge. Kept as a later option for
  the review layer (Greptile Enterprise) if our review quality falls short.
* **CodeRabbit** — the free CLI reviews a local diff, so agents can use it pre-push as an
  interim second opinion while layer 4 is built. Not the system of record.
* **Graphite** — GitHub-centric stacking/review; does not fit the Entire forge.
* **GitHub Actions** — rejected; the owner is leaving GitHub.

## Consequences [#consequences]

* Every app gains a `verify` block in `project.yaml`; onboarding an app includes declaring
  its 2–3 golden journeys.
* Vendor review engines are more tuned than ours at first. We track catch rate against false
  positives per layer and revisit buying the review layer if the gap persists.
* Live checks need a staging-only sign-in path for bots (seeded Exo ID test identities),
  because browser agents cannot pass Google's bot detection.
* **Open question:** does Entire expose webhooks or a checks API? Verify before building the
  trigger; the CLI and polling fallbacks keep the design unblocked either way.


---

# 0006: exo-analyze — Exo-owned product analytics

https://exo.now/docs/adr/0006-exo-analyze-product-analytics

> Status: accepted (owner-approved 2026-09-27) · Date: 2026-09-27



# ADR 0006: exo-analyze — Exo-owned product analytics [#adr-0006-exo-analyze--exo-owned-product-analytics]

* Status: accepted (owner-approved 2026-09-27)
* Date: 2026-09-27
* Roadmap: [`product-analytics`](../../roadmap/items/product-analytics.md) (exo-analyze)

## Context [#context]

Exo provisions shared platform services to every app so apps do not spend time on basic
plumbing. Today RevenueCat and Stripe tell us about payments only. We cannot see visits,
sign-up conversion, or what it costs to serve a user, so we cannot compute per-app unit
economics or margin. Letting each app wire its own analytics vendor would repeat the
integration per app and scatter the data.

## Decision [#decision]

Exo owns product analytics as a platform service, **exo-analyze**:

* **Ingestion and schema live in Exo's Convex backend**, partitioned per app and per
  environment. Apps send events to one Exo endpoint through a thin SDK (web + Expo).
* **Exo stores the funnel end to end:** visits → sign-ups (Exo ID) → paid (RevenueCat /
  Stripe webhooks) → cost-to-serve (`exoAiBrokerJobs`, plus exo-verify and infra cost). That
  join gives per-app unit economics: acquisition, conversion, revenue per user, and margin.
* **PostHog sits behind Exo as a swappable adapter** for dashboards and session replay (free
  tier \~1M events/month). Plausible and GA4 remain possible adapters.
* **Apps never integrate an analytics vendor directly.** Changing vendors is an Exo adapter
  change, not an app change.

## Unit economics [#unit-economics]

* At portfolio scale today, event volume fits PostHog's free tier; the incremental cost is
  Convex storage/function usage for the event table, expected to be a few dollars per month.
* Each app's dashboard reports cost per active user and gross margin per paying user, so
  feature design can be judged on cost per unit and margin, not revenue alone.
* If volume outgrows the free tier, Exo can sample or aggregate before forwarding to the
  adapter while keeping full-fidelity funnel counts in Convex.

## Alternatives considered [#alternatives-considered]

* **Per-app PostHog/Plausible/GA4** — fastest for one app, but no cross-app funnel, no join to
  Exo ID or broker cost, and vendor lock-in in every codebase.
* **PostHog as system of record** — good dashboards, but ties revenue and cost-to-serve joins
  to a vendor schema; kept as an adapter instead.
* **RevenueCat/Stripe only** — status quo; payments without the top of the funnel or cost.

## Consequences [#consequences]

* Exo ships and versions the analytics SDK; exo-mobilize bakes it into the mobile template.
* Event schema changes are platform decisions, reviewed like any other Exo contract.
* Privacy/consent handling (cookie banners, data deletion) is centralized in Exo.


---

# 0007: exo-mobilize — Expo mobile release toolkit

https://exo.now/docs/adr/0007-exo-mobilize-mobile-release-toolkit

> Status: accepted (owner-approved 2026-09-27) · Date: 2026-09-27



# ADR 0007: exo-mobilize — Expo mobile release toolkit [#adr-0007-exo-mobilize--expo-mobile-release-toolkit]

* Status: accepted (owner-approved 2026-09-27)
* Date: 2026-09-27
* Roadmap: [`mobile-delivery-pipeline`](../../roadmap/items/mobile-delivery-pipeline.md) (exo-mobilize)

## Context [#context]

Exo provisions shared platform services to every app so apps do not spend time on basic
plumbing. Shipping to iOS and Android involves the same setup every time: app config,
signing, store listings, TestFlight, native sign-in redirects, analytics. Our stack is
TypeScript, React, Convex, and Exo ID, and most code is written by agents. Universe already
ships on Expo and is the reference.

## Decision [#decision]

* **Expo is the standard mobile framework for Exo apps.** Expo is an MIT-licensed framework on
  React Native.
* **EAS is optional and swappable** for build, submit, and OTA updates. Start on EAS Free
  (15 iOS + 15 Android builds/month, OTA to 1K MAU); move to Starter ($19/month plus usage,
  includes $45 build credit) when several apps ship; run our own Mac builder
  (`eas build --local` / fastlane) only at high volume.
* **exo-mobilize owns the release template:** app config, signing, TestFlight and store
  listings, Exo ID sign-in via a native redirect scheme (e.g. `universe://auth/callback`),
  and the exo-analyze SDK built in.
* **The PWA-required-for-mobile repository contract gate stays.**

## Unit economics [#unit-economics]

| Stage                   | Fixed cost             | Marginal cost                                      |
| ----------------------- | ---------------------- | -------------------------------------------------- |
| Apple Developer Program | $99/year (required)    | —                                                  |
| EAS Free                | $0                     | capped at 15+15 builds/month, OTA to 1K MAU        |
| EAS Starter             | $19/month              | usage beyond the $45 build credit                  |
| Own Mac builder         | hardware or hosted Mac | \~$0 per build; pays off only at high build volume |

At current volume the toolkit costs \~$8/month (Apple fee amortized). Move up a tier only when
build counts or OTA MAU exceed the current tier.

## Alternatives considered [#alternatives-considered]

| Option                         | Why not                                                         |
| ------------------------------ | --------------------------------------------------------------- |
| Bare React Native              | Same runtime without Expo's build, config, and OTA tooling      |
| Flutter                        | Dart rewrite, no official Convex client, OTA only via Shorebird |
| Capacitor / Ionic              | Webview feel                                                    |
| Kotlin / Compose Multiplatform | Kotlin rewrite                                                  |
| Lynx (ByteDance)               | Too young                                                       |
| Native Swift + Kotlin          | Two codebases                                                   |

Expo wins: open source, reuses our TS/React/Convex/Exo ID code, native UI, OTA updates, and
agents are fluent in it.

## Consequences [#consequences]

* New mobile apps start from the exo-mobilize template, not from scratch.
* EAS is a service choice, not a dependency: the template must keep a local-build path.
* Each app registers its native redirect scheme with Exo ID as part of onboarding.


---

# 0009: Exo Key headless sign-in — device authorization grant and personal Exo Keys

https://exo.now/docs/adr/0009-exo-key-headless-sign-in

> Status: proposed · Date: 2026-10-07



# ADR 0009: Exo Key headless sign-in — device authorization grant and personal Exo Keys [#adr-0009-exo-key-headless-sign-in--device-authorization-grant-and-personal-exo-keys]

* Status: proposed
* Date: 2026-10-07
* Extends: [0004-exo-id-central-identity-provider.md](0004-exo-id-central-identity-provider.md),
  ADR 0008 (Exo Platform API, Exo Key API tokens; PR #145)

## Context [#context]

Owner decision: Construct and Exo must be usable headlessly from agents (Hermes, Claude Code,
Codex, T3 Code) through MCP servers and CLIs. Sign-in lives only at Exo (Exo Key); apps trust
Exo-issued RS256 JWTs (`aud` = app). ADR 0004 covers browser apps (authorization code + PKCE with
registered redirect URIs). A CLI or agent has no redirect URI it can register, and a script or
server must not hold a person's browser session.

## Decision [#decision]

Exo Key adds two headless paths. Both end in the same access token apps already verify (issuer
`${EXO_CONVEX_SITE}/exo-id`, `aud` = app, RS256, 15 minutes), plus a `scope` claim.

### 1. Device authorization grant (RFC 8628), for interactive CLIs and agents [#1-device-authorization-grant-rfc-8628-for-interactive-clis-and-agents]

* `POST {prefix}/v1/device/code` with `{app, scope?}` (JSON or form) returns `device_code`,
  `user_code` (8 characters `XXXX-XXXX` from the RFC 8628 §6.1 consonant alphabet
  `BCDFGHJKLMNPQRSTVWXZ`), `verification_uri` (`https://<exo>/id/device`),
  `verification_uri_complete` (`…?code=XXXX-XXXX`), `expires_in` 600 and `interval` 5.
* The person opens `/id/device` on the Exo frontend, signs in to Exo if needed (returning to the
  same URL), sees the app and requested scope, and approves or denies. Approval binds the grant
  to the signed-in Exo user.
* The CLI polls `POST {prefix}/v1/token` with
  `grant_type=urn:ietf:params:oauth:grant-type:device_code`, `device_code`, `app`. Errors are
  RFC 8628 §3.5: `authorization_pending`, `slow_down` (with the new `interval`; each one adds 5 s),
  `access_denied`, `expired_token`; unknown/redeemed codes or the wrong app give `invalid_grant`.
  An approved grant returns the same body as the authorization-code grant (access token, rotating
  refresh token, user) and is single-use.
* The granted `scope` is stored on the refresh-token family and carried to every refreshed
  access token.

### 2. Personal Exo Keys + token exchange (RFC 8693), for scripts, servers and unattended agents [#2-personal-exo-keys--token-exchange-rfc-8693-for-scripts-servers-and-unattended-agents]

* A signed-in Exo user creates, lists and revokes their own Exo Keys (`exoKeyTokens.createMine`,
  `listMine`, `revokeMine`; UI at `/id/keys`). They reuse the ADR 0008 phase 1 table and format
  (`exo_tk_<prefix>_<secret>`, SHA-256 at rest, prefix as the public handle, 1–365 day TTL) and
  are owned by the user (`userId`) under the user's personal tenant `user-<userId>`. The secret is
  returned once. Up to 10 live keys per user; creation is rate-limited per user.
* Scopes: `read` (default), `write`, `ai`, `usage:read`. Apps define what `read`/`write` permit;
  `ai`/`usage:read` keep their platform-API meaning.
* A key is bound to one app or to all headless apps.
* `POST {prefix}/v1/token` with `grant_type=urn:ietf:params:oauth:grant-type:token-exchange`,
  `subject_token=<key>`, `subject_token_type=urn:exo:token-type:exo-key`, `app`, and optional
  `scope` (must be a subset of the key's scopes) returns a 15-minute access token (`aud` = app,
  `sub` = key owner, `scope` claim), `issued_token_type`
  `urn:ietf:params:oauth:token-type:access_token`, and **no refresh token**: the key is the
  long-lived credential and every exchange re-checks it. Errors: `invalid_grant` (unknown,
  revoked, expired, or an operator tenant token with no user), `invalid_target` (the key is bound
  to another app, or the app has not opted in), `invalid_scope`.
* Operator-minted tenant tokens (ADR 0008, no `userId`) are never exchangeable: they identify a
  tenant, not a person.

### Registry opt-in [#registry-opt-in]

`convex/lib/exoIdApps.ts` gains `label` and `deviceFlow`. Only apps with `deviceFlow: true`
accept device grants and app-bound personal keys. Enabled: `construct`, and `exo` (new; Exo's own
API and CLI, `aud` `exo`, no redirect URIs so the browser flow can never target it).

### Security [#security]

* `device_code` and Exo Keys are stored only as SHA-256. Comparisons happen through hash-index
  lookups; the existing constant-time compare covers PKCE and refresh successors.
* User codes are short, so they are only resolvable by a signed-in Exo user, through a
  rate-limited mutation (10/min per user). That is about 2.6e10 codes against at most a
  handful of pending grants per 10 minutes. A decided or expired code no longer resolves.
* `device/code` is rate-limited per client IP (rightmost `X-Forwarded-For`, as for `/v1/token`),
  and every token request still hits the existing per-IP limit. Exchange is additionally limited
  per key.
* The consent screen tells the person to approve only a code they started.

## Consequences [#consequences]

* Agents get two documented options: device flow for a person at a terminal, an Exo Key for
  unattended use. Neither needs a redirect URI or a browser cookie.
* Apps change nothing to accept these tokens. To act on `scope`, they read the `scope` claim.
* The `/id/device` and `/id/keys` pages ship with the Exo frontend. `verification_uri` is derived
  from `EXO_ID_AUTHORIZE_URL` (override: `EXO_KEY_DEVICE_URL`).
* Follow-ups: an `exo login` CLI and MCP helper on top of the device flow; per-app scope
  catalogs; showing active device sessions on `/id/keys`.


---

# 0010: Exo Key OAuth for third-party MCP clients (CIMD, DCR, resource indicators)

https://exo.now/docs/adr/0010-exo-key-mcp-oauth

> Status: proposed · Date: 2026-10-08



# ADR 0010: Exo Key OAuth for third-party MCP clients (CIMD, DCR, resource indicators) [#adr-0010-exo-key-oauth-for-third-party-mcp-clients-cimd-dcr-resource-indicators]

* Status: proposed
* Date: 2026-10-08
* Extends: [0004-exo-id-central-identity-provider.md](0004-exo-id-central-identity-provider.md),
  [0009-exo-key-headless-sign-in.md](0009-exo-key-headless-sign-in.md)

## Context [#context]

Construct (`https://construct.page/mcp`, staging `https://staging.construct.page/mcp`) and Exo
(`<exo convex site>/mcp`) are remote MCP servers. Users connect them from their own agents:
Claude (web/Desktop), Claude Code, ChatGPT, Codex, Hermes, OpenCode, Cursor, VS Code, Muse.
Until now an agent could only connect with a pasted Exo Key. Every one of those clients
implements MCP authorization (OAuth 2.1): it reads the server's protected-resource metadata
(RFC 9728), discovers the authorization server (RFC 8414), identifies itself with a Client ID
Metadata Document (CIMD) or Dynamic Client Registration (RFC 7591), and runs an authorization
code + PKCE flow with an RFC 8707 `resource`. Exo Key stays the ONLY authorization server for all
apps (ADR 0004); the issuer string never changes.

Client facts this design follows (fetched 2026-10-08):

| Client                               | Identification                                                                                            | Redirect                                                                      | Token auth                                                               |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| Claude (web)                         | CIMD if `client_id_metadata_document_supported` and `none` are advertised, else DCR                       | `https://claude.ai/api/mcp/auth_callback`                                     | none                                                                     |
| Claude Code                          | CIMD `https://claude.ai/oauth/claude-code-client-metadata`, else DCR                                      | `http://localhost/callback`, `http://127.0.0.1/callback`, any port            | none                                                                     |
| ChatGPT                              | CIMD `https://chatgpt.com/oauth/client.json`, else DCR                                                    | `https://chatgpt.com/connector_platform_oauth_redirect` (with RFC 9207 `iss`) | `private_key_jwt` (jwks `https://chatgpt.com/oauth/jwks.json`) or `none` |
| Codex                                | CIMD `https://chatgpt.com/oauth/codex/client.json`, else DCR                                              | `http://127.0.0.1/callback`, `http://localhost/callback`, any port            | none                                                                     |
| Hermes                               | CIMD `https://nousresearch.github.io/hermes-agent/docs/oauth/client-metadata.json`, else DCR; device flow | loopback ports 27890-27894                                                    | none                                                                     |
| OpenCode, Cursor, VS Code, Muse Code | DCR                                                                                                       | loopback / `https://vscode.dev/redirect` / `https://www.cursor.com/...`       | none                                                                     |

## Decision [#decision]

### Discovery [#discovery]

One metadata document is served as RFC 8414 AS metadata and OIDC discovery, byte-identical at:

* `<issuer>/.well-known/oauth-authorization-server` and `<issuer>/.well-known/openid-configuration`
* the RFC 8414 §3.1 path-inserted forms `<origin>/.well-known/oauth-authorization-server/exo-id`
  and `<origin>/.well-known/openid-configuration/exo-id`
* the same four under the `/exo-key` endpoint prefix.

It advertises `issuer` (`<site>/exo-id`), `authorization_endpoint` (`EXO_ID_AUTHORIZE_URL`, the
frontend `/id/authorize`), `token_endpoint`, `registration_endpoint`, `revocation_endpoint`,
`jwks_uri`, `device_authorization_endpoint`, `response_types_supported [code]`,
`grant_types_supported [authorization_code, refresh_token, device_code, token-exchange]`,
`code_challenge_methods_supported [S256]`, `token_endpoint_auth_methods_supported [none, private_key_jwt]`, `client_id_metadata_document_supported: true`,
`authorization_response_iss_parameter_supported: true`, `scopes_supported [read, write, ai,
usage:read, offline_access]`. Client secrets are not offered: every MCP client above is public
or uses `private_key_jwt`.

### Clients [#clients]

First-party registry apps (`convex/lib/exoIdApps.ts`, `app=`) are unchanged. Third-party clients
send `client_id` instead of `app`:

* **DCR** `POST {prefix}/v1/register`: open registration of public clients
  (`token_endpoint_auth_method` absent or `none`; secrets rejected), any `client_name`,
  1-20 redirect URIs (https; loopback `http://127.0.0.1|localhost|[::1]`; private-use schemes
  such as `cursor://`; never `javascript:`/`data:`/`file:`, never fragments). Unsupported grant
  types are dropped (RFC 7591 §3.2.1). Rows live in `exoKeyOAuthClients` (`exo_dcr_<random>`).
  Rate-limited per client IP (rightmost `X-Forwarded-For`); clients idle 30 days are deleted by
  a daily cron.
* **CIMD**: a `client_id` that is an https URL is fetched server-side and must contain
  `client_id` equal to the URL. SSRF guard: https only, default port, real DNS name (IP literals,
  `localhost`, single-label and internal suffixes refused), no query/fragment/userinfo/dot
  segments, redirects not followed, 3 s timeout, 10 KB streamed cap, JSON only, global and
  per-host fetch rate limits. Results (and failures, for 60 s) are cached 1 h in
  `exoKeyOAuthFetchCache`. Client secrets in a document are refused. Allowed token auth methods =
  the intersection of `token_endpoint_auth_method` + `token_endpoint_auth_methods_supported`
  (SEP-3149) with `none`/`private_key_jwt`.
* **private\_key\_jwt** (RFC 7523): the assertion is verified against the document's `jwks_uri`
  (must share the client\_id origin) or inline `jwks`; RS256/ES256; `iss = sub = client_id`;
  `aud` = the token endpoint (either prefix) or the issuer; `exp` and `jti` required, lifetime at
  most 10 minutes, `jti` single-use (`exoKeyAssertionJtis`). Unknown `kid` refetches the key set
  once. A presented assertion must verify; without one the client must allow `none`.

DNS resolution is not available in the Convex runtime, so a public name pointing at a private
address is not detected; the remaining controls (https certificate for the name, no redirects,
small budget, caching) bound what such a fetch can do. Revisit if Convex exposes resolution.

### Redirect URIs [#redirect-uris]

Exact string match, except loopback `http` redirects, which match on any port (RFC 8252 §7.3);
host, path and query must still match, so `localhost` and `127.0.0.1` are separate entries (every
client lists both).

### Authorization endpoint [#authorization-endpoint]

`/id/authorize?response_type=code&client_id=…&redirect_uri=…&code_challenge=…&code_challenge_method=S256&state=…&scope=…&resource=…`

`exoKeyOAuth.describeAuthorize` (public action) resolves the client and checks the redirect.
An unknown client or unregistered redirect renders an error page and never redirects. Everything
else that is wrong (`response_type`, missing/`plain` PKCE, missing or unknown `resource`) redirects
to the client with `error`, `state` and `iss`. The consent screen shows the client name (and CIMD
host), the redirect host, the scopes and the resource (`Construct · staging.construct.page`). A
person without a session signs in (or signs up) in place. `exoKeyOAuth.decideAuthorize`
(session-gated, rate-limited per user) re-validates, then on approve stores a 60 s single-use
code in `exoIdCodes` bound to client, redirect URI, PKCE challenge, scope, resource and user, and
redirects with `code`, `state`, `iss` (RFC 9207); on deny, `error=access_denied` + `iss`.

Scopes: unknown values (e.g. `openid`) are dropped; no recognised scope grants
`read write offline_access`. Refresh tokens are always issued.

### Resources and audience (RFC 8707) [#resources-and-audience-rfc-8707]

`EXO_PROTECTED_RESOURCES` (JSON `{"<url>": {"app": "construct", "label": "Construct"}}`) lists
the protected resources; defaults are `https://staging.construct.page/mcp` and
`https://construct.page/mcp` (app `construct`). Exo's own `<site>/mcp` (app `exo`) is always
included. Resources compare as exact strings. Unknown resources are `invalid_target` at authorize
and token time.

Access tokens for third-party clients carry:

```json
{ "iss": "<site>/exo-id", "sub": "<exo user id>", "aud": ["<resource>", "<app>"],
  "client_id": "<cimd url | exo_dcr_…>", "scope": "read write offline_access",
  "email": "…", "email_verified": true, "name": "…", "iat": 0, "exp": 0 }
```

RS256, `kid` from the Exo Key JWKS, 15 minutes. The resource gives MCP servers RFC 8707 audience
binding; the app id keeps Convex `customJwt` providers (`applicationID = <app>`) working, since
Convex accepts any `aud` array that contains the application ID (convex-backend
`crates/authentication`). A token for one resource is refused by the other resource server
(Exo's MCP verifies `aud ∈ {exo, <site>/mcp}`; Construct's MCP must require its own resource URL).

### Token endpoint [#token-endpoint]

`POST {prefix}/v1/token` with `client_id` (and no `app`), form-urlencoded or JSON:

* `authorization_code`: `code`, `redirect_uri` (exact match with the authorize request),
  `code_verifier` (PKCE S256), optional `resource` (must equal the code's). Any attempt consumes
  the code. First-party `app=` requests can never redeem a client's code, and vice versa.
* `refresh_token`: rotation with reuse detection exactly as ADR 0004/#165 (families, 30 s grace,
  family revoke on reuse); the session is bound to `clientId` + `resource`, which every successor
  copies. Another client, a first-party `app`, or a different `resource` cannot use it. `scope`
  may narrow, never widen.
* Response: `access_token`, `token_type`, `expires_in`, `refresh_token`, `scope` (no `user`).
* Errors are RFC 6749 JSON (`invalid_client` is 401 with `WWW-Authenticate`).

`POST {prefix}/v1/revoke` accepts RFC 7009 `token` (+ `client_id`) and revokes the whole family.

### Exo MCP [#exo-mcp]

`convex/mcpHttp.ts` accepts access tokens whose `aud` contains `exo` or `<site>/mcp`; its
protected-resource metadata `resource` is exactly `<site>/mcp` and `authorization_servers` is
`[<issuer>]`.

## Consequences [#consequences]

* Every listed agent can show a "sign in" button for Exo and Construct with no pasted key.
* Construct's MCP must serve RFC 9728 metadata pointing at the Exo issuer and require
  `aud ∋ <its resource URL>` on MCP requests (its Convex `customJwt` provider keeps working).
* New tables: `exoKeyOAuthClients`, `exoKeyOAuthFetchCache`, `exoKeyAssertionJtis`; new optional
  fields on `exoIdCodes` and `exoIdSessions` (`clientId`, `resource`, `scope`).
* Follow-ups: a "Connected agents" list with per-client revoke on `/id/keys`; ID tokens /
  `id_token_hint` for ChatGPT; DNS-resolution SSRF check if the runtime allows it.


---

# 0011: Exo forges Bases that launch Ships

https://exo.now/docs/adr/0011-forge-bases-and-ships

> Status: superseded by ADR 0013 (2026-10-13) · Date: 2026-10-12



# ADR 0011: Exo forges Bases that launch Ships [#adr-0011-exo-forges-bases-that-launch-ships]

* Status: superseded by [ADR 0013](0013-exo-is-mission-control-apps-pick-components.md) (2026-10-13)
* Date: 2026-10-12
* Decided by: Samir (owner)

> **Superseded.** ADR 0013 replaces this model: EXO is mission control for launching apps, its
> products are components, and people build apps that pick the components they need. "Forge", "each owner builds a Base" and Ship as
> a product noun are retired. Kept for history.

## Context [#context]

Exo described itself as "mission control for launching apps", and its docs used App as the top
noun. That hid what Exo actually provides: a well-defined set of software parts (identity, data,
runtime, checks, secrets, messaging) that apps are built on, which each owner uses selectively.
Construct also needed a clear place in the hierarchy for websites, PWAs and native apps, and the
mini-apps that sit on them.

## Decision [#decision]

1. **Exo is the forge.** "Forge" describes Exo; it is not a product noun. The name stays Exo, never
   "Exobase" or "Base Forge". In repository contracts the Entire role is now `primary-host`, because
   "forge" now means Exo.
2. **A Base** is the buildout of parts Exo forges for an owner (an Organization: a person or team).
   Parts come from one catalog. Each Base declares the parts it uses in `project.yaml` as
   `base.parts` (schema 1.9):
   * Required core: `source`, `preflight` (EXO Verify Level 1), `runtime`, `secrets`, `evidence`,
     plus `identity` (Exo ID) unless `base.signIn` is false.
   * Optional: `data` (Convex), `comms` (Bird), `payments`, `models` (Exo Rank), `support`
     (Exo Ask, proposed), `analytics` (Exo Analyze), `surfaces` (Construct).
   * Shared services (the Exo ID provider, Exo Rank, EXO Verify, Bird) run once in the forge. Each
     Base gets its own clients, configuration, data, environments and secrets on top of them. A
     Base is not private or resold infrastructure.
3. **A Ship** is an app a Base launches. A Base has a fleet: it can launch several Ships. Ships
   return, get refitted and fly again: each iteration is a **version**. **Launch** means a version
   reaching Production through the owner-approved promotion. Staging is the launch pad, and EXO
   Verify is the preflight. Rollback means relaunching an earlier version. Environment names stay
   Staging, Release Candidate and Production.
4. **Construct** is the construct that produces **Surfaces**, the interaction layers for users: web
   surfaces (websites), mobile surfaces (PWAs), phone surfaces (iOS and Android apps), or any
   combination. Surfaces carry **Cards**, components that give a Surface its functionality.
   Surfaces and Cards are **published**, not launched.
5. **Placement rule.** Something is a Surface in Construct if it is mostly screens over Construct's
   parts. It is its own Ship if it has its own backend and data. It gets its own Base only when it
   needs walled-off data, secrets and audit trails, for example regulated money or a different
   owner.
   * Hadron is a Ship on the Interspace Base.
   * Foundation gets its own Base because it handles donor money; its landing page stays a Surface
     in Construct.
   * If Hadron is sold, each customer gets their own Base (users, data, agent keys) and launches
     their own Hadron Ship, the same pattern as Parlour's Construct.

## Consequences [#consequences]

* Code, schema, Convex tables and URLs keep `app` (for example `staging.exo.now/{app}`). Ship is
  the product word; no data migrates.
* `project.yaml` schema 1.9 adds `base`; EXO Verify Level 1 checks it. 1.8 projects stay valid and
  get a skip until they migrate.
* Rollout is light: the homepage, docs overview, platform index, README, llms.txt and the
  repository contract. Deeper docs still say App and move over as they are next edited.


---

# 0012: Clerk removed; Convex Auth everywhere

https://exo.now/docs/adr/0012-clerk-removed-convex-auth-everywhere

> Status: accepted · Date: 2026-10-12



# ADR 0012: Clerk removed; Convex Auth everywhere [#adr-0012-clerk-removed-convex-auth-everywhere]

* Status: accepted
* Date: 2026-10-12
* Decided by: Samir (owner)

## Context [#context]

ADR 0003 moved Exo identity to Convex Auth v2 and ADR 0004 made Exo ID (now Exo Key) the central
identity provider, with every App a relying party. Clerk stayed in EXO as a transitional layer:
the staging gateway at `staging.exo.now/{app}` used `clerkMiddleware` and forwarded Clerk session
JWTs to Apps, `/sign-in` and `/sign-up` mounted Clerk's `<SignIn>`/`<SignUp>`, the repository
contract declared `provider: clerk` for staging access, and commerce offered a `web-clerk-stripe`
profile with Clerk Billing. Two identity systems meant two sets of keys, two session models, and
per-MAU pricing for the one we were leaving.

## Decision [#decision]

1. **Exo ID / Convex Auth is the only identity in EXO.** No `@clerk/*` package, import, or
   `CLERK_*` variable remains; a guard test (`tests/int/no-clerk.int.spec.ts`) keeps it that way.
2. **EXO signs in through its own Exo ID.** EXO is a relying party registered as `exo-staging`
   (`convex/lib/exoIdApps.ts`) with exact callbacks `https://staging.exo.now/auth/callback` and
   `http://localhost:3000/auth/callback`. `/sign-in` starts the authorization-code + PKCE round trip
   through `/id/authorize` (Google, GitHub, phone over Bird). The access token and rotating
   refresh token live in host-only HttpOnly cookies. The headless `exo` audience still accepts no
   browser redirect.
3. **Staging access = Exo ID session + exact non-production Exo grant.** The gateway verifies the
   Exo ID access token (RS256 against the Exo ID JWKS, exact issuer and `aud: exo-staging`,
   expiry), refreshes it once when expired, then authorizes the requested App path with
   `authorizeExoAccess` against the `EXO_STAGING_GRANTS` tester grants. It forwards only the
   verified access token to the App and strips EXO's own cookies. Every failure stays closed.
4. **Contracts.** `project.yaml` 1.9 requires `access.provider: exo-id` and forbids Clerk in
   commerce; 1.8 declarations keep accepting `clerk` so existing repositories stay valid while
   they migrate. `convex.application.json` 1.2 declares `identity.provider: exo-id` with an
   `audience` and `EXO_ID_ISSUER`; 1.1 keeps the Clerk shape. Commerce profile `web-clerk-stripe`
   becomes the Stripe-only `web-stripe` (Stripe Checkout and Customer Portal own the experience);
   Stripe and RevenueCat stay.
5. **Data is not broken.** The `billingIntegrations.provider` literal `clerk` stays in the Convex
   schema, marked deprecated, so imported legacy rows remain valid. Nothing new writes it.

## Consequences [#consequences]

* ADR 0002 (Clerk identity boundary) is fully superseded. ADR 0003 and ADR 0004 stand; the
  transitional Clerk `/sign-in` that ADR 0004 kept is gone.
* The gateway depends on a Convex deploy of the `exo-staging` registry entry to the EXO
  deployment, and on the `EXO_STAGING_GRANTS` tester grants being set on the EXO web service.
  Until both happen, staging apps stay closed (fail closed), not open.
* Apps behind the gateway that verified the forwarded Clerk bearer must verify the forwarded
  Exo ID access token instead (issuer `${EXO_CONVEX_SITE_URL}/exo-id`, audience `exo-staging`), or
  rely on their own Exo ID sign-in.
* Repositories outside EXO that still use Clerk (their own `project.yaml` or code) are migration
  debt tracked in `config/project-integrations.json` as `legacy identity`; EXO no longer provides
  or documents Clerk. Cancelling the Clerk account and subscription is an owner action.


---

# 0013: EXO is mission control; apps pick components

https://exo.now/docs/adr/0013-exo-is-mission-control-apps-pick-components

> Status: accepted · Date: 2026-10-13



# ADR 0013: EXO is mission control; apps pick components [#adr-0013-exo-is-mission-control-apps-pick-components]

* Status: accepted
* Date: 2026-10-13
* Decided by: Samir (owner)
* Supersedes: [ADR 0011](0011-forge-bases-and-ships.md) (Exo forges Bases that launch Ships)

## Context [#context]

ADR 0011 described Exo as a forge that builds a Base for each owner, and each Base launching
Ships. In practice that put two invented layers (forge, Base) and a second word for an app (Ship)
between people and what they actually do: build apps on EXO and use the components each app needs.
Nobody builds Exo, and nobody builds a Base. The model needed to be simpler to say and to read.

## Decision [#decision]

1. **EXO is mission control for launching apps**, the platform. There is one of it. "Forge" is
   dropped entirely, and so is "Base" as a product noun and "each owner builds a Base".
2. **EXO's products are components.** The components today are Exo ID (sign-in for an app's users), Exo Key
   (keys and headless sign-in for agents, the CLI and MCP), Exo Deploy, EXO Verify, Exo Analyze,
   Exo Rank (model routing; code and URLs still say `router`), Exo Inform (docs and READMEs that
   agents and people can read) and the other services in the platform and stack docs, such as the
   Bird communications layer and Construct.
3. **People build apps.** Each app picks the components it needs. It is modular, and no app uses
   everything. An app declares its components in `project.yaml` as a top-level `components` list
   (schema 1.10). Components come in two tiers:
   * **Core components**, the ones most apps need: `source`, `identity` (Exo ID), `preflight`
     (EXO Verify Level 1), `runtime`, `secrets`, `evidence` and `data` (Convex).
   * **Specialty components**, the ones only some apps need: `comms` (Bird), `payments`,
     `models` (Exo Rank), `support` (Exo Ask, proposed), `analytics` (Exo Analyze) and `surfaces`
     (Construct).
     Not every app needs comms and support, or an AI router like Exo Rank. (This tier was briefly
     called "add-ons"; the owner renamed it "specialty components" on 2026-10-13.)
   * The tiers are a docs and design grouping. Source control and the control plane are recommended
     but optional, and an app may opt out of either. The schema-required list is therefore
     `preflight`, `runtime`, `secrets` and `evidence`, plus `identity` unless `signIn` is false;
     ADR 0011 also required `source`. `data` is shown as core but is not schema-required.
   * The catalog and the wiring checks are unchanged (a declared `source` must still be wired to
     Entire); apart from `source` becoming optional, only the name and where the list lives change (`base.parts` becomes `components`, `base.signIn` becomes `signIn`).
4. **Apps, in every piece of prose.** An app is visualized as a rocket ship in artwork, but the
   prose always says "app". "Ship" is no longer a product noun.
5. **Starter kits.** A **starter kit** is a saved set of components that new apps start from, for a
   family of apps that share the same components. It is a convenience for creating apps, not a
   runtime layer; each app still declares its own `components`.
6. **Separate accounts, not separate Bases.** Anything that needs walled-off money or data is a
   separate account on EXO, with its own users, data, secrets, environments and audit trail.
   * Foundation's donor money lives in its own account on EXO; its landing page stays a Surface in
     Construct.
   * Hadron is an app in the Interspace account.
   * If Hadron is sold, each customer gets their own account on EXO and their own Hadron app, the
     same pattern as Parlour's Construct.
7. **Launch is unchanged.** Launch means an app version reaching Production through the
   owner-approved promotion. Staging is the launch pad, EXO Verify is the preflight, and rollback
   means relaunching an earlier version. Environment names stay Staging, Release Candidate and
   Production.
8. **Construct is unchanged.** Construct publishes **Surfaces** (web, PWA, iOS and Android, or any
   combination) and the **Cards** on them. Surfaces and Cards are published, not launched.
9. **Placement rule, restated.** Something is a Surface in Construct if it is mostly screens over
   Construct's components. It is its own app if it has its own backend and data. It gets its own account
   on EXO only when it needs walled-off money or data, for example regulated money or a different
   owner.

## Consequences [#consequences]

* Code, schema field names, Convex tables and URLs keep `app` (for example
  `staging.exo.now/{app}`). No data migrates and no URL changes.
* `project.yaml` schema 1.10 moves the catalog to a top-level `components` list and `signIn`
  (`$defs.component` and `$defs.componentsList` in the schema). A 1.10 declaration must list
  `components` and must not keep a `base` block. 1.9 `base.parts` is still accepted as legacy (EXO
  Verify reports a skip pointing at 1.10), and 1.8 declarations without components are still
  skipped.
* The EXO Verify Level 1 module is renamed from `lib/level1/base.mjs` to
  `lib/level1/components.mjs` (exports `COMPONENTS`, `REQUIRED_COMPONENTS`, `validateComponents`,
  `componentsChecksFor`), and its check names say components (`components declared`,
  `components required core`, `component data wired`).
* The Entire contract role stays `primary-host`. Prose that called Entire "the primary forge" now
  says "primary source host". Contract constants such as `forge-change` and `primaryForge` are
  code identifiers and are unchanged.
* Rollout covers the homepage, docs overview, platform index, README, llms.txt, the repository
  contract, EXO Verify Level 1 docs, AGENTS.md, HANDOFF.md and the changelog. Deeper docs that
  still say App, or Exo Router for Exo Rank, are already correct in meaning and move over as they
  are next edited.
* Starter kits and accounts on EXO are named here but not yet modelled in code or schema; that is
  later work.
