Skip to main content

Collections

Collections in ActivityPub are ordered or unordered lists of objects or activities. They're used for inboxes, outboxes, followers lists, and any other groupings of items.

Collection Types​

TypeDescriptionOrder
CollectionUnordered setNone
OrderedCollectionOrdered setMaintained
CollectionPagePage of a CollectionNone
OrderedCollectionPagePage of an OrderedCollectionMaintained

Basic Collection Structure​

Unpaginated Collection​

For small collections:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"id": "https://example.com/users/alice/following",
"totalItems": 3,
"orderedItems": [
"https://example.com/users/bob",
"https://example.com/users/carol",
"https://other.example/users/dave"
]
}

Paginated Collection​

For larger collections:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"id": "https://example.com/users/alice/outbox",
"totalItems": 1542,
"first": "https://example.com/users/alice/outbox?page=true",
"last": "https://example.com/users/alice/outbox?min_id=0"
}

Collection Page​

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollectionPage",
"id": "https://example.com/users/alice/outbox?page=true",
"partOf": "https://example.com/users/alice/outbox",
"next": "https://example.com/users/alice/outbox?max_id=12345",
"prev": "https://example.com/users/alice/outbox?min_id=12350",
"orderedItems": [
{ "type": "Create", "...": "..." },
{ "type": "Announce", "...": "..." }
]
}

Collection Properties​

PropertyTypeDescription
idURLUnique identifier
typeStringCollection type
totalItemsIntegerTotal number of items
firstURLFirst page
lastURLLast page
currentURLCurrent/latest page
itemsArrayItems (for Collection)
orderedItemsArrayItems (for OrderedCollection)

Page Properties​

PropertyTypeDescription
partOfURLParent collection
nextURLNext page
prevURLPrevious page
startIndexIntegerIndex of first item

Standard Collections​

Inbox​

Receives incoming activities:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"id": "https://example.com/users/alice/inbox",
"totalItems": 523,
"first": "https://example.com/users/alice/inbox?page=true"
}

POST to inbox: Receive activities (S2S) GET from inbox: Read activities (C2S, authenticated)

Outbox​

Contains activities by the actor:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"id": "https://example.com/users/alice/outbox",
"totalItems": 156,
"first": "https://example.com/users/alice/outbox?page=true"
}

GET from outbox: Read activities (public) POST to outbox: Submit activities (C2S, authenticated)

Followers​

Collection of actors following this actor:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"id": "https://example.com/users/alice/followers",
"totalItems": 42,
"first": "https://example.com/users/alice/followers?page=true"
}

Following​

Collection of actors this actor follows:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"id": "https://example.com/users/alice/following",
"totalItems": 108,
"first": "https://example.com/users/alice/following?page=true"
}

Liked​

Collection of objects the actor has liked:

{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"id": "https://example.com/users/alice/liked",
"totalItems": 234
}

Pagination Strategies​

Use item IDs as cursors:

/outbox?max_id=12345  → Items older than 12345
/outbox?min_id=12345 → Items newer than 12345
/outbox?since_id=12345 → Items since 12345
{
"type": "OrderedCollectionPage",
"id": "https://example.com/users/alice/outbox?max_id=12345",
"next": "https://example.com/users/alice/outbox?max_id=12340",
"prev": "https://example.com/users/alice/outbox?min_id=12345"
}

Offset-Based​

Simple but less efficient:

/outbox?page=1  → First 20 items
/outbox?page=2 → Items 21-40
caution

Offset pagination can be slow for large collections and can skip/repeat items when the collection changes.

Keyset-Based​

Similar to cursor, but using timestamps:

/outbox?before=2024-01-15T10:00:00Z

Implementation​

Creating a Collection Endpoint​

app.get('/users/:username/outbox', async (req, res) => {
const username = req.params.username;
const page = req.query.page;
const maxId = req.query.max_id;

const baseUrl = `https://example.com/users/${username}/outbox`;

if (!page) {
// Return collection metadata
const count = await db.activities.count({ actor: username });

return res.json({
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'OrderedCollection',
id: baseUrl,
totalItems: count,
first: `${baseUrl}?page=true`,
last: `${baseUrl}?min_id=0`
});
}

// Return a page
const query = { actor: username };
if (maxId) {
query.id = { $lt: maxId };
}

const activities = await db.activities
.find(query)
.sort({ id: -1 })
.limit(20);

const items = activities.map(a => a.data);
const nextMaxId = activities.length > 0
? activities[activities.length - 1].id
: null;

res.json({
'@context': 'https://www.w3.org/ns/activitystreams',
type: 'OrderedCollectionPage',
id: maxId ? `${baseUrl}?max_id=${maxId}` : `${baseUrl}?page=true`,
partOf: baseUrl,
next: nextMaxId ? `${baseUrl}?max_id=${nextMaxId}` : undefined,
orderedItems: items
});
});

Fetching a Collection​

async function fetchCollection(collectionUrl) {
const items = [];

// Fetch collection
let response = await fetch(collectionUrl, {
headers: { 'Accept': 'application/activity+json' }
});
let collection = await response.json();

// If items are inline, return them
if (collection.orderedItems) {
return collection.orderedItems;
}
if (collection.items) {
return collection.items;
}

// Otherwise, paginate
let pageUrl = collection.first;

while (pageUrl) {
response = await fetch(pageUrl, {
headers: { 'Accept': 'application/activity+json' }
});
const page = await response.json();

const pageItems = page.orderedItems || page.items || [];
items.push(...pageItems);

pageUrl = page.next;

// Safety limit
if (items.length > 1000) break;
}

return items;
}

Collection Synchronization​

Synchronizing Followers​

When your followers collection changes, you don't need to notify anyone. The change is reflected when the collection is fetched.

Synchronizing Follow Lists​

After receiving an Accept for a Follow:

  1. Add the actor to your following collection
  2. Start fetching their outbox for past posts (optional)
  3. Start delivering your posts to their inbox

Security Considerations​

Authentication​

  • Inbox: Accept POST from any authenticated server
  • Outbox GET: Usually public
  • Outbox POST: Authenticated owner only (C2S)
  • Followers/Following: May require authentication for privacy

Privacy​

Some servers hide followers/following counts or require authentication:

{
"type": "OrderedCollection",
"id": "https://example.com/users/private/followers",
"totalItems": 0
}

(Returning 0 or omitting totalItems for privacy)

Best Practices​

  1. Always paginate large collections
  2. Use cursor-based pagination for stability
  3. Set reasonable page sizes (20-40 items)
  4. Include totalItems when possible
  5. Cache collection responses appropriately
  6. Handle empty collections gracefully
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "OrderedCollection",
"id": "https://example.com/users/new/outbox",
"totalItems": 0,
"orderedItems": []
}

Next Steps​