Skip to content
View in the app

A better way to browse. Learn more.

ernestdefoe.online

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
ernestdefoe.online

Extensions, themes & support for Flarum and Invision Community

Vibe coding for the community web. Report a bug, request a feature, or dig into the source — this is where the tools you use get built, in the open.

We do custom Bespoke Invision Community apps. If you have an idea for something you want then use the contact form to get in touch with us.
Knowledge base

Things that cost me a day, so they cost you none

Working notes from building Invision Community and Flarum applications. Mostly the failures that give no error at all — the ones where everything installs cleanly and quietly does the wrong thing.

92 articles

Invision Community 5

86 articles

Extensions and contracts

39

What each extension point is for, what it must declare, and what happens when it is wrong — which is usually nothing visible.

Languages and text

5

The string table, translation, and the places where text does not appear where you expected it to.

Theming, templates and forms

9

Theme hooks, CSS that survives both colour schemes, and building forms that do not throw on render.

Background work and scheduled tasks

5

The queue system, work that has to happen after the response, and jobs that finish without doing anything.

Data, settings and storage

11

The database layer, settings, tags, file storage, and backing up a live site.

AI features and expectations

5

What these features do, what they cost, and what buyers reasonably but wrongly assume they do.

Application structure and releases

11

The JSON files an application is made of, versioning and upgrade steps, and testing from the command line.

Realtime, chat and calls

1

WebSocket gateways, relays and the server-side pieces live features depend on — where "it works when I test it" and "it works for your members" are different claims.

Nothing matches that.

A realtime gateway will let any member join any channel unless you sign the channel into the token

A WebSocket gateway that fans Invision Community events out to browsers usually authenticates the connection — a short-lived token, signed with a shared secret, proves the socket came from a page your community served. That is enough while every channel is public, like a forum topic. The moment a channel carries something private — a conversation between two members — it is not enough, because the token says who the member is but not which channels they are allowed to join. Nothing stops a logged-in member from editing the client and subscribing to pm.999, a conversation they are not part of.

Sign the channel into the token, not just the member

The fix is to make the granted channels part of what the signature covers. When the server renders a page, it already knows what that page is allowed to listen to — so it signs those channels into the token:

// PHP, when injecting the client on a page the member may listen to
$exp = time() + 3600;
sort( $channels );                       // deterministic, so both sides rebuild the same string
$csv = implode( ',', $channels );
$sig = hash_hmac( 'sha256', $memberId . ':' . $exp . ':' . $csv, $secret );

The gateway recomputes the same HMAC from the member id, the expiry, and the channel list the client presents, and rejects the connection if they do not match. Because the channels are inside the signature, a member cannot add one: change the channel list and the signature no longer verifies.

// gateway (Node), on auth
const csv = Array.isArray(channels) ? [...channels].sort().join(',') : '';
const expected = createHmac('sha256', RT_SECRET).update(`${memberId}:${exp}:${csv}`).digest('hex');
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) return reject();
ws.grants = new Set(channels);           // the private channels this socket may join

Then enforce the grant on subscribe

Public channels still need no grant — a forum topic carries nothing private. Only the private ones are gated, against the grant the verified token established:

if (/^(topic|forum)\.\d+$/.test(c)) join(c);                 // public
else if (/^pm\.\d+$/.test(c) && ws.grants.has(c)) join(c);   // private: only if signed in

So privacy rests on two things at once: the signature (a member cannot forge a grant) and the enforcement (the gateway refuses a private channel that was not granted). The server only ever signs a private channel after it has checked, in PHP, that the member is genuinely a participant.

Keep the body off the socket. Even on a correctly-scoped private channel, broadcast only that something happened — who replied, and when — not the message itself. The reader loads the new message through the normal, permission-checked page. The socket then never becomes a second path to content, and a mistake in channel scoping cannot leak a message body.

Changing the signed payload is a breaking change for the verifier

The signature is a contract between two programs. The day you add the channel list to it, every token the server mints changes shape, and a gateway still running the old code will reject all of them — the connection simply fails and realtime goes quiet. It fails safe: pages keep working, only the live updates stop. But it means the gateway has to be redeployed in step with the application whenever the token payload changes. Ship the two together, and say so where an administrator will read it.



User Feedback

Recommended Comments

There are no comments to display.

Account

Navigation

Search

Search

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.