Skip to Content
Server SDKCollections

Server: Collections

Collections let you define and expose dynamic groups of records. These groups update automatically when underlying records change, and clients receive diffs of added or removed members.

This is ideal for collaborative apps, dashboards, filtered views, and pagination.

Expose collections

To make a collection subscribable, use exposeCollection(...):

.("collection:all-tasks", async () => { return await .("task:*"); });

listRecordsMatching is a function that takes a Redis glob pattern, and an optional callback (more on that below), and returns the records that match the pattern. For example:

await .("task:1", { : 1 }); await .("task:2", { : 2 }); await .("task:3", { : 3 }); const = await .("task:*"); .();

The above would log:

[ { taskid: 1, id: 'task:1' }, { taskid: 2, id: 'task:2' }, { taskid: 3, id: 'task:3' } ]

The second argument is a resolver that must return an array of records (not just IDs) for the given collection. The resolver runs:

  • Immediately when a client subscribes
  • Again any time a record changes (added, removed, or updated)

Collections can use dynamic patterns too:

.( /^collection:user:(\d+):tasks$/, async (: , : string) => { const = .(":")[2]; return await .(`user:${}:task:*`); } );

Pagination example

You can implement pagination by encoding page numbers in collection names, and making use of the slice, sort, and map options:

.( /^tasks:page:\d+$/, async (: , : string) => { const = (.(":")[2]); const = 10; return await .("task:*", { // Transform records for the client (optional - exclude heavy fields) : () => ({ : .id, : .title, : .status, : .created_at }), // Sort by created_at descending (newest first) : (, ) => new (.created_at).() - new (.created_at).(), // Paginate based on extracted page number : { : ( - 1) * , : }, }); } );

Client-side pagination:

Client-side pagination works by subscribing to a single page at a time:

// Subscribe to page 1 await .("tasks:page:1", { : ({ , , }) => { // } }); // Unsubscribe from page 1, then subscribe to page 2 await .("tasks:page:1"); await .("tasks:page:2");

Per-connection isolation

The resolver runs for each subscribed connection individually, which enables:

  • Connection-specific authorization - each user sees only what they should
  • Different views of the same collection - based on user permissions/context
  • Proper isolation - each connection maintains its own version and state
.("collection:user-tasks", async (: , : string) => { // Each connection gets its own resolver call const = .metadata?.userId; const = .metadata?.role; if ( === 'admin') { // Admins see all tasks return await .("task:*"); } else { // Regular users see only their own tasks return await .(`user:${}:task:*`); } });

Record changes trigger diffs

Whenever a record is added, removed, or updated, Mesh will:

  1. Rerun the resolver for each subscribed connection
  2. Compare new and old record IDs for each connection
  3. Send a diff to clients where membership changed

Diff format:

  • added: Array of full record objects that were added to the collection
  • removed: Array of full record objects that were removed from the collection
  • changed: Array of full record objects that were changed in the collection

Resync logic

Mesh tracks a version number per collection per connection, starting at version 1. If a client receives a diff with a skipped version (e.g. expected v3, got v5), it auto-resubscribes and fetches the full list again.

This ensures correctness across reconnects, network drops, or inter-instance propagation delays.

Flexible attribute-based filtering

You can expose collections that allow clients to filter records by arbitrary attributes, using a pattern that encodes the filter as part of the collection name. This is useful for building flexible search, filter, or view features.

For example, to let clients subscribe to filtered lists of products:

.( /^collection:products:filter:.+$/, async (: , : string) => { const [, , , ] = .(":"); const = .(()); const = await .("product:*"); return .( => .().(([, ]) => [] === ) ); } );

On the client, you encode your filter as a JSON object, then subscribe to the filtered collection:

const = (.({ : "books", : true })); .(`collection:products:filter:${}`);

This pattern is flexible and can be extended to support multi-field filters, value arrays, partial matching, and more. For example, you could filter by { brand: "Acme", priceRange: [10, 50] } or any other combination of product attributes.

Use cases

  • Task lists: real-time filters across shared boards
  • Projects: dynamic membership as records are created/deleted
  • Views: multi-tenant views per user, team, or org
  • Pagination: sort and slice large datasets into pages
Last updated on
© 2025