Return information about the caller
GET /auth/me
Authorizations
Section titled “Authorizations ”Responses
Section titled “ Responses ”Current user
object
How the caller was authenticated for this request.
User’s language pref (BCP47). Empty string = follow system / browser. Joined from user_profiles at /auth/me time so the frontend can hydrate the language store on first paint without a separate round-trip.
User’s theme pref. system follows the OS; empty string
means the account has NO stored preference and the device
falls back to the app default (dark, #590). The two are
deliberately distinct — collapsing them made an explicit
“follow my OS” indistinguishable from “never chose”, so it
could not travel to a second device (#677). Same “join on
/auth/me” rationale as language.
The account’s default-view selections, joined from user_preferences at /auth/me time (#706).
Same rationale as language and theme, and the same
timing guarantee, which is the point: the root layout’s
load awaits /auth/me before any page renders, so the browse
store can consult these synchronously in its init()
rather than seeding itself from a second round-trip that
lands after the grid has already painted.
object
Which feed segment browse lands on. Empty/unset falls back
to latest. trending and for_you were removed in
#736: neither existed in the feed enum, so choosing one
put the plain latest feed under a label promising
otherwise. They come back when a ranking model does.
Default browse-feed layout. Empty/unset falls back to the
per-device default (feed on a coarse pointer, grid
otherwise). feed is selectable here as of #706 — it was
already a mode a phone could land on but not one a user
could ask for.
Default browse ordering: newest (posted_at descending,
the built-in) or oldest. Empty/unset falls back to
newest. popular and trending were removed in #706 —
GET /posts has no ranking parameter and no ranking
model, so both were labels over the default order.
The account’s browse-feed content filters (#891), joined from user_preferences at /auth/me time.
The filtering itself is the SERVER’s — GET /posts reads
this preference and applies it, so no client can forget to
and no client can turn it off. What rides the session is the
fact that it is ON, because a feed that is quietly shorter
than it would otherwise be has to say so on the same paint
that renders it. Learning it from a second round-trip means
the browse page renders a thinner grid with no explanation
and then pops a banner in afterwards — the same first-paint
race #867 and #872 closed for default_views, theme and
capabilities, arriving at the same surface.
OMITTED when every filter is off, which is every account that has not opted in — so this changes no existing session response.
object
Keep the restricted-member placeholders (#883) in
GET /posts instead of subtracting them.
Default false — the feed hides them (#921). #891
shipped this machinery as an opt-in on the theory that the
placeholder is the more informative answer. Measured against
the stock seed dataset, a reader with no capabilities got 82
posts of which 27 were entirely placeholders: a third of the
grid was locked doors. The principle the default now encodes
is that a placeholder belongs where the reader ASKED A
QUESTION or OPENED A CONTAINER, not where they were handed a
feed.
With the default (false), three rules, and the third is the
one that matters:
- A member the caller cannot read is OMITTED from
membersrather than sent as arestricted: trueplaceholder. - A post left with NO visible members — one that HAD members and none survived — is omitted from the page. A post with no members at all (an article, ADR 0073) is unaffected: it was never showing you anything you couldn’t see.
- A post the CALLER AUTHORED is never omitted, however restricted its members are. An author’s post can carry other people’s assets, so rule 2 alone would make your own work vanish from your own feed over a display default.
Set it true and GET /posts returns the pre-#921 payload
exactly: every placeholder present, every all-restricted post
back in the page.
Applies to GET /posts and stops there. GET /posts/{id} is
an explicit request for one post and answers with the
placeholders intact whatever this is set to, which is not an
inconsistency: rule 2 exists BECAUSE an all-restricted post
rendering as an empty card is worse than the placeholder it
replaced, and filtering members on the single-post read would
rebuild that empty card on the one surface rule 2 could not
protect. Collection contents render placeholders for the same
reason — an opened container should say what is in it.
It also keeps the ask alive. #913’s “Request access” button lives on the placeholder, so it survives wherever the placeholder does: the post page and collections. The trade the DEFAULT makes — no placeholders in the feed means no Request access in the feed — is stated in the setting’s own help text rather than left to be discovered.
The account’s browse-rail curation — team chips (#1113) and followed-tag chips (#1123) — joined from user_preferences at /auth/me time.
Here rather than behind a second fetch because these lists
decide WHAT THE RAIL DRAWS. Learning them after first paint
means the rail paints the uncurated list and then rearranges
itself in front of the reader — the layout shift the rail’s
single loaded gate exists to avoid, reintroduced one level
up. Same first-paint argument as default_views (#706).
OMITTED when every list is empty, which is every account that has not curated its rail — so this changes no existing session response.
object
Teams the reader removed from their rail. Empty = nothing hidden.
A hide-list rather than a show-list, deliberately: an empty allow-list would have to mean “show everything”, which is the opposite of what an empty allow-list says, and the first partial write would blank the rail.
The reader’s explicit ordering, applied to the FOLLOWED group (the manage panel’s drag-reorder). Empty = the server’s order.
PARTIAL LISTS ARE LEGAL and are the normal case: the ids named here lead, in this order, and everything else keeps its previous relative position behind them. That is what lets “drag one team to the top” persist one id rather than a full snapshot that the next follow would immediately make stale.
Followed tags whose chip the reader removed from their rail (#1123). Empty = nothing hidden.
HIDING IS NOT UNFOLLOWING, and the two verbs sit next to
each other in the manage panel precisely so the difference
is visible. Unfollowing drops the tag_follows row and
changes what the Following feed CONTAINS. Hiding only takes
the chip off the strip; the tag’s posts keep reaching the
feed exactly as before.
The reader’s explicit ordering of their followed-tag chips. Empty = the server’s order (most recently followed first).
Partial lists are legal and normal, exactly as team_order.
The account’s mature-content consent, joined from user_preferences at /auth/me time (#1115).
Here rather than behind a second fetch for the same
first-paint reason as feed_filters: it decides what the
feed CONTAINS, and a page that learns it after paint renders
one library and then swaps it. #706 is the precedent.
⚠️ IT IS ONE OF THREE CONJUNCTS, NOT THE ANSWER. The reader
also has to be signed in (they are, if they are reading
this) and the INSTANCE has to allow mature content — that
third conjunct is mature_content_allowed below. A client
rendering “mature content is on for you” from THIS field
alone will be wrong on an instance that has switched the
feature off.
OMITTED when the opt-in is off, which is every account that has not opted in — so this changes no existing session response.
object
Opt in to being shown mature content. Absent or false —
the default for every account — hides it.
Whether this INSTANCE allows mature content at all (#1116, ADR 0090 §2) — the operator’s switch, not this account’s.
⚠️ REQUIRED, unlike mature_content above, and the asymmetry
is the point. mature_content may be omitted because absent
and false mean the same thing there (not opted in). Here
they do NOT: the unconfigured default is true, so a client
that read an absent field as false would hide the opt-in
control on every install that has never touched the setting —
which is all of them. Declared required so the guess is
impossible to need: every producer of this schema must answer,
and the compiler says so.
It rides the session response rather than getting a public
endpoint because every consumer is behind authentication:
the upload self-label, the operator’s per-asset override,
and the account opt-in are all signed-in surfaces, and an
anonymous viewer can never opt in (there is nowhere to store
the answer). The operator’s read/write pair is
/admin/system/mature-content.
What a client does with it: when false, the account’s
mature opt-in control is not rendered AT ALL — not rendered
disabled — because a toggle the server will not honour is a
control that lies. Server-side nothing depends on the
client obeying: the same switch is ANDed into the
qualification predicate on every request.
The caller’s resolved GLOBAL capability codes — the same set
GET /auth/me/capabilities reports in its capabilities
field, after role inheritance, per-user grants and revokes
have all been applied. Team-scoped capabilities are NOT
included; ask the scoped endpoints for those.
Rides the session response for the same reason theme and
default_views do, and for a sharper one (#871): the client
marks its auth store “ready” the instant this response
lands, and every capability-gated surface reads the store at
that moment. A capability set that arrives on a SECOND
round-trip therefore arrives after the gate has already
decided, which rendered a “you don’t have permission” panel
at real admins until the follow-up landed. One response, one
decision, no window.
OMITTED when capabilities_status is unavailable. Read
that field first: this one is only meaningful when the
lookup succeeded.
Whether capabilities above could be determined for this
session. THREE outcomes, not two, and the difference between
the last two is the whole point of this field (#956):
resolved+ a non-empty list — the caller holds those codes.resolved+ an empty list — the lookup succeeded and the account genuinely holds nothing.unavailable— the lookup FAILED.capabilitiesis omitted and says nothing whatsoever about what this account holds.
Before this field existed, the second and third collapsed into one wire shape, and the SPA rendered “You don’t have permission to view this page.” for both. That panel is correct for a powerless account and a lie for a resolver blip, and neither the operator reading it nor a test asserting on it could tell which had happened — which is exactly how #956’s nightly failure survived four triage passes.
Clients MUST fail closed on unavailable: grant nothing,
render no gated control. What they MUST NOT do is describe
it as a permission decision. It is the absence of one, and
the honest surface is an error with a retry.
This is a status, not a permission: unavailable never
widens what a caller may do, and every action is enforced
server-side against a freshly resolved set regardless of
what any session response said.
Required, and deliberately so. Making it optional and
treating absent as “unknown” was the smaller diff and the
wrong one: capabilities is ALREADY legitimately absent
from POST /setup/complete, whose response omits it while
the admin’s role is committed. Overloading absence would
have made that response assert a degraded state that is not
true, and would have let any future producer that forgets a
field silently emit an error state on a healthy session. A
required enum makes every producer answer the question.
Non-null when the session was minted via POST /admin/users/{ref}/impersonate. Carries the acting admin’s ref + username so the persistent “you are acting as @target” banner can render without a separate round-trip.
object
Authentication required, missing, or invalid
object
Human-readable error summary
Example
the request could not be completedExample
{ "error": "authentication required: sign in and retry with a valid session or API token"}