AppView-API
Stand: 2026-11-07. Referenz der Lese-API für Clients, von Hand gepflegt. Quelle der Wahrheit ist der Code in
services/appview/src/routes/. Konzept und Einordnung: Architektur §5.2. Die Admin-API für Clients (Registrar) steht auf der Startseite.
Die AppView beantwortet Abfragen aus dem Index. Sie schreibt keine Records: Beiträge schreibt der Client ins Repo der Person, der Store übernimmt sie über den Firehose (ATProto in atvoto).
Grundlagen
Abschnitt betitelt „Grundlagen“Basis-URL (test): https://api.test.atvoto.ch. Alle Methoden liegen unter
/xrpc/<NSID>.
Aufruf: GET mit Query-Parametern, POST mit JSON-Body. Listen-Parameter werden
wiederholt (?dossiers=sv-1&dossiers=sv-2). CORS ist für alle Origins offen, ohne
Cookies.
Dossier: Parameter dossier ist die Dossier-ID inklusive Client-Präfix, z.B.
sv-663.
Fehler: JSON mit Code und Text, z.B. {"error": "not_found", "message": "Argument not found"}. Bei Auth-Fehlern und einigen Eingabefehlern liefert FastAPI stattdessen
{"detail": "…"}.
| Status | Bedeutung |
|---|---|
| 400 | Parameter fehlt oder ungültig (invalid_request) |
| 401 | Service-Auth-Token fehlt (bei Pflicht) oder ist ungültig |
| 403 / 409 | Peer Review: nicht eingeladen, nicht eingecheckt, geschlossen, zu spät |
| 404 | Objekt unbekannt (not_found) |
| 429 | Rate-Limit überschritten |
| 503 | DID der Person nicht auflösbar |
Sprache
Abschnitt betitelt „Sprache“Argumente, Kommentare, Themen und Suche liefern Texte in einer Sprache:
Parameter lang, sonst der erste passende Eintrag von Accept-Language, sonst
de-CH. Unterstützt: de-CH, fr-CH, it-CH, rm, en-GB (ATVOTO_LANGUAGES);
de oder de-DE werden auf de-CH abgebildet.
Ist der Text nicht in der Originalsprache vorhanden, kommt die Übersetzung. Die Antwort sagt, was geliefert wurde:
| Feld | Inhalt |
|---|---|
record.langs |
Originalsprache(n) |
availableLangs |
alle verfügbaren Sprachen (Original + Übersetzungen) |
translationSource |
ai oder manual, wenn übersetzt |
record.translatedFrom |
Originalsprache, wenn übersetzt |
Authentisierung
Abschnitt betitelt „Authentisierung“Es gibt drei Stufen:
| Stufe | Verhalten |
|---|---|
| öffentlich | ohne Token |
| Viewer optional | ohne Token anonym; mit Token zusätzlich Felder zur Person (viewer, eigene Reihenfolge). Ein ungültiges Token ist immer 401. |
| Viewer nötig | 401 ohne gültiges Token |
Das Token ist ein Service-Auth-JWT, ausgestellt vom PDS der Person (iss = DID
der Person, aud = DID der AppView, lxm = aufgerufene Methode). Zwei Wege:
- Über den PDS (üblich): Der Client ruft die Methode beim PDS auf, mit dem
Access-Token der Person und dem Header
atproto-proxy: did:web:api.test.atvoto.ch#atvoto_appview. Der PDS stellt das JWT aus und leitet weiter. - Direkt: Token mit
com.atproto.server.getServiceAuth(aud= AppView-DID,lxm= Methode) holen und alsAuthorization: Bearer <jwt>direkt an die AppView schicken.
PDS-Access-Tokens selbst werden abgelehnt. Die AppView-DID und ihr Service-Eintrag
stehen in /.well-known/did.json.
Rate-Limits
Abschnitt betitelt „Rate-Limits“Gezählt pro Person (DID aus dem Token), ohne Token pro IP.
| Methode | Limit |
|---|---|
argument.findSimilar |
20/Minute und 200/Stunde |
peerreview.duplicateCandidate |
20/Minute |
com.atproto.label.queryLabels |
60/Minute |
Reihenfolge
Abschnitt betitelt „Reihenfolge“argument.list (Standard sort=random), comment.list und taxonomy.get mischen
die Einträge stabil pro Person: Jede Person sieht ihre eigene, feste Reihenfolge,
die sich nicht ändert, wenn neue Beiträge dazukommen. Anonym gilt eine gemeinsame
Reihenfolge.
Sichtbarkeit
Abschnitt betitelt „Sichtbarkeit“Von der Moderation ausgeblendete Beiträge (hidden) und Beiträge zurückgezogener
Dossiers (withdrawn) erscheinen in keiner Antwort. Ein ausgeblendeter Kommentar mit
sichtbaren Antworten bleibt als Platzhalter {uri, hidden: true, parentUri, argumentUri} stehen, damit der Gesprächsverlauf erhalten bleibt.
Übersicht
Abschnitt betitelt „Übersicht“Alle Methoden mit Präfix ch.atvoto..
| Methode | HTTP | Auth | Zweck |
|---|---|---|---|
argument.list |
GET | optional | Argumente eines Dossiers |
argument.get |
GET | optional | ein Argument, mit Themenpfaden |
argument.findSimilar |
POST | nötig | ähnliche Argumente zu einem Entwurf |
comment.list |
GET | optional | Kommentare zu einem Argument |
comment.get |
GET | optional | ein Kommentar mit seinem Argument |
activity.list |
GET | optional | Aktivitäten eines Dossiers |
activity.markSeen |
POST | nötig | Aktivitäten als gesehen markieren |
dossier.getStats |
GET | öffentlich | Zähler pro Dossier |
dossier.search |
GET | optional | Suche in einem Dossier |
dossier.getPermissions |
GET | nötig | was die Person im Dossier darf |
quota.get |
GET | nötig | Kontingent der Person |
taxonomy.get |
GET | optional | Themenbaum mit Argumenten |
peerreview.criteria |
GET | optional | Kriterien des Gutachtens |
peerreview.list |
GET | optional* | Gutachten eines Dossiers |
peerreview.pending |
GET | nötig | offene Einladungen der Person |
peerreview.status |
GET | nötig | Stand eines Gutachtens |
peerreview.duplicateCandidate |
GET | nötig | ähnlichstes Argument für Gutachter:innen |
peerreview.checkIn |
POST | nötig | Platz im Gutachten belegen |
peerreview.activity |
POST | nötig | Nachfrist bei Aktivität verlängern |
* scope=mine braucht ein Token, scope=all nicht.
Dazu: Bluesky-kompatible Endpunkte, Labels, DID-Dokument und Health.
Argumente
Abschnitt betitelt „Argumente“argument.list
Abschnitt betitelt „argument.list“GET /xrpc/ch.atvoto.argument.list · Viewer optional
| Parameter | Pflicht | Beschreibung |
|---|---|---|
dossier |
ja | Dossier-ID |
sort |
random (Standard, stabil pro Person), top (Likes), new, discussed (Kommentare) |
|
type |
PRO oder CONTRA |
|
source |
user, official, organization; ohne = alle |
|
limit |
Standard 100 | |
lang |
Sprache, siehe Sprache |
Antwort {"arguments": [Argument, …]}. Auch abgelehnte Argumente sind enthalten
(peerreviewStatus: "rejected"), der Client kennzeichnet sie.
{ "uri": "at://did:plc:dossier…/ch.atvoto.dossier.argument/3f9c…", "cid": "bafyrei…", "record": { "$type": "ch.atvoto.dossier.argument", "title": "…", "body": "…", "type": "PRO", "dossier": "at://…", "createdAt": "2026-10-30T08:12:00Z", "langs": ["de-CH"], "source": { "$type": "ch.atvoto.dossier.argument#sourceUser", "authorDid": "did:plc:…", "originUri": "at://did:plc:…/ch.atvoto.dossier.argument/3l…", "originCid": "bafyrei…" } }, "author": {"did": "did:plc:…", "displayName": "…"}, "likeCount": 4, "commentCount": 2, "peerreviewStatus": "approved", "indexedAt": "2026-10-30T08:12:03Z", "viewer": {"like": "at://…", "preference": 80}, "availableLangs": ["de-CH", "fr-CH"], "translationSource": "ai"}| Feld | Hinweis |
|---|---|
record.source |
#sourceUser (mit authorDid, originUri, originCid), #sourceOfficial (documentRef, section) oder #sourceOrganization (orgKey, documentRef, verifiedDid) |
author |
nur bei sourceUser; displayName = Pseudonym aus dem Profil |
peerreviewStatus |
preliminary, approved, rejected; fehlt, wenn das Argument kein Peer Review hat |
viewer |
nur mit Token: like = AT-URI der eigenen Bewertung, preference = deren Wert 0–100 |
Felder ohne Wert fehlen in der Antwort.
argument.get
Abschnitt betitelt „argument.get“GET /xrpc/ch.atvoto.argument.get · Viewer optional
| Parameter | Pflicht | Beschreibung |
|---|---|---|
dossier |
ja | Dossier-ID |
rkey |
ja | rkey der kanonischen Kopie |
lang |
Sprache |
Antwort {"argument": Argument} wie oben, zusätzlich topicPaths: die Themen, denen
das Argument zugeordnet ist, je als Pfad von oben nach unten
([[{"name", "key", "description"}, …], …]). 404, wenn unbekannt.
argument.findSimilar
Abschnitt betitelt „argument.findSimilar“POST /xrpc/ch.atvoto.argument.findSimilar · Viewer nötig · 20/min, 200/h
Findet bestehende Argumente, die einem Entwurf ähneln (Embeddings). Neutral: keine Einschätzung zu Position, Thema oder Ton. Der Entwurfstext geht zur Berechnung an den Embedding-Anbieter des Stores. Technik: Duplikat-Check.
{"dossier": "sv-663", "title": "…", "body": "…", "type": "PRO", "lang": "de-CH", "limit": 3}title oder body genügt; type beschränkt auf die gleiche Position; limit 1–10
(Standard 1).
{"lang": "de-CH", "status": "ok", "items": [{"uri": "at://…", "title": "…", "body": "…", "type": "PRO", "similarity": 0.91}]}status: "unavailable", wenn der Embedding-Dienst nicht erreichbar ist. Das soll das
Einreichen nie blockieren.
Kommentare
Abschnitt betitelt „Kommentare“comment.list
Abschnitt betitelt „comment.list“GET /xrpc/ch.atvoto.comment.list · Viewer optional
| Parameter | Pflicht | Beschreibung |
|---|---|---|
argumentUri |
ja | AT-URI des Arguments |
limit |
Standard 50 | |
lang |
Sprache |
Antwort {"comments": [Comment, …]}, alle Ebenen in einer flachen Liste; den Baum
baut der Client über parentUri.
{ "uri": "at://did:plc:…/ch.atvoto.dossier.comment/3l…", "cid": "bafyrei…", "record": {"title": "…", "body": "…", "argument": "at://…", "createdAt": "…", "langs": ["de-CH"]}, "author": {"did": "did:plc:…", "displayName": "…"}, "origin": "intern", "parentUri": "at://…", "argumentUri": "at://…", "likeCount": 1, "indexedAt": "…", "viewer": {"like": "at://…"}, "availableLangs": ["de-CH", "fr-CH"], "translationSource": "ai", "collapsed": true}| Feld | Hinweis |
|---|---|
origin |
intern (atvoto) oder extern (Antwort auf Bluesky; author dann mit handle) |
parentUri |
fehlt bei Kommentaren direkt zum Argument |
collapsed |
nur bei externen Kommentaren von Konten ohne Teilnahmerecht: eingeklappt anzeigen. Das ist keine Moderation. |
Übersetzungen von Kommentaren liegen als eigene Records im Community-Konto
(ch.atvoto.dossier.commentTranslation), weil der Store nicht in fremde Repos
schreibt (Übersetzungen).
comment.get
Abschnitt betitelt „comment.get“GET /xrpc/ch.atvoto.comment.get · Viewer optional
| Parameter | Pflicht | Beschreibung |
|---|---|---|
uri |
ja | AT-URI des Kommentars |
lang |
Sprache |
Antwort {"comment": Comment, "argument": {uri, rkey, title, body, type, likeCount, commentCount, peerreviewStatus, dossierRkey}}. 404, wenn unbekannt.
Aktivität
Abschnitt betitelt „Aktivität“activity.list
Abschnitt betitelt „activity.list“GET /xrpc/ch.atvoto.activity.list · Viewer optional
| Parameter | Pflicht | Beschreibung |
|---|---|---|
dossier |
ja | Dossier-ID |
filter |
all (Standard), comments (Kommentare und Antworten), arguments (neue Argumente und Meilensteine) |
|
limit |
Standard 30 | |
cursor |
aus der vorherigen Antwort |
Antwort {"activities": [Activity, …], "cursor": "…" | null}, neueste zuerst.
| Feld | Inhalt |
|---|---|
type |
new_argument, milestone (Peer Review abgeschlossen: angenommen oder abgelehnt), comment, reply |
activityUri, activityAt |
Kennung und Zeitpunkt; activityUri ist der Schlüssel für markSeen |
actor |
{did, displayName} |
argument |
{uri, rkey, title, body, type, likeCount, commentCount, peerreviewStatus} |
comment |
bei comment/reply: {uri, text, likeCount, replyCount} |
parent |
bei reply: der beantwortete Kommentar {uri, did, displayName, text, hasParent, likeCount, replyCount} |
viewer |
nur mit Token: argumentLike (AT-URI), seen: true |
activity.markSeen
Abschnitt betitelt „activity.markSeen“POST /xrpc/ch.atvoto.activity.markSeen · Viewer nötig
Body {"uris": ["<activityUri>", …]} → {"success": true}.
Dossiers
Abschnitt betitelt „Dossiers“dossier.getStats
Abschnitt betitelt „dossier.getStats“GET /xrpc/ch.atvoto.dossier.getStats · öffentlich
dossiers (wiederholt, höchstens 100) →
{"stats": {"sv-663": {"arguments": 12, "comments": 40}}}. Unbekannte Dossiers kommen
mit Nullen. Gezählt werden nur interne Kommentare.
Titel, Beschreibung und Abstimmungsdatum eines Dossiers hält der Client, nicht der Store.
dossier.search
Abschnitt betitelt „dossier.search“GET /xrpc/ch.atvoto.dossier.search · Viewer optional
| Parameter | Pflicht | Beschreibung |
|---|---|---|
dossier |
ja | Dossier-ID |
q |
ja | Suchtext |
type |
nur eine Gruppe: taxonomy, argument, comment |
|
limit |
pro Gruppe, 1–25, Standard 8 | |
lang |
Sprache |
Sucht nur in der angefragten Sprache (Original oder vorhandene Übersetzung).
{ "query": "…", "lang": "de-CH", "results": { "taxonomy": [{"type": "taxonomy", "dossierRkey": "sv-663", "topic": "<key>", "title": "…", "snippet": "…", "matchField": "name"}], "argument": [{"type": "argument", "rkey": "…", "uri": "…", "argType": "PRO", "title": "…", "snippet": "…", "matchField": "body"}], "comment": [{"type": "comment", "uri": "…", "argumentUri": "…", "title": "…", "snippet": "…", "matchField": "text"}] }, "counts": {"taxonomy": 1, "argument": 1, "comment": 1}}dossier.getPermissions
Abschnitt betitelt „dossier.getPermissions“GET /xrpc/ch.atvoto.dossier.getPermissions · Viewer nötig
dossier oder subject (AT-URI eines Arguments oder Kommentars; das Dossier wird
daraus bestimmt).
{ "dossier": "sv-663", "status": "open", "permissions": { "comment": {"allowed": true}, "rate": {"allowed": true}, "argument.submit": {"allowed": false, "reason": "assurance_too_low"}, "peerreview": {"allowed": true} }}Es ist dieselbe Prüfung wie in der Annahmeschleuse (registry.participation_check),
damit der Client nur anbietet, was erlaubt ist. Verbindlich bleibt die Schleuse.
Ausserhalb der Phase open ist nichts erlaubt, ausser Bewerten in closed; reason
ist dann dossier_not_open, dossier_paused, dossier_closed oder
dossier_withdrawn. Weitere Gründe kommen aus den Teilnahmeregeln (z.B. not_member,
assurance_too_low), siehe Konten und Teilnahme.
quota.get
Abschnitt betitelt „quota.get“GET /xrpc/ch.atvoto.quota.get · Viewer nötig
dossier → Verbrauch und Limits der Person für neue Argumente und Kommentare.
Gelöschte Beiträge geben kein Kontingent zurück.
{ "arguments": {"dailyUsed": 1, "dailyLimit": 3, "dossierUsed": 2, "dossierLimit": 10}, "comments": {"dailyUsed": 0, "dailyLimit": 20, "dossierUsed": 5, "dossierLimit": 100}}daily = letzte 24 Stunden, dossier = insgesamt in diesem Dossier. Die Werte im
Beispiel sind erfunden; die Limits sind Konfiguration.
Themenbaum
Abschnitt betitelt „Themenbaum“taxonomy.get
Abschnitt betitelt „taxonomy.get“GET /xrpc/ch.atvoto.taxonomy.get · Viewer optional
| Parameter | Pflicht | Beschreibung |
|---|---|---|
dossier |
ja | Dossier-ID |
topic |
key eines Themas: zeigt dessen Unterthemen statt der Hauptthemen |
|
shape |
full: ganzer verschachtelter Baum ohne Argumentlisten (für Sunburst-Grafiken) |
|
lang |
Sprache |
Ohne shape kommt der Basisknoten mit genau einer Ebene Kindern; jedes Kind
sammelt die Argumente seines ganzen Teilbaums.
{ "dossierRkey": "sv-663", "breadcrumb": [{"name": "…", "key": "…", "description": "…"}], "tree": { "id": 1, "key": "…", "name": "…", "introduction": "…", "description": "…", "depth": 0, "importance": 0.5, "argumentCount": 34, "availableLangs": ["de-CH"], "arguments": [], "children": [ {"key": "…", "name": "…", "argumentCount": 8, "arguments": [{"uri": "…", "cid": "…", "rkey": "…", "title": "…", "type": "PRO", "sourceType": "user", "likeCount": 3, "viewerPreference": 70, "availableLangs": ["de-CH"]}], "children": []} ] }}introduction ist für Abstimmende gedacht, description intern (Klassifikation). Mit
Token trägt jeder Knoten zusätzlich proLeaning (−1 bis 1): die Neigung der Person
für oder gegen die Vorlage, gewichtet nach ihren Bewertungen. 404, wenn das Dossier
keinen Themenbaum hat. Herkunft der Daten: Snapshot ch.atvoto.taxonomy.snapshot, den
der Client über den Registrar veröffentlicht.
Peer Review
Abschnitt betitelt „Peer Review“Ablauf, Zustände und Parameter: Peer Review. Ein
Gutachten hat die Zustände open → provisional_closed (Quorum erreicht, Nachfrist
läuft) → closed. Das Gutachten selbst ist keine AppView-Methode: Der Client
schreibt ch.atvoto.peerreview.response ins Repo der Person.
Typische Reihenfolge im Client: pending → status → checkIn → während des
Schreibens activity → Record peerreview.response schreiben.
peerreview.criteria
Abschnitt betitelt „peerreview.criteria“GET /xrpc/ch.atvoto.peerreview.criteria · Viewer optional
{"criteria": [{"key": "coherence", "label": "Stimmigkeit"}, {"key": "tone", …}, {"key": "topic", …}, {"key": "unity", …}, {"key": "non_duplication", "label": "Kein Duplikat"}]}. Siehe die fünf Kriterien.
peerreview.list
Abschnitt betitelt „peerreview.list“GET /xrpc/ch.atvoto.peerreview.list
| Parameter | Pflicht | Beschreibung |
|---|---|---|
dossier |
ja | Dossier-ID |
scope |
mine (Standard, Token nötig): nur Gutachten, bei denen die Person eingeladen ist, geantwortet hat oder Autor:in ist. all: alle (Transparenz) |
Antwort {"reviews": [...]}, laufende zuerst, dann geschlossene, je neueste zuerst.
Je Eintrag: argumentUri, title, body, type, authorDid, state,
peerreviewStatus, quorum, approvals, rejections, totalReviews,
invitationCount, openedAt, provisionalClosedAt, graceUntil, closedAt sowie
viewerInvited, viewerCheckedInAt, viewerResponded, viewerIsAuthor.
peerreview.pending
Abschnitt betitelt „peerreview.pending“GET /xrpc/ch.atvoto.peerreview.pending · Viewer nötig
Einladungen, die die Person noch annehmen kann: aktiv eingeladen, Gutachten open,
noch nicht geantwortet.
{"invitations": [{"invitationUri": "at://…", "argumentUri": "at://…", "invitedAt": "…", "argument": {"title": "…", "body": "…", "type": "CONTRA", "dossierUri": "at://…", "dossierRkey": "sv-663", "authorDid": "did:plc:…"}}]}peerreview.status
Abschnitt betitelt „peerreview.status“GET /xrpc/ch.atvoto.peerreview.status · Viewer nötig
argumentUri → Stand eines Gutachtens: state, quorum, peerreviewStatus,
approvals, rejections, totalReviews, invitationCount, provisionalClosedAt,
graceUntil, closedAt, criteriaBreakdown ([{key, label, ok, flagged}], Zahl der
Ok- und Beanstandungs-Stimmen je Kriterium) sowie für die Person checkedInAt und
lastActivityAt. Ist die Person Autor:in des Arguments, kommen die einzelnen
Gutachten dazu: reviews: [{reviewerDid, criteria, vote, createdAt}].
peerreview.duplicateCandidate
Abschnitt betitelt „peerreview.duplicateCandidate“GET /xrpc/ch.atvoto.peerreview.duplicateCandidate · Viewer nötig · 20/min
argumentUri → das ähnlichste andere Argument gleicher Position im selben Dossier,
falls es über der Anzeige-Schwelle liegt: {"status": "ok", "items": [...]} (0 oder 1
Eintrag). Nur wenn ein Kandidat kommt, zeigt der Client das Kriterium „Kein Duplikat“.
{"status": "unavailable"} bei technischen Problemen; das Gutachten wird nie
blockiert.
peerreview.checkIn
Abschnitt betitelt „peerreview.checkIn“POST /xrpc/ch.atvoto.peerreview.checkIn · Viewer nötig
Body {"argumentUri": "at://…"}. Belegt einen Platz und ist vor dem Einreichen
nötig. → {"state", "quorum", "graceUntil", …}.
| Fehler | Status |
|---|---|
not_found (kein Gutachten) |
404 |
not_invited |
403 |
closed |
409 |
too_late: Quorum erreicht, keine neuen Gutachter:innen mehr. Wer schon eingecheckt ist, bekommt den aktuellen Stand. |
409 |
peerreview.activity
Abschnitt betitelt „peerreview.activity“POST /xrpc/ch.atvoto.peerreview.activity · Viewer nötig
Body {"argumentUri": "at://…"}. Der Client meldet echtes Tippen (gedrosselt). In
provisional_closed verlängert das die Nachfrist, damit niemand mitten im Satz
abgeschnitten wird. → {"state", "graceUntil"} (graceUntil = null in open).
Fehler: 404 not_found, 409 closed, 403 not_checked_in.
Bluesky-kompatibel
Abschnitt betitelt „Bluesky-kompatibel“| Methode | Verhalten |
|---|---|
app.bsky.feed.describeFeedGenerator |
beschreibt den atvoto-Feed |
app.bsky.feed.getFeedSkeleton |
feed (URI des atvoto-Feeds), limit 1–100 (Standard 50), cursor: Crossposts von Argumenten und Kommentaren, neueste zuerst |
app.bsky.actor.getProfile, getProfiles, searchActors, getPreferences |
an die Bluesky-AppView weitergereicht |
app.bsky.feed.getTimeline, getAuthorFeed, getPostThread, getPosts, searchPosts, getLikes |
an die Bluesky-AppView weitergereicht |
Hintergrund: Bluesky-Anbindung.
GET /xrpc/com.atproto.label.queryLabels · öffentlich · 60/min
Die Moderations-Labels des Stores, so wie der Registrar sie signiert hat, inklusive
Rücknahmen (neg: true). Andere Dienste können sie übernehmen und die Signatur
prüfen.
| Parameter | Pflicht | Beschreibung |
|---|---|---|
uriPatterns |
ja | wiederholt; exakte AT-URI oder Präfix mit * am Ende (at://did:plc:…/*) |
sources |
DIDs der Labeler; ohne den atvoto-Labeler ist die Antwort leer | |
limit |
1–250, Standard 50 | |
cursor |
aus der vorherigen Antwort |
Antwort {"labels": [{ver, src, uri, cid?, val, neg?, cts, sig}], "cursor"?}.
Konzept: Moderation.
DID-Dokument und Health
Abschnitt betitelt „DID-Dokument und Health“GET /.well-known/did.json: DID-Dokument der AppView (did:web) mit den Services#atvoto_appview(für denatproto-proxy-Header) und#bsky_fg(Feed-Generator).GET /healthz:{"status": "ok"}oder 503, wenn die Datenbank nicht erreichbar ist.