Adventure Labs Logo
Scout Docs

Realtime

Subscribing to row changes over private Supabase broadcast channels

Realtime hooks

Every realtime hook returns [latest, clear]: the most recent change seen on its channels, and a function to acknowledge it. latest is RealtimeData<T>, which pairs the row with an EnumRealtimeOperation of INSERT, UPDATE, or DELETE. Switching herds resubscribes and drops any unacknowledged change.

Rows are delivered on private topics, so a subscription only succeeds when the caller holds the ability that topic requires. The base hook uses the client abilities mint to skip known-denied joins when available; realtime.messages RLS remains authoritative. Unauthorized joins default to stop so Phoenix does not rejoin-spam, then retry once when the Supabase session changes. Every realtime hook accepts { authErrorRetry: "backoff" } for exponential retries.

Channels

HookTopicRequires
useScoutRealtimeEvents{herd_id}-eventsmedia.read
useScoutRealtimeArtifacts{herd_id}-artifactsmedia.read
useScoutRealtimeHerdMediaUsage{herd_id}-herd_media_usagemedia.read
useScoutRealtimeDevices{herd_id}-devicesdevices.read
useScoutRealtimeTags{herd_id}-tagsmedia.read
useScoutRealtimeSessions{herd_id}-sessionssessions.read
useScoutRealtimePins{herd_id}-pinsmap.read
useScoutRealtimePlans{herd_id}-plansmap.read
useScoutRealtimeParts{herd_id}-partsdevices.read
useScoutRealtimeConnectivity{herd_id}-connectivitysessions.read
useScoutRealtimeDocuments{herd_id}-herd_documentsherd_documents.read
{herd_id}-device_documentsdevice_documents.read
{herd_id}-user_documentsuser_documents.read
{herd_id}-operating_context_documentsoperating_contexts.read
user_{user_id}-documentsbeing that user
product_documents_changesa view role in any herd
useScoutRealtimeUserDocumentStatuses{herd_id}-user_document_statusesuser_documents.status.read or user_documents.read
useScoutRealtimeOperatingContexts{herd_id}-operating_contextsoperating_contexts.read
useScoutRealtimeMaintenanceRequests{herd_id}-maintenance_requestsmaintenance_requests.read
useScoutRealtimeContacts{herd_id}-contactscontacts.read
useScoutRealtimeSessionIncidents{herd_id}-session_incidentssessions.read
useScoutRealtimeAnalysisJobsanalysis_jobs_changesplatform superadmin
useScoutRealtimeAnalysisTasksanalysis_tasks_changesplatform superadmin
useScoutRealtimeVersionsSoftwareversions_software_changesauthenticated user

useScoutRealtimeOperatingContexts also emits contact types, point-of-interest types and instances, operating permissions, compliance resources and types, operating-context contacts, and herd operating permissions. Use update.table to narrow the exported OperatingContextRealtimeData or ComplianceRealtimeData union.

Artifact broadcasts contain only id and device_id; fetch the artifact by ID when the full row or signed media URLs are needed.

User document status broadcasts contain only the columns exposed by user_document_statuses. They never contain a file URI, size, or identifier.

Part broadcasts carry the part row only. When a part arrives with a product number the product catalog has not seen, fetch the product and merge it so every consumer keeps sharing one copy:

const product = await get_product_for_product_number(supabase, change.data.product_number);
dispatch(mergeProductCatalog(build_product_catalog([
  { product_number: change.data.product_number, product: product.data },
])));

Dropped subscriptions

A channel lost to a transport drop, CLOSED, or TIMED_OUT rejoins on the same exponential backoff as an auth retry, and returning to a hidden tab or coming back online rejoins whatever is no longer joined. Pass recoverOnFocus: false to leave the focus and online listeners off.

Pass enabled: false to any realtime hook to create no channel, auth subscription, retry timer, or reconnect listener. This lets an application use the sanitized status topic or polling without repeatedly attempting a full-document subscription it cannot authorize.

Broadcasts published while a channel was down are gone, so a view that must not miss a row — a media list waiting on a derivative, for instance — refetches from onMissedUpdates, which fires once the topic is back:

const [change, clearChange] = useScoutRealtimeArtifacts(supabase, {
  onMissedUpdates: () => refetch({ preservePages: true }),
});

System role defaults grant contact and operating-context reads to Viewer/Editor/Admin, maintenance-request reads to Editor/Admin, and document reads to Admin. The domain read abilities (media.read, devices.read, sessions.read, telemetry.read, map.read) and chat.binary belong to all three system roles. Topics without a mapping fall back to herds.read. A platform superadmin may join any {herd_id}-* topic. Custom herd roles only hold the abilities their herd admin granted, so a role created before an ability existed needs that grant before its topics resolve.

A document arrives on the channel of its owner class, so a herd document and a pilot's licence never share an audience. useScoutRealtimeDocuments subscribes to all six channels by default; pass the channels a view actually renders to avoid subscribing to ones the user cannot read.

const [change, clearChange] = useScoutRealtimeDocuments(supabase, ["herd", "device"]);

Auth errors

Private-channel CHANNEL_ERROR with Unauthorized/permission text removes the channel by default. Use backoff when the failure may be transient. Pass backoffMaxAttempts to cap retries.

const [change, clearChange] = useScoutRealtimeParts(
  supabase,
  { authErrorRetry: "backoff", backoffMaxAttempts: 5 },
);

Usage

Apply the change to local state, then clear it so the next one is distinguishable.

"use client";

import { useEffect, useState } from "react";
import {
  EnumRealtimeOperation,
  IMaintenanceRequest,
  useScoutRealtimeMaintenanceRequests,
  useSupabase,
} from "@adventurelabs/scout-core/client";

export function MaintenanceRequests({ initial }: { initial: IMaintenanceRequest[] }) {
  const supabase = useSupabase();
  const [requests, setRequests] = useState(initial);
  const [change, clearChange] = useScoutRealtimeMaintenanceRequests(supabase);

  useEffect(() => {
    if (!change) return;

    setRequests((current) => {
      const others = current.filter((request) => request.id !== change.data.id);
      return change.operation === EnumRealtimeOperation.DELETE
        ? others
        : [change.data, ...others];
    });

    clearChange();
  }, [change, clearChange]);

  return (
    <ul>
      {requests.map((request) => (
        <li key={request.id}>{request.description}</li>
      ))}
    </ul>
  );
}

An upsert-or-remove reducer like this one handles a row moving between herds: the herd losing it receives a DELETE and the herd gaining it receives an INSERT.

Custom topics

useScoutRealtimeBroadcast backs every hook above and takes topics directly, for channels this package does not wrap yet. It applies the same abilities gate and auth-error handling.

const [change, clearChange] = useScoutRealtimeBroadcast<IPart>(
  supabase,
  [`${herdId}-parts`],
);

On this page