Reference

App contract

The complete app contract, enforced by the MCP server, the translator pipeline, and the invariant checker. A violation surfaced at publish time is a bug in Ellivate — not in your code.

This page is the formal specification. For a narrative explanation, see The Ellivate contract. For framework-specific walkthroughs, see the App shapes section of the nav.

§1 — Identity

Ellivate is a shared-shell platform. Viewers arrive already authenticated. Apps never run their own sign-in flow.

Invariants

  • No third-party auth libraries: @supabase/ssr (auth use only — DB client is fine), next-auth, firebase/auth, @clerk/*, @auth0/*, lucia-auth, magic-sdk, hand-rolled JWT for session cookies.
  • No /login, /signup, or /auth/callback routes.
  • Viewer identity is read from one of:
    • ellivate_viewer_token cookie (server components, server actions)
    • X-Ellivate-Viewer-Token header (server-to-server / Python frameworks)
    • window.ELLIVATE_VIEWER_TOKEN (browser, mobile WebView)
    • URL fragment #__ellivate_auth=... on first page load
  • User shape: { id, email, username, verified }.
  • Sign-out is a redirect to /, not a third- party signOut() call.
  • Python apps (Flask / FastAPI) get an auth-gate middleware injected at publish time. The middleware 401s any request without a valid viewer cookie. Bypassed when App.requiresAuth = false.
  • Python apps enforce per-viewer scope dispatch via a contextvars.ContextVar. The middleware stashes the token after verify; the SDK reads the contextvar on every KV call.
  • The handoff cookie must carry the Partitioned attribute (CHIPS). Chrome blocks unpartitioned third-party cookies on subsequent iframe requests.
  • App.isPublic and App.requiresAuth are orthogonal. All four combinations valid; never conflate.

§2 — Persistence

All user-generated data goes through Ellivate's KV SDK.

Invariants

  • No localStorage, sessionStorage,IndexedDB, Dexie, localForage for user data.
  • No AsyncStorage, redux-persist, zustand/persist, pinia-plugin-persistedstate.
  • No filesystem writes for user data: fs.writeFile, sqlite3.connect, shelve.open, pickle.dump, sqlalchemy for app state.
  • No cookies for app state.
  • Keys are strings; values are JSON-serializable.
  • Scope (PERSONAL / SHARED / PUBLIC) is classifier-picked by default. Explicit prefixes (personal:, shared:, public:) override.
  • Scope enforcement never runs on server lane. Client lane with no cookie means SHARED/PERSONAL reads 401 (returned as null for GETs, thrown for writes); it does NOT silently dispatch to app-wide storage.
  • MODULE_SDK treats 401 on GETs as "no data" (returns null); 401 on writes throws.
  • If the app genuinely needs a relational DB, the translator emits a loud warning and the gate asks the user to upgrade. No silent sqlite fallback.

§3 — SDK integration

The Ellivate SDK is a local file at publish time, not a registry package.

Invariants

  • ellivate-client.js exists at project root whenever any JS/TS file references it.
  • ellivate-client.d.ts ships alongside — strict TypeScript apps need the declaration.
  • ellivate_client.py exists at project root whenever any Python file references it.
  • ellivate-client.global.js and ellivate-config.js exist at project root AND public/ for vanilla HTML apps.
  • JS/TS imports use relative paths: "../ellivate-client". Bare specifier "ellivate-client" is forbidden.
  • Python imports are absolute: import ellivate.
  • ellivate-client is NOT in package.json dependencies.
  • ellivate_client is NOT in requirements.txt.
  • package-lock.json is in sync with package.json.
  • SDK fetch calls in MODULE_SDK use cache: "no-store".
  • SDK degrades gracefully when env vars missing — returns null, warns once, never throws.
  • MODULE_SDK sends X-Ellivate-Caller-Lane: client whenever there's per-request viewer context (browser, Next.js server components, explicit token). Server lane is the narrow fallback for non-Next Node with no token.
  • Ellivate-injected routes use a URL path starting with a letter — never _. App Router treats underscore-prefixed folders as private (silent 404). The handoff is at /ellivate-handoff.
  • Redirects emitted by Ellivate routes must derive external origin from x-forwarded-host + x-forwarded-proto. new URL(request.url).origin yields the container's internal address.

§4 — Deploy environment

Invariants

  • App listens on 0.0.0.0, not localhost or 127.0.0.1.
  • Port is read from PORT env var.
  • Runtime env vars Ellivate provides: ELLIVATE_DATA_URL, ELLIVATE_DATA_KEY, ELLIVATE_APP_ID. Python additionally gets ELLIVATE_VIEWER_TOKEN as a server-lane fallback (primary per-request viewer comes from the handoff cookie).
  • No user-facing env vars required for a normal publish. If the app declares one, the gate pauses and asks the user to fill it in.
  • No user-writable paths outside the project directory.

§5 — Out of scope (explicit refusals)

Publish fails loudly with a Class 1 blocker and a user-facing reason.

  • React Native / Expo apps.
  • Electron / native desktop apps.
  • Code that requires GPU, dedicated hardware, or a physical display.
  • Code that reads or writes paths outside the project directory.
  • Playwright apps that hardcode a Chrome channel or require a persistent user profile.

Amending the contract

A new invariant lands with three pieces in parallel:

  1. A server-side enforcement — translator rewrite, gate check, or invariant validator.
  2. An MCP-side steer — new tool or updated tool output.
  3. At least one E2E fixture that fails if the invariant breaks.

Removing a rule (a shape we now support) requires updating the same three pieces. Amendment log lives in git.

What's next