Die AppView hatte keinerlei Authentifizierung: jeder konnte /api/notifications?did=<beliebig> lesen und per /seen als gelesen markieren. Mit Phase 8 sind das die ersten privaten Daten im System. Das Access-JWT der PDS trug von Anfang an sub, scope "com.atproto.access" und aud "did:web:appview…" — es war für die AppView ausgestellt, nur hat sie es nie geprüft. Neu ist deshalb vor allem die Schlüsselbeschaffung: auth.rs holt das DID-Dokument der PDS (PDS_INTERNAL_URL, sonst PDS_PUBLIC_URL), cached den Schlüssel und lädt ihn bei einem Verifikationsfehler nach — höchstens einmal pro Minute, damit Müll-Tokens kein Werkzeug werden, die PDS zu fluten. Ein Schlüsselwechsel braucht damit keinen Neustart. Ist die PDS beim Start weg, warnt die AppView nur und startet trotzdem (sie indiziert den Firehose, der von der lokalen PDS unabhängig ist). Ist der Schlüssel beim Prüfen eines Tokens nicht zu beschaffen, gibt es 503 — fail closed. Geschützt: /api/timeline/home und die drei Notification-Endpoints, jeweils mit sub == did. Öffentlich bleiben Profile, Suche, Posts, Threads und die Follower-Listen; das sind in AT Proto öffentliche Records. 401 AuthMissing / 401 TokenInvalid / 403 Forbidden / 503 AuthUnavailable. TokenInvalid ist ein Vertrag mit dem Client: daran erkennt er, dass er sein Token erneuern und einmal wiederholen muss. Dazu CORS: statt Any für alles jetzt eine Allowlist über APPVIEW_CORS_ORIGINS (unset = altes Verhalten plus Warnung), und /internal/ingest-commit liegt außerhalb der CORS-Schicht — die Route wird server-zu-server aufgerufen, ein Allow-Origin darauf würde nur einer Webseite helfen, in den Index zu schreiben. APPVIEW_AUTH_REQUIRED=false stellt das alte Verhalten her (VPN-Instanz, fail-open-Tests) und warnt beim Start in Großbuchstaben. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
maarcadetweet
AT-Protocol-PDS in Rust + AppView + Tauri/Svelte-Desktop-Client.
Posts sind auf 160 Zeichen limitiert (oldschool Twitter), erzwungen durch eigenes Lexicon app.twi.post.
Architektur
crates/
├── at-lexicon/ Lexicon-Schemas + 160-Char-Validierung
├── at-crypto/ k256, p256, CID, multibase, JWT, PLC-Ops, Repo-Signing
├── at-identity/ DID, PLC, Handle-Resolution
├── at-mst/ Merkle-Search-Tree
├── at-repo/ Repos, Commits, Blöcke, TID-Revs
├── at-blob/ S3-kompatibler Blob-Store (MinIO) + In-Memory-Store
├── at-firehose/ Jetstream-Consumer (WebSocket)
├── at-shared/ Config, Errors, DID, Cursor
├── pds-server/ axum HTTP PDS (bin)
└── appview/ Jetstream-Indexer + REST-API (bin)
crates/tauri-app/ Tauri 2 + Svelte 5 + Vite + TS Desktop-Client
├── src/ Svelte-Components (Terminal, StatusBar, NavRail, PostCard, ComposeBox,
│ LoginScreen, ProfileView, NotificationsView)
├── src/lib/styles/ tokens.css (1:1 vom maarcade-Design)
└── src-tauri/ Rust-IPC-Layer
lexicons/app/twi/post.json Custom Lexicon mit maxLength: 160
migrations/pds/ PDS-DB-Schema (users, repos, blobs, sessions, plc_ops)
migrations/appview/ AppView-DB-Schema (posts, likes, follows, notifications, profiles, jetstream_cursor)
docs/ Deployment, Architektur, Tauri-Release (siehe unten)
Setup
# 1) Datenbanken + MinIO starten
docker compose up -d
# 2) Umgebungsvariablen — beide Binaries laden .env selbst (dotenvy).
# PDS_JWT_SECRET muss Hex sein: openssl rand -hex 32
cp .env.example .env
# 3) Workspace kompilieren + Tests
cargo test --workspace
cargo check --workspace
# 4) Tauri-Frontend (Vite dev)
cd crates/tauri-app
npm install
npm run dev
# → http://127.0.0.1:1420
# 5) PDS / AppView (eigene Terminals)
cargo run -p pds-server
cargo run -p appview
Status
| Phase | Stand |
|---|---|
| 0 Foundation, Workspace, Migrations, Lexicon, Crypto | ✅ done |
| 1 Identity (PLC-Ops vollständig signieren) | ✅ done — did:plc: deterministisch aus signed op CID |
| 2 MST + Repo (Spec-konforme CBOR-Encoding) | ✅ done — encode_key = base64url(sha256(raw_key)) per atproto-Spec, split_around/wrap_with_split threaden den recursive right_sub korrekt als k_tree weiter. 27 MST + 13 Repo + 4 Commit Tests grün. |
| 3 PDS-Server (com.atproto.* XRPC) | ✅ done — createAccount/Session/Refresh, createRecord/deleteRecord, like/repost, follow |
| 4 AppView-Foundation (Jetstream-Index) | ✅ done — Jetstream-Indexer + identity-Event-Backfill + PLC-handle-sync-Worker |
| 5 AppView-REST-API | ✅ done — timeline, profile (by-did + by-handle), search, post-by-uri, thread, notifications, followers/following |
| 6 Tauri-UI-Logik an Backend koppeln | ✅ done — LoginScreen, NavRail, PostCard, ComposeBox, Profile/Compose/Search/Settings-Views |
| 7 Polish (Tray, Notifications, Auto-Update) | ✅ done — Tray-Icon custom (tauri::include_image!), Notification-Click navigiert via app://notification-Event + openThread-Helper zu Thread-Detail, Auto-Update in Dev inert (Production-Weg: docs/tauri-release.md) |
| 8 Social-Graph + Benachrichtigungen | ✅ done — notifications-Tabelle, Schreibpfad im Jetstream-Indexer (idempotent, keine Selbst-Notifications), `/api/notifications[/count |
Tests
cargo test --workspace # Rust: PDS, AppView, at-* Crates
cd crates/tauri-app && npx vitest run # Svelte/TS
cd crates/tauri-app/src-tauri && cargo test # Tauri-IPC (eigener Workspace!)
Stand zuletzt gegen den lokalen Dev-Stack (docker compose + laufender PDS + AppView): Rust-Workspace grün (u.a. 27 MST, 24 PDS-Integration, 49 AppView-Lib, 14 AppView-Integration), Frontend grün. Zwei Vorbehalte:
- Die DB-gestützten Integrationstests sind fail-open — ohne erreichbare Postgres/PDS überspringen sie sich selbst und melden das nur auf stderr. Ein grüner Lauf ohne laufenden Stack sagt also weniger, als er aussieht.
- Einige Tests hängen am Zustand der Dev-Datenbank; auf einer frischen DB können
handle_sync-Tests abweichen. Wer sie ernst nimmt, prüft sie gegen eine definierte DB.
crates/tauri-app/src-tauri hat ein eigenes [workspace] und ist nicht Teil des
Root-Workspace; cargo test --workspace von oben erfasst den IPC-Layer nicht.
Dokumentation
| Datei | Inhalt |
|---|---|
docs/architecture.md |
Crate-Verantwortlichkeiten, Datenfluss PDS → Jetstream → AppView → Client, Tabellenübersicht |
docs/deployment.md |
Betrieb: docker compose, Migrationen, alle Env-Variablen, Release-Build, systemd-Units, Reverse-Proxy, Health-Checks |
docs/tauri-release.md |
Signing-Keys, Updater-Config, latest.json, Build pro Plattform, Artefaktpfade |
Bekannte Lücken
- Die eigene PDS speist keinen Firehose (
com.atproto.sync.subscribeReposfehlt) — eigene Records erreichen die AppView nur über den Best-Effort-PushPOST /internal/ingest-commit. - Die AppView-Leseschnittstelle hat keine Auth und CORS
Any; bei/api/notificationssind das erstmals halbwegs private Daten. - Notifications werden nie gelöscht: Unlike/Unfollow lässt die Zeile stehen, und der Dedupe-Key macht sie „einmal pro (Empfänger, Autor, Art, Subject) für immer".
- Auto-Update ist nur dokumentiert, nicht verdrahtet: niemand ruft
check()auf, das Updater-Plugin ist nicht installiert (siehedocs/tauri-release.md). - Reply-Notifications gehen verloren, wenn die Antwort vor ihrem Parent indiziert wird (kein Nachlauf) — bei Jetstream möglich.
at-blobspricht MinIO ohne Signature V4 — echtes AWS S3 funktioniert damit nicht.- Die PDS liefert kein
.well-known/did.json;describeServergibt die DID hart alsdid:web:pds.maarcadetweet.localzurück.
Design
Orange Akzent, IBM Plex Mono, schwarzer Hintergrund mit 3%-Grid, Terminal-Fenster-Component mit blinkendem Cursor. Tokens sind 1:1 von maarcade-shell/landing/assets/css/tokens.css abgeleitet, plus zwei neue Repos-Tokens (--cid-fg, --rev-fg).