Zum Inhalt springen

Architektur: Store (atvoto) und Client (smartinfo)

Stand: 2026-10-26. Der Store läuft im Test-Cluster (atvoto-test). poltr ist in zwei Projekte aufgeteilt: atvoto (Store, dieses Repo) und den Client (smartinfo, eigenes Repo). Beide gehören demselben Betreiber, die Trennung ist nur technisch. Regeln: VERFASSUNG (smartinfo), VO Konten und Teilnahme. Föderation: ROADMAP.md.

Inhalt: 1 Überblick · 2 Aufgabenteilung · 3 Store · 4 Client smartinfo · 5 Schnittstellen · 6 Sicherheit · 7 Datenbank · 8 Umgebungen · 9 Domains · 10 Sichtbarkeit · 11 Entscheidungen · 12 Offene Fragen


atvoto ist das öffentliche, neutrale Gedächtnis der Abstimmungsdebatte: Argumente, Kommentare, Bewertungen und Gutachten als signierte ATProto-Records, dazu die Identitäten (DIDs, pseudonyme Handles, keine E-Mail-Adressen). Clients wie smartvote bauen darauf eine Anwendung: Login, Redaktion, Oberfläche.

Leitregel für jede Zuordnung:

Was die Integrität und den Nutzen des Stores für alle Clients sichert → Store. Was diesem Client und seinen Nutzern dient → Client.

Kurzform: Das Verfahren läuft im Store, die Zulassung zum Verfahren im Client.

vote.smartvote.ch
┌──────────────────────────────────────────────────────────────────────┐
│ CLIENT smartinfo (eigenes Repo und eigene Namespaces) │
│ │
│ frontend (Next.js) Oberfläche, dünner BFF, host-only Cookies │
│ client-api (Python) Magic Link, Sessions, E-Mail, App-Passwörter, │
│ Schreiben ins User-Repo, Chatbot │
│ cms (Payload) Vorlagen, offizielle Argumente, Taxonomie │
└──────┬─────────────────────┬──────────────────────┬──────────────────┘
│ Schreiben │ Lesen │ Verwaltung
│ (User-Session) │ (öffentlich oder │ (Client-Key)
│ │ Service-Auth) │
▼ ▼ ▼
┌──────────────────────────────────────────────────────────────────────┐
│ STORE atvoto (dieses Repo, Namespace atvoto-test / später prod) │
│ │
│ pds Speicher: User-Repos, Community-Repos │
│ appview öffentliche Lese-API + Viewer-State │
│ registrar Admin-API für Clients │
│ indexer Firehose → Postgres │
│ writer Annahmeschleuse, Peer Review, Übersetzung, Embeddings, │
│ Crosspost │
│ lexicons Schemas ch.atvoto.* │
└──────────────────────────────────────────────────────────────────────┘

Wie in ATProto üblich: geschrieben wird in den PDS, gelesen über die AppView. Der Browser spricht nie direkt mit dem Store; alle Aufrufe laufen über das Client-Backend.


