Firebase Realtime Database WebSocket API Firebase Realtime Database WebSocket API

Let's explore how the Firebase Realtime Database's WebSocket API works under the hood.

Firebase offers a powerful Realtime Database accessible through a WebSocket connection. While the official libraries provide a user-friendly interface, understanding the communication protocol can be valuable for debugging and building custom tools.

The Firebase WebSocket protocol isn't officially documented, so while this information is accurate as of Firebase version 10.12, things could change down the road. Be aware that future Firebase updates might affect the specifics of the message formats and behaviors.

Establishing the WebSocket Connection

To establish a connection, we'll use your Firebase project's secure WebSocket URL. This usually looks like this:

const url =
	'wss://<project-id>.firebaseio.com/.ws?v=5&p=<application-id>&ns=<project-id>';

To create the WebSocket connection, you can use the WebSocket constructor:

const ws = new WebSocket(url);

Before sending messages, we need to ensure a successful connection. You can achieve this by listening for the open event on the ws object:

ws.onopen = function () {
	console.log('WebSocket connection established!');
	// Now it's safe to send messages over the WebSocket connection
};

Decoding the Messages

Communication between client and server utilizes JSON-encoded messages. The initial message from the server relays the server's current time, API version, and hostname. The official SDK retrieves the new hostname from the server's initial message and stores it for future use in localStorage under the key firebase:host:<host>, where <host> is the received hostname. This ensures subsequent connections can use the correct hostname.

{
	"t": "c",
	"d": {
		"t": "h",
		"d": { "ts": 1717335753015, "v": "5", "h": "<hostname>" }
	}
}

Most messages follow a similar structure: a "t" property indicating the message type and a "d" property containing the data.

Keep-Alive

To ensure a persistent connection, the client sends a special "0" message to the server at regular intervals. This message, triggered 45 seconds after the last WebSocket message is sent or received, keeps the connection alive and notifies the server of ongoing activity.

Authentication

Public data can potentially be accessed without authentication, depending on how you've configured your Firebase security rules. To interact with private data, you'll need to authenticate. This involves obtaining an ID token from Google's Secure Token API (details in the Firebase documentation). Once you have the token, send it to the API to authenticate subsequent messages.

Here's an example of sending an authentication message:

ws.send(
	JSON.stringify({
		t: 'd', // Message type
		d: {
			r: msgId++, // Unique message ID (starts at 1)
			a: 'auth', // Action: authentication
			b: { cred: idToken }, // Authentication data (ID token)
		},
	}),
);

This format applies to most messages you send to the server: { "t": "d", "d": { "r": msgId, ... } }, with msgId being a unique identifier incremented for each message.

Reading Data from the Database

Subscribing to a Path

To receive updates for a specific data path, the client sends a subscribe message to the server. It's recommended to store the received data locally in a cache to minimize unnecessary network traffic. Here's an example of a subscribe message:

{ "t": "d", "d": { "r": 3, "a": "q", "b": { "p": "/path/to/data", "h": "" } } }

Receiving Data and Confirmation:

Once subscribed, the server responds with two messages:

  1. Data Message: This message contains the data for the subscribed path, including:

    • p: Path name (same as the one in the subscribe request)
    • d: The actual data for the subscribed path
  2. Status Message: This message confirms the subscription was successful. It includes:

    • r: The same request ID used in the subscribe message
    • s: Status string ("ok" for successful subscription)

Example message:

{"t":"d","d":{"b":{"p":"path/to/data","d":"path-data"},"a":"d"}}
{"t":"d","d":{"r":3,"b":{"s":"ok","d":{}}}}

Unsubscribing

The unsubscribe message follows a similar structure to the subscribe message, but with the action set to "n". By sending this message, the client informs the server to stop sending updates for the specified path. Here's an example:

{ "t": "d", "d": { "r": 4, "a": "n", "b": { "p": "/path/to/data" } } }

Sorting and Filtering Data

orderByChild()

Firebase performs client-side ordering when querying entire collections. When combined with other query parameters, orderByChild corresponds to the "i" field in the protocol.

query(ref(db, 'path/to/data'), limitToFirst(2), orderByChild('name'));

Corresponding JSON:

{
	"t": "d",
	"d": {
		"r": 2,
		"a": "q",
		"b": {
			"p": "/path/to/data",
			"q": { "l": 2, "vf": "l", "i": "name" },
			"t": 1,
			"h": ""
		}
	}
}

When the .indexOn rule is not set in the security rules, Firebase sends the entire matching dataset to the client instead of applying restrictions server-side.

orderByKey() and orderByValue()

These methods use a similar JSON structure to orderByChild(), with the "i" field set to ".key" or ".value" respectively.

