Creating a Real-Time Dashboard with APEX and WebSockets

Creating a Real-Time Dashboard with APEX and WebSockets

A step-by-step guide to building live, push-based dashboards in Oracle APEX with a complete working example using Oracle Advanced Queuing and WebSockets.

What are WebSockets and why APEX needs them

Traditional APEX dashboards rely on page refreshes or Ajax polling, where the browser repeatedly asks the server, “Is there anything new?” This wastes bandwidth, increases latency, and makes the system feel sluggish.

WebSockets completely transform this model. Once the HTTP handshake is complete, a persistent, full-duplex channel opens between the browser and the server. The server can now send data immediately as changes occur, eliminating the need for repeated polling.

❌ Old way polling

The browser calls apex.server.process() every 5 seconds. Most calls do not yield a response. High server load and stale data.

✅ New way WebSocket push

The server transmits data as soon as it changes. No call goes to waste. True real-time latency of less than 100ms.

Architecture overview

Here is how the full stack works for the real-time APEX dashboard:

Oracle AQ (Advanced Queuing) acts as the message broker. When data changes in the DB, a PL/SQL trigger enqueues a message. ORDS listens to the queue and forwards it to all connected WebSocket clients instantly.

Step 1: Set up Oracle Advanced Queuing

First, create a queue table and a queue in the database. Connect as the DBA or APEX schema user:

DBMS_AQADM.CREATE_QUEUE_TABLE(
  queue_table        => 'APEX_DASHBOARD_QT',
  queue_payload_type => 'SYS.AQ$_JMS_TEXT_MESSAGE'
);

-- 2. Create the queue
DBMS_AQADM.CREATE_QUEUE(
  queue_name  => 'APEX_DASHBOARD_Q',
  queue_table => 'APEX_DASHBOARD_QT'
);

-- 3. Start the queue
DBMS_AQADM.START_QUEUE(
  queue_name => 'APEX_DASHBOARD_Q'
);

Create a trigger to enqueue when data changes.

This trigger executes when a row is inserted or updated in your source table (e.g., SALES_DATA) and sends the new values ​​to the queue in JSON format:

CREATE OR REPLACE TRIGGER trg_sales_realtime
AFTER INSERT OR UPDATE ON sales_data
FOR EACH ROW
DECLARE
  l_msg    SYS.AQ$_JMS_TEXT_MESSAGE;
  l_enq_opts  DBMS_AQ.ENQUEUE_OPTIONS_T;
  l_msg_props DBMS_AQ.MESSAGE_PROPERTIES_T;
  l_msg_id    RAW(16);
  l_payload   VARCHAR2(4000);
BEGIN
  -- Build JSON payload
  l_payload := '{'
    || '"region":"'  || :NEW.region      || '",'
    || '"amount":'   || :NEW.sale_amount || ','
    || '"units":'    || :NEW.units_sold  || ','
    || '"ts":"'      || TO_CHAR(SYSDATE,'HH24:MI:SS') || '"'
    || '}';

  l_msg := SYS.AQ$_JMS_TEXT_MESSAGE.construct();
  l_msg.set_text(l_payload);

  DBMS_AQ.ENQUEUE(
    queue_name         => 'APEX_DASHBOARD_Q',
    enqueue_options    => l_enq_opts,
    message_properties => l_msg_props,
    payload            => l_msg,
    msgid              => l_msg_id
  );
END;

Grant EXECUTE ON DBMS_AQ and DBMS_AQADM to your APEX schema user. Without these, the trigger will silently fail.

Step 2: Configure the ORDS WebSocket endpoint

ORDS 22.2+ includes native WebSocket support. Create a WebSocket handler that subscribes to an AQ queue and broadcasts to clients.

BEGIN
  ORDS.define_module(
    p_module_name    => 'dashboard_ws',
    p_base_path      => '/dashboard/',
    p_items_per_page => 0
  );

  ORDS.define_handler(
    p_module_name    => 'dashboard_ws',
    p_pattern        => 'live',
    p_method         => 'WS',         -- WebSocket method
    p_source_type    => ORDS.source_type_plsql,
    p_source         =>
      'BEGIN
         apex_web_socket.send_message(
           p_queue_name => ''APEX_DASHBOARD_Q'',
           p_session_id => :session_id
         );
       END;'
  );

  COMMIT;
END;

The WebSocket URL will be: wss://your-ords-host/ords/schema/dashboard/live. Replace ‘schema’ with the name of your actual APEX schema.

Step 3: Build the APEX dashboard page

In APEX App Builder, create a new blank page. Add the regions listed below:

Add Static Content Area Metric Card

Create a static content region with the ID dashboard-metrics. This will contain our HTML metric cards that update in real-time. Set the region template to “Blank with Attributes”.

<!-- Real-Time Metric Cards -->
<div class="t-Cards t-Cards--3Cols">

  <div class="t-Card" id="card-total-sales">
    <div class="t-Card-wrap">
      <div class="t-Card-titleWrap">
        <h3 class="t-Card-title">Total Sales</h3>
      </div>
      <div class="t-Card-body">
        <span id="val-total-sales" class="t-Card-subTitle">
          $0</span>
      </div>
    </div>
  </div>

  <div class="t-Card" id="card-units">
    <div class="t-Card-wrap">
      <div class="t-Card-titleWrap">
        <h3 class="t-Card-title">Units Sold</h3>
      </div>
      <div class="t-Card-body">
        <span id="val-units" class="t-Card-subTitle">0</span>
      </div>
    </div>
  </div>

  <div class="t-Card" id="card-region">
    <div class="t-Card-wrap">
      <div class="t-Card-titleWrap">
        <h3 class="t-Card-title">Last Region</h3>
      </div>
      <div class="t-Card-body">
        <span id="val-region" class="t-Card-subTitle">—</span>
      </div>
    </div>
  </div>

