# Apps

An **App** is an independently authored user-facing application delivered as
an immutable bundle. The desktop AppHost can open it from Home or while an
Environment is active. It is a third public developer unit beside Environment
and Plugin:

| Unit        | Product job                              | Execution boundary                                     |
| ----------- | ---------------------------------------- | ------------------------------------------------------ |
| Environment | A destination the user enters            | Isolated bundle view, Runtime, scene, and composition  |
| App         | A portable user-facing product           | Exact published bundle in a desktop-owned product view |
| Plugin      | A capability installed by an Environment | Trusted library in the host in v0.x                    |

An App is not a Plugin with a screen. Plugins extend an Environment; Apps have
their own product, data lifecycle, derived origin, optional backend, and immutable
publication history. Opening an App does not enter another Environment or
create another Presence membership.

## One Widget host, two display modes

Every App works in **Compact**. Compact is the current narrow Widget surface and
must be designed like a mobile-width product. An App may additionally declare
**Workspace**, a wide desktop surface for richer work.

Workspace is not browser fullscreen. The same Widget grows across the available
desktop viewport, keeps its designed edge inset, remains visibly part of the
active 3D scene, and preserves platform-owned chrome. On small viewports Compact
is the universal fallback.

```json
{
  "display": {
    "modes": ["compact", "workspace"],
    "preferredMode": "workspace"
  }
}
```

`preferredMode` controls the first desktop open when the user has not explicitly
chosen a mode. A Compact-only App declares `["compact"]` and never receives the
mode button.

Both modes use the same immutable App revision and document. The connected App
session reports `displayMode`. Changing mode updates host presentation in place;
subscribe to presentation changes and re-compose without assuming a reload or
new session. Durable state still belongs in the App backend because explicit
close, update, recovery, or sign-out can end the document.

The Widget owns its header, mode control, close/back behavior, animation,
focus, input capture, edge inset, and product-view policy. App content is revealed at
its final dimensions and is never visually scaled. Apps must not mount another
Widget or depend on Widget DOM measurements.

## Availability and launch

Registry returns exact available App revisions. The platform panel shows
availability separately from launch authority, and every launch requires the
person to be signed in. A launch from Home is platform-hosted; a launch while a
world is active may be environment-hosted.

Before mounting the App view, the platform assesses the immutable capability
request. It explains required and optional disclosures, records consent, and
launches only when every required capability is available. Optional denials
produce a smaller grant set that the App must handle.

The desktop hosts the verified bundle at `alamr-app://<appId>/` in a dedicated
storage partition. The App generates a PKCE verifier and sends only its
challenge through `window.alamr`. Registry issues a one-time code bound to the
exact revision, bridge nonce, capability-set digest, and host generation. An
environment-hosted launch also binds the active parent Runtime lease; a
platform-hosted launch has no parent and cannot receive Environment-scoped
capabilities. The App exchanges the code for a short-lived, memory-only session.

The App never receives the parent Runtime access/refresh token, raw account
subject, or Plugin Grant. It receives an `aact_*` pairwise subject stable for the
same account and App across Environments. Profile and Environment identity are
disclosed only through granted capabilities.

## Host Services

The injected `window.alamr` bridge is the strict, versioned Host Services
channel. It is the only supported bridge between App, platform, and Environment.
An App cannot import Environment code, inspect scene objects, or invoke Plugin
APIs.

The initial capability catalog includes:

| Capability                                         | What it allows                                                                                           |
| -------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `platform.identity.profile.read@1`                 | Read the authorized account display profile                                                              |
| `platform.environment.identity.read@1`             | Read signed identity of the active Environment                                                           |
| `host.spatial.context.read@1`                      | Read current Environment revision, visit, Zone, and semantic location                                    |
| `host.spatial.locations.read@1`                    | List stable locations declared by the Environment                                                        |
| `host.spatial.location.capture@1`                  | Capture a bounded opaque reference to the current place                                                  |
| `host.spatial.navigation.request@1`                | Ask the Environment to navigate through assess/commit                                                    |
| `platform.notifications.publish.self@1`            | Notify the current App principal                                                                         |
| `platform.notifications.connect.background.self@1` | Connect the current account to the App backend for background delivery                                   |
| `platform.companions.list@1`                       | Keep reading each companion's name, face, and App-pairwise pseudonym until revoked, never their location |
| `platform.companions.offer@1`                      | Offer something to one of the person's companions, without learning who they are                         |
| `platform.notify.companion@1`                      | Raise one platform-authored companion notice with no App content                                         |
| `platform.call@1`                                  | Ring a companion on the person's behalf, and let that companion's ring reach them                        |
| `platform.media.microphone@1`                      | Request microphone input inside the granted App frame                                                    |
| `platform.presence.roster.read@1`                  | Read how many people are in this world, and never who they are                                           |

Capabilities are closed and versioned. Declared permission is not a JavaScript
sandbox and not a grant by itself. Registry validates publication, compatibility,
dependency, principal, consent, and Runtime authority; the App checks the actual
grants in its host session. The host does not publish an App as ready, activate
those grants, or accept renewed authority until the App has exchanged and
verified the one-time launch and acknowledged its exact bridge nonce.

Unpublished development bytes and frozen review bytes use a separate
inspection-only handshake. Its answer carries only source, display mode,
document generation, and the nonce the document must acknowledge. It never
carries a Registry URL, launch code, actor, token, publisher backend, native
capability, or Environment Host Service authority.

Spatial identity is Environment-owned. Environments declare stable Zone and
location IDs for public destinations, while Apps may also store bounded,
revision-specific captured locators for arbitrary valid places. Apps do not
interpret those locators as portable world coordinates. Navigation is a request
to the Environment and may require platform confirmation; Apps never move the
player or camera directly.

See [App Host Services](/docs/architecture/app-host-services) for contracts and
implementation examples.

## Lifecycle

Apps register an optional `app.lifecycle.beforeExit` handler through the SDK.
The host invokes it before an explicit destructive close or replacement. A
mode change is presentation-only, and a desktop Environment-to-Environment
switch preserves the document while replacing its Runtime authority. The App
returns `allow` or requests one platform-owned confirmation.

The deadline is one second. A late, malformed, or failing App cannot trap the
user; the host proceeds. Closing or replacing destroys the product view.

## Apps that keep running while out of sight

By default, putting an App away ends it: closing the panel or going back
destroys the product view and its session. An App that needs to survive that declares
it.

```json
"display": { "modes": ["compact"], "preferredMode": "compact", "background": "persistent" }
```

`background` defaults to `"none"`, so an App that says nothing behaves exactly
as it always has. Declare `"persistent"` when your App _runs_ rather than
displays — a timer that must keep counting, a queue that must keep playing.
Closing the App still ends it; only closing the panel stops doing so.

At most three Apps run at once. That is how many marks fit beside the Launcher,
not a security limit, and it drops to two on small screens and low-memory
devices.

**What the platform promises, and what it does not.** It promises the document
survives and the audio thread keeps running. It does **not** promise your code
runs on time. A hidden product view or an operating system suspending the
desktop client may reduce or stop timers. So:

- Store an absolute instant and render from the clock. A timer that counts ticks
  will drift; one that computes `endsAt - Date.now()` cannot.
- Subscribe to the presentation. `state: "background"` means running and not on
  screen — stop drawing, keep playing. `at` carries the host's wall clock, so
  comparing it to your own detects both a long gap and a skewed device.
- Call `setAudible(true)` while you are producing sound. It is how the platform
  knows to mark the Launcher and to put you last in the queue when it has to ask
  which App to close. It is advisory, and it is not a permission: declaring
  silence while playing changes nothing except that the person is told less.

**Background delivery is a different thing.** `notifications.backgroundDelivery`
governs whether the platform may deliver a notification for your App when the
person is nowhere near it. `display.background` governs whether your App's own
document keeps running inside this visit. Neither implies the other.

