Apps SDK v2

Build E2E encrypted apps that run inside noone. chats -- 1:1 and groups.

Concept

Apps are sandboxed modules that run inside a chat conversation. They can send and receive JSON messages through the same E2E encrypted channel used for text messages. Apps have:

Apps do not have access to: private keys, the vault, other contacts, the relay, or any data outside the current conversation.

Sandbox Architecture

NOONE CORE Crypto (WASM) Vault (IndexedDB) Relay Manager Message Channel sendMessage() onMessage() loadHistory() participants[] JSON APP SANDBOX Your App Code container (DOM element) sdk.sendMessage(data) sdk.onMessage(callback) No access to keys, vault, relay, or other chats

NooneAppSDK Class

class NooneAppSDK {
  appId: string           // Your app's unique identifier
  container: HTMLElement  // DOM element to render into
  contact: object         // { label, nostrPkHex } or { label, groupId }
  t: function             // Translation: t('key') -> localized string

  // Messaging
  sendMessage(data: object): Promise  // Send JSON to all participants
  onMessage(callback): unsubscribe    // Receive messages; returns cleanup function
  loadHistory(): Promise<array>       // Load past app messages for state replay

  // Group context (v2)
  isGroup: boolean                    // true in group chats
  participants: array                 // [{ label, nostrPkHex, isMe }]
  myNostrPkHex: string               // Your own Nostr pubkey

  // P2P status (v2)
  isP2PConnected: boolean             // DataChannel open with at least one peer
  onP2PStatusChange(cb): unsubscribe  // Notified on P2P status changes

  // Helpers
  getMe(): object                     // Your participant entry
  getOthers(): array                  // Non-self participants
  getRole(nostrPkHex, maxPlayers=2): 'player' | 'spectator'
}

Message Format

All app messages must be JSON objects with an _app field:

{
  "_app": "your-app-id",
  "type": "move",         // your custom type
  "data": { ... }         // your custom payload
}

Messages are automatically encrypted and delivered to all participants via the existing E2E pipeline. In 1:1 chats, messages go via P2P DataChannel (if connected) or Nostr relay. In groups, messages go to all members.

Creating an App

Minimal Example

// web/src/apps/my-app.js

export function createMyApp(sdk) {
  const container = sdk.container;

  // Render UI
  container.innerHTML = '<div id="my-app">Hello from my app!</div>';

  // Send a message
  document.getElementById('my-app').onclick = () => {
    sdk.sendMessage({ _app: 'my-app', type: 'ping', ts: Date.now() });
  };

  // Receive messages
  const unsubscribe = sdk.onMessage((data) => {
    if (data._app !== 'my-app') return;
    console.log('Received:', data);
  });

  // Return cleanup function
  return function cleanup() {
    unsubscribe();
    container.innerHTML = '';
  };
}

Registering the App

In web/src/main.js, add to the BUILTIN_APPS object:

const BUILTIN_APPS = {
  // ... existing apps ...
  'my-app': { id: 'my-app', icon: '💡', name: () => t('myApp'), color: '#3D8B6E' },
};

And add the lazy import in launchAppInChatScreen():

} else if (appId === 'my-app') {
  import('./apps/my-app.js').then(m => {
    cleanup = m.createMyApp(sdk);
    activeApp = { id: appId, cleanup: cleanup || (() => {}) };
  });
}

Group Support

Detecting Group Context

export function createMyApp(sdk) {
  if (sdk.isGroup) {
    console.log('Running in group with', sdk.participants.length, 'members');
    // Show participant list, handle multi-player logic
  } else {
    console.log('Running in 1:1 chat with', sdk.contact.label);
  }
}

Player vs Spectator (Turn-Based Games)

PLAYER 1 can interact (moves, draws) PLAYER 2 can interact (moves, draws) SPECTATOR read-only view SPECTATOR read-only view First N participants (by join order) are players. Rest are spectators. Chess: N=2. Card game: N=4. Sketch: everyone participates (no roles).
const myRole = sdk.getRole(sdk.myNostrPkHex, 2); // 2 = max players

if (myRole === 'player') {
  // Allow interaction (moves, draws, etc.)
} else {
  // Read-only view -- show board state but disable input
}

Multi-User Collaboration (No Roles)

For apps like Sketch where everyone participates equally:

// All participants draw on the same canvas
// No role distinction needed
// Messages are broadcast to everyone automatically
sdk.sendMessage({ _app: 'sketch', type: 'stroke', points: [...], color: '#000' });

State Replay (History)

Apps should support replaying state from history so they work correctly when re-opened:

// Load past messages on init
const history = await sdk.loadHistory();
for (const msg of history) {
  if (msg._app !== 'my-app') continue;
  // Replay each message to rebuild state
  applyMessage(msg);
}

loadHistory() returns an array of parsed app messages, each with a fromMe boolean indicating direction.

P2P Awareness

// Check current P2P status
if (sdk.isP2PConnected) {
  // Low latency -- enable real-time features
}

// Listen for status changes
sdk.onP2PStatusChange((connected) => {
  if (connected) showLowLatencyUI();
  else showRelayFallbackUI();
});

P2P DataChannel provides ~50ms latency vs ~2-5s via relay. Apps that need real-time interaction (drawing, gaming) should indicate connection quality to users.

Built-in Apps

AppIDGroup ModeDescription
Chesschess2 players + spectatorsFull rules, algebraic notation, move log, captured pieces
SketchsketchAll draw simultaneouslyReal-time whiteboard with live stroke preview, undo, clear
LocationlocationAll share positionsGPS sharing on OSM map tiles (grayscale), distance calculation

Security Model

For third-party apps: Currently, apps must be bundled into the build. A future plugin system with stricter sandboxing (iframed, postMessage API) is planned. For now, all apps are reviewed and included in the open-source codebase.

Message Types Reference

Chess

{ _app: 'chess', type: 'start', color: 'white' }   // Start game
{ _app: 'chess', type: 'move', from: 'e2', to: 'e4' } // Make move
{ _app: 'chess', type: 'resign' }                    // Resign
{ _app: 'chess', type: 'draw_offer' }                // Offer draw
{ _app: 'chess', type: 'draw_accept' }               // Accept draw

Sketch

{ _app: 'sketch', type: 'stroke', points: [[x,y],...], color: '#000', width: 3 }
{ _app: 'sketch', type: 'stroke', ..., partial: true }  // Live preview
{ _app: 'sketch', type: 'clear' }                        // Clear canvas

Location

{ _app: 'location', type: 'position', lat: 51.5, lng: -0.1, acc: 10, ts: 1700000000 }
{ _app: 'location', type: 'stop' }

MIT License. All app code is open source and auditable.