GlopDocs

Build for the feed

A Glop is something you can use, right inside a post. Build a small web app, upload its files, and let people open it in the feed.

Use HTML, CSS, and JavaScript, or export a static build from your preferred tools. Glop hosts your files in an isolated frame. The SDK connects your app to the feed, saved progress, other viewers, and host-managed capabilities.

Quickstart

  1. Create a folder with index.html.
  2. Download glop-sdk.mjs into that folder.
  3. Paste the example below into index.html.
  4. In Glop, choose Upload glop and upload the folder or a ZIP. Review the draft, then approve and publish.

This counter restores saved progress and proposes new progress after each click. The viewer decides whether to save using Glop’s own controls.

index.html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Counter</title>
<style>
  body { margin: 0; min-height: 100dvh; display: grid;
    place-content: center; background: #182322; color: #fff;
    font: 20px system-ui; text-align: center; }
  button { padding: 16px 28px; font: inherit; }
</style>
<button id="counter" disabled>Count: 0</button>
<script type="module">
  import {createGlop} from './glop-sdk.mjs';
  const button = document.querySelector('#counter');
  let count = 0;
  let restored = false;
  const glop = createGlop({
    onState({active, progress}) {
      if (!restored) {
        count = Number.isSafeInteger(progress?.count) ? progress.count : 0;
        restored = true;
      }
      button.disabled = !active;
      button.textContent = `Count: ${count}`;
    }
  });
  button.onclick = () => {
    button.textContent = `Count: ${++count}`;
    glop.save({count});
  };
</script>
</html>

Host features activate inside Glop. Opening the file directly is useful for layout work, but it does not provide a host connection.

How Glops work

An upload becomes a private draft. Preview its layout and behavior before publishing it to the feed. Your uploaded files run in a sandbox; account credentials stay with Glop.

Active
The selected Glop receives lifecycle updates and can use SDK services, including while you use another browser tab. Nearby frames may preload without starting network work.
Inactive
When you scroll away from a Glop, the host pauses animation frames, intervals, and tracked audio. Use lifecycle callbacks to pause your own work too. One-shot timeouts are not paused.
Disposed
The frame can be removed. Clean up listeners and resources; do not rely on in-memory state surviving.

Published posts have a fixed seed. Reset restores the original seed and clears that viewer’s saved progress for the release.

SDK at a glance

Build withAPIAvailable in drafts
Lifecycle and layoutcreateGlop(options)Yes
Personal progressglop.save(value)Proposal only
Rooms and matchmakingglop.connect(options)No
Persistent shared dataglop.storageNo
Cross-Glop shared spacesShared view totalsglop.views.get()No
HTTPS and socketsglop.fetch / glop.socketPublic production
Voice chatglop.voice.enable(options)No
Glop links and portalsglop.navigate / glop.pickGlopPicker only
Links and locationglop.openURL / glop.locationYes
Creator-hosted serversglop.homeserver.request(value)Requires consent

Download the current SDK when using new methods. Existing uploads keep the SDK version bundled with their files.

Lifecycle and layout

createGlop({onState, onViewport, onDispose}) returns your host connection. glop.state reads its current lifecycle state. Viewport dimensions and obstruction rectangles use CSS pixels inside the frame.

Keep controls within the reported viewport and clear of feed obstructions. Center constrained layouts in the remaining safe area. Updates arrive when the viewport or host controls change.

JavaScript
import {createGlop} from './glop-sdk.mjs';
const glop = createGlop({
  onState({active, seed, progress, reducedMotion}) {
    // Start or pause app work. Respect reducedMotion.
  },
  onViewport({viewport, obstructions, fullscreen}) {
    // Fit your layout and keep controls clear of obstructions.
  },
  onDispose() {
    // Disconnect observers and clean up resources.
  }
});
// Seeded randomness; call after the host supplies the seed.
const randomForRun = () => glop.random(glop.state.seed);

Fullscreen, input, and sound

Outside fullscreen, vertical touch and wheel gestures belong to the feed. Design embedded content to fit one viewport. Enter fullscreen for vertical scrolling or panning inside the Glop; the host then reports an empty obstruction list.

Mouse capture is available to every Glop. A click on the active, visible app surface in Glop fullscreen can capture the mouse; outside fullscreen, capture is denied. Use document.pointerLockElement, pointerlockchange, and element.requestPointerLock(). Escape, leaving fullscreen, or hiding the page releases capture. The host forwards relative input; forwarded events do not grant trusted browser privileges.

