tomdeboneandClaude Opus 5 73959f9dde feat(tauri-app): Einladungscode im Client, Produktions-URLs als Default
Zwei Dinge, die einen ausgelieferten Client heute unbrauchbar gemacht
hätten.

Erstens: Seit die öffentliche Instanz Einladungscodes verlangt, bekam
jeder Registrierungsversuch aus dem Client 400 InvalidInviteCode, ohne
dass es ein Feld für einen Code gegeben hätte. auth_register und
create_account nehmen ihn jetzt entgegen, der LoginScreen zeigt das Feld
nur im Registrieren-Modus und leert es beim Moduswechsel — sonst würde
ein getippter Code im ausgeblendeten Feld überleben und beim nächsten
Versuch stillschweigend mitgehen.

Ein leeres Feld wird weggelassen, nicht abgelehnt: ob ein Code nötig
ist, entscheidet der Server (PDS_INVITE_REQUIRED), und eine lokale
Dev-PDS verlangt keinen. "Kein Code angegeben" und "mein Code ist der
Leerstring" sind zwei verschiedene Aussagen; nur die erste ist je wahr.
Deshalb fliegt der Schlüssel per skip_serializing_if ganz aus dem Body.

Der Server unterscheidet bei der Ablehnung bewusst nicht zwischen
fehlend, falsch, gesperrt und verbraucht — sonst wären Codes
enumerierbar. Die Meldung im Client nennt deshalb beide plausiblen
Auswege, statt einen zu raten.

Zweitens: Die Basis-URLs fielen ohne Umgebungsvariable auf
http://127.0.0.1:2583 bzw. :2584 zurück. Ein gebautes Paket hätte also
gegen nichts gesprochen. Beide zeigen jetzt auf
https://tweet.maarcade.com — dieselbe Origin für PDS und AppView, der
Proxy trennt nach Pfadpräfix. Die Variablen überschreiben weiterhin,
damit Entwicklung gegen localhost möglich bleibt; eine leer gesetzte
Variable gilt dabei als ungesetzt, weil eine leere Basis-URL sonst als
kryptischer Relative-URL-Fehler weit weg von der Ursache auftaucht.

Der Settings-View spiegelte dieselben alten Defaults und hätte falsche
Backends angezeigt — mitgezogen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013HC9HLrUU1LNwkzp8nkDLX
2026-09-10 22:58:25 +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
lexicons/app/bsky/            like, repost, follow, actor.profile
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
10 Lokaler Firehose done — com.atproto.sync.subscribeRepos auf der PDS (Event in derselben Transaktion wie der Commit, seq-Cursor mit lückenfreiem Replay, WebSocket-Frames in atproto-Form); die AppView konsumiert ihn mit persistiertem Cursor. Ein verlorener Push ist damit nicht mehr endgültig.

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

  • Der Firehose ist lokal: er verbindet die eigenen zwei Dienste. Ein fremder Relay erfährt von dieser PDS weiterhin nichts.
  • Die Frame-Hülle ist spec-konformes DAG-CBOR, die Blöcke darin nicht: CIDs innerhalb von Commit-Blöcken sind Strings statt Tag-42-Links. Ein fremder atproto-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.
  • firehose_events wird nie beschnitten.
  • 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%