Skip to content

Profile Plugins ​

What it does ​

A profile plugin renders as a card on a user's profile. It displays beneath the user's name, avatar, and bio. Multiple profile plugins stack vertically on the same profile.

When to use this context ​

Use profile plugins for:

  • User metadata: game rank, streaming status, music now playing
  • Activity: recent achievements, contribution graphs, activity stats
  • Social links: verified accounts, portfolio, website
  • Presence badges: online/offline, timezone, location

The identity model ​

Profile plugins have a unique security model because they appear on other users' profiles.

  • Profile owner (ctx.user): The user whose profile is being viewed. Always provided, with no grant needed.
  • Viewer (the current user looking at the profile): Never exposed. Dissent.getUser() and every other call outside the profile lane (below) is refused.

This prevents plugins from leaking the viewer's identity to external services. What a profile plugin can show comes from ctx.user (the owner's display name) and ctx.config (values set for this card), plus the few calls in the profile lane — it cannot fetch anything from the internet.

Minimal example ​

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <script src="https://app.dissent.chat/sdk/v1/dissent-plugin-sdk.iife.js"></script>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: system-ui, sans-serif;
      background: transparent;
      color: rgba(255,255,255,0.85);
      padding: 16px;
    }
    .card {
      display: grid;
      gap: 12px;
    }
    .stat {
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .label {
      font-size: 13px;
      color: rgba(255,255,255,0.5);
      text-transform: uppercase;
      letter-spacing: 0.5px;
      font-weight: 600;
    }
    .value {
      font-size: 16px;
      font-weight: 700;
    }
    .error {
      font-size: 12px;
      color: #ff6b6b;
    }
  </style>
</head>
<body>
  <div class="card">
    <div class="stat">
      <span class="label">Player</span>
      <span class="value" id="player">—</span>
    </div>
    <div class="stat">
      <span class="label">Main game</span>
      <span class="value" id="game">—</span>
    </div>
    <div class="stat">
      <span class="label">Rank</span>
      <span class="value" id="rank">—</span>
    </div>
    <div id="error" class="error"></div>
  </div>
  <script>
    async function main() {
      const ctx = await Dissent.init({
        onFallback: () => {
          document.getElementById('error').textContent = 'Running outside Dissent';
        },
      });

      // Apply theme
      for (const [k, v] of Object.entries(ctx.theme)) {
        document.documentElement.style.setProperty(k, v);
      }

      // ctx.user is the profile owner — the person whose profile is being viewed
      document.getElementById('player').textContent = ctx.user?.displayName ?? '—';

      // ctx.config holds the values set for this card
      document.getElementById('game').textContent = ctx.config.game ?? '—';
      document.getElementById('rank').textContent = ctx.config.rank ?? '—';

      // Theme updates
      Dissent.on('theme:change', (theme) => {
        for (const [k, v] of Object.entries(theme)) {
          document.documentElement.style.setProperty(k, v);
        }
      });
    }

    main().catch(err => console.error(err));
  </script>
</body>
</html>

The profile lane ​

A profile frame has no server install and no channel, and it is shown to visitors. It may make only these requests; everything else is refused with <action> is not available to profile plugins, whatever the manifest declares:

ActionPermissionSDK
presence:readnone— (send the raw request) — the profile owner's public presence
storage:localGet / storage:localSet / storage:localDeletestorage:localDissent.storageLocal.* — kept on the viewer's device

So the only permission a profile plugin can use is storage:local. If its manifest declares it, anyone viewing the card — the owner included — first sees a Requires permission tile, and the consent card offers exactly that permission. Other declared permissions are left off the card, because the frame would refuse them anyway.

Gotchas ​

  • Only the profile lane. getUser(), fetch(), storage.*, postMessage() and the rest are refused in profile context. The manifest tier makes no difference here.
  • Viewer identity never available. Dissent.getUser() is refused in profile context. The plugin can only see the profile owner, in ctx.user.
  • Consent is per device. A profile plugin's grant is stored in the viewer's browser, not on the node, so each device asks once. The record is kept apart from the plugin's own storage:local data, which cannot read or change it.
  • ctx.channel is undefined in profile context. Don't rely on it.
  • Users add their own plugins. Profile owners go to User Settings → Profile and click "Add Plugin" to enable it for themselves. They're in full control.