Use normal Web Audio or audio/video elements. Glop provides a Mute/Unmute button with the feed controls and in draft preview, and remembers the choice across Glops and reloads. You do not need a separate sound-enable button. The host retries browser-blocked audio on normal interaction or Unmute; browser autoplay rules still apply. Inactive Glops stay paused. Microphone permission for voice chat is separate.

Save personal progress

glop.save(value) proposes up to 4 KiB of JSON. It does not immediately persist it: the viewer must choose Save progress in Glop. Proposals are accepted at most once per second.

Progress belongs to the viewer’s account and an immutable release. Restore it from onState({progress}). Drafts have no persistent progress namespace. Treat restored data as app data, never as a trusted score.

JavaScript
glop.save({level: 3, score: 120});
// Restore from the progress field in your onState callback.

Rooms and messages

Connect instances of the same Glop using a named channel. Connections wait until active; draft previews cannot join. A room is scoped to the project, channel, and mode. Releases of the same project can communicate, so version your channel when protocols change.

The connection exposes id, playerId, roomId, peers, state, revision, and locked. Player IDs are scoped to this Glop. Messages support up to 4 KiB of JSON.

JavaScript
const room = await glop.connect({
  channel: 'lobby-v1',
  onPeers(peers) { console.log(peers); },
  onMessage({from, value}) { console.log(from, value); },
  onState(value, {revision}) { console.log(value, revision); },
  onHistoryGap() { console.log('Recover from shared state'); },
  onError(error) { console.error(error.message); }
});
await room.send({kind: 'hello'}); // Includes the sender.
// await room.send({kind: 'hello'}, {to: peerId});
await room.refresh();
// await room.close();

Shared state and matchmaking

room.setState(value, expectedRevision) atomically replaces up to 16 KiB of room state. On a 409 conflict, refresh, merge, and retry. Any member can write; enforce trusted rules on your own server when needed.

For matchmaking, pass mode: 'match' and a capacity from 2 to 64. locked becomes true when the room fills. Full matches do not receive replacement participants. Seats belong to distinct accounts and can resume within 60 seconds; close() explicitly leaves.

Room messages are short-lived: at most 512 events for five minutes. Rooms and state expire after 24 hours without activity. Use persistent storage for data that must survive that window.

JavaScript
const match = await glop.connect({
  channel: 'game-v1', mode: 'match', capacity: 2,
  onState(state, {locked}) { console.log({state, locked}); }
});
try {
  await match.setState({turn: 0}, match.revision);
} catch (error) {
  if (error.status === 409) await match.refresh();
  else throw error;
}

Persistent shared storage

glop.storage stores JSON shared by the published Glop’s signed-in viewers. It survives reloads, closed rooms, and edits. Deleting the Glop deletes its storage. Do not store secrets or trusted scores here.

get(key) returns {key, value, revision}; a missing key has a null value and revision. Pass the revision you read to set or delete. list() returns keys with revisions and byte sizes. Revisions are opaque.

Keys contain 1–128 ASCII letters, digits, dots, slashes, underscores, or hyphens. Limits: 64 KiB per value, 256 keys, 1 MiB per Glop, and 120 writes per minute per viewer.

JavaScript
const current = await glop.storage.get('settings');
try {
  await glop.storage.set('settings', {theme: 'blue'}, {
    revision: current.revision
  });
} catch (error) {
  if (error.status === 409) {
    // Read again, merge concurrent changes, then retry.
  } else throw error;
}

Cross-Glop shared spaces

Different Glops can opt into the same persistent JSON store. Ordinary glop.storage stays isolated to its project.

  1. Open the creator editor and choose Edit → Shared spaces. Create a named space and copy its space ID.
  2. Invite another Glop using its project ID, with read-only or read/write access.
  3. That Glop’s creator accepts the invitation under Shared spaces. Both Glops must be published for runtime access.
JavaScript
const world = glop.spaces.open('SPACE_UUID');
const current = await world.get('plants');
await world.set('plants', {roses: 3}, {
  revision: current.revision
});

The handle supports get, list, set, and delete with ordinary storage’s conflict checks. Read-only connections can only get/list. Requests require a signed-in viewer and wait until the Glop is active. There are no subscriptions; fetch updates when needed.

Creators can revoke or disconnect at any time. Permission changes need fresh acceptance. Data persists across updates and restarts; deleting the owning Glop or space deletes its data. Private-beta projects cannot join. This is shared project data, not personal saves or secrets.

