EmofyEmofy Developers
Domains

The EMA Ecosystem

What an Embedded Mini App is, how it's sandboxed and scoped, its publish lifecycle, and how it talks to the platform.

EMA stands for Embedded Mini App. EMAs are how domain-specific features ship without bloating the core: attendance, gradebooks, portfolios, library management, and more are all EMAs. This page covers the concept and lifecycle; the build instructions and the full scope catalog live in the Developer / Partner API section.

What an EMA is

An EMA is an isolated JavaScript bundle that:

  • is hosted on Cloudflare R2 as an immutable artifact;
  • is loaded in a sandboxed iframe inside the consumer shell, behind CSP;
  • declares a manifest (public/manifest.json) — its id, the scopes it needs, and its views;
  • talks to the host through the @emofy/sdk Penpal bridge (scoped RPC), not direct network calls or DOM access.
host (consumer app)                       EMA (sandboxed iframe)
   │   loads bundle from R2 ──────────────▶│
   │                                        │
   │◀──── Penpal RPC (scoped to manifest) ─▶│  read messages, create tasks…
   │                                        │
   └── enforces scopes + CSP at load time ──┘

Why an iframe + manifest

The manifest is the source of truth for what an EMA may do, and the platform treats R2 as a dumb artifact store, not a root of trust (ADR-012). The chain:

manifest.json  ──▶  build-time validation (@emofy/config)
              ──▶  publish via `emofy` CLI
              ──▶  backend ManifestFetcherService validates + hashes → app_builds
              ──▶  R2 immutable hosting (no secrets in the bundle)
              ──▶  consumer iframe loader: manifest gate + permission check + CSP

Because the bundle is immutable and the manifest hash is stored platform-side, a swapped or tampered bundle won't load — this is the supply-chain defense.

Scopes: what an EMA can request

EMAs declare required/optional scopes in the manifest, e.g. ["messages:read", "tasks:write"]. Scopes are organized into sensitivity tiers (general / sensitive / restricted) and enforced at install time and load time. The complete, auto-generated catalog is the EMA Scopes reference.

A notable high-risk scope is tasks:write_personal, gated server-side by a dedicated guard.

How an EMA talks to the platform

An EMA authenticates with an EMA token — a short-lived JWT distinct from a user JWT:

User JWTEMA token
Audienceconvexema:{appId}
ClaimsorgRole, additionalRolesappId, scopes
TTL~1 hour~5 minutes

The token is minted org-scoped: credentials are looked up by (appId, orgId), so one org can't mint a token against another org's credential, and the effective scopes are the intersection of requested scopes and the credential's granted scopes. The user-JWT verification path explicitly rejects any token carrying an appId, so an EMA token can never escalate into a user session — see Authentication.

Writing back to core

An EMA cannot write to arbitrary core tables. It contributes to the platform through exactly three channels:

  1. Feed cards — post to the social feed.
  2. Messages — send 1:1 or group messages.
  3. Tasks — create org or personal tasks.

Everything else is read-only or out of reach. This is the same boundary described in Core channels.

The platform domain that runs it

The backend platform/ecosystem modules operate the ecosystem:

ModuleRole
apps / appstoreEMA registry and catalog.
app-buildsPublished, hashed bundles.
api-keysPartner credentials (ADR-024).
webhooksSigned outbound events to integrators.
ema-auth / ema-apiEMA token minting and the EMA API surface.

Build an EMA

Ready to build one? The developer journey — scaffold with the emofy CLI, declare scopes, develop against the SDK, and publish per-environment — is documented in the partner section:

On this page