</div>
Add a JavaScript Dynamic Action On Page Load

Create a Dynamic Action: Event = “Page Load”, Action = “Execute JavaScript Code”. Paste the WebSocket client code below.

(function() {

  const WS_URL = 'wss://' + location.host +
                 '/ords/' + apex.env.APP_SCHEMA +
                 '/dashboard/live';

  let socket, totalSales = 0, totalUnits = 0;

  function connect() {
    socket = new WebSocket(WS_URL);

    socket.onopen = function() {
      console.log('WebSocket connected ✓');
      updateStatus('connected');
    };

    socket.onmessage = function(event) {
      const data = JSON.parse(event.data);
      updateDashboard(data);
      appendLog(data);
    };

    socket.onclose = function() {
      updateStatus('reconnecting');
      // Auto-reconnect after 3 seconds
      setTimeout(connect, 3000);
    };

    socket.onerror = function(err) {
      console.error('WebSocket error:', err);
    };
  }

  function updateDashboard(data) {
    // Accumulate totals
    totalSales  += parseFloat(data.amount  || 0);
    totalUnits  += parseInt(data.units    || 0);

    // Update metric cards
    $('#val-total-sales').text(
      '$' + totalSales.toLocaleString('en-US',
        { minimumFractionDigits: 2 })
    );
    $('#val-units').text(totalUnits.toLocaleString());
    $('#val-region').text(data.region);

    // Flash animation on update
    $('.t-Card').addClass('u-highlight');
    setTimeout(function() {
      $('.t-Card').removeClass('u-highlight');
    }, 600);

    // Refresh APEX chart region (if you have one)
    apex.region('sales-chart')?.refresh();
  }

  function updateStatus(state) {
    const el = $('#ws-status');
    if (state === 'connected') {
      el.text('● Live').css('color', '#22c55e');
    } else {
      el.text('○ Reconnecting...').css('color', '#f59e0b');
    }
  }

  function appendLog(data) {
    const log = $('#ws-log');
    const line = `[${data.ts}] ${data.region}: $${data.amount}`;
    log.prepend('<div>' + line + '</div>');
    // Keep last 50 messages
    log.find('div:gt(49)').remove();
  }

  // Kick off connection
  connect();

})();

Step 4: Test it end to end

Once everything is connected, run this PL/SQL in the SQL Workshop to simulate the flow of live data in your dashboard:

BEGIN
  FOR i IN 1..10 LOOP
    INSERT INTO sales_data (
      region,
      sale_amount,
      units_sold,
      created_at
    ) VALUES (
      CASE MOD(i, 4)
        WHEN 0 THEN 'North'
        WHEN 1 THEN 'South'
        WHEN 2 THEN 'East'
        ELSE         'West'
      END,
      ROUND(DBMS_RANDOM.VALUE(100, 9999), 2),
      ROUND(DBMS_RANDOM.VALUE(1, 50)),
      SYSDATE
    );
    DBMS_LOCK.SLEEP(1); -- 1 second between events
  END LOOP;
  COMMIT;
END;

Keep an eye on your APEX dashboard every INSERT triggers a PL/SQL block that enqueues a message, and the WebSocket pushes updates to every open browser in less than 100ms.

Best practices and tips

  1. Always implement auto-reconnect: Network interruptions can occur. The JavaScript example above automatically reconnects after 3 seconds. For production, consider using ‘exponential backoff’: start at 1 second, double the time with each attempt, and cap it at a maximum of 30 seconds.
  2. Use JSON for all payloads: Keep AQ message payloads as compact JSON. Parse them in JavaScript using JSON.parse(event.data). Do not use XML, as it is larger and takes longer to parse in the browser.
  3. Throttle UI updates for high-frequency data: If your queue generates more than 100 messages per second, batch DOM updates using requestAnimationFrame or a debounce function. Updating the DOM 100 times a second can cause jank or stuttering.
  4. Secure with APEX session authentication: Pass the APEX session ID as a query parameter in the WebSocket URL and validate it within your ORDS handler. Never expose an unauthenticated WebSocket endpoint.

Production tip: Use Oracle GoldenGate or CDC (Change Data Capture) instead of row triggers if you’re handling millions of rows per day. Triggers at that scale add overhead to every DML statement.

Conclusion

You now have a complete real-time dashboard pipeline: Oracle Triggers → AQ → ORDS WebSocket → APEX Browser. The key advantage is that WebSockets completely eliminate the need for polling; your dashboard responds instantly as data changes, with a latency of less than 100ms.

This approach works for any event-driven scenario such as live inventory, IoT sensor feeds, financial tickers, or support queue monitoring. Simply adjust the table and JSON structure, and you are all set.

Comments

No comments yet. Why don’t you start the discussion?

    Leave a Reply

    Your email address will not be published. Required fields are marked *