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.

Your video calls work in testing and fail for a third of your members

You set up calling, you test it with a colleague, it works. Then members report that calls ring, connect, and carry no audio — or never connect at all. Nothing appears in any log, and it works every time you try it.

This is almost always the same thing: you tested from two machines that could reach each other directly, and a large share of real members cannot.

Why a relay is not optional

WebRTC prefers to send audio and video straight between the two browsers. That works when at least one side is reachable. It fails when both sides sit behind the kind of NAT that will not accept an unsolicited inbound packet — symmetric NAT, carrier-grade NAT on mobile networks, most corporate firewalls. Roughly a third of real-world connections land in that case.

For those, the media has to be forwarded by a server that both sides can reach. That server is a TURN relay, and coturn is the usual choice. STUN alone does not do this — STUN only tells a browser what its public address looks like, which does not help when the problem is that nothing may connect inbound.

Two machines in the same building prove nothing. They will connect directly whether or not TURN is configured, so the test that feels most convincing is the one that cannot detect this failure. Test from two different networks — one on wifi, one on mobile data — or test the relay directly as described below.

There are three common ways this ends up half-working. Each leaves a relay that looks healthy and is not doing its job, and none of them produces an error anywhere an administrator would look.

Docker's NAT, if you did not use host networking

coturn advertises the addresses it believes it has. Inside a bridge network those are private addresses no caller can reach, so the call negotiates happily and then carries no media at all. Run it with network_mode: host, and set external-ip to the server's real public address.

A configuration file coturn cannot read

The official image drops privileges to nobody. A config file you tightened to chmod 600 as root is then unreadable, and coturn starts on its defaults — no shared secret, no relay range, none of your settings — logging a single Cannot find config file line and otherwise appearing to run normally. Mode 644 is correct here; the file holds a secret, but one that is useless without the server it belongs to.

TLS that never actually switched on

The same permission problem applies to certificates, and it matters more because the failure is invisible for months. Let's Encrypt private keys are mode 0600 owned by root; coturn runs as nobody; so a tls-listening-port with a pkey it cannot read gives you an open port serving nothing.

Copy the certificate somewhere coturn can read rather than loosening /etc/letsencrypt for every service on the machine:

install -m 0644 -o root -g root  "$SRC/fullchain.pem" "$DST/fullchain.pem"
install -m 0640 -o root -g 65534 "$SRC/privkey.pem"   "$DST/privkey.pem"

Then register that copy step as a certbot deploy hook, in /etc/letsencrypt/renewal-hooks/deploy/. Skip it and TLS works for ninety days and then stops — for exactly the members who most needed it, so you may not hear about it quickly.

A certificate cannot validate against a bare IP address. TURN over TLS therefore has to be reached by hostname — turns:chat.example.com:5349, never turns:203.0.113.5:5349. The hostname must resolve to the relay itself, so a domain sitting behind a proxy that terminates connections elsewhere will not work.

Offer every route, not just one

Most software that accepts TURN settings accepts a list. Give it all of them; they share one credential and the browser uses whichever gets through.

turn:chat.example.com:3478
turn:chat.example.com:3478?transport=tcp
turns:chat.example.com:5349

The middle line costs nothing, needs no certificate, and rescues networks that permit TCP but not UDP. The last covers networks that block UDP outright — corporate guest wifi, hotels, some mobile carriers — because on port 5349 with TLS the traffic is indistinguishable from ordinary HTTPS to whatever is filtering it.

Prove it relays, rather than assuming

"coturn is listening" and "calls will connect" are different claims. The honest test forbids every direct route and checks that a candidate still appears — if one does, the browser reached your relay across the public internet, authenticated, and was granted a relay address.

const pc = new RTCPeerConnection( {
    iceServers: [ { urls: 'turns:chat.example.com:5349', username, credential } ],
    iceTransportPolicy: 'relay',   // host and reflexive candidates forbidden
} );
pc.createDataChannel( 'probe' );
await pc.setLocalDescription( await pc.createOffer() );
// listen on icecandidate; anything ending "typ relay" came from TURN

Run it from outside your own network, in a page served over HTTPS. No relay candidate means the firewall, the credentials or the certificate — and it is far better to learn that now than from a member.

Failing that, watch the relay during a real call with docker logs -f on the coturn container. A line mentioning an allocation appears when a browser genuinely uses it. Silence during a successful call is fine, and simply means the two sides went direct. Silence during a failed one means TURN is not being reached at all.

On credentials

Use coturn's use-auth-secret mode rather than static user accounts. The application derives a short-lived username and credential from a shared secret for each member, so nothing long-lived is ever handed to a browser, and revoking access is a matter of the application no longer minting one. A relay configured with fixed credentials in client-side settings is an open proxy the moment anyone reads the page source.

Also deny the private ranges, or your relay will forward traffic into your own network on request:

no-multicast-peers
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.168.0.0-192.168.255.255

Related

The pattern here is worth generalising: every one of these failures is silent, and every one of them passes the test an administrator would naturally run. When a feature depends on infrastructure you configured yourself, decide in advance what evidence would distinguish "working" from "appears to work", and go and get that evidence.


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.