Appearance
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:
| Action | Permission | SDK |
|---|---|---|
presence:read | none | — (send the raw request) — the profile owner's public presence |
storage:localGet / storage:localSet / storage:localDelete | storage:local | Dissent.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 manifesttiermakes no difference here. - Viewer identity never available.
Dissent.getUser()is refused in profile context. The plugin can only see the profile owner, inctx.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:localdata, which cannot read or change it. ctx.channelis 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.