**Sessions renew, and the session id moves.** An App session lasts five minutes
and is re-minted before it expires; the host keeps the clock and your App does
the crypto, all inside the SDK. Each renewal is a fresh launch, so
`session.id` changes. **Key stored data on `session.appActorSub`**, which is
stable for as long as the person and the App are the same two parties.

Renewal is also what bounds a background App: it repeats every check a first
launch runs, so a revoked capability stops applying within one renewal period —
under 180 seconds — and a revoked account or a sign-out ends the App within the
token's own 300.

`Escape` inside the App view forwards a close request to the platform. The host owns
the final decision, accessible confirmation, focus return, and pointer-lock
behavior.

## Notifications

Registry owns the durable News projection, while the source product remains
authoritative for its own history. A new App declares reviewed event policy and
signals an immutable Occurrence; Registry reduces it onto a keyed or
per-occurrence Subject without letting the caller choose sound, treatment, or
recipient state:

- an open App signals through its App session;
- an active Environment publishes through its Runtime session;
- an Environment backend uses its short-lived exact-endpoint Grant;
- an App backend uses an expiring Project token plus a user-created opaque
  notification delegation and a durable source outbox.

No-backend Apps can signal while their authorized frontend is active. Offline,
scheduled, or server-originated delivery needs the App backend model. An active
Environment and its backend continue to use their additive legacy publish
routes until that authority plane gains an explicit occurrence API.

The immutable App manifest fixes family, Subject mode, effect vocabulary,
preference identity, Pulse policy, and source or platform-template
presentation. Source-authored content stays bounded plain text and may only ask
to open its own App. Platform templates accept only their reviewed data shape.
Arbitrary URLs, HTML, scripts, custom Widget callbacks, and raw user identifiers
are forbidden. Legacy publish remains available for immutable callers, but it
does not gain Subject ordering, acknowledgement watermarks, or Pulse authority.

See [Platform notifications](/docs/architecture/notifications) for occurrence,
acknowledgement, delegation, reader, and revocation rules.

## Data and backend boundary

Durable App state belongs in the App's backend and is keyed by the verified
App-pairwise subject. Storage inside the App's product partition is cache only;
it is not portable identity or durable backend authority.

Use `client.authorizationHeader()` when calling the App backend. The backend
uses `@al-amr/backend` App-session introspection and fails closed when the
parent Runtime lease is inactive. It must not accept identity supplied in
browser JSON.

`@al-amr/backend` is a library, not a mandatory Al-Amr service. An App that has
no backend does not install or run it.

Environment-private is the default personal-corpus shape: scope every record by
the Environment the introspected token names, not only by the actor, and refuse
a token with no Environment. An `actor_portable` product is an explicit trust
decision, not the result of deleting that column.
[ADR-0091](../decisions/ADR-0091-portable-personal-app-corpus.md) requires the
Notes variant to be account-only, use `private_key_jwt`, retain the introspected
current Environment, and replace `backend.environments: "any"` with a reviewed
non-empty allow-list. The client can request only no attachment, the current
Environment, or the current captured location; it can never select another
Environment or rewrite that provenance. Read `apps/notes/packages/backend` for
the first-party implementation and
[ADR-0068](../decisions/ADR-0068-app-owned-backends.md) for the default scope and
shared introspection-cache rules.

## Public discovery and publication

The Developer Portal shows only active public App revisions and exposes stable
identity, Publisher, exact revision, access policy, Compact/Workspace support,
preferred mode, requested capabilities, declared notification event types,
background-delivery model, trust, and reviewed documentation.

A catalog page does not grant launch authority or install the App. Apps still
open only through an authorized Runtime and the launch flow above. Public
creation, validation, testing, and publication remain available through the
documented `alamr` CLI and Registry APIs; the graphical Console is optional.

