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
1. Überblick
Abschnitt betitelt „1. Überblick“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.
2. Aufgabenteilung
Abschnitt betitelt „2. Aufgabenteilung“| 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) |
3. Store
Abschnitt betitelt „3. Store“| 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).
4. Client smartinfo
Abschnitt betitelt „4. Client smartinfo“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).
5. Schnittstellen
Abschnitt betitelt „5. Schnittstellen“Der Client spricht den Store nur über diese Wege an. Keine geteilte DB, keine geteilten Secrets.
5.1 Registrar (Admin-API)
Abschnitt betitelt „5.1 Registrar (Admin-API)“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).
5.2 AppView
Abschnitt betitelt „5.2 AppView“Ö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 |
5.3 PDS
Abschnitt betitelt „5.3 PDS“- Session:
createSession/refreshSessionmit 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).
5.4 Store → Client
Abschnitt betitelt „5.4 Store → Client“| 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).
5.5 Gemeinsame Verträge
Abschnitt betitelt „5.5 Gemeinsame Verträge“| 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, …) |
6. Sicherheit
Abschnitt betitelt „6. Sicherheit“6.1 Vertrauensgrenzen
Abschnitt betitelt „6.1 Vertrauensgrenzen“- 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.
6.2 Authentisierung
Abschnitt betitelt „6.2 Authentisierung“| 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 |
6.3 Secrets
Abschnitt betitelt „6.3 Secrets“| 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.
6.4 Exposition
Abschnitt betitelt „6.4 Exposition“- Ö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.*undcreateInviteCodedes PDS (pds-admin-block.yaml). - Nur intern: Indexer, Writer (inkl.
/internal/embed), Postgres. - prod:
ipAllowListmit den Egress-IPs des Clients vor dem Registrar (PRODUCTION_DEPLOYMENT.md, Checkliste); NetworkPolicies zwischen Client- und Store-Namespaces.
6.5 Datenschutz
Abschnitt betitelt „6.5 Datenschutz“- 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.findSimilarschickt 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.
6.6 Bekannte Schwächen
Abschnitt betitelt „6.6 Bekannte Schwächen“- 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
createAccountpro 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.
7. Datenbank
Abschnitt betitelt „7. Datenbank“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.
8. Umgebungen und Deployment
Abschnitt betitelt „8. Umgebungen und Deployment“| 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
newTagim 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.
9. Domains
Abschnitt betitelt „9. Domains“| 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.
10. Sichtbarkeit der Store-Domain
Abschnitt betitelt „10. Sichtbarkeit der Store-Domain“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:
- in DID-Dokumenten (
serviceEndpoint https://pds.atvoto.ch) und at://-URIs, - auf der Profilseite, falls der Client PDS und Handle zeigt (Portabilität),
- in der Datenschutzerklärung (Pflicht): öffentliche Beiträge liegen dauerhaft in einem öffentlichen ATProto-Store, betrieben von atvoto,
- 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.
11. Entscheidungen
Abschnitt betitelt „11. Entscheidungen“| # | 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).
12. Offene Fragen
Abschnitt betitelt „12. Offene Fragen“- Konto-Löschung: Grundsatz entschieden (Art. 16a, PDS-Betreiber löscht). Offen: Umsetzung und Einzelfragen in ACCOUNT_DELETION.md.
- Moderation: Haftung, Impressum, Melde-Knopf im Client, Umsetzung des Ausblendens (MODERATION.md).
- Zulassung von Clients (ROADMAP Stufe 2): wer entscheidet, mit welchem Vertrag.
- Vertrauen pro Client: Rate-Limits oder Kontingente für
createAccount; vorerst nur Key sperren. - App-Passwörter: Rotation über den Registrar (ROADMAP Stufe 1); bis dahin von Hand. Einstellung des Betriebs: Art. 16b.
- Benachrichtigungen: ✅ keine eigene API (E24). Offen, ob später ein Endpoint für den Stand der Einreichungen nötig ist (Art. 6).
- Pseudonym-Profil: eigenes Lexicon für die Pseudonym-Felder des Clients oder
nur
displayName? - Änderung der Verfassung (Art. 50).