limitToFirst() and limitToLast()

{
	"t": "d",
	"d": {
		"r": 2,
		"a": "q",
		"b": {
			"p": "/path/to/data",
			"q": {
				"l": 1,
				"vf": "l"
			},
			"t": 1,
			"h": ""
		}
	}
}

startAt()

The startAt() argument type depends on which orderBy() function was used in this query. Specify a value that matches the orderBy() type. When used in combination with orderByKey(), the value must be a string.

query(ref(db, '/path/to/data'), startAt('Second Item'), orderByValue());

corresponds to:

{
	"t": "d",
	"d": {
		"r": 2,
		"a": "q",
		"b": {
			"p": "/path/to/data",
			"q": {
				"sp": "Second Item",
				"sin": true,
				"i": ".value"
			},
			"t": 1,
			"h": ""
		}
	}
}

startAfter()

Similar to startAt(), but with "sin" set to false and "sn" set to "[MAX_NAME]":

{
	"t": "d",
	"d": {
		"r": 2,
		"a": "q",
		"b": {
			"p": "/path/to/data",
			"q": {
				"sp": "Second Item",
				"sn": "[MAX_NAME]",
				"sin": false,
				"i": ".value"
			},
			"t": 1,
			"h": ""
		}
	}
}

endAt() and endBefore()

These methods use similar structures to startAt() and startAfter(), but with "ep" (end point), and "ein" (end inclusive) fields. The "en" field is set to "[MIN_NAME]"

query(ref(db, 'path/to/data'), endAt('Second Item'), orderByValue());
{
	"t": "d",
	"d": {
		"r": 2,
		"a": "q",
		"b": {
			"p": "/path/to/data",
			"q": { "ep": "Second Item", "ein": true, "i": ".value" },
			"t": 1,
			"h": ""
		}
	}
}
query(ref(db, 'fb/test/collection'), endBefore('Second Item'), orderByValue());
{
	"t": "d",
	"d": {
		"r": 2,
		"a": "q",
		"b": {
			"p": "/fb/test/collection",
			"q": {
				"ep": "Second Item",
				"en": "[MIN_NAME]",
				"ein": false,
				"i": ".value"
			},
			"t": 1,
			"h": ""
		}
	}
}

equalTo()

The equalTo() method combines startAt() and endAt() with identical values:

query(ref(db, 'path/to/data'), equalTo('Second Item'), orderByValue());
{
	"t": "d",
	"d": {
		"r": 2,
		"a": "q",
		"b": {
			"p": "/path/to/data",
			"q": {
				"sp": "Second Item",
				"sin": true,
				"ep": "Second Item",
				"ein": true,
				"i": ".value"
			},
			"t": 1,
			"h": ""
		}
	}
}

Writing Data

set()

set(refB, Math.random());
{
	"t": "d",
	"d": {
		"r": 2,
		"a": "p",
		"b": { "p": "/path/to/data", "d": 0.24184493823740127 }
	}
}

push()

Similar to set(), but generates a new unique key on the client-side.

push(refB, { name: 'push-test', value: Math.random() });
{
	"t": "d",
	"d": {
		"r": 2,
		"a": "p",
		"b": {
			"p": "/path/to/data/-O1c1OTqd-img9zzEvLA",
			"d": { "name": "push-test", "value": 0.943367049653683 }
		}
	}
}

update()

update(refB, { '-O1c1OTqd-img9zzEvLA': { value: Math.random() } });
{"t":"d","d":{"r":2,"a":"m","b":{"p":"/path/to/data","d":{"-O1c1OTqd-img9zzEvLA":{"value":0.626719018744212}}}}}}

remove()

Deletion is performed using a "put" operation with a null value:

remove(refB);

JSON:

{
	"t": "d",
	"d": { "r": 2, "a": "p", "b": { "p": "/fb/test/variable", "d": null } }
}

Transactions

Transactions are similar to "put" operations but include a hash (possibly the base64 SHA1 of the current data):

runTransaction(refB, post => {
	post.value = Math.random();
	return post;
});
{
	"t": "d",
	"d": {
		"r": 4,
		"a": "p",
		"b": {
			"p": "/path/to/data",
			"d": { "name": "test", "value": 0.9155112294078509 },
			"h": "7deOIp56v5nShyYq231WnG/QGgQ="
		}
	}
}

Server Side increment

Firebase supports atomic increments using a special server value (".sv"):

update(refB, {
	value: increment(1),
});
{
	"t": "d",
	"d": {
		"r": 2,
		"a": "m",
		"b": {
			"p": "/fb/test/variable",
			"d": { "value": { ".sv": { "increment": 1 } } }
		}
	}
}
Back to Main Page