Bereich Client (smartinfo) Store (atvoto)
Konten Login per Magic Link, E-Mail-Adresse, Pseudonym vorschlagen, Prüfstufe melden (email), App-Passwort verschlüsselt halten PDS-Konto anlegen (createAccount), Handle unter der Domain des Clients, Mitgliedschaft im Register
Mitgliedschaft bürgt für seine Mitglieder, widerruft sie (Art. 10, 16) setzt Widerruf durch; Konto kann Mitglied mehrerer Clients sein
Teilnahmeregeln legt fest, wer in seinen Vorlagen was darf (Art. 17a, VO Konten und Teilnahme § 3) speichert, protokolliert und setzt durch (participation_check)
Vorlagen eröffnet sie, Fachverantwortung für die offiziellen Abstimmungen, Redaktion, offizielle Argumente, Themenbaum berechnen und kuratieren (Art. 18) Community-Konto, Phase (draft/open/paused/closed/withdrawn), Metadaten, Themenbaum als Snapshot speichern und anzeigen
Beiträge Record bauen und ins Repo der Person schreiben; vorher Rechte, Kontingent und Vorprüfung abfragen (UX) Annahmeschleuse (autoritativ): Regeln, Phase, Kontingente; kanonische Kopie im Community-Konto
Peer Review Einladungen anzeigen, Gutachten schreiben, Anfrage auslösen Los, Einladungen, Quorum, Ergebnis (peerreview.result)
Moderation Melde-Knopf, Meldestelle, Benachrichtigungen, Warteschlange und Entscheid im CMS Labels, Widerruf und Takedown im Registrar, Ausblenden im Read-Model, öffentliche Abfrage der Labels (MODERATION.md)
Löschung nimmt den Antrag entgegen, weist beim Einreichen auf die Abtretung hin (Art. 16a Abs. 3) Betreiber löscht das Konto, Indexer zieht nach (ACCOUNT_DELETION.md)
Benachrichtigung E-Mails, Hinweise in der App keine eigene API; Ereignisse sind Records im Firehose
KI eigene LLM-Anbindung: Einschätzung von Entwürfen (Position, Thema, Ton), Themenbaum, Chatbot nur Neutrales, im Writer: Übersetzungen, Embeddings der Argumente; darauf argument.findSimilar und das Duplikat im Peer Review
Datenschutz Datenschutzerklärung, Impressum, E-Mail-Daten keine E-Mail-Adressen, pseudonyme Konten, Datensparsamkeit (Art. 7)

Dienst Aufgabe Erreichbar
pds Repos der Teilnehmenden und der Community-Konten (Bluesky-PDS) öffentlich (pds.…, Handles *.<handle-domain>); Admin-Pfade am Ingress gesperrt
appview Lese-API, Viewer-State, Feed-Generator, did:web öffentlich (api.…)
registrar Admin-API für Clients (§5.1); hält PDS-Admin-Passwort und Community-Master-Key öffentlich nur /xrpc/ch.atvoto.registrar.* (registrar.…)
indexer Firehose des eigenen PDS → Postgres; Gate für Kommentare und Bewertungen; Bluesky-Poller (Flag) intern
writer Annahmeschleuse, Peer-Review-Zuteilung und -Abschluss, Übersetzungen, Embeddings, Crosspost (Flag) intern; /internal/embed für die AppView (Service writer)
postgres eine DB atvoto, Schemas public und registry (§7) intern

Lexicons ch.atvoto.* liegen in lexicons/ und sind veröffentlicht (LEXICONS.md).


Eigenes Projekt, aus den Client-Teilen von poltr hervorgegangen. Eigenes Repo, eigene Namespaces (vote-test, vote), eigene Datenbank.