Limits: 8 owned spaces per Glop, 32 invited Glops per space, 64 KiB per value, 256 keys and 1 MiB per space, 120 writes per minute per viewer per space across connected Glops. Full SDK contract · Management API

Shared view totals

glop.views.get() returns {total}, the cumulative view count across releases of a project. It waits until active and rejects draft previews with status 409. Poll about every five seconds while active if updates are needed.

Reading does not count a view. The host credits qualified visible visits, at most once per signed-in account per project in 30 minutes. Preloads, drafts, and brief flybys do not count. Counts are aggregate and cannot be set by the SDK.

JavaScript
const {total} = await glop.views.get();
console.log(`${total} views`);

Internet requests and sockets

On public production, including owner draft previews, glop.fetch relays public HTTPS requests and returns a standard Response. glop.socket connects to public WSS endpoints. The separate private Sites beta does not provide this relay.

Only public domain names on port 443 are allowed. Redirects and private addresses are rejected. Glop credentials are not forwarded. Never bundle secret API keys: uploaded code is readable.

Requests wait until active and are aborted when you scroll away or close the Glop. Switching browser tabs keeps the Glop and its SDK connections active; the browser may still throttle background work. Sockets close and must be reopened on resume. Remote images should use fetched blobs; direct remote tags and native external fetch remain blocked.

