Skip to content

Server configuration examples

lovasoa edited this page May 14, 2026 · 3 revisions

Server configuration examples

WBO reads server configuration from environment variables when the server starts. Restart the container after changing these values.

The complete source of truth is server/configuration.mjs. The examples below cover common deployment requests.

The examples use the official Docker image, lovasoa/wbo:latest, expose WBO on host port 5001, and persist board data in ./wbo-boards on the host.

Redirect the root page to a board

Set WBO_DEFAULT_BOARD to a board name:

mkdir -p wbo-boards
chown -R 1000:1000 wbo-boards

docker run \
  --publish 5001:80 \
  --volume "$(pwd)/wbo-boards:/opt/app/server-data" \
  --env WBO_DEFAULT_BOARD=anonymous \
  lovasoa/wbo:latest

When this variable is set, requests to / redirect to /boards/<board>. The board name is normalized the same way user-entered board names are normalized, so uppercase letters become lowercase and unsupported characters become dashes.

Add trusted HTML to the page head

Set WBO_HTML_HEAD_SNIPPET_PATH to a file containing the exact HTML you want WBO to inject before </head>:

mkdir -p wbo-boards wbo-config
chown -R 1000:1000 wbo-boards

docker run \
  --publish 5001:80 \
  --volume "$(pwd)/wbo-boards:/opt/app/server-data" \
  --volume "$(pwd)/wbo-config/head-snippet.html:/opt/app/config/head-snippet.html:ro" \
  --env WBO_HTML_HEAD_SNIPPET_PATH=/opt/app/config/head-snippet.html \
  lovasoa/wbo:latest

The snippet is inserted into rendered HTML pages, including the landing page and board pages. The file is read once at startup. WBO_HTML_HEAD_SNIPPET_PATH must point to the path inside the container, not the host path.

Only use a file controlled by the server administrator. The snippet is inserted as raw HTML, so it can run scripts or change page behavior.

Google Analytics example

Create ./wbo-config/head-snippet.html on the Docker host with your own measurement ID:

<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag("js", new Date());
  gtag("config", "G-XXXXXXXXXX");
</script>

Then start WBO with:

mkdir -p wbo-boards
chown -R 1000:1000 wbo-boards

docker run \
  --publish 5001:80 \
  --volume "$(pwd)/wbo-boards:/opt/app/server-data" \
  --volume "$(pwd)/wbo-config/head-snippet.html:/opt/app/config/head-snippet.html:ro" \
  --env WBO_HTML_HEAD_SNIPPET_PATH=/opt/app/config/head-snippet.html \
  lovasoa/wbo:latest

Self-hosted deployments should make sure this is acceptable for their privacy policy and local legal requirements.

Add a legal notice, custom meta tag, or stylesheet

The same head snippet can add links or metadata:

<link rel="me" href="https://example.com/legal-notice">

This is intended for deployment-owned additions. It is not a theme engine and it does not replace WBO templates.

If you also want to serve a custom stylesheet from WBO itself, mount it into the container's web root and link to it from the snippet:

<link rel="stylesheet" href="/custom-wbo.css">
mkdir -p wbo-boards wbo-config
chown -R 1000:1000 wbo-boards

docker run \
  --publish 5001:80 \
  --volume "$(pwd)/wbo-boards:/opt/app/server-data" \
  --volume "$(pwd)/wbo-config/head-snippet.html:/opt/app/config/head-snippet.html:ro" \
  --volume "$(pwd)/wbo-config/custom-wbo.css:/opt/app/client-data/custom-wbo.css:ro" \
  --env WBO_HTML_HEAD_SNIPPET_PATH=/opt/app/config/head-snippet.html \
  lovasoa/wbo:latest

Enable JWT board access

Set AUTH_SECRET_KEY to require JWTs for opening boards:

mkdir -p wbo-boards
chown -R 1000:1000 wbo-boards

docker run \
  --publish 5001:80 \
  --volume "$(pwd)/wbo-boards:/opt/app/server-data" \
  --env AUTH_SECRET_KEY="replace-with-a-long-random-wbo-signing-secret" \
  lovasoa/wbo:latest

Clients pass a token with ?token=.... Token roles can grant:

  • reader: can open the board but cannot edit.
  • editor: can open and edit.
  • moderator: can open, edit, and clear.

Roles can be scoped to a board name, for example reader:class-a or moderator:class-a.

WBO verifies these tokens with the shared AUTH_SECRET_KEY and expects a roles claim that is an array of strings.

Keycloak example

A common Keycloak setup is:

  • Create a Keycloak realm, for example school.
  • Create an application client, for example course-app, using the normal Authorization Code + PKCE flow for your web application.
  • Create client or realm roles such as wbo-reader, wbo-editor, and wbo-moderator.
  • Assign those roles to users or groups in Keycloak.
  • In your application backend, validate the Keycloak login/session, map the user's Keycloak role to a WBO role, and sign a short-lived WBO token with the same secret configured as WBO's AUTH_SECRET_KEY.

Do not put AUTH_SECRET_KEY in browser code. The browser should only receive the short-lived WBO token created by your backend.

Keycloak usually signs its own access tokens with realm keys such as RS256. WBO's current AUTH_SECRET_KEY flow verifies a shared-secret token, so the most portable integration is to validate Keycloak in your app and then mint a WBO-specific token.

Very short backend example:

import jwt from "jsonwebtoken";

app.get("/wbo-token", requireKeycloakUser, (req, res) => {
  const board = String(req.query.board || "class-a");
  const keycloakRoles = req.user.roles || [];
  const role = keycloakRoles.includes("wbo-moderator")
    ? "moderator"
    : keycloakRoles.includes("wbo-editor")
      ? "editor"
      : "reader";

  const token = jwt.sign(
    { roles: [`${role}:${board}`] },
    process.env.WBO_AUTH_SECRET,
    { expiresIn: "15m" },
  );

  res.json({ token });
});

process.env.WBO_AUTH_SECRET in this embedding application must match the AUTH_SECRET_KEY passed to the WBO container.

Very short iframe example:

<iframe id="wbo" title="Whiteboard" style="width: 100%; height: 600px; border: 0"></iframe>

<script type="module">
  const board = "class-a";
  const response = await fetch(`/wbo-token?board=${encodeURIComponent(board)}`);
  const { token } = await response.json();
  const iframe = document.getElementById("wbo");
  iframe.src = `https://wbo.example.com/boards/${encodeURIComponent(board)}?token=${encodeURIComponent(token)}`;
</script>

See the repository README for the current JWT payload rules.

Clone this wiki locally