Skip to main content
If you can build a web page, you can build a webAI app. Apps are single HTML files that run inside the platform and get access to on-device AI, real-time collaboration, user identity, and more — with zero server infrastructure. This guide walks you through the entire process, starting with the simplest possible app and progressively adding platform features.

Before you start

You don’t need any special tooling. For the quick-start path, all you need is a text editor. For framework-based apps (React, Vue, Svelte, etc.) you’ll want Node.js installed and a bundler that can produce a single self-contained HTML file. Here’s a quick look at the two paths:
PathBest forBuild step?
Vanilla HTMLPrototypes, simple tools, learning the platformNo
Framework (React / Vue / etc.)Production apps, complex UIs, team projectsYes — bundle to a single HTML file
Start with vanilla HTML to learn the concepts, then move to a framework when your app outgrows a single hand-written file.

Step 1: Build your first app

A webAI app is just an HTML file that declares which platform facades it needs through a shell manifest, and then reads from window.apogeeSDK to call them. Here’s a complete, working example that demonstrates AI inference, identity, and theme integration — all in a single file:
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>My First webAI App</title>

  <!-- Shell manifest: declare what facades this app needs -->
  <script id="apogee-shell-manifest"
    type="application/apogee-shell-manifest+json">
  {
    "schemaVersion": 1,
    "name": "My First App",
    "version": "1.0.0",
    "requires": {
      "managers": ["intelligence", "identity", "theme"]
    }
  }
  </script>

  <style>
    :root { --bg: #ffffff; --text: #1a1a1a; --border: #e0e0e0; --accent: #3b82f6; }
    [data-theme="dark"] { --bg: #1a1a1a; --text: #f0f0f0; --border: #333; --accent: #60a5fa; }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: system-ui, sans-serif; background: var(--bg); color: var(--text); padding: 24px; }
    h1 { font-size: 1.5rem; margin-bottom: 8px; }
    textarea { width: 100%; height: 80px; padding: 12px; border: 1px solid var(--border);
      border-radius: 8px; background: var(--bg); color: var(--text); font-size: 1rem; resize: vertical; }
    button { margin-top: 8px; padding: 8px 16px; border: none; border-radius: 6px;
      background: var(--accent); color: #fff; font-size: 0.9rem; cursor: pointer; }
    button:disabled { opacity: 0.5; cursor: not-allowed; }
    pre { margin-top: 16px; padding: 12px; background: var(--border); border-radius: 8px;
      white-space: pre-wrap; font-size: 0.9rem; min-height: 40px; }
  </style>
</head>
<body>
  <h1 id="greeting">webAI App</h1>
  <textarea id="prompt" placeholder="Ask the AI something..."></textarea>
  <button id="run">Ask</button>
  <pre id="output"></pre>

  <script>
    const sdk = window.apogeeSDK || null;

    // --- Theme ---
    function applyTheme() {
      const theme = sdk?.theme?.getState?.() === "light" ? "light" : "dark";
      document.documentElement.setAttribute("data-theme", theme);
    }
    applyTheme();
    sdk?.theme?.subscribe?.(applyTheme);

    // --- Identity ---
    const identity = sdk?.identity?.getState?.();
    if (identity?.displayName) {
      document.getElementById("greeting").textContent = `Hello, ${identity.displayName}`;
    }

    // --- Ask AI ---
    document.getElementById("run").addEventListener("click", async () => {
      if (!sdk?.intelligence) {
        document.getElementById("output").textContent =
          "AI is only available inside webAI.";
        return;
      }

      const prompt = document.getElementById("prompt").value.trim();
      if (!prompt) return;

      const btn = document.getElementById("run");
      const output = document.getElementById("output");
      btn.disabled = true;
      output.textContent = "";

      try {
        const response = await sdk.intelligence.chatCompletion({
          model: "auto",
          messages: [
            { role: "system", content: "You are a helpful assistant. Be concise." },
            { role: "user", content: prompt }
          ],
          max_tokens: 512
        });
        output.textContent = response.content;
      } catch (err) {
        output.textContent = "Error: " + err.message;
      } finally {
        btn.disabled = false;
      }
    });
  </script>
</body>
</html>
Save this as index.html and upload it to webAI — you’ll have a working AI chat app with theme support and a personalized greeting. The sections below break down each piece of the pattern.

Step 2: Understand the app model

Before adding more platform features, it helps to know how your app fits into webAI.

How apps run

When you upload an app, the webAI shell:
  1. Stores your HTML locally
  2. Registers it in the launcher alongside built-in apps
  3. When launched, loads it into a sandboxed iframe
  4. Reads your shell manifest and injects window.apogeeSDK with the facades you declared
Your app runs entirely client-side. There is no server.

Key constraints

ConstraintWhat it means
Single fileAll JS, CSS, and assets must be inlined into one HTML file
No external fetchesDon’t depend on CDN-hosted libraries at runtime
Iframe sandboxYour app runs in an iframe — some browser APIs may be restricted
Manifest requiredDeclare every facade you need; only declared facades are injected
Graceful degradationwindow.apogeeSDK is null outside webAI. Always guard before calling

Deep dive: App architecture

Learn more about the single-file model, iframe sandbox, and recommended project structure.

Step 3: Connect to platform APIs

The webAI shell injects a single bridge object — window.apogeeSDK — into your app. It exposes platform capabilities as domain-specific facades (sdk.intelligence, sdk.identity, sdk.room, sdk.messaging, and so on).

The access pattern

const sdk = window.apogeeSDK || null;
if (!sdk) return;
When your app runs outside the webAI shell (e.g., during local development with npm run dev, or if you double-click the HTML file), sdk is null. Always guard before calling.

Declaring your requirements

Your app tells the shell which facades it needs via the manifest you saw in Step 1:
<script id="apogee-shell-manifest"
  type="application/apogee-shell-manifest+json">
{
  "schemaVersion": 1,
  "name": "My App",
  "version": "1.0.0",
  "requires": {
    "managers": ["intelligence", "identity", "theme", "messaging"]
  }
}
</script>
Only the facades you list under requires.managers are injected. After loading, check sdk.supportedFacades to see what’s actually available — a facade may be omitted if the current shell doesn’t ship it (for example, a browser-only environment may expose fewer facades than the desktop app).

Available platform facades

DomainFacadesReference
AIintelligenceIntelligence →
Collaborationroom, messaging, canvas, filesCollaboration APIs →
Identityidentity, cryptoIdentity & Encryption →
Shellshell, theme, windows, settingsNavigation →

Deep dive: Accessing shell APIs

See the full access pattern, manifest declaration, and facade domain reference.

Step 4: Add platform features

Now that you know how to access APIs, let’s put them to use. Each section below is independent — add only what your app needs, and declare the matching facade in your manifest.

Add on-device AI

The intelligence facade lets your app run AI inference directly on the user’s device (or route to a registered cloud backend). Use chatCompletion for a simple request/response pattern:
async function askAI(prompt) {
  const sdk = window.apogeeSDK || null;
  if (!sdk?.intelligence) {
    console.warn("AI not available outside webAI.");
    return null;
  }

  const response = await sdk.intelligence.chatCompletion({
    model: "auto",
    messages: [
      { role: "system", content: "You are a helpful assistant." },
      { role: "user", content: prompt }
    ],
    max_tokens: 2048,
    temperature: 0.7
  });

  return response.content;
}
For token-by-token streaming UIs, use chatCompletionStream instead:
async function askAIStreaming(prompt, onDelta) {
  const sdk = window.apogeeSDK || null;
  if (!sdk?.intelligence) return;

  const stream = sdk.intelligence.chatCompletionStream({
    model: "auto",
    messages: [{ role: "user", content: prompt }],
    max_tokens: 2048
  });

  for await (const chunk of stream) {
    if (chunk.delta) onDelta(chunk.delta);
    if (chunk.finish_reason) break;
  }
}
Key points:
  • model: "auto" lets the runtime pick the best available backend for the device.
  • chatCompletion returns the full response after generation completes.
  • chatCompletionStream is an async iterator — each chunk carries a delta string and an optional finish_reason.

Intelligence reference

Covers chat completions, streaming, backend selection, cloud keys, and runtime state.

Identify the user

Read the user’s display name and device identity (ODID) through the identity facade:
function getIdentity() {
  const sdk = window.apogeeSDK || null;
  if (!sdk?.identity) return { odid: "local-dev", displayName: "You (dev mode)" };
  const state = sdk.identity.getState();
  return state ?? { odid: "unknown", displayName: "Guest" };
}

const user = getIdentity();
console.log(`Hello, ${user.displayName}`);
There are no accounts or usernames in webAI. Every device has a unique, auto-generated ODID that serves as its identity.

Identity & Encryption reference

Covers ODID, display name updates, and end-to-end encryption via sdk.crypto.

Respond to theme changes

The theme facade exposes the user’s theme preference and lets your app react to changes:
function applyTheme() {
  const sdk = window.apogeeSDK || null;
  const theme = sdk?.theme?.getState?.() === "light" ? "light" : "dark";
  document.documentElement.setAttribute("data-theme", theme);
}

applyTheme();
const sdk = window.apogeeSDK;
const unsubscribe = sdk?.theme?.subscribe?.(applyTheme);

// Call unsubscribe() on teardown to avoid leaks

Make it collaborative

Real-time collaboration is split across four facades — room hosts and joins spaces, messaging handles chat, canvas syncs shared app state, and files shares files inside a space.
const sdk = window.apogeeSDK || null;

async function startRoom() {
  if (!sdk?.room) return;
  const roomCode = await sdk.room.host({ userName: "Host" });
  console.log("Space created! Code:", roomCode);
}

async function enterRoom(code) {
  if (!sdk?.room) return;
  return sdk.room.join({ roomCode: code, userName: "Guest" });
}
The collaboration flow:
  1. One user hosts a space with sdk.room.host() — this returns a room code
  2. Others join using that code with sdk.room.join({ roomCode })
  3. Participants exchange state through the platform (chat via messaging, shared app state via canvas)
  4. The platform broadcasts changes, persists state, and resolves conflicts via

Collaboration APIs reference

Covers hosting, joining, canvas state, messaging, file sharing, and best practices.

Step 5: Set up a framework project (optional)

For anything beyond a simple prototype, a framework with a bundler gives you a better development experience. The only hard requirement is that your build outputs a single self-contained HTML file. Vite with vite-plugin-singlefile is one common path; any bundler that can inline JS, CSS, and assets into one HTML file will work.

Scaffold the project

npm create vite@latest my-app -- --template react
cd my-app
npm install
npm install --save-dev vite-plugin-singlefile

Configure the bundler

Update vite.config.js to inline all assets into a single HTML file:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { viteSingleFile } from "vite-plugin-singlefile";

export default defineConfig({
  plugins: [react(), viteSingleFile()],
  build: { outDir: "dist" }
});

Include the shell manifest

Add the manifest to index.html in the <head>:
<script id="apogee-shell-manifest"
  type="application/apogee-shell-manifest+json">
{
  "schemaVersion": 1,
  "name": "My App",
  "version": "1.0.0",
  "requires": {
    "managers": ["intelligence", "identity", "theme"]
  }
}
</script>

Create the integration module

Rather than scattering window.apogeeSDK || null throughout your codebase, create a src/webai.js (or .ts) file as your single integration layer:
export const getSdk = () => window.apogeeSDK || null;

export function getIdentity() {
  const sdk = getSdk();
  return sdk?.identity?.getState?.() ?? null;
}

export function getThemeState() {
  const sdk = getSdk();
  return sdk?.theme?.getState?.() ?? "dark";
}

export async function chatCompletion(messages, options = {}) {
  const sdk = getSdk();
  if (!sdk?.intelligence) {
    throw new Error("AI is not available outside webAI.");
  }
  return sdk.intelligence.chatCompletion({
    model: options.model ?? "auto",
    messages,
    max_tokens: options.maxTokens ?? 2048,
    temperature: options.temperature ?? 0.7
  });
}

export async function* streamCompletion(messages, options = {}) {
  const sdk = getSdk();
  if (!sdk?.intelligence) return;
  const stream = sdk.intelligence.chatCompletionStream({
    model: options.model ?? "auto",
    messages,
    max_tokens: options.maxTokens ?? 2048,
    temperature: options.temperature ?? 0.7
  });
  for await (const chunk of stream) yield chunk;
}
Then import from it anywhere in your app:
import { getIdentity, chatCompletion } from "./webai";

Develop locally

npm run dev
Your app opens in a normal browser tab. window.apogeeSDK will be null — this is expected. Design your UI with graceful fallbacks so you can iterate without the full shell running.
Show a subtle banner in dev mode like “Running outside webAI — AI and collaboration unavailable.” This makes it obvious which features need the shell while you focus on building your UI.

Deep dive: App lifecycle

Covers the full development workflow, project structure, bundler config, and uploading.

Step 6: Upload your app

Apps are uploaded to the webAI shell as a single HTML file — whether you wrote that file by hand or produced it via a bundler.

Vanilla HTML apps

Use the New App flow directly in the webAI launcher:
  1. Open the launcher
  2. Click New App
  3. Select your .html file
  4. Choose an icon and color
  5. Give it a name
Your app appears in the launcher immediately.

Framework apps

Framework apps need a build step first. Then upload the single-file output the same way as a vanilla app:
1

Build to a single file

npm run build
This produces dist/index.html — a single self-contained HTML file with all JS, CSS, and assets inlined.
2

Upload via the launcher

Open the launcher, click New App, and select the built dist/index.html. Pick an icon, color, and name.

Deep dive: App lifecycle

Covers the full build and upload flow, including versioning.

Step 7: Share it

Once your app is uploaded, sharing is as simple as being in a space:
  1. Open your app in a space
  2. Right-click it and choose Share App, or pin it for the group
  3. Other space members receive it automatically
There’s no app store, no publish step, no approval process. The space is the distribution channel.

Guide: Share apps

Learn all the ways to distribute apps — direct sharing, pinning, and responding to requests.

Putting it all together

Here’s a complete example of a framework-based app that uses AI, identity, and theme — the most common combination:
import { useState, useEffect } from "react";
import { getIdentity, getThemeState, chatCompletion } from "./webai";

export default function App() {
  const [userName, setUserName] = useState("Developer");
  const [theme, setTheme] = useState("dark");
  const [prompt, setPrompt] = useState("");
  const [output, setOutput] = useState("");
  const [generating, setGenerating] = useState(false);

  useEffect(() => {
    const identity = getIdentity();
    if (identity?.displayName) setUserName(identity.displayName);
    setTheme(getThemeState());

    const sdk = window.apogeeSDK;
    const unsub = sdk?.theme?.subscribe?.(() => setTheme(getThemeState()));
    return () => unsub?.();
  }, []);

  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
  }, [theme]);

  async function handleSubmit(e) {
    e.preventDefault();
    if (!prompt.trim() || generating) return;
    setGenerating(true);
    setOutput("");
    try {
      const response = await chatCompletion([
        { role: "system", content: "You are a helpful assistant." },
        { role: "user", content: prompt }
      ]);
      setOutput(response.content);
    } catch (err) {
      setOutput("Error: " + err.message);
    } finally {
      setGenerating(false);
    }
  }

  return (
    <div style={{ padding: "1.5rem", fontFamily: "system-ui, sans-serif" }}>
      <header><h1>My AI App</h1></header>
      <p>Hello, {userName}!</p>

      <form onSubmit={handleSubmit}>
        <textarea
          value={prompt}
          onChange={(e) => setPrompt(e.target.value)}
          placeholder="Ask something..."
          rows={3}
          style={{ width: "100%", fontSize: "1rem" }}
        />
        <button type="submit" disabled={generating}>
          {generating ? "Generating..." : "Ask AI"}
        </button>
      </form>

      {output && <pre style={{ whiteSpace: "pre-wrap", marginTop: "1rem" }}>{output}</pre>}
    </div>
  );
}

Best practices

window.apogeeSDK is null outside of webAI. Wrap every access in a check so your app works during local development and doesn’t crash when a facade isn’t available. Optional chaining (sdk?.intelligence?.chatCompletion?.(...)) is a concise way to do this.
Only facades listed under requires.managers are injected. If sdk.room is missing at runtime, check your manifest first. Inspect sdk.supportedFacades to see what the shell actually provided.
Apps under 1MB load quickly. If your build exceeds 5MB, consider optimizing images, removing unused dependencies, or lazy-loading heavy components.
When using chatCompletionStream, let users cancel long generations. Break out of the for await loop as soon as the user asks to stop, and ignore any remaining chunks. This keeps the UI responsive and avoids wasted compute.
Every facade with subscribe(handler) returns an unsubscribe function. Call it on teardown (useEffect cleanup, onUnmounted, etc.) to avoid memory leaks.
Peers can drop out at any time. Design your state model to handle partial participation. See the Collaboration APIs best practices.
Build your UI and core logic so it works in a normal browser tab. Add platform features on top with graceful fallbacks. This makes development much faster.

Quick reference

Everything you need in one place:
TopicGuideAPI reference
How apps are structuredThis pageApp architecture
Accessing platform APIsThis pageAccessing shell APIs
On-device AIThis pageIntelligence
User identityThis pageIdentity & Encryption
Real-time collaborationThis pageCollaboration APIs
NavigationNavigation
Build & deployThis pageApp lifecycle
SharingShare apps guide

Next steps

Share apps

Distribute your apps to others through spaces — no app store needed.

API Reference

Explore the full API documentation for all platform capabilities.