JavaScript
const response = await glop.fetch('https://api.example.com/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();

const socket = await glop.socket('wss://game.example.com', {
  onMessage: value => console.log(value),
  onClose: () => console.log('Connection closed'),
  onError: error => console.error(error)
});
await socket.send(JSON.stringify({action: 'join'}));
// socket.close();

Voice chat

glop.voice.enable() adds host-owned voice controls. Enabling voice does not open the microphone. Joining and unmuting require explicit actions in Glop’s trusted UI.

Up to eight people can join a project-scoped channel. Your app receives participant identities and speaking indicators, never audio streams. Joined voice continues across tab switches; leaving voice or leaving the Glop stops capture. Drafts cannot join.

See the voice protocol and setup reference for state fields and relay configuration.

JavaScript
const voice = await glop.voice.enable({
  channel: 'lounge-v1',
  onState(state) { console.log(state); }
});
// voice.requestJoin() requests the host join flow.
// voice.mute();
// voice.leave();

Glop links and portals

Use glop.navigate(releaseIdOrURL) to open another published Glop in the same feed. The host places it immediately below the current Glop and visibly scrolls down into it, even if it was not in the feed. Reduced-motion settings disable the animation.

Navigation works for guests. Only the active feed Glop can request it. The host enforces a five-second cooldown shared across all Glops, including the destination, to prevent redirect loops. Requests during cooldown reject with status: 429 and retryAfterMs; they are not queued. Show the error and let the visitor try again. Manual scrolling remains available.

Use await glop.pickGlop() from a button click to let a visitor choose one of their own published Glops. The host shows the picker; your app receives only the chosen {releaseId, name, url}, or null if canceled. Guests are asked to sign in and receive a 401 error; after signing in they can choose again. Private drafts and beta content are excluded.

Connect your own buttons and labels to a portal
import {createGlop} from './glop-sdk.mjs';
const glop = createGlop();
let destination;

chooseButton.onclick = async () => {
  try {
    const chosen = await glop.pickGlop();
    if (chosen) {
      destination = chosen; // {releaseId, name, url}
      portalLabel.textContent = chosen.name;
    }
  } catch (error) { statusLabel.textContent = error.message; }
};
enterButton.onclick = async () => {
  if (!destination) return;
  try { await glop.navigate(destination.releaseId); }
  catch (error) { statusLabel.textContent = error.message; }
};

The example keeps the destination in memory. To make a shared persistent portal, read const {revision} = await glop.storage.get('portal'), then save with await glop.storage.set('portal', destination, {revision}). Restore the reference from (await glop.storage.get('portal')).value. Shared storage requires sign-in. Use your app's editing rules to decide who may replace it. You can also send the reference through a room.

The picker works in draft previews, but navigation requires a published feed Glop. External URLs are rejected. Missing or unavailable destinations reject without leaving the source. A successful navigation returns {opened: true} when the scroll begins; the destination may still be loading. The current release returns {opened: false}. Choosing a reference does not expose source files or embed the destination inside your app.

External links and location

Call these APIs from a click while your Glop is active. Glop asks the viewer to confirm each request. Canceling, hiding the tab, or leaving the Glop cancels the request. Both APIs work in drafts.

openURL accepts an absolute HTTPS URL without credentials. It resolves with {opened: true} when a tab opens, not when the destination loads. location.get() returns one approved location; browser permission is also required. Glop does not persist coordinates.

JavaScript
openButton.onclick = async () => {
  try { await glop.openURL('https://example.com/'); }
  catch (error) { console.error(error.message); }
};
locationButton.onclick = async () => {
  try {
    const {latitude, longitude, accuracy} = await glop.location.get();
    console.log({latitude, longitude, accuracy});
  } catch (error) { console.error(error.message); }
};

Creator-hosted servers

For a server you operate, include a glop.json manifest with one HTTPS homeserver origin. After the viewer approves Glop’s Connect control, glop.homeserver.request(value) sends JSON through the consent-gated relay.

The host enforces consent, lifecycle, and request limits. Use the homeserver protocol and example for the manifest schema, identity verification, and server implementation.

Host a Glop on your own domain

Choose Or external HTTPS page in the creator, enter your page URL, preview it, and approve. You can also replace an existing Glop’s files with an external URL through Edit. Its link, stars, and comments stay the same.

The browser loads the page and its assets directly from your server. Glop stores the URL, not a copy of your build, so Glop’s file and bundle size limits do not apply. Your server controls bandwidth, availability, and updates. Use versioned URLs if you want a publication to keep showing the same build. A private draft does not make your external website private.

Prepare the page

Add this script near the beginning of your HTML head, before your app scripts. It supplies feed gestures, sound controls, layout readiness, and lifecycle handling. The SDK remains optional for storage, rooms, and other Glop APIs.

<script src="https://glop.it/glop-embed.js"></script>

You can download and host that script yourself. The URL must use a public HTTPS domain, without credentials or a fragment. Glop reserves the fragment for its embed channel. The page must work without login cookies and in an allow-scripts sandbox: cookies, localStorage, service workers, popups, and top-level navigation are unavailable. Save shared data with the SDK or your own CORS-enabled API.

Allow embedding with Content-Security-Policy: frame-ancestors https://glop.it and remove conflicting X-Frame-Options headers. If you set a script policy, permit the embed script. Serve module scripts, fonts, fetched assets (including WASM), and APIs with Access-Control-Allow-Origin: * and use credential-free requests, because the sandbox has an opaque origin. The external server receives visitors’ network requests. Glop does not send its account credentials.

Preview requires a readiness message from the embed script. If it fails, check your page’s embedding policy, script loading, and CORS headers. The host removes unavailable frames after 30 seconds. Glop cannot provide source downloads or enforce hidden source files on an external server.

CLI

node glop-cli.mjs external https://your-domain.com/glop/ --name "My Glop"
# Open the returned draftUrl to preview and approve.

Preview and publish

Upload an HTML file, ZIP, or built folder. Set Supported devices to All devices, Mobile only, or Desktop only. Phones and tablets count as mobile. Unsupported devices see a notice instead of running your Glop; creator previews work on any device. Include your assets and use relative paths. Preview phone, landscape, desktop, and current-window sizes; enable Simulate feed chrome to test obstructions. These sizes change the frame’s actual CSS viewport without resetting its state.

For coding tools, download the standalone CLI (Node 20+):

Terminal
node glop-cli.mjs login
node glop-cli.mjs upload ./dist --name "My Glop"

Authorize the tool in your browser. Upload returns a private draft link for review and publication. Keep creator tokens in your local tool, never in an uploaded app. Revoke access in Your account → Connected tools.

See the Creator API reference for authorization, uploads, and draft workflows.

Limits and error handling

ResourceLimit
Upload256 files · 64 MiB per file · 128 MiB total
Progress proposal4 KiB JSON · one per second
Rooms4 connections per frame · 64 live instances per shared room
Room messages / state4 KiB / 16 KiB
Shared storage64 KiB per value · 256 keys · 1 MiB total
HTTP relay64 KiB text request · 64 MiB response · 120-second timeout
Sockets2 per mount · 64 KiB text sends · 30-minute lifetime
Network operations180 per minute per account

Handle 409 by refreshing and reconciling revisions. Handle 429 with backoff and retryAfterMs when provided. A room write rejected with room_changed needs reconciliation before retrying. Use message nonces to retry uncertain sends without duplicating them.

Socket messages arrive in batches about once per second. Use them for chat and live updates; account for that latency in interactive applications.

Uploads cannot run server processes, load CDN scripts, use eval, or create Web Workers. Single-threaded WebAssembly compilation and instantiation are supported. Host safeguards do not guarantee CPU or memory limits. See the complete SDK contract for protocol details and additional quotas.