Before submission, the CLI packs and uploads one immutable bundle and binds its
bounded file inventory to the App revision by relative path, byte size, and
SHA-512 integrity. Review shows that exact build digest. Publication, platform
enable, update, and rollback select those held bytes; no publisher host is
contacted. A missing, changed, or incompatible bundle fails closed. See
[ADR-0094](../decisions/ADR-0094-al-amr-is-entered-through-a-desktop-client.md).

Every newly authored App revision also supplies a bundle-relative
`presentation.iconUrl`. Registry reads the bounded PNG or JPEG from the
reviewed archive, validates its container, headers, and dimensions and
allowlist-rewrites its metadata,
binds the result to the exact revision, and exposes a Registry-owned `cardIcon`
descriptor to catalog and Widget clients. A missing or invalid icon fails
publication. The scaffold includes `public/app-icon.png`; publishers replace it
with their own square 128–1024 pixel icon before publication. See
[ADR-0078](../decisions/ADR-0078-registry-managed-presentation-media.md).

Each App revision represents one build generation. `alamr build` produces the
real production bytes; `alamr test --yes --json` inventories, packs, uploads,
binds, and checks them. A changed generation gets a fresh immutable revision,
so a code-only release never overwrites an older build binding. Bundle assets
remain relative to the archive root.

## Current limits

- App frontend revisions are reviewed immutable bundles, not trusted Plugin
  packages and not a claimed complete JavaScript sandbox.
- App-to-Plugin consumption and App-authored in-world UI are not available.
- Camera, microphone, browser geolocation, display capture, clipboard, browser
  fullscreen, and picture-in-picture remain denied unless the matching host
  capability is declared, granted, and enforced by the desktop main process.
- Raw pose streaming, scene graph access, social graph data, OS push, email,
  and marketing notification channels are not implied by current capabilities.
- App sessions are short-lived and memory-only. Renewal repeats the launch-code
  exchange; an App session is not an ecosystem-wide account token.
- Platform chrome never loads a URL from your servers. `presentation.iconUrl`
  names a file in the reviewed bundle; catalog cards receive only the
  Registry-owned immutable `cardIcon`. A running App's mark still comes from
  `presentation.mark`, a closed set the platform draws itself.
- The desktop AppHost preserves an already-running background App across an
  Environment switch and renews its authority against the active successor; it
  does not carry the outgoing Environment's grants forward. Nothing starts an
  App into the background. An App becomes a background App by being put away by
  somebody.
- Two Apps are never on screen at once. Tiling, resizable windows, and mobile
  Workspace stay unavailable.

Notes is the first-party personal knowledge App. Its complete Compact mode and
preferred Workspace mode open one actor-portable library with independent
folders and one canonical block document shown through either normal editing or
a Word-like outline. Environment and captured-location attachments are optional,
immutable provenance; location is secondary to the note, and navigation is
offered only while the owning Environment is current. Its portability reaches
only the six explicitly reviewed first-party Environments. A future
Registry-origin user-presence ceremony is required before this corpus can admit
arbitrary or third-party Environments.

هم‌قدم is the first-party App whose data is **shared by an organisation** rather
than scoped to one person. It is the reference for what that costs: an App whose
corpus is common to many readers cannot use the composite scope key
[ADR-0068](../decisions/ADR-0068-app-owned-backends.md) makes a control by
construction, so it builds home-Environment pinning of authority, an allow-list
that gates reads, compartments that keep bulk reading slow, and audit rows that
name the acting session instead.
[ADR-0072](../decisions/ADR-0072-an-organisation-inside-an-app.md) records the
whole trade, including the residual risk that stands until App client
authentication ships. It also shows the two roster rules a publisher in this
shape will meet: membership is an admission list rather than a log of who opened
the App, and `platform.identity.profile.read@1` authorises showing a person's
name **to the App**, never to that App's other users — rendering one person to
another needs its own recorded consent.

Read [Build an App](/docs/start/app), the
[manifest reference](/reference/manifests), the
[SDK reference](/reference/sdk), and
[Trust and security](/docs/security) before publication.
