Skip to content

Which collections this post of mine is pinned in

GET
/posts/{id}/collections

The AUTHOR’s answer to “where has my post been shelved”, and the read the post editor’s membership section is built on (#1119).

Not the same question as GET /collections/{id}/posts

That operation walks one collection and lists its posts. This one walks one post and lists its collections, which nothing in the product could do before: collection_posts had a writer, a remover and a per-collection listing, and no way to ask the question from the post’s side at all.

Shape

It is GET /assets/{id}/posts turned around, deliberately, down to the field names. The two answer the same KIND of question (“where did my thing end up, including in other people’s hands”) and a second shape for it would be a second disclosure rule to keep right.

  • items: the collections holding this post that the ordinary collection read rule ALREADY grants this caller, each carried whole. Nothing new is disclosed here.
  • withheld_count: how many FURTHER live collections hold it. An integer and nothing else: no id, no name, no curator, no timestamp, and no cursor over the withheld set. The count is the whole disclosure, and it is what makes “my post is shelved somewhere I cannot see” sayable at all.

can_remove is the caller’s authority, not the collection’s

Membership is COLLECTION-owned (#882): removing a post from a collection is the curator’s act, and authoring the post confers nothing. So each item says whether THIS caller may remove from THAT collection, resolved by the same predicate DELETE /collections/{id}/posts/{post_id} applies. A client that derived it from owner_user_ref would be a second expression of an authorization rule, and it would be wrong for the two capability holders the predicate also admits.

A false can_remove is not a weaker read. The collection is listed, named and linkable exactly as a mutable one is, because the caller could already see it; the flag decides whether a REMOVE control is offered, and the server refuses the call regardless.

Who may ask

The post’s AUTHOR, plus posts.admin / system.admin. Anyone else gets the SAME 404 a nonexistent post gets, not a 403, because “this post sits in 3 collections you cannot see” is the author’s information about their own work, and answering it for a stranger would make the endpoint a shelving oracle over every collection on the instance.

id
required
string format: uuid

The collections this post is pinned in

The answer to GET /posts/{id}/collections (#1119).

AssetPostUsage turned around. The split between the two fields IS the disclosure rule, and it is that schema’s: everything the caller is entitled to arrives whole in items, and everything they are not is compressed to a single integer that carries no handle back to the collection it counted.

object
items
required

The collections holding this post that the collection read rule grants this caller, each with the caller’s own removal authority over it.

Array<object>

One collection this post is pinned in, and whether the CALLER may take it off that shelf (#1119).

object
collection
required

A user-owned collection of assets, per ADR 0009. UUID-keyed, federation-prepared (origin_server_id), with three orthogonal axes: visibility, membership, and lifecycle.

object
id
required
string format: uuid
owner_user_ref
required
integer format: int64
name
required
string
description
required
string
visibility
required

The collection’s read tier.

public means anonymous visitors may read it when the instance has public mode on. It has been in the column’s CHECK constraint since migration 00008 and in the read rule (visibility.Predicate over EntityCollection admits visibility = 'public') ever since; only this schema, and therefore every client, kept saying otherwise — the same defect #1176 fixed for posts. A collection could be made public by no route the API offered (#1195).

string
Allowed values: public private org-only followers explicit-share
membership
required

manual only for 1.11.A. query and hybrid ship in 1.11.B once the search DSL (ADR 0010) lands.

string
Allowed values: manual query hybrid
expires_at

TTL. Null = permanent.

string format: date-time
nullable
purpose

Free-form admin-set hint (“for_review”, “for_proofing”, …). Replaces RS’s single-purpose archived flag.

string
nullable
origin_server_id

Federation home server. Null = local. Populated when collections sync in from a peer in a later phase.

string format: uuid
nullable
created_at
required
string format: date-time
updated_at
required
string format: date-time
deleted_at

Soft-delete timestamp. Non-null only on rows surfaced by the admin include_deleted=true listing (the trash view); null on live rows.

string format: date-time
nullable
deleted_reason

Optional reason captured at soft-delete time.

string
nullable
cover_asset_id

The curator’s CHOSEN cover picture (#1027), or null to compose the derived mosaic from members instead.

ANY asset the curator could picture when they set it — not necessarily a member of this collection. That is what makes it survive the member being removed, which is the failure a member-only pointer has. “Upload a dedicated banner” is still available: upload it as an ordinary asset, then pick it. One extra step, no separate upload machinery, and one representation for a cover instead of two.

THIS IS THE CURATOR’S SETTING, NOT THE RENDER ANSWER. Read covers to know what to paint; read this to populate the edit form’s picker. They disagree on purpose whenever the reader may not picture the chosen asset — see covers.

It does NOT federate. A local asset id names something that exists only on this server, which is ADR 0083’s exclusion criterion; a peer composes its own mosaic from the members it holds.

string format: uuid
nullable
featured_cover_asset_id

The curator’s cover for the FEATURED RAIL specifically (#1207), or null for “no separate choice”.

The rail’s card is locked to 890:500 while a collection card is 4:3, so the picture that reads well on one is often not the one that reads well on the other. This is the second pointer that lets them differ.

NULL IS A FALLBACK, NOT A BLANK. The rail’s preference order is this cover, then cover_asset_id, then the derived hero-card cover of ADR 0027 — and each rung is re-checked against the reader’s own picture plane, so a chosen cover the reader may not picture falls through to the next rung rather than leaving a blank tile or leaking the withheld picture.

Like cover_asset_id: any asset the curator may picture, not necessarily a member; the curator’s SETTING rather than the render answer; and not federated.

string format: uuid
nullable
featured_cover_focal_x

Where the featured rail’s 890:500 crop is centred horizontally, as a FRACTION of the picture’s width — 0 is the left edge, 1 the right (#1207).

A fraction rather than a pixel offset because the rail renders whichever preview rung the ladder serves at whatever width the viewport gives it. A fraction is correct at every rung and every width; a pixel offset measured against the original is correct at none of them. It maps straight onto CSS object-position.

Null means centre — the CSS default and what every collection had before this existed. Null and an explicit 0.5 are stored differently on purpose: the editor’s reset is a clear, so “never positioned” stays distinguishable from “positioned dead centre”.

Always null or set TOGETHER with featured_cover_focal_y; a focal point is a point, and the database constrains the pair.

number format: double
nullable
featured_cover_focal_y

The vertical half of the featured crop’s focal point — 0 is the top edge, 1 the bottom (#1207). See featured_cover_focal_x for why it is a fraction, why null means centre, and why the two travel together.

number format: double
nullable
cover_focal_x

Where the COLLECTION cover’s 4:3 crop is centred horizontally, as a fraction of the picture’s width (#1207).

THE DESTINATION SHAPE IS 4:3, NOT A SQUARE (#1334). CollectionCard paints a chosen cover inside an aspect-[4/3] tile on the hub, on a profile and in search, and that tile is the only collection surface that crops this picture. The square is the tempting wrong answer because the col rendition IS one (fit: cover at 320px, a 320x320 centre-crop, what every small collection thumbnail is made of), but col is a SOURCE and not a destination: a client that positioned against it would show a region the card never displays. A crop locks to the dimensions of the thing that renders it.

A separate pair from featured_cover_focal_x because the featured card is 890:500: the point that centres a face in a wide band is not the point that centres it in a 4:3 tile, so one fraction cannot be right for both destinations.

⚠️ IT IS CHOSEN AGAINST THE ORIGINAL PICTURE. A consumer honours it by rendering a contain rung with object-position; applying it to col crops a crop, since col’s square was already taken at the centre before this value could act.

Null means centre. Swapping or removing the cover picture without supplying new framing clears it (#1333), because a fraction chosen against one photograph means nothing on the next.

number format: double
nullable
cover_focal_y

The vertical half of the collection cover’s focal point (#1207). See cover_focal_x.

number format: double
nullable
featured_cover_zoom

How far the featured rail’s 890:500 crop is tightened, as a multiplier on the fitting rectangle (#1212).

The crop window is the FIT window divided by this, so 1 is the fit itself and 2 shows a quarter of the area. It exists because the fit window always has one axis equal to the whole picture — object-fit: cover keeps one axis whole — so at the fit exactly one axis can travel, and a subject sitting in the left half of a portrait cover can never be brought to the middle. Below the fit BOTH axes gain travel, which is what makes featured_cover_focal_x mean anything for such a picture.

Null means fit: what every collection rendered before this existed, so a client that ignores it renders exactly what it rendered before. Null and an explicit 1 paint the same picture and are stored differently on purpose — the editor’s reset is a clear, so “never zoomed” stays distinguishable from “zoomed and came back”.

Bounded 1..4. Below 1 the window would be larger than the picture. The upper bound comes from the preview ladder’s real rungs: a cover carrying a crop is painted from a CONTAIN rung, the rung it is guaranteed to have is preview (1024) and the tallest one is hires (4096), so 4x is the last step the ladder can still answer with real pixels.

number format: double
nullable
cover_zoom

How far the collection card’s 4:3 crop is tightened (#1212). Separate from featured_cover_zoom for the reason the focal pairs are separate: the tightening that frames a subject in a wide band is not the one that frames it in a 4:3 tile. See featured_cover_zoom for the semantics and the 1..4 bound.

number format: double
nullable
covers

What to PAINT for this collection’s tile — the curator’s chosen cover if there is a renderable one (#1027), else up to four members whose picture this caller may render, the derived mosaic (#1026).

A COVER THIS READER MAY NOT PICTURE FALLS BACK TO THE MOSAIC; it never yields a blank tile, and it never leaks the withheld picture either. The choice is the curator’s, but whether it renders is still decided per reader, so two readers of the same collection can correctly get different answers here. A client cannot tell an overridden cover from a one-member mosaic, and does not need to.

POSTS ARE THE ONLY SOURCE (#1236). Each contributing member is a collection_posts row, supplying cover_thumbnail_asset_id ?? cover_asset_id — the same preference a feed card uses. #882 made a post-only collection an ordinary thing to own (“save someone else’s post”), and until #1026 such a collection had no cover source at all and rendered as an empty folder.

collection_resources rows used to contribute their asset directly, and stopped when #1161 removed bare-asset membership as a publication path: a tile summarises what a collection CONTAINS, and after #1185 took the asset section off the page those assets were inside nothing a reader could open. A COLLECTION WITH NO POSTS THEREFORE HAS NO COVERS — the field is absent, and the client paints its empty state.

ORDER is the curator’s, added_at ascending: the arrangement is the curation.

EACH ASSET APPEARS AT MOST ONCE, at its earliest position. Two posts in one collection can share a cover, and the same picture twice summarises nothing. Clients may therefore key a list on asset_id.

WITHHELD MEMBERS ARE SKIPPED, NOT SLOTTED. Only members passing the caller’s picture plane (ADR 0064) and having a col rendition appear, so a restricted member never occupies a slot a renderable one behind it could fill. A blank quarter of a mosaic says nothing and costs a picture.

ABSENT means “not composed on this surface”, never “no members” — an empty array is the honest “nothing to show”.

Array<object>

One tile of a collection’s composed mosaic cover (#1026). An entry exists only if this caller may render the asset, so clients paint every entry unconditionally — there is no preview_available to branch on and no placeholder rung.

object
asset_id
required

Fetch the picture from /assets/{asset_id}/variants/col. Guaranteed to have that rendition — an asset without one is not a cover candidate.

string format: uuid
preview_available
required

A CONTAIN rung (preview) exists for this asset (#1207) — a different question from the col guarantee above, and the one a focal point depends on.

⚠️ A COVER WITH A FOCAL POINT MUST BE PAINTED FROM A CONTAIN RUNG. col is fit: cover at 320px, a 320x320 CENTRE-CROP, so the picture’s edges are gone before object-position could act on them: applying collections.cover_focal_* to col crops a crop and lands somewhere the curator did not choose. The focal fractions are stored against the ORIGINAL, so the source has to be one that still is the original shape.

When this is false there is no such rung and the client falls back to col with a centred crop — which is exactly what every collection card did before focal points existed, so the fallback is the old behaviour rather than a degradation.

Populated for every entry, mosaic tiles included, even though only the single-cover case consumes it: a field that is present on some rows and absent on others is a field every reader has to branch on twice.

boolean
can_remove
required

Whether this caller may remove the post from THIS collection: the verdict DELETE /collections/{id}/posts/{post_id} will reach, resolved here by the same predicate rather than restated.

⛔ AUTHORING THE POST DOES NOT SET IT. Membership is the curator’s (#882), so an author looking at their own post sees false for every collection they do not own or administer, and a client must offer no removal control for those. It is a statement about authority, not about readability: the collection is listed either way.

boolean
withheld_count
required

How many FURTHER live collections hold this post: ones the caller may not read.

Existence and count, never identity. No companion array of ids, no cursor, and no filter that moves this number selectively, because any of those would turn the count back into the collections it counted. 0 means the caller can already see every collection this post is in.

Soft-deleted collections are not counted: a tombstoned shelf is not somewhere the post appears.

⚠️ INDEPENDENT OF HOW MANY items CAME BACK. items is a bounded page; this counts what the caller may NOT read, so a post in more readable collections than one page holds still reports 0 here. It is not total − items.length, which is the arithmetic that made truncation look like withholding on the asset side (#1237).

integer format: int64

Authentication required, missing, or invalid

object
error
required

Human-readable error summary

string
Example
the request could not be completed
Example
{
"error": "authentication required: sign in and retry with a valid session or API token"
}

Resource not found

object
error
required

Human-readable error summary

string
Example
the request could not be completed

Unexpected server error

object
error
required

Human-readable error summary

string
Example
the request could not be completed