Komponente Aufgabe Herkunft in poltr
frontend (Next.js) Oberfläche unter vote.smartvote.ch, dünner BFF, host-only Cookies services/frontend; Proxies zeigen auf client-api statt AppView
client-api (Python) Magic Link, Sessions, E-Mail mit smartinfo-Absender, Pseudonym-Generator, App-Passwörter (verschlüsselt, eigener Master-Key), Schreiben ins User-Repo, peerreview.request bei aktiver Session, Aufrufe an AppView und Registrar, Chatbot aus services/appview: auth/*, routes/auth, core/email_service.py, User-Teil von pds_creds.py/atproto_api.py; Schreib-Teile von arguments.create, comment.create, rating, peerreview.submit
cms (Payload) Vorlagen, offizielle Argumente, Taxonomie-Editor, Moderation als Plugin; Publish-Hook ruft createDossier/updateDossier (mit voteRef) services/cms; ohne Master-Key, ohne PDS-Zugang, ohne Store-Tabellen
Client-DB Login-Daten, Sessions, verschlüsselte App-Passwörter, Pseudonym-Vorlagen, Payload Schema auth und CMS-DB aus poltr

Registrierung: E-Mail bestätigen (Magic Link) → createAccount(handle, displayName, assurance="email") → App-Passwort verschlüsselt speichern → Pseudonym als Profil schreiben.

Beitrag: dossier.getPermissions und quota.get abfragen, bei Argumenten die eigene Vorprüfung (z. B. mit argument.findSimilar) → Record mit der PDS-Session ins eigene Repo → Ergebnis kommt über den Firehose (kanonische Kopie, später peerreview.result).

Session-Schicht: Der Zugriff auf den PDS liegt hinter einer kleinen Schnittstelle (DID + „schreibe ins Repo“). Dahinter steckt heute das App-Passwort, später zusätzlich OAuth für externe Konten (ROADMAP Stufe 1).


Der Client spricht den Store nur über diese Wege an. Keine geteilte DB, keine geteilten Secrets.

Eigener Dienst mit eigener DB-Rolle (E19). POST/GET /xrpc/ch.atvoto.registrar.*, Authorization: Bearer <Client-Key>. Alles gehört dem aufrufenden Client: seine Mitglieder, seine Vorlagen, seine Regeln.

Methode Zweck
createAccount(handle, displayName, description?, assurance?) → {did, handle, appPassword} PDS-Konto + Profil + App-Passwort, Mitgliedschaft (origin=hosted, Standard assurance=email). Handle-Teil vom Client, Domain aus registry.clients.handle_domain; Präfixe dossier-/atvoto- reserviert
revokeParticipant(did) eigene Mitgliedschaft widerrufen; künftig nur mit Moderations-Key (MODERATION.md)
reinstateParticipant(did), moderation.* geplant, nur mit Moderations-Key: Widerruf aufheben, Daten zum Fall, Labels, Takedown, Restore, Löschen (MODERATION.md)
updateParticipant(did, assurance) Prüfstufe eines eigenen Mitglieds ändern
getParticipationPolicy() / setParticipationPolicy(policy) Teilnahmeregeln der eigenen Vorlagen, jede Änderung protokolliert (NEW_CLIENT.md §4)
getPeerReviewSettings() / setPeerReviewSettings(settings) Peer Review der eigenen Vorlagen ein/aus und Quorum, jede Änderung protokolliert (NEW_CLIENT.md §5)
createDossier(dossier, title, description?, lang?, langs?, voteDate?, voteRef?) Community-Konto + Metadaten, Phase draft; dossier beginnt mit dem Präfix des Clients (sv-663); langs = Sprachen, in die der Store übersetzt (leer = alle)
updateDossier(dossier, status?, title?, description?, lang?, langs?, voteDate?, voteRef?) / getDossier(dossier) Metadaten, Profil des Community-Kontos, Phase
putOfficialArgument(…) / deleteOfficialArgument(dossier, key) offizielle Argumente direkt ins Community-Repo (rkey official-<key>), ohne Annahmeschleuse
taxonomy.getSnapshot(dossier) letzter veröffentlichter Themenbaum (Ausgangspunkt des Clients zum Bearbeiten)
taxonomy.publishSnapshot(dossier, tree, model?) Baum als ch.atvoto.taxonomy.snapshot ins Community-Repo (append-only, Kette über prev)

Phasen einer Vorlage:

Phase Argumente, Gutachten (Writer) Kommentare (Indexer) Bewertungen (Indexer) AppView
draft abgelehnt dossier_not_open verworfen verworfen sichtbar
open angenommen (nach Regeln, Kontingent) übernommen (nach Regeln) übernommen (nach Regeln) sichtbar
paused zurückgehalten zurückgehalten zurückgehalten sichtbar
closed (archiviert) zurückgehalten abgelehnt dossier_closed übernommen (nach Regeln) sichtbar
withdrawn (zurückgezogen) zurückgehalten zurückgehalten zurückgehalten ausgeblendet

Zurückgehalten heisst: nichts geht verloren. Argumente und Gutachten bleiben in app_acceptance_queue pending, Kommentare und Bewertungen liegen in app_held_contributions; verarbeitet werden sie, sobald die Phase sie zulässt (Wieder öffnen; zurückgehaltene Bewertungen schon bei closed). Eine archivierte Vorlage hält beim Indexer nichts zurück: nur aktive Vorlagen werden moderiert. Peer Reviews ruhen ausserhalb von open: keine neuen Einladungen, keine Abschlüsse, offene Aufgaben werden nicht angezeigt. dossier.getPermissions erlaubt ausserhalb von open nur das Bewerten einer archivierten Vorlage. withdrawn ist die Notbremse statt Löschen: Die Vorlage ist in der AppView nirgends sichtbar (Argumente, Kommentare, Themenbaum, Suche, Feed), die Daten bleiben (Art. 16a Abs. 3).

Eine Vorlage ist erst offen, wenn ihr Community-Konto existiert: createDossier startet als draft, geöffnet wird mit updateDossier(status=open).

Nicht im Registrar: Konto löschen (macht der PDS-Betreiber, Art. 16a), handle_domain setzen (macht der Betreiber, scripts/create_client.sh).

Öffentlich unter api.…. Mit Viewer-Bezug über Service-Auth: Der Client ruft über den PDS auf (atproto-proxy: did:web:api.…#atvoto_appview), der PDS stellt ein JWT aus (iss = User-DID, aud = AppView-DID, lxm = Methode), die AppView prüft es gegen das DID-Dokument der Person.

Art Methoden (ch.atvoto.*)
öffentlich, Viewer optional argument.list/get, comment.list/get, activity.list, dossier.search, dossier.getStats, taxonomy.get, peerreview.list, peerreview.criteria
Viewer nötig, lesend dossier.getPermissions (mit dossier oder subject = Argument/Kommentar), quota.get, peerreview.pending, peerreview.status, peerreview.duplicateCandidate
Viewer nötig, schreibt Viewer-State activity.markSeen, peerreview.checkIn, peerreview.activity
Ähnliche Argumente argument.findSimilar: Entwurfstext geht an den Store (Embedding durch den Writer, beim LLM-Anbieter). Neutral, keine Einschätzung; Stance, Thema, Ton prüft der Client selbst
Bluesky-kompatibel app.bsky.actor.getProfile(s), Feed-Generator (describeFeedGenerator, getFeedSkeleton), weitere app.bsky.* werden an Bluesky weitergereicht
  • Session: createSession/refreshSession mit dem App-Passwort, das der Client verschlüsselt hält. Tokens bleiben im Client.
  • Schreiben ins Repo der Person: ch.atvoto.dossier.argument, ch.atvoto.dossier.comment, ch.atvoto.content.rating (create/delete), ch.atvoto.peerreview.response, ch.atvoto.peerreview.request, app.bsky.actor.profile.
  • Proxy zur AppView für Service-Auth (§5.2).
Weg Inhalt
Firehose bzw. AppView-Abfrage Ergebnis der Annahmeschleuse (kanonische Kopie im Community-Konto), Einladungen (peerreview.invitation), Ergebnis (peerreview.result), Taxonomie-Snapshots
Bluesky Crossposts der Argumente unter dem Community-Konto (Writer, per Flag; in test aus), siehe INTEROPERABILITY.md

Keine Webhooks, keine Benachrichtigungs-API (§12 Nr. 6). Gründe einer Ablehnung stehen vorerst nur in app_acceptance_queue.reason; der Client fängt vorher mit dossier.getPermissions ab (ROADMAP Stufe 0).

Vertrag Regel
Lexicons ch.atvoto.* im Store-Repo, veröffentlicht; nach dem Einfrieren nur additiv (Art. 48)
Dossier-ID (dossier_rkey) Schlüssel zwischen CMS und Store, Handle dossier-<id>; eindeutig pro Client: <dossier_prefix>-<id> (z.B. sv-663, Präfix in registry.clients, vom Betreiber gesetzt)
voteRef offizielle Vorlagen-ID; verbindet Dossiers verschiedener Clients
Sprachliste definiert der Store (ATVOTO_LANGUAGES), der Client übernimmt sie
Pseudonym/Profil nur app.bsky.actor.profile mit displayName; der Indexer projiziert ihn nach app_profiles, die AppView liefert ihn als author.displayName. Weitere Pseudonym-Felder (z.B. Kanton, Farbe) sind Darstellung des Clients und kommen nicht von der AppView; als Store-Daten bräuchten sie ein eigenes Lexicon (offen)
AppView-DID und Service-ID Konfiguration des Clients pro Umgebung
Client-Key Secret pro Client und Umgebung
Fehlercodes stabile Codes (not_member, assurance_too_low, dossier_closed, quota_daily, …)

  • Die Person kontrolliert ihr Repo. Sie kann am Client vorbei direkt in den PDS schreiben. Deshalb sind alle verbindlichen Prüfungen im Store: Writer (Argumente, Gutachten) und Indexer (Kommentare, Bewertungen) prüfen Regeln, Phase und Kontingente. Prüfungen im Client sind nur UX.
  • Der Store vertraut dem Client bei der Prüfstufe seiner Mitglieder (Art. 17). Ein Client kann nur seine eigenen Mitglieder, Vorlagen und Regeln ändern.
  • Der Client vertraut dem Store bei Integrität und Verfügbarkeit der Daten; er hält keine Kopie, aus der er Ergebnisse ableitet.
Weg Verfahren Widerruf
Client → Registrar Client-Key (Bearer), im Store nur SHA-256 (registry.clients.key_hash) revoked_at, sofort
CMS → Registrar (Moderation, geplant) eigener Moderations-Key, nur im CMS sofort
Viewer → AppView Service-Auth-JWT über den PDS; aud und lxm gebunden, PDS-Access-Tokens (at+jwt) abgelehnt kurzlebig, an die PDS-Session gebunden
Client → PDS (für die Person) App-Passwort client:<id>, Hauptpasswort beim Anlegen verworfen heute nur von Hand über PDS-Admin; Rotation über den Registrar: ROADMAP Stufe 1
Person → Client Magic Link an die E-Mail, Session im Client, host-only Cookies auf vote.smartvote.ch Sache des Clients
Store (nur im Store-Namespace) Client (nur im Client-Namespace)
PDS-Admin-Passwort (nur Registrar), Signaturschlüssel der Labels (nur Registrar), PLC-Rotation-Key, Community-Master-Key (Writer, Registrar), DB-Rollen-Passwörter, Infomaniak-Token (Writer) Client-Key, Moderations-Key (nur CMS), Master-Key für App-Passwörter, Session-Secret, Mail-Zugang, CMS-Secrets

Kein Secret liegt auf beiden Seiten. Test und prod haben getrennte Secrets.

  • Öffentlich: PDS, AppView, Registrar (nur /xrpc/ch.atvoto.registrar.*, Rate-Limit 10/s pro IP, Burst 30, Anfragen ≤ 2 MiB), Handle-Auflösung.
  • Am Ingress gesperrt: com.atproto.admin.* und createInviteCode des PDS (pds-admin-block.yaml).
  • Nur intern: Indexer, Writer (inkl. /internal/embed), Postgres.
  • prod: ipAllowList mit den Egress-IPs des Clients vor dem Registrar (PRODUCTION_DEPLOYMENT.md, Checkliste); NetworkPolicies zwischen Client- und Store-Namespaces.
  • Der Store kennt keine E-Mail-Adressen und keine Klarnamen.
  • Alle Beiträge sind öffentlich und dauerhaft (ATProto); das gehört in die Datenschutzerklärung des Clients (§10).
  • argument.findSimilar schickt unveröffentlichten Text an den Embedding-Anbieter (über den Writer). Was ein Client für seine eigene Vorprüfung an ein LLM schickt, gehört in seine Datenschutzerklärung.
  • Löschung: ACCOUNT_DELETION.md. Einstellung des Betriebs: Art. 16b.
  • Der Indexer darf die projizierten Tabellen ganz ändern; welche Zeilen ein Dienst ändert, begrenzt der Code (DB-Rollen: DB_ROLES.md, Abschnitt Grenzen).
  • Kein Kontingent für createAccount pro Client: Ein gestohlener Client-Key kann massenhaft Konten anlegen. Notbremse: Key sperren (§12 Nr. 4).
  • Registrar mit einem Replikat; die Sperre gegen parallele Snapshot-Veröffentlichung gilt pro Prozess.

Eine Postgres-Instanz pro Umgebung im Store-Namespace, Datenbank atvoto (db-setup.sql, Migrationen in db/migrations/).

Bereich Inhalt
public Projektion: app_arguments, app_comments, app_likes, app_peerreview*, app_taxonomy_*, app_embeddings, Viewer-State, Kontingent-Ledger, Annahme-Warteschlange, app_dossier_meta
registry clients (Key-Hash, Handle-Domain, Teilnahmeregeln), participation_policy_log, participants (Mitgliedschaften), community_accounts (verschlüsselte Passwörter), participation_check()
Rolle Rechte (kurz)
indexer public lesen/schreiben, registry nur Community-DIDs, participation_check
writer wie Indexer, dazu Community-Passwörter
appview public DML, Community-DIDs, participation_check
registrar Register, Community-Konten anlegen, app_dossier_meta, nur Spalte participation_policy der Clients

Der Client hat keine Rolle in dieser DB. Er hat eine eigene Datenbank in seinem Namespace.


Store Client
test Namespace atvoto-test (läuft) vote-test (geplant)
prod eigener Cluster (PRODUCTION_DEPLOYMENT.md) vote (geplant)
  • Test-Client ↔ Test-Store, prod ↔ prod, nie über Kreuz.
  • Manifeste mit Kustomize (deploy/base, deploy/overlays/test).
  • CI baut bei jedem Push alle Images (Tag = Commit-SHA); ausgerollt wird von Hand über newTag im Overlay (COMMANDS → Images, Deploy).
  • Store und Client releasen unabhängig; Schnittstellen ändern sich nur additiv.
  • Testumgebung: eigener PDS, kein Crosspost, kein Bluesky-Poller, nicht am Relay (höchstens temporär, E25); PDS-Reset erlaubt. Die Regeln aus FEDERATION.md gelten für prod.

Domain Verwendung
atvoto.ch (Store) pds.atvoto.ch, api.atvoto.ch (did:web:api.atvoto.ch), registrar.atvoto.ch; test unter *.test.atvoto.ch
smartvote.social (Handles von smartvote) <name>.smartvote.social, Community-Konten dossier-<id>.smartvote.social; test *.test.smartvote.social. Ganze Zone bei uns, Wildcard-Zertifikat per DNS-01
smartvote.ch (fremdes DNS) vote.smartvote.ch → Frontend; SPF/DKIM für den Mailversand

Handle-Domain pro Client (registry.clients.handle_domain, setzt der Betreiber): Ein weiterer Client bekommt eine eigene Domain. Der PDS muss jede Domain bedienen (PDS_SERVICE_HANDLE_DOMAINS, DNS, Zertifikat). Handles werden über https://<handle>/.well-known/atproto-did aufgelöst, die Daten liegen laut DID-Dokument auf pds.atvoto.ch.

Was an Namen klebt (vor prod endgültig): Lexicon-NSIDs ch.atvoto.* (in jedem Record), Service-DIDs (did:web:api.atvoto.ch), PDS-Hostname (Server-DID; ein Wechsel heisst Relay-Neuanfang), Handle-Domains (in Bluesky sichtbar). Übrige Hostnamen sind jederzeit änderbar.


Die Store-Domain ist öffentlich erreichbar (nötig für die Föderation), für Nutzer:innen aber kaum sichtbar: Sie sehen vote.smartvote.ch, Mails von smartvote und Pseudonyme. atvoto erscheint:

  1. in DID-Dokumenten (serviceEndpoint https://pds.atvoto.ch) und at://-URIs,
  2. auf der Profilseite, falls der Client PDS und Handle zeigt (Portabilität),
  3. in der Datenschutzerklärung (Pflicht): öffentliche Beiträge liegen dauerhaft in einem öffentlichen ATProto-Store, betrieben von atvoto,
  4. bei Medien, falls der Client Blobs direkt vom PDS lädt (dann besser über den Client ausliefern).

Crossposts laufen unter @dossier-<id>.smartvote.social, also unter der Marke des Clients.


# Entscheidung Begründung
E1 Login beim Client, nicht beim Store E-Mail-Beziehung und Datenschutzverantwortung beim Client; der Store kennt keine E-Mail-Adressen
E2 Host-only Cookies auf der Frontend-Domain keine Cookies an alle *.smartvote.ch
E3 Magic Link bleibt (passwortlos) gehört zum Client; die OAuth-Seite des PDS kennt keinen Magic Link
E5 Client-Backend nicht in Next.js sicherheitskritischer Code (Master-Key, Mail) gehört nicht in den Frontend-Prozess
E6 Viewer-Identität per Service-Auth über den PDS ATProto-Standard, funktioniert auch für weitere Clients und fremde PDS
E7 Viewer-State bleibt im Store steckt direkt in den Feed-Abfragen
E8 Ähnlichkeitssuche als Store-API, Einschätzungen beim Client Embeddings sind neutral und für alle gleich; ob ein Entwurf passt, beurteilt jeder Client anders (bis 2026-11: argument.precheck mit Stance-LLM im Store)
E9 Verbindliche Prüfungen nur im Store (Writer, Indexer) die Person kann am Client vorbei schreiben
E10 Client spricht den Store nur über XRPC an Master-Key und PDS-Admin bleiben im Store
E12 Labels sind ab dem Start signiert, mit eigenem Labeler-Konto auf did:plc; der Registrar signiert, die AppView liefert nur aus alle Labels sind von Anfang an prüfbar, und die öffentliche Lese-API hält keinen Schlüssel (E19)
E14 KI im Store nur im Writer, nur Neutrales (Übersetzungen, Embeddings) der Store bleibt schlank; Themenbaum und Einschätzungen sind Sache der Clients (Calculator 2026-11 abgebaut)
E15 Chatbot im Client, eigene LLM-Anbindung liest über die öffentliche API
E19 Admin-API als eigener Dienst registrar die öffentliche Lese-API hält keine Schreib-Secrets
E20 Community-Konten unter der Domain des Clients die Vorlage gehört sichtbar zum Client
E21 Moderation als Payload-Plugin, Labels als Modul im Registrar, kein Ozone; Payload erreicht den Store nur über den Registrar, mit eigenem Moderations-Key kleines Team, wenig Betrieb; Client und Store teilen kein internes Netz; Art. 43, VO Moderation; föderal ab ROADMAP Stufe 2
E22 Teilnahmeregeln und Handle-Domain pro Client, Mitgliedschaft (did, client_id) ROADMAP Stufe 0; Rechte folgen der Bürgschaft eines Clients
E23 Jeder Client arbeitet in eigenen Dossiers, verbunden über voteRef; Dossier-IDs tragen das Präfix des Clients (Migration 011) klare Hoheit über Redaktion und Taxonomie; zwei Clients zur selben Abstimmung kollidieren nicht
E24 Keine Benachrichtigungs-API; Ereignisse sind Records Clients lesen Firehose oder AppView
E25 Test-PDS nicht am Relay (höchstens temporär), Prod-PDS vorgesehen, Zeitpunkt offen der Test-PDS darf zurückgesetzt werden, was am Relay zu Throttling führt; ROADMAP Stufe R

Nicht mehr gültig: E4/E17/E18 (Umbau und Repo-Split aus dem poltr-Monorepo, durch das eigene atvoto-Repo erledigt), E11 (Handle vorlage-<BFS>, ersetzt durch dossier-<id>), E13 (in E3 aufgegangen), E16 (*.id.<domain>, ersetzt durch Handle-Domains pro Client).


  1. Konto-Löschung: Grundsatz entschieden (Art. 16a, PDS-Betreiber löscht). Offen: Umsetzung und Einzelfragen in ACCOUNT_DELETION.md.
  2. Moderation: Haftung, Impressum, Melde-Knopf im Client, Umsetzung des Ausblendens (MODERATION.md).
  3. Zulassung von Clients (ROADMAP Stufe 2): wer entscheidet, mit welchem Vertrag.
  4. Vertrauen pro Client: Rate-Limits oder Kontingente für createAccount; vorerst nur Key sperren.
  5. App-Passwörter: Rotation über den Registrar (ROADMAP Stufe 1); bis dahin von Hand. Einstellung des Betriebs: Art. 16b.
  6. Benachrichtigungen: ✅ keine eigene API (E24). Offen, ob später ein Endpoint für den Stand der Einreichungen nötig ist (Art. 6).
  7. Pseudonym-Profil: eigenes Lexicon für die Pseudonym-Felder des Clients oder nur displayName?
  8. Änderung der Verfassung (Art. 50).