tomdeboneandClaude Opus 5 0646fbeebe feat(pds): com.atproto.sync.subscribeRepos — lokaler Firehose
Bisher erreichten eigene Records die AppView nur über den Best-Effort-Push
/internal/ingest-commit. Ging der verloren (AppView kurz weg, Netzwerk-
fehler), war der Post dauerhaft weg: der öffentliche Jetstream kennt diese
PDS nicht, es gab also keinen zweiten Weg.

Jeder Commit schreibt sein Event in derselben Transaktion nach
firehose_events. Damit kann es keinen Commit ohne Event geben — und keine
Sequenz ohne Commit.

Die seq muss lückenfrei sein, sonst ist sie als Cursor wertlos: BIGSERIAL
vergibt Nummern bei INSERT, nicht bei COMMIT, also können zwei Schreiber 5
und 6 ziehen und in umgekehrter Reihenfolge sichtbar werden — ein Leser
dazwischen sieht 6, merkt sich das und erfährt von 5 nie. Ein globaler
pg_advisory_xact_lock unmittelbar vor dem INSERT erzwingt Commit-Reihenfolge
== seq-Reihenfolge. Er wird nach dem per-Repo-FOR-UPDATE genommen, überall in
derselben Reihenfolge, also ohne Deadlock-Risiko. Preis: das Ende jeder
schreibenden Transaktion ist global serialisiert; das steht im Modulkopf.

Der WebSocket-Handler abonniert den Broadcast, *bevor* er die Datenbank
liest, und filtert Live-Events auf seq > Wasserstand. Aus einem Rennen wird
so eine Dublette, die sich filtern lässt, statt einer Lücke, die es nicht
gibt. Ein zu langsamer Consumer bekommt #info/OutdatedCursor und fällt auf
den DB-Replay zurück, statt getrennt zu werden — die Events sind durabel,
also ist der Rückfall verlustfrei.

Frame-Hülle ist konformes DAG-CBOR mit Tag-42-Links (neues Modul dag_cbor,
aus car.rs herausgezogen statt dupliziert). Die Blöcke darin behalten die
Konvention dieses Repos: CIDs als Strings. Ein fremder Consumer liest die
Frames, scheitert aber an den Blockinhalten — das zu ändern hieße, jede CID
im System zu ändern, inklusive der did:plc-Ableitung. Steht so im Modulkopf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
2026-09-10 07:08:02 +02:00

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
9 Auth + Performance done — AppView prüft Bearer-Tokens (ES256, Schlüssel aus dem neuen /.well-known/did.json der PDS, fail closed); Timeline und Notifications nur noch für die eigene DID; CORS-Allowlist statt Any; Indizes für Handle-Lookup und Cold-Start-Feed

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 Tests sind fail-open: ohne DATABASE_URL_APPVIEW in der Umgebung überspringen sie sich selbst und melden das nur auf stderr. cargo test --workspace in einer nackten Shell meldet dann grün, ohne sie ausgeführt zu haben — für einen aussagekräftigen Lauf set -a; . ./.env; set +a voranstellen und PDS + AppView laufen lassen.
  • Läuft die Auth (Default), holen sich die Integrationstests echte Tokens von der PDS bzw. signieren sie aus PDS_JWT_SECRET; ohne erreichbare PDS überspringen sie.

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.subscribeRepos fehlt) — eigene Records erreichen die AppView nur über den Best-Effort-Push POST /internal/ingest-commit.
  • aud wird beim Token-Check nicht validiert (Signatur, Ablauf, scope und sub schon).
  • 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 (siehe docs/tauri-release.md).
  • Reply-Notifications gehen verloren, wenn die Antwort vor ihrem Parent indiziert wird (kein Nachlauf) — bei Jetstream möglich.
  • at-blob spricht MinIO ohne Signature V4 — echtes AWS S3 funktioniert damit nicht.

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).

S
Description
AT Protocol PDS + AppView + Tauri Desktop Client (160-char posts). Maarcade design.
Readme
1.9 MiB
Languages
Rust 80.6%
Svelte 11.6%
TypeScript 7.2%
PLpgSQL 0.3%
CSS 0.3%