Guide
Real-time (LiveQuery)
Clients subscribe to a query and receive updates as rows change — with the same ACL deciding who sees what.
What it is
A client sends a query and keeps the connection open. When a row that matches is created, changed or removed, the server pushes the event. No polling, no websocket layer to design, and — the part that matters — the same ACL applies, so a subscriber only receives rows they were already allowed to read.
Enabling it
Two settings on Parse Server: which classes may be subscribed to, and the websocket server itself.
const parseServer = ParseServer({
// … your usual options …
liveQuery: { classNames: ['Task', 'Notification'] },
});
// after listen()
const httpServer = app.listen(1337);
ParseServer.createLiveQueryServer(httpServer);
classNames simply never emits. Subscribing to it
succeeds and nothing ever arrives — one of those failures with no error attached.
Across several instances, LiveQuery needs Redis so an event on one is delivered by all —
set liveQuery.redisURL. A single instance does not need it.
Subscribing, from a client
const query = new Parse.Query(Task);
query.equalTo('project', Project.pointer(projectId));
query.equalTo('done', false);
const subscription = await query.subscribe();
subscription.on('create', task => { // added, and matches });
subscription.on('update', task => { // changed, still matches });
subscription.on('enter', task => { // changed INTO matching });
subscription.on('leave', task => { // changed OUT of matching });
subscription.on('delete', task => { // removed });
subscription.unsubscribe();
| Event | Means |
|---|---|
create | A new row that matches the query |
update | A matching row changed and still matches |
enter | An existing row changed so that it now matches |
leave | It changed so that it no longer matches |
delete | A matching row was deleted |
enter and leave are the ones people miss. Marking a task done
when the subscription filters done: false emits leave, not
update — a list that only handles update will never remove it.
Permissions
A subscription carries the client's session token, and every event is checked against the row's ACL before it is sent. Two users subscribed to the same query receive different events, because they may read different rows.
Server-side hooks
import {
ParseClass, BaseModel, BeforeConnect, BeforeSubscribe, AfterEvent,
} from 'parse-server-kit';
@ParseClass('Task')
export default class Task extends BaseModel {
@BeforeConnect()
static async onConnect(req: any) {
// Refuse the websocket entirely - runs once per connection.
if (!req.user) throw new Parse.Error(101, 'Sign in first');
}
@BeforeSubscribe()
static async onSubscribe(req: any) {
// Narrow what a subscriber may watch, before it starts.
if (!req.user) throw new Parse.Error(101, 'Sign in first');
req.query.equalTo('archived', false);
}
@AfterEvent()
static async onEvent(req: any) {
// Inspect or adjust an event before it is delivered.
// req.event is create | update | enter | leave | delete
}
}
@BeforeSubscribe is the useful one: constraints you add there apply to every
subscriber, so a client cannot widen its subscription beyond what you allow.
What it costs
- Every save on a live class is matched against every open subscription for it. Keep
classNamesto the classes that genuinely need it. - Connections are stateful — they hold memory and survive as long as the client does. Plan for reconnection on the client.
- Several instances need Redis, or events are only delivered by the instance that handled the write.
- A bulk job on a live class fans out to every subscriber. Use the master key and consider excluding that class, or do the work in batches.
When not to use it
| Need | Better |
|---|---|
| Notify a user who is not on the page | Push notification |
| Refresh a dashboard every minute | Polling — simpler and cheaper |
| Stream a high-frequency feed | A dedicated channel; LiveQuery matches per save |
| React to a change server-side | @AfterSave, which needs no connection |
For the client API, adapters and Redis configuration, see the Parse LiveQuery guide.