Skip to main content
This page walks through the full lifecycle of a webAI app: setting up your project, developing locally, bundling for production, and uploading to the webAI shell.

Setting up your project

Start by scaffolding a new project with Vite and installing the single-file bundler plugin:
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm install --save-dev vite-plugin-singlefile

Configure Vite for single-file output

Update your vite.config.js to use vite-plugin-singlefile. This inlines all JavaScript, CSS, and assets into a single index.html at build time:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; // or vue() for Vue projects
import { viteSingleFile } from 'vite-plugin-singlefile';

export default defineConfig({
  plugins: [react(), viteSingleFile()],
  build: { outDir: 'dist' },
});
The vite-plugin-singlefile plugin is required for webAI compatibility. Without it, your build will produce separate JS and CSS files that can’t be loaded inside the shell.

Create the integration module

Create src/webai.js as your single integration point with the shell APIs:
export const getShellAPI = (name) =>
  window[name] ?? window.parent?.[name] ?? null;

export const getOasisHost            = () => getShellAPI('OasisHost');
export const getApogeeShell          = () => getShellAPI('ApogeeShell');
export const getCollaborationManager = () => getShellAPI('CollaborationManager');
export const getUserIdentityManager  = () => getShellAPI('UserIdentityManager');
export const getE2ECrypto            = () => getShellAPI('E2ECrypto');

export function getOasisState() {
  const host = getOasisHost();
  if (!host?.getStatus) return 'waiting';
  const s = host.getStatus();
  if (s?.lastModel) return 'ready';
  if (s?.loadingModel || s?.isGenerating) return 'loading';
  return 'waiting';
}

export async function streamCompletion(prompt, systemPrompt, onToken) {
  const host = getOasisHost();
  if (!host) throw new Error('Oasis AI is not available in this environment.');
  const release = await host.acquire({ warmRuntime: true });
  try {
    return await host.request(prompt, {
      systemPrompt: systemPrompt ?? '',
      maxTokens: 2048,
      temperature: 0.7,
      onToken,
    });
  } finally {
    if (release) release();
  }
}

Local development

Run the Vite dev server to iterate on your app:
npm run dev
Your app opens in a normal browser tab. Shell APIs (OasisHost, CollaborationManager, etc.) will be null — this is expected. Build your UI and logic around graceful fallbacks for these APIs so you can develop comfortably without the full shell running.

Building for production

When you’re ready to deploy, build the single-file output:
npm run build
This produces dist/index.html — a single self-contained HTML file with all JavaScript, CSS, and assets inlined.
Check the file size of your build output. Apps under 1MB load quickly. If your build exceeds 5MB, consider optimizing — large apps may be slow to load inside the shell.

Uploading to the shell

webAI apps are stored in the browser’s localStorage under the key apogee-uploaded-apps. The shell reads this on startup and registers each entry as an app in the launcher. There are two ways to upload.

Option 1: Direct install (desktop app)

If you’re running the webAI desktop app (Tauri), the app exposes a local install server at http://127.0.0.1:44280/install. You can POST your app directly:
node scripts/upload.js
The upload script detects whether the Tauri app is running and installs directly. If it’s not running, it falls back to Option 2.

Option 2: Browser console script

Generate a paste-able script and run it in the browser console:
  1. Build your app: npm run build
  2. Run the upload generator: node scripts/upload.js
  3. Copy the output script
  4. Open your webAI shell in Chrome
  5. Open DevTools (F12 or Cmd+Option+I)
  6. Paste the script into the Console and press Enter
  7. Refresh the launcher — your app appears

Creating the upload script

Add scripts/upload.js to your project:
#!/usr/bin/env node
import { readFileSync } from 'fs';
import { resolve } from 'path';

const htmlPath = resolve('./dist/index.html');
let html;
try {
  html = readFileSync(htmlPath, 'utf8');
} catch {
  console.error('dist/index.html not found. Run `npm run build` first.');
  process.exit(1);
}

const pkg = JSON.parse(readFileSync('./package.json', 'utf8'));
const appId = pkg.name;
const displayName = pkg.description || pkg.name;

const uploadScript = `
(async function uploadToApogee() {
  const htmlContent = ${JSON.stringify(html)};
  const appId = ${JSON.stringify(appId)};
  const displayName = ${JSON.stringify(displayName)};

  const hashBuffer = await crypto.subtle.digest(
    'SHA-256',
    new TextEncoder().encode(htmlContent.replace(/\\s+/g, ' ').replace(/<!--[\\s\\S]*?-->/g, ''))
  );
  const sourceId = Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0')).join('');

  const stored = JSON.parse(localStorage.getItem('apogee-uploaded-apps') || '[]');
  const filtered = stored.filter(app => app.appId !== appId);
  filtered.push({ appId, displayName, htmlContent, sourceId, uploadedAt: Date.now(), version: 1 });
  localStorage.setItem('apogee-uploaded-apps', JSON.stringify(filtered));

  console.log('[webAI] Uploaded: ' + displayName + ' (' + appId + ')');
  console.log('[webAI] Refresh the Apogee launcher to see your app.');
})();
`.trim();

console.log('=== Paste this in your browser console on the Apogee shell page ===\\n');
console.log(uploadScript);
console.log('\\n=== End of script ===');
And add the corresponding npm script to package.json:
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "upload": "node scripts/upload.js"
  }
}

How versioning works

Each uploaded app includes a sourceId — a SHA-256 hash of the HTML content. The shell uses this for:
  • Deduplication: re-uploading the same content overwrites the existing entry
  • Cross-device sync: the sourceId matches the approach used by the shell’s internal ApogeeShellManager
  • Version tracking: each upload increments the version field
The appId is derived from your package.json name field. The displayName is derived from the description field. Keep these meaningful — they’re what users see in the launcher.

Full workflow summary

1

Scaffold

Create a new Vite project with React or Vue and install vite-plugin-singlefile.
2

Develop

Run npm run dev and build your app. Shell APIs are null during local dev — design around it.
3

Integrate

Import from src/webai.js to use AI, navigation, collaboration, or identity features.
4

Build

Run npm run build to produce a single dist/index.html.
5

Upload

Run npm run upload and follow the instructions — either direct install or browser console paste.
6

Share

Open your app in a space and share it with others. They receive it automatically.

Next steps