to select ↑↓ to navigate
Framework

Framework

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

Realtime (socket.io)

Frappe ships with an API for realtime events based on socket.io. Since socket.io needs a Node server to run, we run a Node process in parallel to the main web server.

Client APIs (JavaScript)

frappe.realtime.on

To listen to realtime events on the client (browser), you can use the frappe.realtime.on method:

frappe.realtime.on('event_name', (data) => {
    console.log(data)
})

frappe.realtime.off

Stop listening to an event you have subscribed to:

frappe.realtime.off('event_name')

Server APIs (Python)

frappe.publish_realtime

To publish a realtime event from the server, you can use the frappe.publish_realtime method:

frappe.publish_realtime('event_name', data={'key': 'value'})

frappe.publish_progress

You can use this method to show a progress bar in a dialog:

frappe.publish_progress(25, title='Some title', description='Some description')

frappe publish realtime

Custom Event Handlers (Python)

Note: This feature is only available in nightly version. This feature is considered experimental.

You can implement custom real-time event handlers by creating a file your_app/your_app/realtime/handlers.py in your custom app. The syntax is fairly similar with API whitelists.

from frappe.realtime import realtime

@realtime.on(
  "project_subscribe",
  frappe_context=False,   # open a Frappe context (DB + session) for the handler body
  allow_guest=False,      # if False, the event is dropped when socket.user == "Guest"
)

Defaults: frappe_context=False, allow_guest=False

Example

import frappe
from frappe.realtime import Socket, realtime

@realtime.on("project_subscribe")
def project_subscribe(socket: Socket, project: str) -> None:
    if socket.has_permission("Project", project):
        socket.join(f"project:{project}")

The first argument is always the typed Socket. The rest are the payload the client sent, positionally. The event name "project_subscribe" is what the browser emits with frappe.realtime.emit(...).

Working example with frappe_context=True

With frappe_context=True the full ORM is available in the handler body — frappe.get_doc, frappe.local.site, frappe.session.user are all populated and reflect the authenticated socket user.

This example handler loads a doc, checks permission, and emits the result back to the client:

@realtime.on("test_get_doc", frappe_context=True)
def test_get_doc(socket: Socket, doctype: str, docname: str) -> None:
    import frappe
    try:
        doc = frappe.get_doc(doctype, docname)
        doc.check_permission()
        socket.emit(
            "test_get_doc_result",
            {
                "ok": True,
                "site": frappe.local.site,
                "user": frappe.session.user,
                "doctype": doc.doctype,
                "name": doc.name,
                "modified": str(doc.get("modified")),
            },
        )
    except Exception as e:
        socket.emit("test_get_doc_result", {"ok": False, "error": f"{type(e).__name__}: {e}"})

From the browser:

frappe.realtime.on("test_get_doc_result", (r) => console.log(r));
frappe.realtime.emit("test_get_doc", "User", "Administrator");

See Permission checks section for the cost and when to prefer the cheap HTTP based permission check instead.

A handler runs only if its owning app is installed on the connecting site. The owning app is detected automatically from the import.

You may have to restart the socketio server to see code changes take effect. See the Socket.IO docs for more on writing custom event handlers.

Socket object

Read-only identity, populated at connect:

socket.site            # the site this socket is on
socket.user            # "Guest" for anonymous
socket.user_type       # e.g. "System User"
socket.installed_apps  # list of installed apps

Rooms, emit and transient state:

socket.join(room)                        # add this socket to a room
socket.leave(room)                       # remove it
socket.emit(event, data=None, room=None) # emit to a room, or to this client if room is None
socket.get(key, default=None)            # read transient per-socket state
socket.set(key, value)                   # persist transient per-socket state (cleared on disconnect)

Permission checks

There are two ways to perform doctype permission checks.

A. HTTP (default, recommended) : socket.has_permission(doctype, name) asks the web process, exactly like the core handlers. No DB connection in the realtime process. Cheap. Use this unless you have a reason not to.

