diff --git a/NOSTR_CLIENT_INTEGRATION.md b/NOSTR_CLIENT_INTEGRATION.md new file mode 100644 index 00000000..6e579a02 --- /dev/null +++ b/NOSTR_CLIENT_INTEGRATION.md @@ -0,0 +1,720 @@ +# Nostr Client Integration Guide for Groups Relay + +This document provides comprehensive instructions for Nostr clients on how to integrate with NIP-29 compliant group relays, including creating groups, managing domains/subdomains, and handling group hierarchies. + +## Table of Contents + +1. [Overview](#overview) +2. [Connection and Authentication](#connection-and-authentication) +3. [Group Operations](#group-operations) +4. [Domain and Subdomain Structure](#domain-and-subdomain-structure) +5. [Event Kinds Reference](#event-kinds-reference) +6. [Best Practices](#best-practices) +7. [Example Flows](#example-flows) + +## Overview + +NIP-29 defines relay-based groups where the relay acts as the authority for group management. Groups are identified by `` and referenced in events using the `h` tag. + +### Key Concepts + +- **Group ID**: Unique identifier for a group (e.g., `general`, `bitcoin-dev`) +- **Group Address**: Relay-specific identifier format: `'` (e.g., `general'wss://groups.example.com`) +- **Relay Authority**: The relay generates and signs all group state events +- **Role-Based Access**: Admin, Member, and Custom roles with different permissions + +## Connection and Authentication + +### 1. Initial Connection + +```javascript +// Connect to the group relay +const relay = new WebSocket('wss://groups.example.com'); + +// For private groups, authenticate using NIP-42 +relay.send(JSON.stringify([ + "AUTH", + { + "id": "", + "pubkey": "", + "created_at": , + "kind": 22242, + "tags": [ + ["relay", "wss://groups.example.com"], + ["challenge", ""] + ], + "content": "", + "sig": "" + } +])); +``` + +### 2. Subscribing to Groups + +```javascript +// Subscribe to a specific group's content +relay.send(JSON.stringify([ + "REQ", + "", + { + "kinds": [9, 10, 11, 12, 39000, 39001, 39002], + "#h": [""] + } +])); + +// Subscribe to all groups metadata +relay.send(JSON.stringify([ + "REQ", + "", + { + "kinds": [39000] + } +])); +``` + +## Group Operations + +### Creating a Group + +Only relay admins or authorized users can create groups: + +```javascript +// Send group creation event +const createGroupEvent = { + "id": "", + "pubkey": "", + "created_at": , + "kind": 9007, + "tags": [ + ["h", ""], + ["name", ""], + ["about", ""], + ["picture", ""], + ["private"], // Optional: make group private + ["closed"] // Optional: require approval to join + ], + "content": "", + "sig": "" +}; + +relay.send(JSON.stringify(["EVENT", createGroupEvent])); +``` + +### Joining a Group + +For open groups: +```javascript +const joinRequest = { + "kind": 9021, + "tags": [ + ["h", ""], + ["relay", "wss://groups.example.com"] + ], + "content": "Optional message" +}; +``` + +For closed groups with invite: +```javascript +const joinRequest = { + "kind": 9021, + "tags": [ + ["h", ""], + ["relay", "wss://groups.example.com"], + ["invite", ""] + ], + "content": "Optional message" +}; +``` + +### Posting to a Group + +```javascript +const groupPost = { + "kind": 9, // or 10, 11, 12 + "tags": [ + ["h", ""] + ], + "content": "Hello group!" +}; +``` + +### Managing Group Members (Admin Only) + +```javascript +// Add member +const addMember = { + "kind": 9000, + "tags": [ + ["h", ""], + ["p", ""] + ] +}; + +// Remove member +const removeMember = { + "kind": 9001, + "tags": [ + ["h", ""], + ["p", ""] + ] +}; + +// Set user role +const setRole = { + "kind": 9006, + "tags": [ + ["h", ""], + ["p", ""], + ["role", "admin"] // or "member", "moderator", etc. + ] +}; +``` + +## Domain and Subdomain Structure + +### Hierarchical Group Organization + +Groups in NIP-29 can be organized hierarchically using a forward-slash (`/`) naming convention to create domains, subdomains, and sub-groups. This is achieved through the group ID structure: + +``` +company # Top-level domain group +├── company/engineering # Subdomain under company +│ ├── company/engineering/frontend # Sub-group under engineering +│ ├── company/engineering/backend # Sub-group under engineering +│ └── company/engineering/qa # Sub-group under engineering +├── company/marketing # Another subdomain +│ ├── company/marketing/social # Sub-group under marketing +│ └── company/marketing/content # Sub-group under marketing +└── company/hr # Another subdomain +``` + +### How to Create Domain/Subdomain Structure + +#### 1. Creating a Top-Level Domain Group + +First, create the main domain group: + +```javascript +// Create the top-level domain "company" +const createDomain = { + "kind": 9007, + "tags": [ + ["h", "company"], // This is the domain ID + ["name", "ACME Company"], + ["about", "Main company group for all employees"], + ["picture", "https://example.com/company-logo.png"] + ], + "content": "", + "sig": "" +}; +``` + +#### 2. Creating Subdomains + +Then create subdomain groups with the domain prefix: + +```javascript +// Create subdomain "company/engineering" +const createSubdomain = { + "kind": 9007, + "tags": [ + ["h", "company/engineering"], // Note the forward slash + ["name", "Engineering Team"], + ["about", "All engineering staff"], + ["picture", "https://example.com/eng-logo.png"], + ["closed"] // Optionally make it closed + ], + "content": "", + "sig": "" +}; +``` + +#### 3. Creating Sub-groups + +Create deeper sub-groups following the same pattern: + +```javascript +// Create sub-group "company/engineering/frontend" +const createSubGroup = { + "kind": 9007, + "tags": [ + ["h", "company/engineering/frontend"], // Full path + ["name", "Frontend Team"], + ["about", "Frontend developers"], + ["picture", "https://example.com/frontend-logo.png"] + ], + "content": "", + "sig": "" +}; +``` + +### Important Rules for Hierarchical Groups + +1. **Group IDs use forward slashes** - The `/` character separates hierarchy levels +2. **Each group must be created individually** - Creating `company/engineering` does NOT automatically create `company` +3. **Create from top to bottom** - Create parent groups before child groups +4. **Full path in group ID** - Always use the complete path in the `h` tag + +### Step-by-Step Example: Setting Up a Complete Domain Structure + +Here's a complete example of setting up a company domain with subdomains: + +```javascript +// Step 1: Create the main domain +await relay.send(JSON.stringify(["EVENT", { + "kind": 9007, + "tags": [ + ["h", "acme"], + ["name", "ACME Corporation"], + ["about", "Main company group"], + ["picture", "https://acme.com/logo.png"] + ], + "content": "", + "sig": "..." +}])); + +// Step 2: Create subdomains +const subdomains = [ + { id: "acme/engineering", name: "Engineering", about: "Engineering teams" }, + { id: "acme/marketing", name: "Marketing", about: "Marketing teams" }, + { id: "acme/sales", name: "Sales", about: "Sales teams" } +]; + +for (const subdomain of subdomains) { + await relay.send(JSON.stringify(["EVENT", { + "kind": 9007, + "tags": [ + ["h", subdomain.id], + ["name", subdomain.name], + ["about", subdomain.about] + ], + "content": "", + "sig": "..." + }])); +} + +// Step 3: Create sub-groups under engineering +const engineeringTeams = [ + { id: "acme/engineering/frontend", name: "Frontend Team" }, + { id: "acme/engineering/backend", name: "Backend Team" }, + { id: "acme/engineering/devops", name: "DevOps Team" } +]; + +for (const team of engineeringTeams) { + await relay.send(JSON.stringify(["EVENT", { + "kind": 9007, + "tags": [ + ["h", team.id], + ["name", team.name], + ["closed"] // Make sub-groups closed by default + ], + "content": "", + "sig": "..." + }])); +} +``` + +### Group ID Naming Convention Summary + +| Level | Example | Description | +|-------|---------|-------------| +| Domain | `acme` | Top-level organization | +| Subdomain | `acme/engineering` | Department or major division | +| Sub-group | `acme/engineering/frontend` | Team or project | +| Deep sub-group | `acme/engineering/frontend/react` | Specific technology or sub-team | + +#### 2. Automatic Parent Group Membership + +When implementing hierarchical groups: + +```javascript +// When user joins a subgroup, optionally auto-join parent groups +async function joinGroupWithHierarchy(groupId, relay) { + const parts = groupId.split('/'); + + // Join from top-level down + for (let i = 1; i <= parts.length; i++) { + const parentGroupId = parts.slice(0, i).join('/'); + await sendJoinRequest(parentGroupId, relay); + } +} +``` + +#### 3. Permission Inheritance + +Implement permission cascading: + +```javascript +function checkGroupPermission(userPubkey, groupId, permission) { + // Check permission in current group + if (hasPermission(userPubkey, groupId, permission)) { + return true; + } + + // Check parent groups + const parts = groupId.split('/'); + for (let i = parts.length - 1; i > 0; i--) { + const parentGroupId = parts.slice(0, i).join('/'); + if (hasPermission(userPubkey, parentGroupId, permission)) { + return true; + } + } + + return false; +} +``` + +#### 4. Group Discovery + +Implement group browsing by hierarchy: + +```javascript +// Fetch all groups and organize by hierarchy +async function getGroupHierarchy(relay) { + const groups = await fetchAllGroups(relay); + const hierarchy = {}; + + groups.forEach(group => { + const parts = group.id.split('/'); + let current = hierarchy; + + parts.forEach((part, index) => { + if (!current[part]) { + current[part] = { + id: parts.slice(0, index + 1).join('/'), + children: {} + }; + } + current = current[part].children; + }); + }); + + return hierarchy; +} +``` + +### Domain-Specific Features + +#### 1. Domain Admin Rights + +Domain admins should have elevated permissions: + +```javascript +function isDomainAdmin(userPubkey, groupId) { + const domain = groupId.split('/')[0]; + return isGroupAdmin(userPubkey, domain); +} +``` + +#### 2. Cross-Domain Posting + +Allow posting to multiple related groups: + +```javascript +const crossPost = { + "kind": 9, + "tags": [ + ["h", "company/engineering"], + ["h", "company/announcements"], + ["h", "company"] + ], + "content": "Important engineering update!" +}; +``` + +#### 3. Domain-Wide Moderation + +Implement domain-level moderation: + +```javascript +// Domain admin can moderate all subgroups +const deleteEvent = { + "kind": 9005, + "tags": [ + ["h", "company/engineering/frontend"], // Subgroup + ["e", ""] + ] +}; +``` + +## Event Kinds Reference + +### User Actions +- `9` - Public chat message +- `10` - Public threaded reply +- `11` - Public forum post +- `12` - Public comment +- `10010` - General public content +- `9021` - Join request +- `9022` - Leave request + +### Admin Actions +- `9000` - Add user +- `9001` - Remove user +- `9002` - Edit metadata +- `9005` - Delete event +- `9006` - Set user role +- `9007` - Create group +- `9008` - Delete group +- `9009` - Create invite + +### Relay-Generated Events +- `39000` - Group metadata (parameterized replaceable) +- `39001` - Group admins list +- `39002` - Group members list +- `39003` - Supported roles + +## Best Practices + +### 1. Caching Group Metadata + +```javascript +class GroupCache { + constructor() { + this.groups = new Map(); + this.members = new Map(); + } + + updateFromEvent(event) { + switch(event.kind) { + case 39000: // Group metadata + this.groups.set(event.tags.find(t => t[0] === 'h')[1], { + name: event.tags.find(t => t[0] === 'name')?.[1], + about: event.tags.find(t => t[0] === 'about')?.[1], + picture: event.tags.find(t => t[0] === 'picture')?.[1], + private: event.tags.some(t => t[0] === 'private'), + closed: event.tags.some(t => t[0] === 'closed') + }); + break; + case 39002: // Members list + const groupId = event.tags.find(t => t[0] === 'h')[1]; + const members = event.tags.filter(t => t[0] === 'p').map(t => t[1]); + this.members.set(groupId, members); + break; + } + } +} +``` + +### 2. Handling Connection Failures + +```javascript +class ResilientGroupConnection { + constructor(relayUrl) { + this.relayUrl = relayUrl; + this.reconnectDelay = 1000; + this.maxReconnectDelay = 30000; + } + + connect() { + this.ws = new WebSocket(this.relayUrl); + + this.ws.onclose = () => { + setTimeout(() => { + this.reconnectDelay = Math.min( + this.reconnectDelay * 2, + this.maxReconnectDelay + ); + this.connect(); + }, this.reconnectDelay); + }; + + this.ws.onopen = () => { + this.reconnectDelay = 1000; + this.resubscribe(); + }; + } +} +``` + +### 3. Optimistic UI Updates + +```javascript +// Show join request immediately, revert if failed +async function joinGroup(groupId) { + // Optimistic update + updateUI({ status: 'joining', groupId }); + + try { + const response = await sendJoinRequest(groupId); + if (response.accepted) { + updateUI({ status: 'member', groupId }); + } else { + updateUI({ status: 'pending', groupId }); + } + } catch (error) { + // Revert optimistic update + updateUI({ status: 'not-member', groupId }); + } +} +``` + +### 4. Batch Operations + +```javascript +// Batch multiple group operations +async function batchGroupOperations(operations) { + const events = operations.map(op => ({ + id: generateId(), + pubkey: userPubkey, + created_at: Math.floor(Date.now() / 1000), + kind: op.kind, + tags: op.tags, + content: op.content || "", + sig: signEvent(...) + })); + + // Send all events at once + for (const event of events) { + relay.send(JSON.stringify(["EVENT", event])); + } +} +``` + +## Example Flows + +### Complete Group Creation and Setup Flow + +```javascript +async function createAndSetupGroup(groupConfig) { + // 1. Create the group + await sendEvent({ + kind: 9007, + tags: [ + ["h", groupConfig.id], + ["name", groupConfig.name], + ["about", groupConfig.description], + ["picture", groupConfig.picture], + ...(groupConfig.private ? [["private"]] : []), + ...(groupConfig.closed ? [["closed"]] : []) + ] + }); + + // 2. Wait for group creation confirmation + await waitForEvent(39000, { h: groupConfig.id }); + + // 3. Add initial members + for (const member of groupConfig.initialMembers) { + await sendEvent({ + kind: 9000, + tags: [ + ["h", groupConfig.id], + ["p", member.pubkey] + ] + }); + } + + // 4. Set roles for admins + for (const admin of groupConfig.admins) { + await sendEvent({ + kind: 9006, + tags: [ + ["h", groupConfig.id], + ["p", admin.pubkey], + ["role", "admin"] + ] + }); + } + + // 5. Create initial invites if closed + if (groupConfig.closed && groupConfig.inviteCount) { + for (let i = 0; i < groupConfig.inviteCount; i++) { + await sendEvent({ + kind: 9009, + tags: [ + ["h", groupConfig.id], + ["uses", "1"] + ] + }); + } + } +} +``` + +### Hierarchical Group Navigation + +```javascript +class GroupNavigator { + constructor(relay) { + this.relay = relay; + this.groupTree = new Map(); + } + + async loadGroups() { + const groups = await this.fetchAllGroups(); + + groups.forEach(group => { + const path = group.id.split('/'); + let current = this.groupTree; + + path.forEach((segment, index) => { + if (!current.has(segment)) { + current.set(segment, { + id: path.slice(0, index + 1).join('/'), + metadata: null, + children: new Map() + }); + } + + if (index === path.length - 1) { + current.get(segment).metadata = group; + } + + current = current.get(segment).children; + }); + }); + } + + getSubgroups(parentId = '') { + const path = parentId ? parentId.split('/') : []; + let current = this.groupTree; + + path.forEach(segment => { + current = current.get(segment)?.children || new Map(); + }); + + return Array.from(current.values()); + } + + getBreadcrumbs(groupId) { + const path = groupId.split('/'); + const breadcrumbs = []; + + path.forEach((segment, index) => { + breadcrumbs.push({ + id: path.slice(0, index + 1).join('/'), + name: segment + }); + }); + + return breadcrumbs; + } +} +``` + +## Security Considerations + +1. **Always verify relay signatures** on events kinds 39000-39003 +2. **Validate group membership** before showing private content +3. **Check user permissions** before allowing admin actions +4. **Implement rate limiting** for join requests and posts +5. **Cache membership status** but periodically refresh +6. **Handle invite codes securely** - don't expose in URLs +7. **Implement proper error handling** for all group operations + +## Testing Checklist + +- [ ] Connect to relay and authenticate +- [ ] Fetch and display all public groups +- [ ] Join an open group +- [ ] Post content to a group +- [ ] Leave a group +- [ ] Request to join a closed group +- [ ] Use invite code for closed group +- [ ] Create subgroups (if authorized) +- [ ] Navigate group hierarchy +- [ ] Handle offline/reconnection scenarios +- [ ] Verify permission inheritance +- [ ] Test batch operations +- [ ] Validate optimistic UI updates \ No newline at end of file diff --git a/relay_nip29_notes.md b/relay_nip29_notes.md new file mode 100644 index 00000000..aaf7a26a --- /dev/null +++ b/relay_nip29_notes.md @@ -0,0 +1,480 @@ +# Groups Relay - NIP-29 Implementation Notes + +## Overview + +The groups_relay is a WebSocket proxy middleware that adds NIP-29 group chat functionality to any standard Nostr relay. It sits between Nostr clients and a backing relay (like strfry), intercepting and enriching events without modifying the underlying relay implementation. + +## Architecture + +### Core Design Principles + +1. **Proxy Pattern**: Acts as a transparent proxy, forwarding events between clients and the backing relay +2. **Middleware Stack**: Uses a composable middleware architecture for modular functionality +3. **In-Memory State**: Maintains group state in memory with DashMap for thread-safe concurrent access +4. **Database Persistence**: SQLite database for event storage and recovery after restarts +5. **Relay-Signed Events**: Generates addressable events signed by the relay's private key + +### Middleware Stack (Order Matters) + +``` +Client → LoggerMiddleware → Nip42Middleware → ValidationMiddleware → +EventVerifierMiddleware → Nip70Middleware → Nip29Middleware → +EventStoreMiddleware → Backing Relay +``` + +1. **LoggerMiddleware**: Logs all incoming/outgoing messages for debugging +2. **Nip42Middleware**: Handles NIP-42 authentication (AUTH challenge/response) +3. **ValidationMiddleware**: Validates event structure and required fields +4. **EventVerifierMiddleware**: Verifies event signatures +5. **Nip70Middleware**: Handles protected events (encryption/decryption) +6. **Nip29Middleware**: Core group management logic +7. **EventStoreMiddleware**: Database storage and relay forwarding + +## NIP-29 Event Types + +### Admin/Management Events (9000-9009) + +These events must be signed by group admins: + +- **9007 (GROUP_CREATE)**: Create a new group + ```json + { + "content": "", + "tags": [ + ["name", "Group Name"], + ["about", "Group Description"], + ["picture", "https://example.com/image.jpg"], + ["private"], // Optional: makes group private + ["closed"] // Optional: requires approval to join + ] + } + ``` + +- **9000 (GROUP_ADD_USER)**: Add user to group + ```json + { + "tags": [ + ["h", ""], + ["p", ""] + ] + } + ``` + +- **9001 (GROUP_REMOVE_USER)**: Remove user from group + ```json + { + "tags": [ + ["h", ""], + ["p", ""] + ] + } + ``` + +- **9002 (GROUP_EDIT_METADATA)**: Update group metadata + ```json + { + "tags": [ + ["h", ""], + ["name", "New Name"], + ["about", "New Description"], + ["picture", "https://example.com/new-image.jpg"], + ["private", ""], + ["closed", ""] + ] + } + ``` + +- **9005 (GROUP_DELETE_EVENT)**: Delete a specific event + ```json + { + "tags": [ + ["h", ""], + ["e", ""] + ] + } + ``` + +- **9006 (GROUP_SET_ROLES)**: Assign roles to users + ```json + { + "tags": [ + ["h", ""], + ["p", "", ""] + ] + } + ``` + +- **9008 (GROUP_DELETE)**: Delete entire group + ```json + { + "tags": [ + ["h", ""] + ] + } + ``` + +- **9009 (GROUP_CREATE_INVITE)**: Create invite code for closed groups + ```json + { + "tags": [ + ["h", ""] + ] + } + ``` + +### User Events (9021-9022) + +These can be created by any authenticated user: + +- **9021 (GROUP_USER_JOIN_REQUEST)**: Request to join a group + ```json + { + "content": "Optional message", + "tags": [ + ["h", ""], + ["code", ""] // Required for closed groups + ] + } + ``` + +- **9022 (GROUP_USER_LEAVE_REQUEST)**: Leave a group + ```json + { + "tags": [ + ["h", ""] + ] + } + ``` + +### Relay-Generated Events (39000-39003) + +These addressable events are generated and signed by the relay: + +- **39000 (GROUP_METADATA)**: Current group metadata + ```json + { + "content": "", + "tags": [ + ["d", ""], + ["name", "Group Name"], + ["about", "Description"], + ["picture", "https://example.com/image.jpg"], + ["private"], // If private + ["closed"] // If closed + ] + } + ``` + +- **39001 (GROUP_ADMINS)**: List of group administrators + ```json + { + "tags": [ + ["d", ""], + ["p", "", "admin"], + ["p", "", "admin"] + ] + } + ``` + +- **39002 (GROUP_MEMBERS)**: List of all group members + ```json + { + "tags": [ + ["d", ""], + ["p", ""], + ["p", ""] + ] + } + ``` + +- **39003 (GROUP_ROLES)**: Available roles in the group + ```json + { + "tags": [ + ["d", ""], + ["role", "admin"], + ["role", "moderator"], + ["role", "member"] + ] + } + ``` + +## Group Types and Permissions + +### Group Visibility Types + +1. **Public Groups** (default) + - Anyone can read events + - Members can write events + - No authentication required to read + +2. **Private Groups** (with `private` tag) + - Only members can read events + - Only members can write events + - Requires NIP-42 authentication + +### Group Access Types + +1. **Open Groups** (default) + - Join requests are automatically approved + - Anyone can become a member + +2. **Closed Groups** (with `closed` tag) + - Requires invite code or admin approval + - Join requests are pending until approved + +### Permission Matrix + +| Action | Public+Open | Public+Closed | Private+Open | Private+Closed | +|--------|-------------|---------------|--------------|----------------| +| Read | Anyone | Anyone | Members only | Members only | +| Write | Members | Members | Members | Members | +| Join | Auto-approve | Invite/Approval | Auto-approve* | Invite/Approval* | + +*Requires authentication + +## Event Flow + +### Inbound Event Processing (Client → Relay) + +1. **WebSocket Message Received** + - Parsed as `ClientMessage` (EVENT, REQ, CLOSE, AUTH) + +2. **Middleware Processing** + - Each middleware can modify, reject, or pass through + - NIP-29 middleware validates group permissions + +3. **Group Event Handling** + - For group management events (9000-9009): + - Validate user has required role + - Execute group operation + - Generate addressable events (39000-39003) + - For regular events with `h` tag: + - Verify user is group member + - Add group-specific tags + +4. **Storage and Forwarding** + - Store in SQLite database + - Forward to backing relay + - Broadcast generated events to subscribers + +### Outbound Event Processing (Relay → Client) + +1. **Event Reception** + - From backing relay subscription + - From database queries + +2. **Filtering** + - NIP-29 middleware checks group visibility + - Private groups: only send to authenticated members + - Remove events from deleted groups + +3. **Client Delivery** + - Convert to `RelayMessage` + - Send over WebSocket connection + +## Data Structures + +### Group State + +```rust +struct Group { + id: String, // Group identifier + name: String, // Display name + about: Option, // Description + picture: Option, // Avatar URL + private: bool, // Requires auth to read + closed: bool, // Requires invite to join + created_at: u64, // Unix timestamp + admins: HashSet, // Admin public keys + members: HashSet, // All member public keys + roles: HashMap, // pubkey → role mapping + join_requests: HashMap, // Pending requests + invites: HashMap, // Active invite codes +} +``` + +### Groups Manager + +```rust +struct Groups { + groups: Arc>, // Thread-safe group storage + relay_keys: Keys, // Relay's signing keys +} +``` + +## Configuration + +### YAML Configuration Files + +**settings.yml** (default configuration): +```yaml +relay_secret_key: "<64-char-hex>" # Relay's private key +local_addr: "0.0.0.0:8080" # Listen address +relay_url: "ws://127.0.0.1:7777" # Backing relay WebSocket +public_url: "wss://example.com" # Public-facing URL +db_path: "./db/groups.db" # SQLite database path +auth_required: false # Global auth requirement +auth_url: "wss://example.com" # Expected URL in AUTH +``` + +**settings.local.yml** (overrides for local development) + +### Environment Variables + +- `DATABASE_URL`: Override database path +- `RUST_LOG`: Logging level (debug, info, warn, error) + +## Database Schema + +### Events Table +```sql +CREATE TABLE events ( + id TEXT PRIMARY KEY, + pubkey TEXT NOT NULL, + created_at INTEGER NOT NULL, + kind INTEGER NOT NULL, + tags TEXT NOT NULL, + content TEXT NOT NULL, + sig TEXT NOT NULL +); + +CREATE INDEX idx_events_pubkey ON events(pubkey); +CREATE INDEX idx_events_created_at ON events(created_at); +CREATE INDEX idx_events_kind ON events(kind); +``` + +## Frontend Integration + +The relay includes a React-based web UI for group management: + +- **Create groups**: Web form for creating new groups +- **Browse groups**: List all available groups +- **Join groups**: Request to join or use invite codes +- **Manage groups**: Admin interface for member management + +The frontend is served from the same port as the WebSocket relay, with routing handled by Axum. + +## Deployment + +### Docker Deployment + +```dockerfile +# Multi-stage build +FROM rust:alpine AS builder +# Build relay + +FROM node:alpine AS frontend-builder +# Build frontend + +FROM alpine:latest +# Copy artifacts and run +``` + +### Docker Compose Setup + +```yaml +services: + strfry: + image: dockurr/strfry + volumes: + - ./config/strfry.conf:/app/strfry.conf + - ./db/strfry:/app/db + ports: + - "7777:7777" + + groups-relay: + build: . + depends_on: + - strfry + environment: + - DATABASE_URL=/app/db/groups.db + volumes: + - ./db:/app/db + - ./config:/app/config + ports: + - "8080:8080" +``` + +## Testing with Nostr Clients + +### Using nostril (CLI) + +```bash +# Create a group +nostril --envelope --sec $PRIVKEY --content "" \ + --kind 9007 \ + --tag name "Test Group" \ + --tag about "A test group" | \ + websocat ws://localhost:8080 + +# Join a group +nostril --envelope --sec $PRIVKEY --content "" \ + --kind 9021 \ + --tag h "group-id" | \ + websocat ws://localhost:8080 + +# Send message to group +nostril --envelope --sec $PRIVKEY --content "Hello group!" \ + --kind 1 \ + --tag h "group-id" | \ + websocat ws://localhost:8080 +``` + +### REQ Filters + +```json +// Get group metadata +{"kinds": [39000], "authors": ["relay-pubkey"], "#d": ["group-id"]} + +// Get group members +{"kinds": [39002], "authors": ["relay-pubkey"], "#d": ["group-id"]} + +// Get group messages +{"kinds": [1], "#h": ["group-id"]} + +// Get all groups +{"kinds": [39000], "authors": ["relay-pubkey"]} +``` + +## Security Considerations + +1. **Authentication**: Uses NIP-42 AUTH for user verification +2. **Authorization**: Role-based access control (Admin, Member, Custom roles) +3. **Signature Verification**: All events are cryptographically verified +4. **Relay Signatures**: Addressable events are signed by relay's key +5. **Rate Limiting**: Should be implemented at reverse proxy level +6. **Database Security**: SQLite with prepared statements to prevent injection + +## Performance Optimizations + +1. **Concurrent HashMap**: DashMap allows concurrent read/write access +2. **Lazy Loading**: Groups loaded from DB only on startup +3. **Caching**: In-memory group state avoids database queries +4. **Batch Processing**: Multiple events can be processed in parallel +5. **Connection Pooling**: Reuses WebSocket connections to backing relay + +## Monitoring and Debugging + +1. **Health Endpoint**: GET `/health` returns relay status +2. **Logging**: Configurable via `RUST_LOG` environment variable +3. **Metrics**: Can add Prometheus metrics to middleware +4. **Event Inspection**: Logger middleware shows all events + +## Known Limitations + +1. **Single Relay**: Currently supports only one backing relay +2. **Memory Usage**: All groups kept in memory (could be issue at scale) +3. **No Clustering**: Single instance only, no horizontal scaling +4. **Invite Expiry**: Invites don't automatically expire +5. **Migration**: No built-in data migration tools + +## Future Enhancements + +1. **Multiple Backing Relays**: Support relay pools +2. **Redis State Store**: For horizontal scaling +3. **Advanced Roles**: More granular permissions +4. **Moderation Tools**: Spam filtering, word filters +5. **Analytics**: Group activity metrics +6. **Webhooks**: External integrations +7. **Federation**: Cross-relay group synchronization \ No newline at end of file