diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,93 @@ +# openletter — design + +openletter is an ATProto-native petition system: anyone can write an open +letter (a petition) and anyone can endorse or object to any number of them. +`openletter.pub` is the lexicon (`pub.openletter.*`); `openletter.vote` is the +AppView. See the [README](./README.md) for the pitch. + +## Principles + +These fall out of ATProto's grain, and everything below follows from them: + +- **Account-centric ownership.** A petition lives in its author's repo; each + endorsement, objection, and co-author attestation lives in *that person's* + repo. Nobody's signature is held by a central service. +- **CID-pinned, so version-honest.** Every signature references the *exact + version* it signed, by content hash. You can always prove what was actually + signed — the property a centralized petition site can't offer. +- **Single-writer.** A record is signed by one account. "Collaboration" is + modeled as handshakes between repos, never shared writes. +- **The AppView interprets; it never owns or gatekeeps.** `openletter.vote` + aggregates records off the network and computes the views below. Counts are + eventually-consistent, and "closing" a petition is advisory — anyone can + still write an endorsement to their own repo. + +## Records — `pub.openletter.*` + +| record | lives in | what it is | +|---|---|---| +| `petition` | author | one immutable version of a letter | +| `draft` | author | a mutable work-in-progress that publishes a `petition` | +| `endorsement` | signer | version-pinned support | +| `objection` | objector | version-pinned dissent | +| `contribution` | co-author | the attestation half of co-authorship | + +**petition** — `title`, markdown `body` (CommonMark, no raw HTML), +`recipients[]` (each a `name` plus an optional ATProto identity and/or URL), +`createdAt`, optional `summary` / `closesAt` / `langs` / `tags` / `banner`. +Plus two structural fields: `supersedes?` (chains versions) and +`contributors[]` (DIDs the author claims as co-authors). + +**draft** — the same content fields, mutable and edited in place. Publishing +creates a `petition`, so the `petition` collection only ever holds finished, +signable versions. A draft lives on the PDS, so it is *unlisted, not private*. + +**endorsement / objection** — a CID-pinned reference to a petition version, +`createdAt`, an optional reason-for-signing `comment`, and a `listed` flag (a +display hint; the signer's DID is always on the record). The record key is +derived from the signed version, so a signer holds at most one of each per +version and re-signing is idempotent. Holding both on the same version is +resolved in the AppView by recency. + +**contribution** — published by a claimed co-author: a CID-pinned reference to +the petition version they affirm. A co-author is *confirmed* only where the +author's `contributors[]` and the co-author's `contribution` agree — a +two-sided handshake, version by version. + +## Versioning + +A published `petition` is never edited in place. To change a letter, the +author publishes a *new* `petition` whose `supersedes` points back at the +prior version, forming a chain. The genesis version's URI is the petition's +stable identity (what you share); the AppView walks the chain to find the +current head and to resolve forks, cycles, and dangling links. Because every +version is its own live record, anyone can fetch and verify exactly what was +signed — no trusted archive needed. + +Amendments are staged in a draft seeded from the current version. Signatures +pin a version, so a material amendment never silently carries old support +forward — the AppView shows who signed which version and can invite +re-affirmation. Deleting a version tombstones it; deleting the genesis +withdraws the petition. + +`supersedes` is our convention, not an ATProto primitive — the protocol sees +unrelated records, so the chain's meaning and its validation live in the +lexicon and the AppView. + +## Tradeoffs we accept + +- **No privacy at the data layer.** Repos are public: drafts are unlisted not + secret, and an "unlisted" endorsement only hides a name in the UI. +- **Integrity over stickiness.** Re-affirming after a material amendment is + friction by design — better than faking continuity of support. +- **Meaning lives in convention + AppView.** The lexicon stores records; the + chain/head/count/confirmation semantics are ours. Interop requires others to + adopt `pub.openletter`, which is why the schemas are published (resolvable + via `_lexicon` DNS). + +## Deferred past v1 + +First-class amendments (an amendment as its own endorsable record), +comments/discussion, suggestion-based collaborative editing, signature +`goal`s, structured target responses, and real-time co-editing. Each is a +clean additive extension; none requires reworking the above.