if socket.has_permission("Project", project):
    socket.join(f"project:{project}")

B. In-process (frappe_context=True) : opens a full Frappe context for the handler body so you can call frappe.has_permission(...), query the DB, etc. directly. Cost: every such event pays frappe.init -> connect -> set_user -> commit/rollback -> destroy and forces a DB connection into the realtime process. Use sparingly.

@realtime.on("project_subscribe", frappe_context=True)
def project_subscribe(socket: Socket, project: str) -> None:
    if frappe.has_permission("Project", doc=project, ptype="read"):
        socket.join(f"project:{project}")

Full reference: ++https://github.com/frappe/frappe/blob/develop/frappe/realtime/README.md++

Custom Event Handlers (NodeJS)

Available from v16. Considered experimental. This is the previous implementation and will be phased out in newer version.

Socket.IO server path; new apps should prefer the Python handlers above.

You can implement custom real-time event handlers by creating a handlers.js file in the realtime folder of your app.

You need a single export from this file — a function that sets up event handlers on the socket instance. E.g. for an app called "chat":

// bench/apps/chat/realtime/handlers.js

function chat_app_handlers(socket) {
    socket.on("hello_chat", () => {
        console.log("hello world!");
    });
}

module.exports = chat_app_handlers;

Trigger this event from client-side code with frappe.realtime.emit("hello_chat"). You may have to restart the socketio server to see code changes take effect. See the Socket.IO docs for more on writing custom event handlers.

Pushing events to clients

You do not emit from the web process directly — you publish through Redis, and the realtime server bridges it to the connected sockets. The room string you publish to must match the room your handler join-ed.

from frappe.realtime import publish_to_room
publish_to_room("project:PROJ-0001", "project_updated", {"status": "Open"})

Other named helpers, each thin wrapper over publish_realtime.

publish_to_user(user, event, message=None)
publish_to_doc(doctype, docname, event, message=None)
publish_to_doctype(doctype, event, message=None)
publish_task_progress(task_id, message=None)
publish_to_website(event, message=None)
publish_to_all(event, message=None)
publish_to_room(room, event, message=None)

Full reference: ++https://github.com/frappe/frappe/blob/develop/frappe/realtime/README.md++

Custom Client

You can write a custom client to connect to the socket.io server if you're developing a SPA or a mobile app that doesn't use the Desk interface. Refer to the official Socket.IO client docs.

Example custom clients:

Authorization in custom clients

Two ways to authenticate a connection with the socket.io server:

  • Cookies — in a browser-like environment, the connection automatically sends cookies and the socketio server authenticates using them.
  • Authorization header — if cookies are unavailable (e.g. mobile apps), use an Authorization header just like API requests. See the REST API authentication docs and Socket.IO extraHeaders.

Implementation Notes

  • Realtime server uses the socket.io server, written in node.js, found in the /realtime directory.
  • The realtime client is a wrapper around the socket.io client library, found in public/js/frappe/socketio_client.js.
  • Python processes publish events to the node server over a Redis pub-sub channel. The realtime server subscribes to the Redis channel and republishes to all subscribed clients.
  • The realtime server is multi-tenant: all site traffic is namespaced by sitename. Namespaces are created dynamically as /{sitename}, where sitename is the site's folder name in the sites directory (or frappe.local.site).
  • The realtime server uses the main Frappe web server to authenticate connections. The SID cookie or authorization header is passed to the client and used to ensure the connection is a valid user that can subscribe to DocTypes/documents per permissions.

Available rooms

Room Access
all Connected by default for all System Users
website Accessible to any user, including Guests
user:{username} Per-user room. Allowed without permission checks
doctype:{doctype} Per-DocType room. Only users with DocType permission may join; auto-subscribed on opening list/form view
doc:{doctype}/{name} Per-document room. Only users with document permission may join; auto-subscribed on opening form view
Last updated 4 days ago
Was this helpful?
Thanks!