Skip to content

Instantly share code, notes, and snippets.

@esafwan
Created October 19, 2025 18:52
Show Gist options
  • Select an option

  • Save esafwan/8529f5e8c5459087bd3fd45423332495 to your computer and use it in GitHub Desktop.

Select an option

Save esafwan/8529f5e8c5459087bd3fd45423332495 to your computer and use it in GitHub Desktop.
Realtime websocket-based listeners are implemented in Frappe CRM

Realtime websocket-based listeners are implemented in Frappe CRM

πŸ”Œ Realtime WebSocket Implementation in Frappe CRM

1. Socket Initialization (src/socket.js)

The app initializes a Socket.IO connection that connects to the Frappe realtime server:

export function initSocket() {
  let host = window.location.hostname
  let siteName = window.site_name
  let port = window.location.port ? `:${socketio_port}` : ''
  let protocol = port ? 'http' : 'https'
  let url = `${protocol}://${host}${port}/${siteName}`

  let socket = io(url, {
    withCredentials: true,
    reconnectionAttempts: 5,
  })
  socket.on('refetch_resource', (data) => {
    if (data.cache_key) {
      let resource =
        getCachedResource(data.cache_key) ||
        getCachedListResource(data.cache_key)
      if (resource) {
        resource.reload()
      }
    }
  })
  return socket
}

Key features:

  • Reads socketio_port from Frappe config
  • Multi-tenant support via site namespace (/${siteName})
  • Automatic resource refetching via refetch_resource event
  • Integrates with frappe-ui's resource caching system

2. Global Socket Access (src/main.js)

The socket is made globally available throughout the Vue app:

let socket
if (import.meta.env.DEV) {
  frappeRequest({ url: '/api/method/crm.www.crm.get_context_for_dev' }).then(
    (values) => {
      for (let key in values) {
        window[key] = values[key]
      }
      socket = initSocket()
      app.config.globalProperties.$socket = socket
      app.mount('#app')
    },
  )
} else {
  socket = initSocket()
  app.config.globalProperties.$socket = socket
  app.mount('#app')
}

3. Store-Level Access (src/stores/global.js)

Stores provide centralized access to the socket:

import { defineStore } from 'pinia'
import { getCurrentInstance, ref } from 'vue'

export const globalStore = defineStore('crm-global', () => {
  const app = getCurrentInstance()
  const { $dialog, $socket } = app.appContext.config.globalProperties

  // ... other store logic
  
  return {
    $dialog,
    $socket,
    // ... other exports
  }
})

4. Component-Level Usage

Example 1: Notifications (src/components/Notifications.vue)

onMounted(() => {
  $socket.on('crm_notification', () => {
    notificationsStore().notifications.reload()
  })
})

onBeforeUnmount(() => {
  $socket.off('crm_notification')
})

Example 2: WhatsApp Messages (src/components/Activities/Activities.vue)

onMounted(() => {
  $socket.on('whatsapp_message', (data) => {
    if (
      data.reference_doctype === props.doctype &&
      data.reference_name === doc.value.data.name
    ) {
      whatsappMessages.reload()
    }
  })
})

onBeforeUnmount(() => {
  $socket.off('whatsapp_message')
})

5. Backend Event Publishing (Python)

Notifications (crm/fcrm/doctype/crm_notification/crm_notification.py)

class CRMNotification(Document):
    def on_update(self):
        frappe.publish_realtime("crm_notification")

WhatsApp Messages (crm/api/whatsapp.py)

def on_update(doc, method):
    frappe.publish_realtime(
        "whatsapp_message",
        {
            "reference_doctype": doc.reference_doctype,
            "reference_name": doc.reference_name,
        },
    )

6. Automatic Resource Refetching

Resources created with a cache key can be automatically refetched from the backend:

const lead = createResource({
  url: 'crm.fcrm.doctype.crm_lead.api.get_lead',
  params: { name: props.leadId },
  cache: ['lead', props.leadId],  // Cache key enables auto-refetch
  auto: true,
})

When the backend calls:

frappe.publish_realtime('refetch_resource', {'cache_key': ['lead', 'LEAD-001']})

The socket handler automatically finds and reloads the matching resource.

πŸ“Š Architecture Summary

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                     Vue Frontend                         β”‚
β”‚                                                          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
β”‚  β”‚  Component   │───▢│  $socket.on  β”‚                  β”‚
β”‚  β”‚  (Mounted)   β”‚    β”‚  (listener)  β”‚                  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
β”‚                             β”‚                            β”‚
β”‚                             β–Ό                            β”‚
β”‚                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚
β”‚                    β”‚  socket.io-clientβ”‚                 β”‚
β”‚                    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚ WebSocket
                             β”‚
                             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Frappe SocketIO Server (Node.js)           β”‚
β”‚                     /realtime                            β”‚
β”‚                                                          β”‚
β”‚  β€’ Multi-tenant (namespaced by site)                    β”‚
β”‚  β€’ Redis pub/sub integration                            β”‚
β”‚  β€’ Cookie/Token authentication                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                             β”‚ Redis Pub/Sub
                             β”‚
                             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  Frappe Backend (Python)                β”‚
β”‚                                                          β”‚
β”‚  frappe.publish_realtime('event_name', data)           β”‚
β”‚  frappe.publish_progress(...)                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

🎯 Key Patterns Used

  1. Global Socket Instance: Socket is initialized once and made globally available
  2. Lifecycle Management: Always clean up listeners in onBeforeUnmount
  3. Filtered Events: Components filter events based on context (e.g., checking doctype/name)
  4. Resource Reloading: Trigger resource.reload() on realtime events
  5. Cache-based Auto-refetch: Resources with cache keys can be auto-refetched
  6. Backend Hooks: Use DocType hooks (on_update, etc.) to publish realtime events

This implementation follows Frappe's realtime documentation and integrates seamlessly with frappe-ui's resource management system!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment