Choosing how your web app saves to HashBin

SDK reference · Register an app · Manage app access

  1. Save only when the user clicks Save? Use publish(). The user reviews the document, retention and exact price on HashBin and approves each save. Your app receives the CID and never holds a credential.
  2. Need autosave or background saves? Call authorize() to open HashBin Publish's allowance entry page, then ask the user to set up silent saves. The SDK suggests $1/month, a maximum file size of 1 MiB and a 30-day expiry. Consent also offers 1- and 7-day expiries. Users can tighten the cap, file size and expiry on HashBin. Call save() for silent uploads, with a maxCostCents ceiling. Call disconnect() to revoke the grant server-side; clearTokens() only removes the local copy.
  3. Hosted on a shared origin? All apps under https://username.github.io/ share browser storage, regardless of their paths. The same applies to GitLab user sites, university ~user pages and other path-based hosts. Registration requires acknowledging known shared hosts and origins already used by another registered app. Consent lists matching registered apps. A count of zero does not mean the origin has no other apps. Prefer per-save approval, or give each allowance app its own origin.
  4. Run code from users or the internet? Put it in an opaque-origin sandboxed iframe without allow-same-origin, or on another origin. Treat restored drafts, caches and share links as untrusted. Ask before replacing unsaved work.
  5. Read content? retrieve() reads and verifies the full body against its CID before returning a Response. It checks inline bytes up to 64 bytes and SHA-512 for larger content, including the CID's length prefix. For your own fetches use await verifyCID(cid, bytes). A false result means the content must not be used.
  6. Pin the SDK for apps that spend money. Use the immutable version URL and SRI script tag in the SDK guide. A plain JavaScript import pins a version but does not enforce SRI. Pinned clients must upgrade deliberately to receive fixes. The unversioned URL receives automatic updates and is convenient for experiments.

Per-save approval

const client = hashbin.createClient({
  clientId: 'YOUR_REGISTERED_CLIENT_ID',
  redirectUri: 'https://your-app.example/callback'
});
// Invoke directly in a click handler so the popup opens before any awaits.
saveButton.onclick = async () => {
  const result = await client.publish(editor.value, { contentType: 'text/plain' });
  if (!result.redirected) showContentLink(client.getContentUrl(result.cid));
};

Popup mode supports documents up to 5 MiB. It authenticates both the sender's origin and window, correlates a random state, and verifies the returned CID. If the popup is blocked, the SDK redirects with a fragment for documents up to 512 KiB. Larger documents require enabling popups and retrying. The fragment never reaches the server. Use mode: 'redirect' explicitly when your app uses Cross-Origin-Opener-Policy: same-origin, or use same-origin-allow-popups. On return, call handlePublishCallback(); it verifies the saved bytes and restores your optional clientState. Your app should persist any other editor state before navigation.

HashBin shows text as text, including HTML and SVG; it never executes a document preview. The displayed approval expires in five minutes, works once, and binds the content, content type, user, retention and exact charge. A failed upload requires a new review. Always check that the approval address is your configured HashBin host.

Allowance protections and limits

New SDK grants use DPoP with a non-extractable P-256 private key in IndexedDB. Every authenticated SDK API request and refresh proves possession of that key; the dedicated content worker enforces it for funded OAuth downloads too. The server verifies the signature, method, URL, access-token hash, timestamp, nonce and replay identifier. If IndexedDB is unavailable, use popup authorization: the key and tokens stay in memory and sign-in is required next session. Redirect authorization requires persistent key storage. Existing unbound tokens retain their existing expiry; raw OAuth clients without DPoP still receive bearer tokens. The bundled native and command-line examples use that raw flow and do not automatically gain device binding from the browser SDK. Refreshes require the registered client ID.

DPoP makes a copied token unusable on another device. Code already running on the same origin can still ask the browser key to sign. Use small caps and a separate origin for allowance apps. Access tokens last 15 minutes. Refresh tokens expire after seven days without renewal, and a refresh family lasts at most 90 days. Grant expiry can shorten both. Reusing a rotated refresh token revokes the whole grant and its replacement tokens. Already-reserved funded download leases may finish within their existing 60-second window. The SDK uses Web Locks to serialize refreshes across tabs and rereads tokens after acquiring the lock. Browsers without Web Locks should use per-save approval or keep an allowance in one active tab.

The account page shows last use, monthly spending against the cap, recent uploads per grant and in-app notifications for first use, refresh reuse revocation and reaching 80% of the monthly cap. “Revoke all app access” revokes all OAuth grants and refresh tokens. It does not revoke separately managed API keys.

Give each app its own origin

A custom domain on a GitHub user site still puts project paths on one origin. Isolation requires a custom domain per project (for example pages.example.com and editor.example.com), a separate GitHub organization per app, or a host that assigns a distinct origin per project. Never share parent-domain cookies. Release a migration build before switching origins: GitHub's redirect can strand drafts and settings on the old origin. Revoke and clear old tokens as part of migration.

Use a Content Security Policy to restrict executable code, verify app-code caches before serving them, and keep HashBin price and credential prompts outside all user-content areas. These are app responsibilities; HashBin cannot protect drafts or code stored on a shared origin.

DPoP follows RFC 9449; refresh reuse revocation follows RFC 9700. Test real sign-in and popup behavior in your supported browsers, particularly Safari with tracking prevention.

Upgrade an existing client

For credential-free saves, remove OAuth setup and use publish() with handlePublishCallback() for redirect results. To keep silent uploads, move old token-based publish() calls to save() and add maxCostCents. Include client_id on raw refreshes and serialize renewals. Use disconnect() for server revocation. Custom authorizedFetch() calls still need their own CID checks and price ceiling. The SDK migration instructions include the separate publish and OAuth callbacks.