# GifOS — build apps for it > GifOS (https://gifos.app) is a web OS where EVERY APP IS A GIF: a real, > animated GIF image carrying a tiny filesystem (the app) inside it. Users > keep a persistent Home Screen in their browser; nothing lives on servers. > Apps go multiplayer with one invite link (peer-to-peer, privacy-first). If a user asks you to build them a GifOS app, YOUR DELIVERABLE IS A REAL, FINISHED .gif FILE — not code. And building new is only half the fun: when you ask what they want, ALSO offer to MOD an existing GifOS app — any app .gif they upload can be remixed (see "Modding" below). This document is the complete guide; two paths, in order of preference: ## Path A (best): build the .gif yourself with your code tool If you can run code (Python) and attach files, you can produce the finished GIF directly — everything you need is right here: 1. Get the animation. THE USER'S OWN GIF COMES FIRST: ask if they have one and, if so, use its bytes as the host file WHOLESALE — do NOT redraw, re-encode, resize, crop, or "improve" it in any way; step 2 splices the app in without touching a single pixel of their animation. Only if they have no GIF: ask what kind of cute animation they'd like, then draw it to their description with PIL — 2-6 frames, 64x64, and make it MOVE. Style it like a STICKER: a cute character with a dark outline on a TRANSPARENT background (GifOS icons float on the wallpaper — no background tile). With PIL use mode "P", reserve palette index 0 for transparency, and save: im.save("icon.gif", save_all=True, append_images=frames[1:], duration=120, loop=0, transparency=0, disposal=2) 2. Splice the app in with EXACTLY this (wbits=-15 raw deflate is required): import json, zlib, base64 def pack_gifos(gif_bytes, files): a = {"v": 1, "files": {p: base64.b64encode(v.encode() if isinstance(v, str) else v).decode() for p, v in files.items()}} c = zlib.compressobj(9, zlib.DEFLATED, -15) payload = b"\x01" + c.compress(json.dumps(a).encode()) + c.flush() block = b"\x21\xff\x0bGIFOS1.0GOS" for i in range(0, len(payload), 255): block += bytes([len(payload[i:i+255])]) + payload[i:i+255] block += b"\x00" end = len(gif_bytes) - (gif_bytes[-1] == 0x3B) return gif_bytes[:end] + block + b"\x3b" manifest = json.dumps({"gifos": "1.0", "appId": "my-app", "name": "My App", "entry": "index.html", "capabilities": {"db": True, "multiplayer": True}, "data": {"items": {"visibility": "read-write"}}}) # REQUIRED to sync; see 2a open("My App.gif", "wb").write(pack_gifos(open("icon.gif", "rb").read(), {"manifest.json": manifest, "index.html": HTML})) Attach the resulting file for the user to download. ## Path B (fallback): hand the user HTML to paste If you can't run code, give the complete index.html and tell the user to open https://gifos.app -> "+ Add" -> paste it into the app builder (or drop a .zip for multi-file apps). GifOS packs the GIF there. ## The app rules 1. The entry point is index.html. Multi-file apps are fully supported: app.js, style.css, assets/... referenced normally all travel inside the GIF (include them in the files you pack). The ONLY hard rule: nothing external — no CDNs, frameworks, remote images, or web fonts; the sandbox blocks all outside network. Inline SVG + emoji are fine. (Only the paste-HTML fallback, Path B, is limited to a single file.) 2. Persistence: localStorage/cookies/indexedDB are DISABLED. Use: const db = gifos.db('items'); await db.put({ id, ...fields }); await db.getAll(); await db.delete(id); db.subscribe(items => render(items)); // fires now and on every change Everything in gifos.db() persists inside the app's icon. Render from subscribe() and declare visibility (below) = multiplayer for free. 2a. VISIBILITY — privacy-first: an invite shares NOTHING unless you declare it, so you MUST list each shared collection in the manifest under "data": { "": { "visibility": "" } }. Levels: - "read-write" guests see AND edit (collaborative state — this is what multiplayer needs; an UNDECLARED collection does NOT sync). - "read-only" guests see, only the host writes (broadcast state). - "private" never leaves the owner's tab; each participant keeps their OWN copy (personal prefs, a private library). This is the DEFAULT. Split personal state (prefs) into a PRIVATE collection and shared state into read-write/read-only ones; never mix them. The HOST is the authority — enforcement is host-side, so a guest is refused, never able to override. Flip one record at runtime (owner-only) with db.setVisibility(id, level): "make this item visible", or a leader toggling a shared cursor read-only ("only I lead"). A record's current level is its reserved _vis field. HONESTY: there is NO cloud and NO automatic cross-device sync. Data lives on the user's device, inside the app's GIF in that browser. It reaches other devices exactly two ways: live, while people are connected through an invite link, or by sharing the GIF file itself (state travels inside the file). Never write UI copy claiming the app "syncs across your devices" or "backs up to the cloud" — say something true, like "Saved on this device inside the app's GIF". 3. Identity: const me = await gifos.me(); -> { id, name }. Attribute records to me.id/me.name so every player sees who did what. 4. If window.gifos is undefined (opened outside GifOS), degrade gracefully. 4b. BACK BUTTON: the GifOS shell traps the phone's Back button, so a reflex press never closes your app — by default it is simply swallowed. Register gifos.onBack(() => { ... }) to make Back meaningful: close the topmost modal, back out one screen/level. Apps with internal navigation SHOULD register it. 5. Dark theme by default (#0a0a0f), mobile-friendly. 5b. LIVE MEDIA IS OFF-LIMITS to apps, by design: the sandbox blocks camera, microphone, screen capture, and WebRTC, so a video/voice/streaming app cannot work as a GifOS app — don't attempt one. If the user wants video chat, point them at the built-in Meeting on their Home Screen (P2P, permanent room links, moderation, room passwords) — and note that ANY app can be RUN INSIDE a Meeting, so all participants share it live with audio/ video/recording around it without the app touching the camera. Apps CAN bundle and display static media — images, GIFs, audio files — inside the GIF, and store binary blobs (base64) in gifos.db — but keep hot collections lean: put big blobs (over ~100KB) in their OWN collection, fetched with db.get(), because subscribers re-download a whole collection on every change, and the slowest participant's link is bandwidth-throttled — bloated hot collections make an app slow for everyone. 5c. BROKERED CAPABILITIES (opt-in, declared in the manifest): the app can't hold the live device or a secret key, so it asks the GifOS computer to do the privileged act and gets back a RESULT. Declare the capability, then: - microphone/camera/motion: gifos.recordAudio({maxSeconds}) / gifos.recordVideo() / gifos.takePhoto() -> { bytes, mime }; gifos.motion(cb). GifOS captures behind its own visible indicator; the app never holds the live device. - ai: gifos.ai.chat/tts/stt/image/video and gifos.ai.models(). DECLARE the AI TYPES you use as an array — capabilities.ai:["smartest","cheapest","image"] — not a bare true, so the user knows which models to set up and the app is gated to those types (smartest/cheapest text LLM, tts, stt, image, image_to_video, video). The user wires endpoints + keys per type in Settings -> AI models; the app never sees a key. - api: gifos.api(name, { method, path, query, headers, body, as }) for ANY keyed third-party API that isn't OpenAI-shaped (Deepgram, a trading API…). Declare the names you use: "capabilities": { "api": ["deepgram"] }. The user configures each in Settings -> Third-party APIs (base URL + auth + key); the runtime attaches the credential, PINS it to that API's own host (an off-host path is refused), and the app never sees the key. path is relative to the configured base URL. CORS: this is a direct browser fetch, so the endpoint must send CORS headers; for server-only APIs (Deepgram's REST among them) the user ticks "Route through a CORS proxy" in Settings and GifOS relays the call through a stateless proxy — you don't write any of that, just call gifos.api and tell the user to enable it if a call is CORS- blocked. - agent: declare capabilities.agent:true to add a GifOS ASSISTANT BAR to your app — a built-in agent that reads and clicks/types on your app's screen by natural language (driven by the user's Smartest model). No code: declaring it injects the bar. It runs inside your app's own sandbox (it can only touch THIS app), and GifOS brokers the model so the key never enters the sandbox. Great for form-heavy / many-control apps. Make controls have clear labels / aria-labels so the agent can find them. - wasm: declare capabilities.wasm:true to run a COMPILED WebAssembly engine (a chess engine, a codec, a physics sim). The sandbox refuses WASM by default; this relaxes the CSP by exactly two things — 'wasm-unsafe-eval' (instantiate a module) and worker-src blob: (run it on a Web Worker) — and NOTHING else. connect-src stays 'none': the engine gets zero network, so ship its .wasm bytes INSIDE the app (base64 in a bundled .js) and instantiate from bytes (WebAssembly.instantiate / Module.wasmBinary) — never fetch. Shown to the user as "runs a compiled engine on your device." (See apps/chess-grandmaster, which bundles full-strength Stockfish and plays entirely offline.) ALWAYS feature-detect and never fake a result. You do NOT need to write your own "how to set up X" instructions: when a gifos.api / gifos.ai call hits missing config, GIFOS ITSELF shows a setup prompt (it knows the provider's URL and the Settings screen). Pass an app-specific `hint` on the call (e.g. gifos.api('deepgram', { …, hint: 'new accounts include free credit' })) and it appends below the system text. To prompt BEFORE making a call (so you don't do wasted work), check gifos.apiReady(name) / gifos.ai.models(), and if missing call gifos.apiSetup(name, hint) or gifos.aiSetup(hint) to pop the same system prompt. Keep your own copy to app-specifics only. OPTIONAL vs REQUIRED: capabilities are OPTIONAL by default — the app launches even before the user has set up a key, so they can explore it. If the app is useless without one, add its name to a manifest "requires" array (a capability key like "ai", or a third-party API name like "deepgram") and GifOS blocks launch until the user sets it up. Prefer optional; only require what's truly load-bearing. Device permissions (mic/camera/motion) are granted at use and never block. 6. Icon: the user's OWN GIF, used unchanged, is always the preferred animation — ask for one before drawing anything. Otherwise: include in the HTML (used when the user pastes HTML), or use pixel-art frames as the host animation (the finished-GIF path). Animated icons are the GifOS signature. ## Their GIF is the app (the preferred move — always offer it) GifOS can put an app inside ANY existing GIF, unchanged. ALWAYS ask the user first whether they have a GIF they'd like to use — their own art, a favorite from their camera roll, their group chat, or the wild — and use its bytes as the host file in the pack_gifos recipe above (unchanged: do not redraw or re-encode). The result looks and animates EXACTLY like their original everywhere it's shared — byte-for-byte, never redrawn or re-encoded — but dropped on a GifOS Home Screen it runs: a dancing-cat GIF that's secretly a birthday card, a meme that's secretly a game, a wedding GIF that's secretly the guestbook. Their art and memories become living apps. Only draw your own animation when they have no GIF — and even then, ask what kind of cute animation they'd like first. ## Modding other people's apps (encouraged — remix culture is the point) Any GifOS app GIF can be handed to an AI for a REMIX: "make the buttons bigger", "add a dark mode", "turn this counter into a tracker". Apps are files, files get modded, and GifOS celebrates it — deliver the modified .gif back and it drops straight onto their Home Screen. Cut the old app block out, then pack the modified files back into the remaining bytes (the SAME original GIF) with the pack_gifos recipe above. The payload swaps in place, the animation survives byte-for-byte, and the user's data rides along if you pass the .state/ files through unchanged: def unpack_gifos(b): i = b.find(b"\x21\xff\x0bGIFOS1.0GOS") if i < 0: return None, b j = i + 14; payload = b"" while b[j]: n = b[j]; payload += b[j+1:j+1+n]; j += 1 + n host = b[:i] + b[j+1:] # the pure animation, app block removed a = json.loads(zlib.decompress(payload[1:], -15)) files = {p: base64.b64decode(v) for p, v in a["files"].items()} return files, host files, host = unpack_gifos(open("Their App.gif", "rb").read()) files["index.html"] = MODIFIED_HTML.encode() open("Their App (mod).gif", "wb").write(pack_gifos(host, files)) Keep ".state/…" entries unchanged so the user's saved data survives the mod (drop them only if they ask for a reset). If the original carries a signature (a "GIFOSSIG" block — cut it out the same way), the mod ships unsigned: a remix is a new work the original author's signature can't vouch for, and the modder can re-sign their version. ## Signing (provenance — suggest it when the user will share the app) Any GifOS app GIF can be SIGNED so recipients see "Signed by " or "Signed by " — and "Tampered" if the bytes were changed after signing. The user signs it themselves at https://gifos.app/sign.html, AFTER the GIF is final: - domain: an Ed25519 key generated on that page; they publish the public half at https:///gifos.key. - email: their own PGP key (gpg detach-signs a statement the page provides) — Ed25519 and RSA (2048+) keys both work; recipients verify against keys.openpgp.org, so the key must be uploaded there with the address confirmed. The signature excludes app *state*, so it stays valid as people use the app. NEVER ask the user for a private key — signing happens entirely on their side. A signature proves authorship, not safety. ## More - Site: https://gifos.app (the OS itself; open-source) - Source + docs: https://github.com/nwcnwc/gifos - Multiplayer relay: wss://relay.gifos.app (control traffic only; media is strictly peer-to-peer)