Context
For anyone using Realtime features with the official WeWeb Supabase plugin and who wants the socket to automatically reconnect when the browser tab regains focus (a common situation with PWAs or inactive tabs), here’s the solution I implemented in my project — it might help you too.
Solution idea
When the tab regains focus, a small listener script triggers a WeWeb workflow. This workflow checks whether the Realtime channel is still joined
. If it’s not, it runs Unsubscribe → Subscribe
(WeWeb’s native Realtime actions) and optionally refetches the data to keep everything in sync.
Important note: I noticed that if you run only
Subscribe
directly, the channel won’t reopen. The only reliable way I found is to firstUnsubscribe
and thenSubscribe
again.
Step 1 — Listener script (Custom Code → “Run code on page load”)
Replace
<YOUR_WORKFLOW_ID>
and the channel name (e.g.,realtime_task_1
).
(() => {
// ============================================================
// 📡 SCRIPT: Simple listener to trigger a workflow on page events
// ============================================================
// This script:
// - Listens only to the following events: focus, pageshow, visibilitychange('visible'), and resume
// - Uses debounce and cooldown to prevent repeated executions
// - Triggers a single workflow with a static payload
// - Allows enabling/disabling listeners by channel
// ============================================================
const workflowId = ‘<YOUR_WORKFLOW_ID>’; // e.g., “wflow_123”
const _expectedKey = ‘realtime_task_1’; // e.g., your logical channel name
// Prevents duplicate listeners per channel
if (!_expectedKey) {
console.warn('[INIT] Empty channel — listeners not registered.');
return;
}
window.__rtListenerRegistry = window.__rtListenerRegistry || new Set();
if (window.__rtListenerRegistry.has(_expectedKey)) {
console.log(`[INIT] Listeners already active for "${_expectedKey}".`);
return;
}
window.__rtListenerRegistry.add(_expectedKey);
// ------------------------------------------------------------
// 🕒 Utilities
// ------------------------------------------------------------
const now = () => Date.now();
function debounce(fn, wait = 300) {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), wait);
};
}
function withCooldown(fn, cooldownMs = 5000) {
let last = 0;
return async (eventName) => {
const t = now();
if (t - last < cooldownMs) {
console.log(`[SKIP] ${eventName} ignored (cooldown active)`);
return;
}
last = t;
return fn(eventName);
};
}
// ------------------------------------------------------------
// 🚀 Simplified trigger function
// ------------------------------------------------------------
async function triggerWorkflow(triggerEvent) {
wwLib.executeWorkflow(workflowId, {
debug: {
type: 'event_trigger',
trigger: triggerEvent,
channel: _expectedKey,
timestamp: new Date().toISOString(),
}
});
}
// Combine debounce + cooldown
const runTrigger = debounce(withCooldown(triggerWorkflow, 5000), 300);
// ------------------------------------------------------------
// 🪝 Event listeners
// ------------------------------------------------------------
const onFocus = () => runTrigger('focus');
const onPageShow = () => runTrigger('pageshow');
const onVisibilityChange = () => {
if (document.visibilityState === 'visible') runTrigger("visibilitychange('visible')");
};
const onResume = () => runTrigger('resume');
window.addEventListener('focus', onFocus);
window.addEventListener('pageshow', onPageShow);
document.addEventListener('visibilitychange', onVisibilityChange);
document.addEventListener('resume', onResume);
// ------------------------------------------------------------
// 🛑 Disable listeners by channel
// ------------------------------------------------------------
window[`disableRealtimeListeners_${_expectedKey}`] = () => {
window.removeEventListener('focus', onFocus);
window.removeEventListener('pageshow', onPageShow);
document.removeEventListener('visibilitychange', onVisibilityChange);
document.removeEventListener('resume', onResume);
window.__rtListenerRegistry?.delete(_expectedKey);
console.log(`🛑 Realtime listeners disabled for "${_expectedKey}"`);
};
// ------------------------------------------------------------
// 📝 Initialization log
// ------------------------------------------------------------
console.log(`✅ Listeners active for "${_expectedKey}" → focus | pageshow | visibility | resume`);
console.log(`ℹ️ To disable: window.disableRealtimeListeners_${_expectedKey}()`);
})();
Step 2 — Workflow triggered by the script
Create a new Workflow in WeWeb — this is the workflow whose ID you’ll use in the listener script above. Whenever the script is triggered, it will call this workflow automatically.
Inside the workflow:
- Run JavaScript → get the list of active channels.
const supabase = wwLib.wwPlugins.supabase.instance;
// get active channels
const channels = (supabase.getChannels?.() || [])
.filter(c => c?.state === 'joined' && typeof c?.topic === 'string');
// transform into [{ realtime: '...' }]
const result = channels.map(c => {
const t = c.topic;
const i = t.indexOf(':');
// get everything after the first ":"; if there’s none, use the topic itself
const val = i >= 0 ? t.slice(i + 1) : t;
return { realtime: val.trim() };
});
// optional: remove duplicates
const unique = Array.from(new Map(result.map(o => [o.realtime, o])).values());
// if it's “Return a value” in WeWeb:
return unique;
- Check if your channel is active
-
If active → do nothing.
-
If inactive → execute the reconnection flow:
-
Realtime → Unsubscribe from channel
Use the same channel name you used in the original subscribe (e.g.,realtime_task_1
). -
Realtime → Subscribe to channel
-
(Optional) Refetch data
After reopening the channel, reload your data in case any changes happened while the tab was inactive.
I’d like to ask the WeWeb dev team if it would be possible to make this improvement natively. I managed to solve the issue with a custom script, but I believe that everyone using the Realtime feature will eventually face the same problem and have to build their own workaround. Having a native auto-reconnect option for Realtime would make the experience more stable and easier to implement for all users.