Websites

The web SDK runs in every current browser and needs no framework. It mirrors the tab as page structure (DOM, using rrweb), not as video: this saves bandwidth and keeps text sharp.

Adding the SDK

With a script tag

<script src="https://relyo.de/sdk/relyo.js"></script>
<script>
  const relyo = Relyo.init({ relayUrl: 'wss://relyo.de', projectKey: 'pk_yourproject' });
</script>

The script creates the global object Relyo (about 40 kB gzip). Call Relyo.init once per page; a second call returns the same instance.

Two addresses are available:

AddressBehaviour
https://relyo.de/sdk/relyo.jsalways the latest version, browsers check for changes every time (no-cache)
https://relyo.de/sdk/v0.1.0/relyo.jsfixed version, cached for one year

Load only on click

The leanest option: the SDK is only loaded when someone taps “Support”. Until then there is not a single request to relyo.de. This is how the support link on jochen-maurer.com works.

<a href="mailto:[email protected]" id="support">Support</a>

<script>
  let relyo;
  function loadRelyo() {
    relyo ??= new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = 'https://relyo.de/sdk/relyo.js';
      script.onload = () =>
        resolve(
          Relyo.init({ relayUrl: 'wss://relyo.de', projectKey: 'pk_yourproject', connect: 'lazy' }),
        );
      script.onerror = () => {
        relyo = undefined;
        reject(new Error('Relyo not reachable'));
      };
      document.head.append(script);
    });
    return relyo;
  }

  document.querySelector('#support').addEventListener('click', (event) => {
    event.preventDefault();
    const fallback = event.currentTarget.href; // fallback if relyo.de cannot be reached
    loadRelyo()
      .then((r) => r.showCode())
      .catch(() => (location.href = fallback));
  });
</script>

As a module

relyo.mjs is the same SDK as an ES module and can be loaded from any page:

const { Relyo } = await import('https://relyo.de/sdk/relyo.mjs');
const relyo = Relyo.init({ relayUrl: 'wss://relyo.de', projectKey: 'pk_yourproject' });

There is no npm package yet.

React and Next.js

The SDK needs window and document. In Next.js it therefore belongs in a client component and is only loaded on click, never during server rendering.

'use client';

interface RelyoInstance {
  showCode(): void;
}
declare global {
  interface Window {
    Relyo?: { init(options: Record<string, unknown>): RelyoInstance };
  }
}

let relyo: Promise<RelyoInstance> | undefined;

function loadRelyo(): Promise<RelyoInstance> {
  relyo ??= new Promise<RelyoInstance>((resolve, reject) => {
    const script = document.createElement('script');
    script.src = 'https://relyo.de/sdk/relyo.js';
    script.onload = () =>
      window.Relyo
        ? resolve(
            window.Relyo.init({
              relayUrl: 'wss://relyo.de',
              projectKey: 'pk_yourproject',
              connect: 'lazy',
            }),
          )
        : reject(new Error('Relyo not loaded'));
    script.onerror = () => reject(new Error('Relyo not reachable'));
    document.head.append(script);
  }).catch((err: unknown) => {
    relyo = undefined;
    throw err;
  });
  return relyo;
}

export function SupportLink() {
  return (
    <button type="button" onClick={() => loadRelyo().then((r) => r.showCode())}>
      Support
    </button>
  );
}

Navigation inside the app (React Router, Next.js links) needs nothing else: the SDK notices new addresses by itself.

Options

Relyo.init takes an object with these fields:

OptionDefaultMeaning
relayUrl–Required. Server address, for example wss://relyo.de; https:// becomes wss://.
projectKey–Your website’s project key. Without a key, a server with the open service assigns the page to its address (origin).
user–Signed-in user, { id, name }. Display only for your supporters, not verified. See Signed-in users.
connect'auto'When the SDK connects, see Connection.
redactSelector–Additional CSS selector for areas to mask, for example '.account-details, [data-private]'. .relyo-redact always applies.
maskAllInputsfalsetrue masks all input fields, not just the sensitive ones.
consentText–Your own explanation in the consent dialog; {agent} is replaced by the supporter’s name.
cspNonce–Nonce for the SDK’s style element if your CSP only allows inline styles with a nonce.
appVersion–Version of your site; shown in the console.
locale'de'Language of the dialogs. Only German so far.

Relyo.init returns an instance with these methods:

MethodEffect
showCode()requests a code and shows it in a dialog
end()ends a running session or cancels the code or consent
identify(user)sets the signed-in user, identify(null) on logout
destroy()tears down the SDK; a running session can be resumed by a new init in the same tab

Connection

connectBehaviour
'auto'with user, connect at once (the device appears in the console); anonymous visitors only on showCode()
'eager'always connect at once, even anonymously
'lazy'only on showCode(); the connection closes again when the code or session ends

For a support link in the footer, 'lazy' is the right choice. Before the first connection the SDK stores nothing in the browser, not even an identifier.

Signed-in users

With user (or later identify), the devices of signed-in users appear in the console’s device list. Supporters with their own account can request a session there without the user reading out a code; consent on the device is still required.

relyo.identify({ id: 'u_123', name: 'Max M.' }); // after login
relyo.identify(null); // on logout

The device list is only available for projects with a project key, not with the open service. id and name are for display only; do not send data your supporters should not see.

Masking

Masking happens in the user’s browser before any data leaves it:

  • Always: fields with type="password", type="email" and type="tel", fields with autocomplete="cc-…" (card data) and autocomplete="one-time-code".
  • .relyo-redact: elements with this class and everything inside. The supporter sees an empty area in their place; input inside arrives as *.
  • redactSelector: works like .relyo-redact for your own selectors.
  • maskAllInputs: true: all input fields.
<div class="relyo-redact">IBAN: DE12 3456 …</div>

Mask everything with sensitive data that the rules above do not cover, such as health data or ID numbers in ordinary text fields. The SDK rejects remote input into masked fields.

Content Security Policy

If your page has a CSP, Relyo needs:

DirectiveValuePurpose
script-srchttps://relyo.dethe script or module
connect-srcwss://relyo.dethe connection to the server
style-src'unsafe-inline' or 'nonce-…' with the cspNonce optionthe SDK dialogs (in a shadow DOM)

With your own server, use its address instead of relyo.de.

Flow and page changes

  • Consent: transmission only starts after “Share”. The dialog states what is shared (this tab) and what is not.
  • Status bar: during the session a bar with the supporter’s name and “End” sits at the top. It cannot be turned off.
  • Reloads and links: if the page reloads or the user follows a link to another page with Relyo, the SDK resumes the session in the same tab. The server keeps it open for 30 seconds.
  • Control: clicks, input, scrolling and the keys Enter, Escape, Tab and arrows. Any click or key press of the user’s own ends control at once.

Limitations

  • Content from third-party iframes (for example embedded videos or payment forms) appears as an empty area.
  • <video> and <canvas> are not mirrored.
  • Remote keys do not trigger browser default actions: Tab does not move focus, arrow keys do not scroll. Enter in a form field submits the form.
  • The dialogs are in German only so far.
  • Images are sent along so that the supporter can see them even behind a login. Pages with many large images need correspondingly more bandwidth.

Error messages

If no code can be requested, the dialog shows a message instead of the code (currently in German):

MessageCause
“Keine Verbindung zum Support-Server …”server not reachable, network or CSP blocks it, or too many connections from the same IP address
“Gerade sind alle Support-Plätze belegt …”limit for concurrent sessions reached
“Support ist für diese Seite derzeit nicht verfügbar.”site blocked
“Der Support-Code konnte nicht angefordert werden.”for example an unknown project key or an address that does not match the key

This page as Markdown