Module 2: Nostr and the Relay¶
How the Nostr protocol works, why CivicOS builds on it, and what a relay actually does.
Prerequisite: Module 1 (Cryptographic Foundations) — you should understand key pairs, signatures, and hashing.
Why Not Just Build a Normal App?¶
Before diving into Nostr, let's understand why CivicOS doesn't just use a traditional database with user accounts.
The Platform Trap¶
Every civic engagement platform before CivicOS followed the same pattern:
This creates three fatal problems for civic infrastructure:
1. Platform dependency. If the platform goes bankrupt, gets acquired, or changes direction, all civic participation data disappears. This has happened repeatedly — EveryBlock (shut down 2013), Neighborland (pivoted), SeeClickFix (acquired, changed focus).
2. Vendor lock-in. Your civic history, your connections, your voice — all locked inside one company's database. You can't take it with you. You can't verify it independently.
3. Incentive misalignment. Platforms optimize for engagement (time on site, clicks, outrage) because that's how they make money. Civic coordination requires the opposite: informed, deliberate participation. Nextdoor makes money when neighbors argue. CivicOS has no engagement metrics at all.
The Protocol Alternative¶
Instead of a platform, CivicOS uses a protocol — a shared set of rules that anyone can implement.
Analogy: Platform vs. Protocol
Email (protocol):
You can use Gmail, Outlook, Proton, or run your own server.
You can switch providers and keep your address.
No single company controls email.
Facebook Messenger (platform):
You can only use Facebook's app.
You can't take your messages to another service.
Facebook controls everything.
CivicOS uses Nostr (protocol):
You can use any Nostr client.
You can connect to any relay.
No single entity controls civic participation.
Nostr is the specific protocol CivicOS builds on.
What Is Nostr?¶
Nostr stands for Notes and Other Stuff Transmitted by Relays. It's a decentralized protocol for publishing signed messages, originally designed for social networking but general enough for any application.
The Core Idea¶
Nostr has just three concepts:
- Events — signed JSON messages (the only data type in Nostr)
- Relays — servers that store and forward events
- Clients — apps that create, sign, and read events
That's it. The entire protocol fits on a few pages. This simplicity is intentional — complex protocols are hard to implement correctly and create barriers to adoption.
An Event¶
Every piece of data in Nostr is an "event." An event is a JSON object with a fixed structure:
{
"id": "hash of all fields below",
"pubkey": "author's public key (64 hex chars)",
"created_at": 1738464000,
"kind": 1,
"tags": [
["p", "someone's pubkey"],
["t", "topic"]
],
"content": "Hello, Nostr!",
"sig": "Schnorr signature (128 hex chars)"
}
Let's break this down:
| Field | What It Is | Analogy |
|---|---|---|
id | Hash of the event contents | Fingerprint |
pubkey | Who created this event | Return address on a letter |
created_at | When it was created (Unix timestamp) | Postmark |
kind | What type of event this is | "Letter" vs. "Postcard" vs. "Package" |
tags | Structured metadata | Labels on the envelope |
content | The actual message | What's inside the envelope |
sig | Cryptographic signature | Wax seal proving it's from the sender |
The id is computed, not assigned. It's a SHA-256 hash of [0, pubkey, created_at, kind, tags, content]. This means: - No central authority assigns IDs (no auto-increment, no UUID server) - The ID is deterministic — the same event always produces the same ID - Tampering with any field changes the ID, breaking the signature
Event Kinds¶
The kind field tells relays and clients what type of event this is. Think of it like file extensions — .jpg tells your computer "this is an image," and kind: 1 tells a Nostr client "this is a text note."
Standard Nostr kinds:
| Kind | What | Example |
|---|---|---|
| 0 | User metadata (profile) | Display name, bio, avatar |
| 1 | Text note (like a tweet) | "Hello, Nostr!" |
| 3 | Contact list | Who you follow |
| 4 | Encrypted DM | Private message |
CivicOS civic kinds (custom, in the 30000+ range):
| Kind | What | Example |
|---|---|---|
| 30800 | Civic Voice | "I support the bike lane proposal" |
| 30801 | Civic Entity | The bike lane proposal itself |
| 30802 | Civic Subscription | "Notify me about housing topics" |
| 30850 | Civic Attestation | "This person attended a community event" |
| 30810 | Civic Action | "Write a letter to the council by Feb 14" |
| 30811 | Civic Commitment | "I committed to writing that letter" |
| 30812 | Civic Completion | "I wrote and sent the letter" |
Event Categories¶
Nostr events fall into three categories based on their kind number:
Regular events (kinds 1-9999):
Every event is stored. Duplicates rejected by ID.
Like email: each message is a separate thing.
Example: Kind 1800 (Civic Vouch) — "I vouch for this person"
You can vouch for someone multiple times. Each vouch is stored.
Replaceable events (kinds 10000-19999):
One per kind per pubkey. Newer replaces older.
Like a profile: you update it, not create a new one.
Example: Kind 10800 (Civic Provenance) — your reputation record.
When your voice count changes, the old provenance is replaced.
Addressable events (kinds 30000-39999):
One per kind per pubkey per "d-tag". Newer replaces older.
Like a wiki page: identified by a name, editable.
Example: Kind 30800 (Civic Voice) — your stance on entity X.
d-tag = "decision:city-san-rafael:2026-02-03:item-6a"
If you change your stance, the old voice is replaced.
Why this matters for civic coordination:
Voices are addressable (kind 30800) because you should only have one stance on an entity at a time. If you change your mind from "support" to "oppose," the new voice replaces the old one — there's no ambiguity.
Attestations are also addressable (kind 30850) because you should only have one attestation per jurisdiction. The d-tag attest:city-san-rafael:{your_pubkey} ensures one attestation per person per city.
Tags¶
Tags are how events reference other events, topics, and metadata. They're arrays of strings:
"tags": [
["d", "decision:city-san-rafael:2026-02-03:item-6a"],
["j", "city-san-rafael"],
["stance", "support"],
["t", "housing"],
["p", "someone_elses_pubkey"]
]
| Tag | Convention | CivicOS Meaning |
|---|---|---|
d | Addressable identifier | Entity being voiced on |
p | Referenced pubkey | Person being attested / mentioned |
t | Topic | Housing, transportation, etc. |
j | (CivicOS custom) | Jurisdiction: city-san-rafael, marin-county |
a | Referenced addressable event | Parent entity for actions |
Tags are the primary way to query for events. When you ask "what are the voices on this agenda item?", you're filtering by the d tag.
What Is a Relay?¶
A relay is a server that:
- Receives signed events from clients
- Stores them
- Serves them to clients that request matching events
That's it. A relay is a dumb pipe — it doesn't filter, rank, recommend, or manipulate. It stores and serves.
Analogy: Post Office vs. Social Media¶
Social media (Facebook, Nextdoor):
You post a message.
The PLATFORM decides who sees it.
The PLATFORM decides the order.
The PLATFORM decides if it's shown at all.
The platform optimizes for engagement.
Relay (Nostr):
You publish a signed event.
The RELAY stores it.
CLIENTS decide what to request.
CLIENTS decide the order and filtering.
The relay has no opinion on content.
A relay is more like a bulletin board than a newspaper. Anyone can pin something up. Anyone can come read. The bulletin board doesn't editorialized or curate.
Analogy: Library Shelf¶
A relay is a library shelf: - Authors (clients) place books (events) on the shelf - Readers (other clients) browse the shelf by category (tags/kinds) - The shelf doesn't decide which books are "good" — it just holds them - Anyone can run a library (relay), and authors can place books on multiple shelves
How Clients Talk to Relays¶
Relays use WebSocket connections (persistent, bidirectional):
Client → Relay:
Publish an event:
["EVENT", { kind: 30800, pubkey: "...", ... }]
Subscribe to events matching a filter:
["REQ", "subscription-id", { kinds: [30800], "#j": ["city-san-rafael"] }]
Stop a subscription:
["CLOSE", "subscription-id"]
Relay → Client:
Here's an event matching your subscription:
["EVENT", "subscription-id", { kind: 30800, ... }]
End of stored events (now streaming new ones):
["EOSE", "subscription-id"]
Acknowledgment:
["OK", "event-id", true, ""]
Three messages from clients. Three messages from relays. That's the entire protocol.
What Makes This Powerful¶
No accounts. Clients don't "log in" to relays. They just connect and publish signed events. The relay verifies the signature. There's no session, no cookie, no token.
Multiple relays. A client can connect to many relays simultaneously. If one goes down, others still work. If you don't trust one relay operator, use a different one.
Relay independence. Each relay can set its own policies: what kinds of events to accept, storage limits, access controls. But all relays speak the same protocol, so clients work with any of them.
How CivicOS Uses Nostr¶
The CivicOS Relay¶
CivicOS runs a relay that speaks standard Nostr protocol with civic-specific extensions. The relay:
- Accepts standard NIP-01 WebSocket connections
- Stores civic events (voices, entities, subscriptions, attestations)
- Maintains voice count aggregations per entity
- Verifies attestation proofs on voice/comment submission
- Exposes REST API endpoints for simpler client integration
CivicOS Relay
┌──────────────────────────────┐
│ │
Browser │ Event Storage │
Extension ──────►│ (PostgreSQL) │
(WebSocket) │ voices, entities, │
│ attestations, actions │
│ │
REST API ───────►│ Verification │
(HTTP) │ signature check │
│ attestation proof check │
│ deduplication │
│ │
Other Nostr │ Aggregation │
Clients ────────►│ voice counts per entity │
(WebSocket) │ attested vs total │
│ │
└──────────────────────────────┘
Civic Voices as Nostr Events¶
When you voice "support" on an agenda item, the browser extension creates this Nostr event:
{
"kind": 30800,
"pubkey": "your_public_key",
"created_at": 1738464000,
"tags": [
["d", "decision:city-san-rafael:2026-02-03:item-6a"],
["j", "city-san-rafael"],
["stance", "support"],
["t", "housing"]
],
"content": "",
"sig": "your_schnorr_signature"
}
This event, along with your attestation proof, is sent to the relay. The relay verifies everything and stores it.
Why kind 30800 (addressable)? Because the d-tag identifies the specific civic entity. The "addressable" property means if you later change your stance, the new event replaces the old one. One voice per person per entity.
Entity Namespaces¶
CivicOS entities have structured identifiers:
Government-originated (signed by jurisdiction key):
decision:city-san-rafael:2026-02-03:item-6a
agenda:2026-02-03:city-council
meeting:city-san-rafael:2026-02-03
Community-originated (signed by creator's key):
initiative:san-rafael:bike-lane-4th-st
initiative:marin-county:transit-expansion
Both types receive voices. The entity's namespace makes its origin clear — you can tell whether something was created by the city government or by a community member.
Federation: Multiple Relays¶
Why Multiple Relays?¶
A single relay is a single point of failure and control. CivicOS is designed to work across multiple relays.
Single relay (fragile):
If the relay goes down → all civic coordination stops
If the operator is compromised → voice counts can be manipulated
If the operator censors → voices can be suppressed
Multiple relays (resilient):
If one relay goes down → others continue
If one is compromised → others provide correct counts
If one censors → voices are on other relays too
How Federation Works¶
Relays that host the same entities can peer with each other — synchronizing voice records across relays.
Relay A Relay B
(run by CivicOS) (run by local newspaper)
┌────────────────┐ ┌────────────────┐
│ bike-lane: │ │ bike-lane: │
│ 12 support │◄─── sync ───►│ 12 support │
│ 3 oppose │ │ 3 oppose │
│ │ │ │
│ housing-plan: │ │ │
│ 8 support │ │ │
└────────────────┘ └────────────────┘
User casts voice on Relay A:
→ Relay A stores it, sends to Relay B
→ Relay B verifies the signature, stores it
→ Both relays now show the same count
Voices are self-verifying (from Module 1). When Relay B receives a voice from Relay A, it doesn't have to trust Relay A. It independently verifies the Schnorr signature and attestation proof. If anything is wrong, it rejects the voice.
Jurisdictional Relays¶
Each jurisdiction can run its own relay:
San Rafael relay → hosts city-san-rafael entities
Marin County relay → hosts marin-county entities
California relay → hosts california entities
Federal relay → hosts us-federal entities
Your agent (we'll cover this in Module 3) subscribes to whichever relays match your civic life. The agent handles the complexity — you just see your local civic landscape.
Cross-Jurisdiction Coordination¶
A single civic issue often spans multiple levels of government:
Issue: Affordable housing in San Rafael
Federal level: CDBG funding (Community Development Block Grants)
State level: SB 1234 (density bonus law)
County level: Marin housing allocation plan
City level: 4th Street rezoning proposal
Each level has its own relay, its own entities, its own voices.
An agent subscribed to all four relays can connect the dots:
"Monday's rezoning implements the state density bonus law.
The affordable units depend on CDBG funding, which Congress
is debating cutting. These three things are connected."
No single relay can produce that synthesis. It requires reasoning across multiple relays — which is what the agent layer does (Module 3).
The Relay as Infrastructure¶
What the Relay Stores¶
| Data | Purpose | Visible To |
|---|---|---|
| Voice records | Who supports/opposes what | Everyone (public) |
| Entity metadata | What's being decided | Everyone (public) |
| Voice counts | How many people care | Everyone (public) |
| Attestation proofs | Who is attested | Everyone (embedded on voices) |
| Subscriptions | Who wants notifications | Only the subscriber |
What the Relay Does NOT Store¶
| Data | Where It Lives |
|---|---|
| User profiles | Nowhere (keys are identities) |
| Browsing history | Nowhere (no tracking) |
| Private keys | User's device only |
| Engagement metrics | Nowhere (no metrics) |
| Rankings or scores | Nowhere (no algorithm) |
Analogy: The Relay as Public Square¶
Traditional platform = Shopping mall
- Owned by a corporation
- They control who enters
- They track where you go
- They optimize the layout to make you buy things
- They can kick you out
CivicOS relay = Public square
- Open to anyone
- Nobody tracks who comes and goes
- Bulletin boards show what people care about
- You can stand up and be counted (voice)
- Nobody can revoke your presence
The square doesn't have an algorithm. It doesn't send you push
notifications optimized for engagement. It just... exists. And
anyone can build another square next door.
The Browser Extension as Client¶
In the CivicOS system, the browser extension is a Nostr client. It:
- Generates and stores your key pair (private key in localStorage)
- Signs events when you voice or comment (using your private key)
- Reads events from the relay (voices, entities, counts)
- Stores your attestation proof locally
- Embeds the proof on every voice/comment before sending
Browser Extension (Client) CivicOS Relay
┌─────────────────────────┐ ┌─────────────────┐
│ │ │ │
│ Private key (secret) │ │ │
│ Public key │ │ Stores events │
│ Attestation proof │ │ Verifies sigs │
│ │ │ Counts voices │
│ When you tap "Support":│ HTTP/WS │ │
│ 1. Build event │────────────►│ 1. Check sig │
│ 2. Attach attest proof │ │ 2. Check proof │
│ 3. Sign with priv key │ │ 3. Store │
│ 4. Send to relay │ │ 4. Update count│
│ │ │ │
│ Display: │◄────────────│ Return counts │
│ "23 support, 5 oppose" │ │ & voice list │
│ │ │ │
└─────────────────────────┘ └─────────────────┘
The extension also works as a standard NIP-07 signer — meaning other Nostr clients can ask it to sign events. This is how you could use a general Nostr client (like Damus or Primal) to interact with civic entities, though the CivicOS extension provides a much better civic-specific UX.
Recap: The Full Picture So Far¶
Module 1: Cryptographic Foundations
→ Keys give you a self-sovereign identity
→ Signatures prove messages are yours
→ Hashes create tamper-proof event IDs
Module 2: Nostr and the Relay
→ Events are signed JSON messages
→ Relays store and serve events (dumb pipes)
→ Clients create, sign, and read events
→ CivicOS extends Nostr with civic event kinds
→ Federation enables resilience and pluralism
→ The browser extension is a Nostr client
Still missing:
→ How do we know a voice is from a REAL PERSON? (Module 3)
→ How does the system prevent spam from autonomous agents? (Module 3)
→ How does the agent layer provide intelligent filtering? (Module 3)
→ Why did we choose this architecture over alternatives? (Module 3)
Module 3 brings everything together: attestation (the gate that proves you're real), edge intelligence (the agent that makes civic data useful), and the design decisions that led to this architecture.