Highly ambitious ATProtocol AppView service and sdks
138
fork

Configure Feed

Select the types of activity you want to include in your feed.

start getting some docs together

+1729 -1105
+194 -107
README.md
··· 1 - # Slices Social - A social app for sharing/creating AT Protocol Appviews 1 + # Slices 2 2 3 - Slices Social is an AT Protocol appview that allows users to create and share 4 - slices (appviews). 3 + An open-source platform for building AT Protocol appviews with custom data 4 + schemas, automatic SDK generation, and built-in sync capabilities. 5 5 6 - ## Architecture 6 + ## Overview 7 7 8 - - **API** (`/api`) - Rust backend with AT Protocol integration, PostgreSQL 9 - database, and dynamic XRPC handlers 10 - - **Frontend** (`/frontend`) - Deno server-side rendered application with OAuth 11 - authentication 8 + Slices enables developers to create "slices" - custom appviews within the AT 9 + Protocol ecosystem. Each slice can define its own lexicons (schemas), sync data 10 + from other AT Protocol services, and automatically generate type-safe SDKs. 11 + Think of it as your own customizable view into the AT Protocol network. 12 12 13 - ## ✅ Completed Features 13 + ### Key Features 14 14 15 - ### Core Infrastructure 15 + - **Custom Lexicons**: Define your own data schemas using AT Protocol lexicons 16 + - **Automatic SDK Generation**: Get type-safe TypeScript clients generated from 17 + your lexicons 18 + - **Data Synchronization**: Import and index data from Bluesky and other AT 19 + Protocol services 20 + - **OAuth Integration**: Built-in AT Protocol authentication 21 + - **Multi-tenant Architecture**: Each slice operates independently with its own 22 + data validated against its lexicons 23 + - **Dynamic API Endpoints**: CRUD operations automatically created for each 24 + lexicon record type (collection) 16 25 17 - - [x] AT Protocol OAuth integration with AIP server 18 - - [x] PostgreSQL database with slice-aware queries 19 - - [x] Docker containerization with Nix flakes 20 - - [x] Production deployment pipeline 26 + ## Documentation 21 27 22 - ### Slice Management 28 + - [Introduction](./docs/intro.md) - What is Slices and why use it 29 + - [Getting Started](./docs/getting-started.md) - Set up your first slice 30 + - [Core Concepts](./docs/concepts.md) - Understand slices, lexicons, and 31 + collections 32 + - [API Reference](./docs/api-reference.md) - Complete API documentation 33 + - [SDK Usage](./docs/sdk-usage.md) - Using generated TypeScript clients 23 34 24 - - [x] Create and manage slice records 25 - - [x] Define custom lexicons for slices (appviews) 26 - - [x] Slice-specific data filtering 27 - - [x] Slice statistics (records, collections, actors) 35 + ## Quick Start 28 36 29 - ### Data Synchronization 37 + ### Prerequisites 30 38 31 - - [x] Bulk sync from AT Protocol repositories based on domain specific lexicons 32 - - [x] Collection-specific sync with repo filtering 33 - - [x] Indexing of records into local database 34 - - [x] Slice-aware record filtering during sync 39 + - Docker and Docker Compose 40 + - PostgreSQL 41 + - Rust (for API development) 42 + - Deno (for frontend) 35 43 36 - ### TypeScript Client Generation 44 + ### Installation 45 + 46 + 1. Clone the repository: 47 + 48 + ```bash 49 + git clone https://tangled.sh/justslices.net/core 50 + cd core 51 + ``` 52 + 53 + 2. Set up environment variables: 37 54 38 - - [x] Dynamic TypeScript client generation from lexicons 39 - - [x] Nested collection structure (e.g., 40 - `client.social.slices.slice.listRecords()`) 41 - - [x] CRUD operations (create, read, update, delete) 42 - - [x] OAuth client with PKCE authentication flow 43 - - [x] Slice-aware collection operations 44 - - [x] Optional authentication for read-only operations (e.g., listRecords, 45 - getRecord) 55 + Create `.env` files in both `/api` and `/frontend` directories (see 56 + [Getting Started](./docs/getting-started.md) for details). 57 + 58 + 3. Start the services: 46 59 47 - ### Frontend 60 + ```bash 61 + # Start the API 62 + cd api 63 + cargo run 48 64 49 - - [x] Server-side rendered pages with HTMX 50 - - [x] User authentication and session management 51 - - [x] Slice overview with index statistics 52 - - [x] Records browser with collection/author filtering 53 - - [x] Sync interface with collection prefilling from lexicons 54 - - [x] Settings page to edit profile 55 - - [x] Encrypted cookie-based sessions 65 + # In another terminal, start the frontend 66 + cd frontend 67 + deno task dev 68 + ``` 56 69 57 - ### API Endpoints 70 + 4. Visit `http://localhost:8000` to access the web interface. 58 71 59 - - [x] `/xrpc/social.slices.slice.sync` - Bulk synchronization 60 - - [x] `/xrpc/social.slices.slice.stats` - Slice statistics 61 - - [x] `/xrpc/social.slices.slice.records` - Slice records with filtering 62 - - [x] `/xrpc/social.slices.slice.codegen` - Client code generation (currently 63 - TypeScript only, but designed for extensibility) 64 - - [x] Dynamic collection XRPC endpoints (`*.list`, `*.get`, `*.create`, etc.) 65 - - [x] OAuth endpoints for authentication flow 72 + ## Project Structure 66 73 67 - ## 🚧 In Progress/Next Up 74 + ### API (`/api`) 68 75 69 - - [ ] Connect to Jetstream 70 - - [ ] Add search and filtering functionality to records browser, and generated 71 - client 72 - - [ ] Support more complex lexicon types (e.g., unions, arrays, refs) 73 - - [ ] Lexicon validation 74 - - [ ] Lexicon verification 75 - - [ ] SDK examples and tutorials, implement Statusphere Slice 76 - - [ ] Pagination for large record sets, maybe even a table view would be cool 77 - - [ ] Slice timeline and user profile pages 78 - - [ ] Display xprc docs for dynamic endpoints (looking at 79 - [Scalar](https://github.com/scalar/scalar)) 76 + The backend is built in Rust and serves as the core AT Protocol integration 77 + layer. It provides: 80 78 81 - ## ❌ The Future 79 + - **AT Protocol XRPC Handlers**: Dynamic endpoints for slice-specific 80 + collections with full CRUD operations 81 + - **Sync Engine**: Bulk synchronization from AT Protocol repositories 82 + - **Jetstream Integration**: Real-time data streaming from AT Protocol firehose 83 + - **Database Layer**: PostgreSQL integration with slice-aware queries 84 + - **SDK Generation**: Automatically generates type-safe TypeScript clients and 85 + OpenAPI specifications 86 + - **OAuth Integration**: Handles AT Protocol OAuth flows and token management 82 87 83 - - [ ] Integrated labeler service for basic moderation 84 - - [ ] Auto view hydration strategies, maybe configureable in the UI 85 - - [ ] Add pre-defined lexicons straight from Lexicon Community or other sources 86 - - [ ] Support more languages for client code generation (e.g., Python, 87 - Flutter/Dart, Swift) 88 - - [ ] Integrate/interop with various Microcosm services 89 - - [ ] Cli tool for codegen and other utilities 90 - - [ ] Background job processing queue for syncs 91 - - [ ] Rate limiting and API quotas 92 - - [ ] API docs 93 - - [ ] Appview bug tracking, waitlists, feature flags, analytics, etc. 94 - - [ ] Fork a slice! 95 - - [ ] Strategies for managing/migrating lexicon changes over time 88 + ### Frontend (`/frontend`) 96 89 97 - ## 🛠️ Development 90 + A server-side rendered web application built with Deno that provides: 98 91 99 - ### Prerequisites 92 + - **User Interface**: Web-based slice management, records browsing, and sync 93 + controls 94 + - **Authentication**: OAuth integration with session management 95 + - **Server-Side Rendering**: HTMX-powered interactive UI with minimal 96 + client-side JavaScript 97 + - **Generated Client Integration**: Uses auto-generated TypeScript clients for 98 + API communication 100 99 101 - - Nix with flakes enabled (not required, used for deployment) 102 - - Docker 103 - - PostgreSQL 100 + ## Development 104 101 105 - ### Getting Started 102 + ### Running Tests 106 103 107 104 ```bash 108 - # Start API development 105 + # API tests 109 106 cd api 110 - cargo run 107 + cargo test 111 108 112 - # Start frontend development 109 + # Frontend tests 113 110 cd frontend 114 - deno task dev 111 + deno test 115 112 ``` 116 113 117 - ### Useful Scripts 114 + ### Database Migrations 118 115 119 - - `api/scripts/test_sync.sh` - Test local sync endpoint (pre-seed all slices 120 - from the atmosphere) 121 - - `frontend/scripts/register-oauth-client.sh` - Register OAuth client with AIP 116 + ```bash 117 + cd api 118 + sqlx migrate run 119 + 120 + # If you modify database queries, update the query cache 121 + cargo sqlx prepare 122 + ``` 123 + 124 + ### Building for Production 125 + 126 + ```bash 127 + # Build the API 128 + cd api 129 + cargo build --release 130 + ``` 131 + 132 + ## Contributing 122 133 123 - ## 🚀 Deployment 134 + ### How to Contribute 124 135 125 - The service is deployed using Nix-built Docker containers with: 136 + 1. Fork the repository 137 + 2. Create a feature branch (`git checkout -b feature/amazing-feature`) 138 + 3. Commit your changes (`git commit -m 'Add amazing feature'`) 139 + 4. Push to the branch (`git push origin feature/amazing-feature`) 140 + 5. Open a Pull Request 126 141 127 - - Fly.io for hosting (move to Upscale?) 128 - - PostgreSQL for data storage 129 - - Environment-based configuration 142 + ### Development Guidelines 130 143 131 - ## 📝 Environment Variables 144 + - Follow Rust conventions for API code 145 + - Use Deno formatting for TypeScript/JavaScript 146 + - Write tests for new features following existing patterns 147 + - Update documentation as needed 148 + - Keep commits focused and descriptive 132 149 133 - ### API 150 + ### Areas for Contribution 134 151 135 - - `DATABASE_URL` - PostgreSQL connection string 136 - - `AUTH_BASE_URL` - AIP OAuth service URL 137 - - `PORT` - Server port (default: 3000) 152 + - Lexicon validation and verification 153 + - UI/UX improvements 154 + - Documentation and examples 155 + - Bug fixes and performance improvements 138 156 139 - ### Frontend 157 + ## Deployment 140 158 141 - - `OAUTH_CLIENT_ID` - OAuth application client ID 142 - - `OAUTH_CLIENT_SECRET` - OAuth application client secret 143 - - `OAUTH_REDIRECT_URI` - OAuth callback URL 144 - - `OAUTH_AIP_BASE_URL` - AIP OAuth service URL 145 - - `SESSION_ENCRYPTION_KEY` - Session cookie encryption key 146 - - `API_URL` - Backend API base URL 147 - - `SLICE_URI` - Filters collection based queries by slice 159 + The service can be deployed using Docker containers. We provide: 160 + 161 + - Docker Compose configuration for local development 162 + - Nix flakes for reproducible builds 163 + - Fly.io configuration for cloud deployment 164 + 165 + See the [deployment guide](./docs/deployment.md) for detailed instructions. 166 + 167 + ## Environment Variables 168 + 169 + ### API Configuration 170 + 171 + | Variable | Description | Required | 172 + | --------------- | ---------------------------- | -------- | 173 + | `DATABASE_URL` | PostgreSQL connection string | Yes | 174 + | `AUTH_BASE_URL` | AIP OAuth service URL | Yes | 175 + | `PORT` | Server port (default: 3000) | No | 176 + 177 + ### Frontend Configuration 178 + 179 + | Variable | Description | Required | 180 + | --------------------- | ------------------------------- | -------- | 181 + | `OAUTH_CLIENT_ID` | OAuth application client ID | Yes | 182 + | `OAUTH_CLIENT_SECRET` | OAuth application client secret | Yes | 183 + | `OAUTH_REDIRECT_URI` | OAuth callback URL | Yes | 184 + | `OAUTH_AIP_BASE_URL` | AIP OAuth service URL | Yes | 185 + | `API_URL` | Backend API base URL | Yes | 186 + | `SLICE_URI` | Default slice URI for queries | Yes | 187 + 188 + ## Roadmap 189 + 190 + ### In Progress 191 + 192 + - Documentation 193 + - Frontend UX improvements/social features 194 + - Support more search and filtering params in collection xrpx handlers and SDK 195 + - Surface jetstream and sync logs in the UI 196 + - Improve sync and jetstream reliability 197 + - Monitor api container performance and resource usage 198 + 199 + ### Planned Features 200 + 201 + - Labeler service integration 202 + - CLI tool 203 + - API rate limiting 204 + - Enhanced lexicon management UI 205 + - Lexicon discovery and sharing 206 + 207 + ## Community 208 + 209 + - **Bluesky**: [@justslices.net](https://bsky.app/profile/justslices.net) 210 + - **Discord**: [Join our server](https://discord.gg/your-invite) 211 + 212 + ## Support 213 + 214 + - [Documentation](./docs/) 215 + - [Discord Community](https://discord.gg/your-invite) 216 + 217 + ## License 218 + 219 + This project is licensed under the MIT License - see the [LICENSE](LICENSE) file 220 + for details. 221 + 222 + ## Acknowledgments 223 + 224 + - Built on the [AT Protocol](https://atproto.com) 225 + - Inspired by the AT Protocol community 226 + - Thanks to all contributors 227 + 228 + ## Status 229 + 230 + This project is in active development. APIs may change as we approach v1.0. 231 + 232 + --- 233 + 234 + Made with love for the AT Protocol ecosystem ❤️
-91
api/README.md
··· 1 - # Slice - AT Protocol Indexer 2 - 3 - A Rust-based AT Protocol indexer service with HTMX web interface for syncing and viewing AT Protocol records. 4 - 5 - ## Features 6 - 7 - - 📚 **Bulk Collection Sync**: Efficiently sync entire AT Protocol collections 8 - - 🔄 **Smart Discovery**: Automatically find repositories with target collections 9 - - 🌐 **Web Interface**: HTMX-powered UI for easy bulk operations 10 - - 🚀 **XRPC API**: Native AT Protocol XRPC endpoints 11 - - 🗄️ **PostgreSQL Storage**: Efficient JSONB storage with smart indexing 12 - 13 - ## Quick Start 14 - 15 - ### Prerequisites 16 - 17 - - Rust 1.70+ 18 - - PostgreSQL 12+ 19 - 20 - ### Setup 21 - 22 - 1. **Clone and setup**: 23 - ```bash 24 - git clone <repo> 25 - cd slice 26 - ``` 27 - 28 - 2. **Database setup**: 29 - ```bash 30 - createdb slice 31 - export DATABASE_URL="postgresql://localhost/slice" 32 - ``` 33 - 34 - 3. **Run the server**: 35 - ```bash 36 - cargo run 37 - ``` 38 - 39 - 4. **Open web interface**: http://127.0.0.1:3000 40 - 41 - ## Usage 42 - 43 - ### Web Interface 44 - 45 - - **Home**: Overview and quick links 46 - - **Records**: Browse indexed records by collection 47 - - **Sync**: Manually sync individual records 48 - 49 - ### API Endpoints 50 - 51 - - `GET /xrpc/social.slices.records.list?collection=app.bsky.feed.post` - List records 52 - - `POST /xrpc/social.slices.collections.bulkSync` - Bulk sync collections 53 - 54 - ### Example: Bulk Sync Collections 55 - 56 - ```bash 57 - curl -X POST "http://127.0.0.1:3000/xrpc/social.slices.collections.bulkSync" \ 58 - -H "Content-Type: application/json" \ 59 - -d '{"collections": ["app.bsky.feed.post", "app.bsky.actor.profile"]}' 60 - ``` 61 - 62 - ### Popular Collections to Sync 63 - 64 - ``` 65 - app.bsky.feed.post # Bluesky posts 66 - app.bsky.actor.profile # User profiles 67 - app.bsky.feed.like # Likes 68 - app.bsky.feed.repost # Reposts 69 - app.bsky.graph.follow # Follows 70 - ``` 71 - 72 - ## Architecture 73 - 74 - Built following the [AT Protocol Indexer Specification](docs/atproto_indexer_spec.md): 75 - 76 - - **Single Table Design**: All records in one `record` table with JSONB for flexibility 77 - - **Smart Syncing**: Hybrid approach supporting both individual record fetch and bulk operations 78 - - **Future CAR Support**: Architecture ready for CAR file import for efficient bulk syncing 79 - 80 - ## Development 81 - 82 - ```bash 83 - # Run with auto-reload 84 - cargo watch -x run 85 - 86 - # Run tests 87 - cargo test 88 - 89 - # Check code 90 - cargo clippy 91 - ```
-907
api/docs/atproto_indexer_spec.md
··· 1 - # AT Protocol Indexing Service - Technical Specification 2 - 3 - ## Project Overview 4 - 5 - Build a high-performance, scalable indexing service for AT Protocol that 6 - automatically generates typed APIs for any lexicon, with intelligent data 7 - fetching strategies and real-time synchronization. 8 - 9 - ### Core Goals 10 - 11 - - **Universal Lexicon Support**: Automatically handle any AT Protocol lexicon 12 - without manual configuration 13 - - **Multi-Language Client Generation**: Generate typed API clients for 14 - TypeScript, Rust, Python, Go, etc. 15 - - **High Performance**: Handle millions of records efficiently with smart 16 - caching and batching 17 - - **Real-time Sync**: Support both bulk imports and live firehose updates 18 - - **Developer Experience**: Hasura-style auto-generated APIs with full type 19 - safety 20 - 21 - ## Architecture Overview 22 - 23 - ### Data Storage Strategy 24 - 25 - **Primary Database: PostgreSQL** 26 - 27 - - Single source of truth for all indexed records 28 - - Single table approach for maximum flexibility across arbitrary lexicons 29 - - JSONB for complete record storage and sophisticated querying 30 - - Optional partitioning by collection for very high volume deployments 31 - 32 - ```sql 33 - -- Single table for all AT Protocol records 34 - CREATE TABLE IF NOT EXISTS "record" ( 35 - "uri" TEXT PRIMARY KEY NOT NULL, 36 - "cid" TEXT NOT NULL, 37 - "did" TEXT NOT NULL, 38 - "collection" TEXT NOT NULL, 39 - "json" JSONB NOT NULL, -- Use JSONB for performance and querying 40 - "indexedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() 41 - ); 42 - 43 - -- Essential indexes for performance 44 - CREATE INDEX IF NOT EXISTS idx_record_collection ON "record"("collection"); 45 - CREATE INDEX IF NOT EXISTS idx_record_did ON "record"("did"); 46 - CREATE INDEX IF NOT EXISTS idx_record_indexed_at ON "record"("indexedAt"); 47 - CREATE INDEX IF NOT EXISTS idx_record_json_gin ON "record" USING GIN("json"); 48 - 49 - -- Collection-specific indexes for common queries 50 - CREATE INDEX IF NOT EXISTS idx_record_collection_did ON "record"("collection", "did"); 51 - CREATE INDEX IF NOT EXISTS idx_record_cid ON "record"("cid"); 52 - ``` 53 - 54 - **Caching Strategy** 55 - 56 - - **Redis**: Hot data caching, query result caching, rate limiting 57 - - **Application-level**: Compiled lexicon handlers, parsed schemas 58 - - **CDN**: Public API endpoints with appropriate cache headers 59 - 60 - **PostgreSQL JSONB Advantages** 61 - 62 - - **GIN indexes**: Fast querying on JSON content with `@>`, `?`, `?&`, `?|` 63 - operators 64 - - **JSON operators**: Rich querying with `->`, `->>`, `#>`, `#>>` for nested 65 - access 66 - - **JSON path queries**: Complex nested field access and filtering 67 - - **Performance**: JSONB stored in optimized binary format for fast access 68 - - **Flexibility**: Handle arbitrary lexicon schemas without schema migrations 69 - 70 - ### Search Implementation 71 - 72 - **Hybrid Approach**: 73 - 74 - - **PostgreSQL**: Primary queries, exact matches, admin operations, complex 75 - joins 76 - - **Optional Search Engine**: User-facing search, fuzzy matching, aggregations, 77 - analytics 78 - 79 - **Search Engine Options**: 80 - 81 - - **Typesense**: Easy setup, good performance for smaller deployments 82 - - **Meilisearch**: Excellent for instant search experiences 83 - - **Elasticsearch/OpenSearch**: Full-featured for large-scale deployments 84 - 85 - ## Record Fetching Strategies 86 - 87 - ### Decision Matrix 88 - 89 - | Scenario | Strategy | Reasoning | 90 - | -------------------- | ----------------------- | -------------------------------------------- | 91 - | Initial sync | CAR file download | Most efficient for bulk data | 92 - | Real-time updates | Firehose stream | Live updates as they happen | 93 - | Catch-up sync (<24h) | List + individual fetch | Good for small gaps | 94 - | Catch-up sync (>24h) | CAR file re-download | More efficient than many individual requests | 95 - | Single record update | Individual fetch | Targeted and fast | 96 - 97 - ### Implementation Strategy 98 - 99 - ```rust 100 - async fn smart_sync(&self, did: &str) -> Result<()> { 101 - let last_sync = self.get_last_sync_time(did).await?; 102 - 103 - match last_sync { 104 - None => self.sync_repo_car(did).await?, // Initial: CAR file 105 - Some(last) if Utc::now() - last > Duration::hours(24) => { 106 - self.sync_repo_car(did).await? // Full resync: CAR file 107 - } 108 - Some(last) => { 109 - self.incremental_sync(did, last).await? // Incremental: List + fetch 110 - } 111 - } 112 - 113 - Ok(()) 114 - } 115 - ``` 116 - 117 - ## Dynamic Lexicon System 118 - 119 - ### Why Single Table Works Better for AT Protocol 120 - 121 - **Lexicon characteristics that favor single table:** 122 - 123 - - **Runtime schema definition**: Lexicons can be arbitrary and defined by any 124 - developer 125 - - **Shared metadata**: All records have common fields (CID, timestamp, author, 126 - etc.) 127 - - **Flexible querying**: Query across different record types seamlessly 128 - - **Unknown schema count**: Could have hundreds of different lexicons 129 - 130 - ### Unified Query Interface 131 - 132 - **Cross-lexicon querying capabilities:** 133 - 134 - ```sql 135 - -- Posts with specific hashtags 136 - SELECT * FROM "record" 137 - WHERE "collection" = 'app.bsky.feed.post' 138 - AND "json"->>'text' ILIKE '%#atproto%'; 139 - 140 - -- All records by author across all lexicons 141 - SELECT "collection", COUNT(*) FROM "record" 142 - WHERE "did" = 'did:plc:example' 143 - GROUP BY "collection"; 144 - 145 - -- Cross-lexicon search for any record with text content 146 - SELECT * FROM "record" 147 - WHERE "json" ? 'text' 148 - AND "json"->>'text' ILIKE '%search term%'; 149 - 150 - -- Recent records across all collections 151 - SELECT "uri", "collection", "json"->>'$type' as record_type, "indexedAt" 152 - FROM "record" 153 - WHERE "indexedAt" > NOW() - INTERVAL '24 hours' 154 - ORDER BY "indexedAt" DESC; 155 - ``` 156 - 157 - ### Schema Management 158 - 159 - **Components**: 160 - 161 - 1. **Lexicon Registry**: Parse and store lexicon definitions for validation 162 - 2. **Indexer Lexicons**: Define the indexer's own XRPC procedures with proper 163 - lexicons 164 - 3. **Validation Layer**: Ensure records conform to their lexicon schemas 165 - 4. **XRPC Server**: Serve both indexed AT Protocol data and indexer's own 166 - procedures 167 - 5. **Type Generator**: Generate typed interfaces for all lexicons (AT Protocol + 168 - indexer) 169 - 170 - ### Dynamic Index Creation 171 - 172 - ```sql 173 - -- Add lexicon-specific indexes as needed for performance 174 - CREATE INDEX IF NOT EXISTS idx_posts_text ON "record" USING GIN(("json"->'text')) 175 - WHERE "collection" = 'app.bsky.feed.post'; 176 - 177 - CREATE INDEX IF NOT EXISTS idx_profiles_handle ON "record"(("json"->>'handle')) 178 - WHERE "collection" = 'app.bsky.actor.profile'; 179 - 180 - -- For very high volume, consider partitioning by collection 181 - CREATE TABLE "record_posts" PARTITION OF "record" 182 - FOR VALUES IN ('app.bsky.feed.post'); 183 - 184 - -- Composite indexes for common query patterns 185 - CREATE INDEX IF NOT EXISTS idx_record_collection_created_at ON "record"("collection", ("json"->>'createdAt')) 186 - WHERE "json" ? 'createdAt'; 187 - ``` 188 - 189 - ### Implementation Strategy 190 - 191 - ```rust 192 - async fn register_lexicon(lexicon: LexiconDoc) -> Result<()> { 193 - // 1. Store lexicon definition for validation 194 - self.store_lexicon_schema(lexicon).await?; 195 - 196 - // 2. Create collection-specific indexes if needed 197 - self.create_performance_indexes(&lexicon.id).await?; 198 - 199 - // 3. Register XRPC handlers for core AT Protocol lexicons 200 - if lexicon.id.starts_with("com.atproto.") { 201 - self.register_atproto_handlers(&lexicon.id).await?; 202 - } 203 - 204 - // 4. Generate TypeScript types for all lexicons (AT Protocol + indexer) 205 - self.generate_client_types(&lexicon.id).await?; 206 - 207 - Ok(()) 208 - } 209 - 210 - async fn initialize_indexer_lexicons(&self) -> Result<()> { 211 - // Define and register the indexer's own XRPC procedures 212 - let indexer_lexicons = vec![ 213 - self.create_list_records_lexicon(), 214 - self.create_search_records_lexicon(), 215 - self.create_get_record_lexicon(), 216 - // ... other indexer procedures 217 - ]; 218 - 219 - for lexicon in indexer_lexicons { 220 - self.register_indexer_procedure(lexicon).await?; 221 - } 222 - 223 - Ok(()) 224 - } 225 - ``` 226 - 227 - ### Record Validation 228 - 229 - **Validation Layer**: Ensure data integrity with lexicon schema validation 230 - 231 - ```rust 232 - async fn insert_record(&self, record: ATProtoRecord) -> Result<()> { 233 - // 1. Validate against lexicon schema 234 - let lexicon = self.get_lexicon_schema(&record.collection).await?; 235 - self.validate_record_against_schema(&record.json, &lexicon)?; 236 - 237 - // 2. Insert with proper indexing 238 - sqlx::query!( 239 - r#"INSERT INTO "record" ("uri", "cid", "did", "collection", "json", "indexedAt") 240 - VALUES ($1, $2, $3, $4, $5, $6) 241 - ON CONFLICT ("uri") 242 - DO UPDATE SET 243 - "cid" = EXCLUDED."cid", 244 - "json" = EXCLUDED."json", 245 - "indexedAt" = EXCLUDED."indexedAt""#, 246 - record.uri, 247 - record.cid, 248 - record.did, 249 - record.collection, 250 - record.json, 251 - record.indexed_at 252 - ).execute(&self.db).await?; 253 - 254 - Ok(()) 255 - } 256 - 257 - // Batch processing for CAR file imports 258 - async fn batch_insert_records(&self, records: &[ATProtoRecord]) -> Result<()> { 259 - let mut tx = self.db.begin().await?; 260 - 261 - for record in records { 262 - sqlx::query!( 263 - r#"INSERT INTO "record" ("uri", "cid", "did", "collection", "json", "indexedAt") 264 - VALUES ($1, $2, $3, $4, $5, $6) 265 - ON CONFLICT ("uri") 266 - DO UPDATE SET 267 - "cid" = EXCLUDED."cid", 268 - "json" = EXCLUDED."json", 269 - "indexedAt" = EXCLUDED."indexedAt""#, 270 - record.uri, 271 - record.cid, 272 - record.did, 273 - record.collection, 274 - record.json, 275 - record.indexed_at 276 - ).execute(&mut *tx).await?; 277 - } 278 - 279 - tx.commit().await?; 280 - Ok(()) 281 - } 282 - ``` 283 - 284 - ### API Generation Strategy 285 - 286 - **XRPC Endpoints** with proper lexicon definitions: 287 - 288 - ``` 289 - GET /xrpc/social.slices.records.list # List records for collection 290 - GET /xrpc/social.slices.records.get # Get specific record 291 - POST /xrpc/social.slices.records.create # Create record 292 - POST /xrpc/social.slices.records.update # Update record 293 - POST /xrpc/social.slices.records.delete # Delete record 294 - 295 - # Advanced query procedures 296 - GET /xrpc/social.slices.records.search # Full-text search on record content 297 - GET /xrpc/social.slices.records.filter # JSON field filtering 298 - GET /xrpc/social.slices.author.listRecords # All records by author (cross-collection) 299 - GET /xrpc/social.slices.search.global # Global search across all collections 300 - ``` 301 - 302 - **Lexicon Definitions** for indexer procedures: 303 - 304 - ```json 305 - { 306 - "lexicon": 1, 307 - "id": "social.slices.records.list", 308 - "defs": { 309 - "main": { 310 - "type": "query", 311 - "description": "List records for a specific collection", 312 - "parameters": { 313 - "collection": { 314 - "type": "string", 315 - "description": "Collection/lexicon ID (e.g. app.bsky.feed.post)", 316 - "required": true 317 - }, 318 - "author": { 319 - "type": "string", 320 - "description": "Filter by author DID" 321 - }, 322 - "limit": { 323 - "type": "integer", 324 - "minimum": 1, 325 - "maximum": 100, 326 - "default": 25 327 - }, 328 - "cursor": { 329 - "type": "string", 330 - "description": "Pagination cursor" 331 - } 332 - }, 333 - "output": { 334 - "encoding": "application/json", 335 - "schema": { 336 - "type": "object", 337 - "required": ["records"], 338 - "properties": { 339 - "records": { 340 - "type": "array", 341 - "items": { "$ref": "#/defs/indexedRecord" } 342 - }, 343 - "cursor": { "type": "string" } 344 - } 345 - } 346 - } 347 - }, 348 - "indexedRecord": { 349 - "type": "object", 350 - "required": ["uri", "cid", "value", "indexedAt"], 351 - "properties": { 352 - "uri": { "type": "string", "format": "at-uri" }, 353 - "cid": { "type": "string" }, 354 - "value": { "type": "unknown" }, 355 - "indexedAt": { "type": "string", "format": "datetime" }, 356 - "collection": { "type": "string" }, 357 - "rkey": { "type": "string" }, 358 - "authorDid": { "type": "string", "format": "did" } 359 - } 360 - } 361 - } 362 - } 363 - ``` 364 - 365 - **Benefits of XRPC + Lexicons**: 366 - 367 - - **Native AT Protocol**: Indexer becomes a proper AT Protocol service 368 - - **Discoverable APIs**: Lexicons can be fetched and introspected 369 - - **Type Generation**: Same code generation works for indexer APIs 370 - - **Consistent**: Uses established AT Protocol patterns 371 - - **Composable**: Can be mixed with other AT Protocol services 372 - 373 - **XRPC Implementation Examples**: 374 - 375 - ```rust 376 - // XRPC query handler for listing records 377 - async fn handle_list_records(&self, params: ListRecordsParams) -> Result<ListRecordsOutput> { 378 - let records = sqlx::query!( 379 - r#"SELECT "uri", "cid", "did", "collection", "json", "indexedAt" 380 - FROM "record" 381 - WHERE "collection" = $1 382 - AND ($2::text IS NULL OR "did" = $2) 383 - ORDER BY "indexedAt" DESC 384 - LIMIT $3"#, 385 - params.collection, 386 - params.author, 387 - params.limit.unwrap_or(25) as i32 388 - ).fetch_all(&self.db).await?; 389 - 390 - let indexed_records: Vec<IndexedRecord> = records.into_iter().map(|row| { 391 - IndexedRecord { 392 - uri: row.uri, 393 - cid: row.cid, 394 - did: row.did, 395 - collection: row.collection, 396 - value: serde_json::from_str(&row.json.to_string()).unwrap_or_default(), 397 - indexed_at: row.indexedAt.to_rfc3339(), 398 - } 399 - }).collect(); 400 - 401 - Ok(ListRecordsOutput { 402 - records: indexed_records, 403 - cursor: self.generate_cursor(&records).await?, 404 - }) 405 - } 406 - 407 - // XRPC search handler with JSONB queries 408 - async fn handle_search_records(&self, params: SearchParams) -> Result<SearchOutput> { 409 - let records = sqlx::query!( 410 - r#"SELECT "uri", "cid", "did", "collection", "json", "indexedAt" 411 - FROM "record" 412 - WHERE ($1::text IS NULL OR "collection" = $1) 413 - AND "json"->>'text' ILIKE $2 414 - ORDER BY "indexedAt" DESC 415 - LIMIT $3"#, 416 - params.collection, 417 - format!("%{}%", params.query), 418 - params.limit.unwrap_or(25) as i32 419 - ).fetch_all(&self.db).await?; 420 - 421 - Ok(SearchOutput { 422 - records: records.into_iter().map(|row| IndexedRecord { 423 - uri: row.uri, 424 - cid: row.cid, 425 - did: row.did, 426 - collection: row.collection, 427 - value: serde_json::from_str(&row.json.to_string()).unwrap_or_default(), 428 - indexed_at: row.indexedAt.to_rfc3339(), 429 - }).collect() 430 - }) 431 - } 432 - ``` 433 - 434 - ## Multi-Language Client Generation 435 - 436 - ### Initial Target: TypeScript 437 - 438 - **Primary focus**: Generate fully typed TypeScript clients for web applications 439 - and Node.js services 440 - 441 - - **Type Safety**: Complete interfaces for all request/response objects 442 - - **Auto-completion**: Full IDE support with generated types 443 - - **Runtime Validation**: Optional runtime type checking 444 - - **Documentation**: Auto-generated JSDoc comments from lexicon descriptions 445 - 446 - ### Future Language Support 447 - 448 - **Planned targets** for multi-language expansion: 449 - 450 - - **Rust**: High-performance services, CLI tools 451 - - **Python**: Data analysis, ML workflows, web backends 452 - - **Go**: Microservices, system tools 453 - 454 - ### Code Generation Pipeline 455 - 456 - **Extensible architecture** designed for multiple languages: 457 - 458 - ```rust 459 - trait CodeGenerator { 460 - fn generate_client(&self, lexicons: &[LexiconDoc]) -> Result<String>; 461 - fn generate_types(&self, lexicon: &LexiconDoc) -> Result<String>; 462 - fn generate_method(&self, nsid: &str, def: &LexiconDef) -> Result<String>; 463 - } 464 - 465 - // Initial implementation: TypeScript 466 - impl CodeGenerator for TypeScriptGenerator { 467 - fn generate_client(&self, lexicons: &[LexiconDoc]) -> Result<String> { 468 - // Generate TypeScript client with full type safety 469 - } 470 - } 471 - 472 - // Future implementations: 473 - // impl CodeGenerator for RustGenerator { /* ... */ } 474 - // impl CodeGenerator for PythonGenerator { /* ... */ } 475 - // impl CodeGenerator for GoGenerator { /* ... */ } 476 - ``` 477 - 478 - ### TypeScript Client Generation 479 - 480 - **Type-Safe Generic XRPC Client with Auto-Discovery:** 481 - 482 - ```typescript 483 - // Registry of all known collections -> their record types 484 - interface CollectionRecordMap { 485 - // Core AT Protocol (always included) 486 - "app.bsky.feed.post": PostRecord; 487 - "app.bsky.actor.profile": ProfileRecord; 488 - "app.bsky.feed.like": LikeRecord; 489 - 490 - // Dynamically discovered custom lexicons 491 - "recipes.cooking-app.com": RecipeRecord; 492 - "tasks.productivity-tool.io": TaskRecord; 493 - "photos.gallery-app.net": PhotoRecord; 494 - "someRecord.something-cool.indexer.com": SomeCustomRecord; 495 - } 496 - 497 - // Generic input/output types with conditional typing 498 - interface CreateRecordInput<T extends keyof CollectionRecordMap> { 499 - collection: T; 500 - repo: string; // The DID that will become the 'did' field 501 - rkey?: string; // Used to construct the URI 502 - record: CollectionRecordMap[T]; // Type depends on collection! 503 - } 504 - 505 - interface ListRecordsParams<T extends keyof CollectionRecordMap> { 506 - collection: T; 507 - author?: string; 508 - limit?: number; 509 - cursor?: string; 510 - } 511 - 512 - interface ListRecordsOutput<T extends keyof CollectionRecordMap> { 513 - records: Array<{ 514 - uri: string; 515 - cid: string; 516 - did: string; // Author DID 517 - collection: T; 518 - value: CollectionRecordMap[T]; // Typed based on collection (parsed from json field) 519 - indexedAt: string; 520 - }>; 521 - cursor?: string; 522 - } 523 - 524 - // Generated client class with conditional types 525 - export class ATProtoIndexerClient { 526 - private client: AxiosInstance; 527 - 528 - constructor(baseURL: string, accessToken?: string) { 529 - this.client = axios.create({ 530 - baseURL, 531 - headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {}, 532 - }); 533 - } 534 - 535 - // Generic method - fully typed based on collection parameter 536 - async createRecord<T extends keyof CollectionRecordMap>( 537 - input: CreateRecordInput<T>, 538 - ): Promise<CreateRecordOutput>; 539 - 540 - // Fallback for unknown collections 541 - async createRecord(input: { 542 - collection: string; 543 - repo: string; 544 - rkey?: string; 545 - record: unknown; 546 - }): Promise<CreateRecordOutput>; 547 - 548 - // Implementation handles both cases 549 - async createRecord(input: any): Promise<CreateRecordOutput> { 550 - const response = await this.client.post( 551 - "/xrpc/social.slices.records.create", 552 - input, 553 - ); 554 - return response.data; 555 - } 556 - 557 - // Generic typed list method 558 - async listRecords<T extends keyof CollectionRecordMap>( 559 - params: ListRecordsParams<T>, 560 - ): Promise<ListRecordsOutput<T>>; 561 - 562 - // Fallback for unknown collections 563 - async listRecords(params: { 564 - collection: string; 565 - author?: string; 566 - limit?: number; 567 - cursor?: string; 568 - }): Promise<ListRecordsOutput<string>>; 569 - 570 - async listRecords(params: any): Promise<any> { 571 - const response = await this.client.get("/xrpc/social.slices.records.list", { 572 - params, 573 - }); 574 - return response.data; 575 - } 576 - 577 - // Convenience methods for popular collections 578 - async createPost( 579 - input: Omit<CreateRecordInput<"app.bsky.feed.post">, "collection">, 580 - ) { 581 - return this.createRecord({ ...input, collection: "app.bsky.feed.post" }); 582 - } 583 - 584 - async listPosts( 585 - params: Omit<ListRecordsParams<"app.bsky.feed.post">, "collection">, 586 - ) { 587 - return this.listRecords({ ...params, collection: "app.bsky.feed.post" }); 588 - } 589 - 590 - // Auto-generated convenience methods for custom lexicons 591 - async createRecipe( 592 - input: Omit<CreateRecordInput<"recipes.cooking-app.com">, "collection">, 593 - ) { 594 - return this.createRecord({ 595 - ...input, 596 - collection: "recipes.cooking-app.com", 597 - }); 598 - } 599 - 600 - async searchRecords(params: SearchRecordsParams): Promise<SearchOutput> { 601 - const response = await this.client.get("/xrpc/social.slices.records.search", { 602 - params, 603 - }); 604 - return response.data; 605 - } 606 - } 607 - ``` 608 - 609 - **Usage Examples with Full Type Safety:** 610 - 611 - ```typescript 612 - const indexer = new ATProtoIndexerClient("https://indexer.example.com"); 613 - 614 - // ✅ Fully typed for known collections 615 - await indexer.createPost({ 616 - repo: "did:plc:user123", 617 - record: { 618 - $type: "app.bsky.feed.post", 619 - text: "Hello!", 620 - createdAt: new Date().toISOString(), 621 - // TypeScript knows this must be a PostRecord 622 - }, 623 - }); 624 - 625 - // ✅ Custom lexicon with full typing 626 - await indexer.createRecord({ 627 - collection: "recipes.cooking-app.com", 628 - repo: "did:plc:chef456", 629 - record: { 630 - $type: "recipes.cooking-app.com", 631 - title: "Pizza", 632 - ingredients: ["dough", "sauce", "cheese"], 633 - difficulty: "easy", 634 - // TypeScript enforces RecipeRecord structure 635 - }, 636 - }); 637 - 638 - // ✅ Query with same type safety - returns typed results 639 - const posts = await indexer.listPosts({ 640 - author: "did:plc:user123", 641 - limit: 50, 642 - }); 643 - // posts.records[0].value is typed as PostRecord! 644 - 645 - // ✅ Unknown collection - falls back gracefully 646 - await indexer.createRecord({ 647 - collection: "new-app.startup.xyz", 648 - repo: "did:plc:user789", 649 - record: { 650 - customField: "value", // No type checking, but still works 651 - }, 652 - }); 653 - ``` 654 - 655 - **Auto-Discovery Implementation:** 656 - 657 - ```rust 658 - // Indexer discovers and registers custom lexicons dynamically 659 - impl ATProtoIndexer { 660 - async fn discover_lexicons(&self) -> Result<Vec<LexiconDoc>> { 661 - let mut lexicons = Vec::new(); 662 - 663 - // Core AT Protocol lexicons 664 - lexicons.extend(self.load_core_lexicons().await?); 665 - 666 - // Custom lexicons from indexed records 667 - let custom_collections = sqlx::query!( 668 - r#"SELECT DISTINCT "collection" FROM "record" 669 - WHERE "collection" NOT LIKE 'app.bsky.%' 670 - AND "collection" NOT LIKE 'com.atproto.%'"# 671 - ).fetch_all(&self.db).await?; 672 - 673 - for row in custom_collections { 674 - if let Ok(lexicon) = self.fetch_lexicon_definition(&row.collection).await { 675 - lexicons.push(lexicon); 676 - } 677 - } 678 - 679 - Ok(lexicons) 680 - } 681 - 682 - async fn fetch_lexicon_definition(&self, nsid: &str) -> Result<LexiconDoc> { 683 - // Fetch from domain's well-known endpoint 684 - let domain = nsid.split('.').last().unwrap_or(""); 685 - let lexicon_url = format!("https://{}/.well-known/atproto/lexicon/{}", domain, nsid); 686 - 687 - let response = self.client.get(&lexicon_url).send().await?; 688 - let lexicon: LexiconDoc = response.json().await?; 689 - Ok(lexicon) 690 - } 691 - 692 - async fn regenerate_typescript_client(&self) -> Result<()> { 693 - let all_lexicons = self.discover_lexicons().await?; 694 - let typescript_code = self.typescript_generator.generate_client(&all_lexicons)?; 695 - 696 - // Write to file or serve via API endpoint 697 - self.write_client_code("typescript", &typescript_code).await?; 698 - Ok(()) 699 - } 700 - 701 - // Get statistics about indexed collections 702 - async fn get_collection_stats(&self) -> Result<Vec<CollectionStats>> { 703 - let stats = sqlx::query!( 704 - r#"SELECT "collection", 705 - COUNT(*) as record_count, 706 - COUNT(DISTINCT "did") as unique_authors, 707 - MIN("indexedAt") as first_indexed, 708 - MAX("indexedAt") as last_indexed 709 - FROM "record" 710 - GROUP BY "collection" 711 - ORDER BY record_count DESC"# 712 - ).fetch_all(&self.db).await?; 713 - 714 - Ok(stats.into_iter().map(|row| CollectionStats { 715 - collection: row.collection, 716 - record_count: row.record_count.unwrap_or(0) as u64, 717 - unique_authors: row.unique_authors.unwrap_or(0) as u64, 718 - first_indexed: row.first_indexed, 719 - last_indexed: row.last_indexed, 720 - }).collect()) 721 - } 722 - } 723 - ``` 724 - 725 - **Lexicon Discovery Protocol:** 726 - 727 - ```json 728 - // GET https://cooking-app.com/.well-known/atproto/lexicon/recipes.cooking-app.com 729 - { 730 - "lexicon": 1, 731 - "id": "recipes.cooking-app.com", 732 - "description": "Recipe sharing lexicon", 733 - "defs": { 734 - "main": { 735 - "type": "record", 736 - "record": { 737 - "type": "object", 738 - "required": ["$type", "title", "ingredients"], 739 - "properties": { 740 - "$type": { "const": "recipes.cooking-app.com" }, 741 - "title": { "type": "string" }, 742 - "ingredients": { "type": "array", "items": { "type": "string" } }, 743 - "cookingTime": { "type": "integer" }, 744 - "difficulty": { "type": "string", "enum": ["easy", "medium", "hard"] } 745 - } 746 - } 747 - } 748 - } 749 - } 750 - ``` 751 - 752 - **Generated CLI with Discovery:** 753 - 754 - ```bash 755 - # Generate TypeScript client with auto-discovered lexicons 756 - npx atproto-codegen typescript \ 757 - --discover \ 758 - --output ./src/generated/indexer-client.ts \ 759 - --endpoint https://your-indexer.com 760 - 761 - # Or specify additional custom lexicons 762 - npx atproto-codegen typescript \ 763 - --lexicons recipes.cooking-app.com,tasks.productivity-tool.io \ 764 - --output ./src/generated/indexer-client.ts \ 765 - --endpoint https://your-indexer.com 766 - ``` 767 - 768 - ## Implementation Technology Stack 769 - 770 - ### Backend: Rust 771 - 772 - **Rationale**: 773 - 774 - - Zero-copy parsing of CAR files and CBOR data 775 - - Memory safety for long-running indexing processes 776 - - High-performance concurrent processing 777 - - Strong type system prevents runtime errors 778 - - Excellent async ecosystem (Tokio) 779 - 780 - ### Client Generation: TypeScript (Initial Target) 781 - 782 - **Rationale**: 783 - 784 - - **Primary ecosystem**: Most AT Protocol developers use JavaScript/TypeScript 785 - - **Immediate value**: Web apps and Node.js services are common use cases 786 - - **Type safety**: Excellent TypeScript support for generated interfaces 787 - - **Developer experience**: Full IDE support with auto-completion 788 - - **Ecosystem compatibility**: Works with React, Next.js, Express, etc. 789 - 790 - ### Key Dependencies 791 - 792 - ```toml 793 - [dependencies] 794 - tokio = { version = "1.0", features = ["full"] } 795 - sqlx = { version = "0.7", features = ["postgres", "chrono", "serde_json"] } 796 - serde = { version = "1.0", features = ["derive"] } 797 - reqwest = { version = "0.11", features = ["json", "stream"] } 798 - libipld = { version = "0.16", features = ["dag-cbor", "car"] } 799 - tokio-tungstenite = "0.20" # WebSocket for firehose 800 - redis = { version = "0.23", features = ["tokio-comp"] } 801 - tracing = "0.1" 802 - 803 - # Code generation dependencies 804 - handlebars = "4.0" # Template engine for TypeScript generation 805 - ``` 806 - 807 - ## Performance Optimizations 808 - 809 - ### Concurrent Processing 810 - 811 - - **Bounded concurrency**: Limit simultaneous CAR file processing 812 - - **Streaming**: Process large CAR files without loading entirely into memory 813 - - **Batching**: Group database operations for better throughput 814 - - **Connection pooling**: Efficient database connection management 815 - 816 - ### Rate Limiting 817 - 818 - ```rust 819 - // Token bucket implementation for API rate limiting 820 - struct RateLimiter { 821 - tokens: Arc<Mutex<f64>>, 822 - max_tokens: f64, 823 - refill_rate: f64, // tokens per second 824 - } 825 - ``` 826 - 827 - ### Memory Management 828 - 829 - - **Streaming CAR processing**: Avoid loading entire repos into memory 830 - - **LRU caches**: Intelligent caching of frequently accessed data 831 - - **Pagination**: Cursor-based pagination for large result sets 832 - 833 - ## Real-Time Synchronization 834 - 835 - ### Firehose Integration 836 - 837 - ```rust 838 - async fn start_firehose_listener(&self) -> Result<()> { 839 - let (ws_stream, _) = connect_async( 840 - "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos" 841 - ).await?; 842 - 843 - // Process commits in real-time 844 - while let Some(msg) = read.next().await { 845 - if let Ok(commit) = self.parse_commit(&msg) { 846 - self.process_commit(commit).await?; 847 - } 848 - } 849 - 850 - Ok(()) 851 - } 852 - ``` 853 - 854 - ### Sync Strategies 855 - 856 - 1. **Initial Bootstrap**: Download existing data via CAR files 857 - 2. **Real-time Updates**: Process firehose stream for live changes 858 - 3. **Periodic Reconciliation**: Compare local state with remote to catch missed 859 - updates 860 - 4. **Backfill**: Handle gaps in data due to downtime 861 - 862 - ## API Design 863 - 864 - ### Core Principles 865 - 866 - - **RESTful**: Follow REST conventions where applicable 867 - - **Lexicon-Agnostic**: Work with any current or future AT Protocol lexicon 868 - - **Type-Safe**: Generate strongly typed clients 869 - - **Cacheable**: Design for HTTP caching and CDN distribution 870 - - **Paginated**: Support cursor-based pagination for large datasets 871 - 872 - ### Authentication 873 - 874 - - **Optional**: Support authenticated requests for private data 875 - - **Bearer tokens**: Standard AT Protocol authentication 876 - - **Rate limiting**: Per-user and global rate limits 877 - 878 - ### Response Format 879 - 880 - ```json 881 - { 882 - "data": [...], 883 - "cursor": "next_page_token", 884 - "count": 42, 885 - "total": 1337 886 - } 887 - ``` 888 - 889 - ## Risk Mitigation 890 - 891 - ### Data Consistency 892 - 893 - - **Idempotent operations**: Safe to retry any indexing operation 894 - - **Checksum validation**: Verify CAR file integrity 895 - - **Reconciliation**: Periodic comparison with authoritative sources 896 - 897 - ### Scalability 898 - 899 - - **Horizontal scaling**: Design for multiple indexer instances 900 - - **Database sharding**: Partition by lexicon type or DID prefix 901 - - **Caching layers**: Multiple levels of caching for performance 902 - 903 - ### Operational 904 - 905 - - **Circuit breakers**: Prevent cascade failures 906 - - **Graceful degradation**: Continue operating with reduced functionality 907 - - **Monitoring**: Comprehensive observability for quick issue detection
+483
docs/api-reference.md
··· 1 + # API Reference 2 + 3 + Complete reference for Slices API endpoints. 4 + 5 + ## Base URL 6 + 7 + ``` 8 + https://your-api-domain.com/xrpc/ 9 + ``` 10 + 11 + ## Authentication 12 + 13 + Most write operations require OAuth 2.0 authentication. Include the access token in the Authorization header: 14 + 15 + ``` 16 + Authorization: Bearer YOUR_ACCESS_TOKEN 17 + ``` 18 + 19 + Read operations typically work without authentication. 20 + 21 + ## Core Endpoints 22 + 23 + ### Slice Management 24 + 25 + #### `social.slices.slice.listRecords` 26 + 27 + List all slices. 28 + 29 + **Method**: GET 30 + 31 + **Parameters**: 32 + - `limit` (number, optional): Maximum records to return (default: 50) 33 + - `cursor` (string, optional): Pagination cursor 34 + - `sort` (string, optional): Sort field and order (e.g., `createdAt:desc`) 35 + - `author` (string, optional): Filter by author DID 36 + - `authors` (string[], optional): Filter by multiple author DIDs 37 + 38 + **Response**: 39 + ```json 40 + { 41 + "records": [ 42 + { 43 + "uri": "at://did:plc:abc/social.slices.slice/xyz", 44 + "cid": "bafyrei...", 45 + "did": "did:plc:abc", 46 + "collection": "social.slices.slice", 47 + "value": { 48 + "name": "My Slice", 49 + "domain": "com.example", 50 + "createdAt": "2024-01-01T00:00:00Z" 51 + }, 52 + "indexedAt": "2024-01-01T00:00:00Z" 53 + } 54 + ], 55 + "cursor": "next-page-cursor" 56 + } 57 + ``` 58 + 59 + #### `social.slices.slice.getRecord` 60 + 61 + Get a specific slice by URI. 62 + 63 + **Method**: GET 64 + 65 + **Parameters**: 66 + - `uri` (string, required): AT Protocol URI of the slice 67 + 68 + **Response**: Single record object (same structure as listRecords item) 69 + 70 + #### `social.slices.slice.createRecord` 71 + 72 + Create a new slice. 73 + 74 + **Method**: POST 75 + 76 + **Authentication**: Required 77 + 78 + **Body**: 79 + ```json 80 + { 81 + "slice": "at://your-slice-uri", 82 + "record": { 83 + "$type": "social.slices.slice", 84 + "name": "My New Slice", 85 + "domain": "com.example", 86 + "createdAt": "2024-01-01T00:00:00Z" 87 + }, 88 + "rkey": "optional-record-key" 89 + } 90 + ``` 91 + 92 + **Response**: 93 + ```json 94 + { 95 + "uri": "at://did:plc:abc/social.slices.slice/xyz", 96 + "cid": "bafyrei..." 97 + } 98 + ``` 99 + 100 + ### Slice Operations 101 + 102 + #### `social.slices.slice.stats` 103 + 104 + Get statistics for a slice. 105 + 106 + **Method**: POST 107 + 108 + **Body**: 109 + ```json 110 + { 111 + "slice": "at://your-slice-uri" 112 + } 113 + ``` 114 + 115 + **Response**: 116 + ```json 117 + { 118 + "success": true, 119 + "collections": ["com.example.post", "app.bsky.actor.profile"], 120 + "collectionStats": [ 121 + { 122 + "collection": "com.example.post", 123 + "recordCount": 150, 124 + "uniqueActors": 10 125 + } 126 + ], 127 + "totalLexicons": 5, 128 + "totalRecords": 500, 129 + "totalActors": 25, 130 + "message": "Statistics retrieved successfully" 131 + } 132 + ``` 133 + 134 + #### `social.slices.slice.records` 135 + 136 + Browse records in a slice. 137 + 138 + **Method**: POST 139 + 140 + **Body**: 141 + ```json 142 + { 143 + "slice": "at://your-slice-uri", 144 + "collection": "com.example.post", 145 + "repo": "did:plc:optional-filter", 146 + "limit": 20, 147 + "cursor": "pagination-cursor" 148 + } 149 + ``` 150 + 151 + **Response**: 152 + ```json 153 + { 154 + "success": true, 155 + "records": [ 156 + { 157 + "uri": "at://did:plc:abc/com.example.post/xyz", 158 + "cid": "bafyrei...", 159 + "did": "did:plc:abc", 160 + "collection": "com.example.post", 161 + "value": { /* record data */ }, 162 + "indexedAt": "2024-01-01T00:00:00Z" 163 + } 164 + ], 165 + "cursor": "next-page-cursor" 166 + } 167 + ``` 168 + 169 + #### `social.slices.slice.syncUserCollections` 170 + 171 + Synchronously sync collections for the authenticated user. 172 + 173 + **Method**: POST 174 + 175 + **Authentication**: Required 176 + 177 + **Body**: 178 + ```json 179 + { 180 + "slice": "at://your-slice-uri", 181 + "timeoutSeconds": 30 182 + } 183 + ``` 184 + 185 + **Response**: 186 + ```json 187 + { 188 + "success": true, 189 + "reposProcessed": 1, 190 + "recordsSynced": 45, 191 + "timedOut": false, 192 + "message": "Sync completed successfully" 193 + } 194 + ``` 195 + 196 + #### `social.slices.slice.startSync` 197 + 198 + Start an asynchronous bulk sync job. 199 + 200 + **Method**: POST 201 + 202 + **Authentication**: Required 203 + 204 + **Body**: 205 + ```json 206 + { 207 + "slice": "at://your-slice-uri", 208 + "collections": ["com.example.post"], 209 + "externalCollections": ["app.bsky.actor.profile"], 210 + "repos": ["did:plc:abc", "did:plc:xyz"], 211 + "limitPerRepo": 100 212 + } 213 + ``` 214 + 215 + **Response**: 216 + ```json 217 + { 218 + "success": true, 219 + "jobId": "job-uuid", 220 + "message": "Sync job started" 221 + } 222 + ``` 223 + 224 + #### `social.slices.slice.codegen` 225 + 226 + Generate TypeScript client code. 227 + 228 + **Method**: POST 229 + 230 + **Body**: 231 + ```json 232 + { 233 + "target": "typescript", 234 + "slice": "at://your-slice-uri" 235 + } 236 + ``` 237 + 238 + **Response**: 239 + ```json 240 + { 241 + "success": true, 242 + "generatedCode": "// Generated TypeScript client code..." 243 + } 244 + ``` 245 + 246 + ## Dynamic Collection Endpoints 247 + 248 + For each collection in your slice, the following endpoints are automatically generated: 249 + 250 + ### `[collection].listRecords` 251 + 252 + List records in a collection. 253 + 254 + **Method**: GET 255 + 256 + **Parameters**: 257 + - `slice` (string, required): Slice URI 258 + - `limit` (number, optional): Maximum records (default: 50) 259 + - `cursor` (string, optional): Pagination cursor 260 + - `sort` (string, optional): Sort specification 261 + - `author` (string, optional): Filter by author DID 262 + - `authors` (string[], optional): Filter by multiple DIDs 263 + 264 + ### `[collection].getRecord` 265 + 266 + Get a single record. 267 + 268 + **Method**: GET 269 + 270 + **Parameters**: 271 + - `slice` (string, required): Slice URI 272 + - `uri` (string, required): Record URI 273 + 274 + ### `[collection].searchRecords` 275 + 276 + Search within a collection. 277 + 278 + **Method**: GET 279 + 280 + **Parameters**: 281 + - `slice` (string, required): Slice URI 282 + - `query` (string, required): Search query 283 + - `field` (string, optional): Specific field to search 284 + - `limit` (number, optional): Maximum results 285 + - `cursor` (string, optional): Pagination cursor 286 + - `sort` (string, optional): Sort specification 287 + 288 + ### `[collection].createRecord` 289 + 290 + Create a new record. 291 + 292 + **Method**: POST 293 + 294 + **Authentication**: Required 295 + 296 + **Body**: 297 + ```json 298 + { 299 + "slice": "at://your-slice-uri", 300 + "record": { 301 + "$type": "collection.name", 302 + /* record fields */ 303 + }, 304 + "rkey": "optional-key" 305 + } 306 + ``` 307 + 308 + ### `[collection].updateRecord` 309 + 310 + Update an existing record. 311 + 312 + **Method**: POST 313 + 314 + **Authentication**: Required 315 + 316 + **Body**: 317 + ```json 318 + { 319 + "slice": "at://your-slice-uri", 320 + "rkey": "record-key", 321 + "record": { 322 + "$type": "collection.name", 323 + /* updated fields */ 324 + } 325 + } 326 + ``` 327 + 328 + ### `[collection].deleteRecord` 329 + 330 + Delete a record. 331 + 332 + **Method**: POST 333 + 334 + **Authentication**: Required 335 + 336 + **Body**: 337 + ```json 338 + { 339 + "rkey": "record-key" 340 + } 341 + ``` 342 + 343 + ## Lexicon Management 344 + 345 + ### `social.slices.lexicon.listRecords` 346 + 347 + List lexicons in a slice. 348 + 349 + **Method**: GET 350 + 351 + **Parameters**: Same as collection.listRecords 352 + 353 + ### `social.slices.lexicon.createRecord` 354 + 355 + Add a lexicon to a slice. 356 + 357 + **Method**: POST 358 + 359 + **Authentication**: Required 360 + 361 + **Body**: 362 + ```json 363 + { 364 + "slice": "at://your-slice-uri", 365 + "record": { 366 + "$type": "social.slices.lexicon", 367 + "nsid": "com.example.post", 368 + "definitions": "{\"lexicon\": 1, ...}", 369 + "createdAt": "2024-01-01T00:00:00Z", 370 + "slice": "at://your-slice-uri" 371 + } 372 + } 373 + ``` 374 + 375 + ## Actor Management 376 + 377 + ### `social.slices.slice.getActors` 378 + 379 + Get actors (users) in a slice. 380 + 381 + **Method**: GET 382 + 383 + **Parameters**: 384 + - `slice` (string, required): Slice URI 385 + - `search` (string, optional): Search query 386 + - `dids` (string[], optional): Filter by DIDs 387 + - `limit` (number, optional): Maximum results 388 + - `cursor` (string, optional): Pagination cursor 389 + 390 + **Response**: 391 + ```json 392 + { 393 + "actors": [ 394 + { 395 + "did": "did:plc:abc", 396 + "handle": "user.bsky.social", 397 + "sliceUri": "at://slice-uri", 398 + "indexedAt": "2024-01-01T00:00:00Z" 399 + } 400 + ], 401 + "cursor": "next-page" 402 + } 403 + ``` 404 + 405 + ## Blob Upload 406 + 407 + ### `com.atproto.repo.uploadBlob` 408 + 409 + Upload a blob (image, file). 410 + 411 + **Method**: POST 412 + 413 + **Authentication**: Required 414 + 415 + **Headers**: 416 + - `Content-Type`: MIME type of the blob 417 + 418 + **Body**: Raw binary data 419 + 420 + **Response**: 421 + ```json 422 + { 423 + "blob": { 424 + "$type": "blob", 425 + "ref": { "$link": "bafkrei..." }, 426 + "mimeType": "image/jpeg", 427 + "size": 127198 428 + } 429 + } 430 + ``` 431 + 432 + ## Error Responses 433 + 434 + All endpoints may return error responses: 435 + 436 + ```json 437 + { 438 + "error": "InvalidRequest", 439 + "message": "Detailed error message" 440 + } 441 + ``` 442 + 443 + Common HTTP status codes: 444 + - `200`: Success 445 + - `400`: Bad request 446 + - `401`: Authentication required 447 + - `403`: Forbidden 448 + - `404`: Not found 449 + - `500`: Internal server error 450 + 451 + ## Pagination 452 + 453 + List endpoints support cursor-based pagination: 454 + 455 + 1. Make initial request without cursor 456 + 2. Use returned cursor for next page 457 + 3. Continue until no cursor returned 458 + 459 + Example: 460 + ```javascript 461 + let cursor = undefined; 462 + do { 463 + const response = await fetch(`/xrpc/collection.listRecords?cursor=${cursor}`); 464 + const data = await response.json(); 465 + // Process records 466 + cursor = data.cursor; 467 + } while (cursor); 468 + ``` 469 + 470 + ## Sorting 471 + 472 + Sort parameter format: `field:order` or `field1:order1,field2:order2` 473 + 474 + Examples: 475 + - `createdAt:desc` - Newest first 476 + - `name:asc` - Alphabetical 477 + - `createdAt:desc,name:asc` - Newest first, then alphabetical 478 + 479 + ## Next Steps 480 + 481 + - [SDK Usage](./sdk-usage.md) - Using generated TypeScript clients 482 + - [Getting Started](./getting-started.md) - Build your first application 483 + - [Concepts](./concepts.md) - Understand the architecture
+314
docs/concepts.md
··· 1 + # Core Concepts 2 + 3 + Understanding these core concepts will help you effectively use Slices. 4 + 5 + ## Slices 6 + 7 + A slice is an independent appview within the AT Protocol ecosystem. Think of it 8 + as your own data universe with custom schemas and records. 9 + 10 + ### Key Properties 11 + 12 + - **URI**: Unique AT Protocol URI (e.g., 13 + `at://did:plc:abc123/social.slices.slice/3xyz`) 14 + - **Name**: Human-readable identifier 15 + - **Domain**: Namespace for lexicons (e.g., `com.example`, `social.grain`) 16 + - **Creation Date**: When the slice was created 17 + 18 + ### Slice Isolation 19 + 20 + Each slice maintains complete data isolation: 21 + 22 + - Records are filtered by slice URI in all queries 23 + - Sync operations respect slice boundaries 24 + - Statistics are calculated per-slice 25 + - Users can have different data in different slices 26 + 27 + ## Lexicons 28 + 29 + Lexicons are JSON schemas that define record types in AT Protocol. They specify 30 + the structure, validation rules, and metadata for records. 31 + 32 + ### Lexicon Structure 33 + 34 + ```json 35 + { 36 + "lexicon": 1, 37 + "id": "com.example.blogPost", 38 + "defs": { 39 + "main": { 40 + "type": "record", 41 + "description": "A blog post record", 42 + "record": { 43 + "type": "object", 44 + "properties": { 45 + "title": { "type": "string" }, 46 + "content": { "type": "string" }, 47 + "publishedAt": { "type": "string", "format": "datetime" } 48 + }, 49 + "required": ["title", "content"] 50 + } 51 + } 52 + } 53 + } 54 + ``` 55 + 56 + ### Supported Types 57 + 58 + - **Primitives**: string, number, integer, boolean 59 + - **Complex**: object, array, union, ref 60 + - **Special**: blob (for media), cid-link, at-uri 61 + - **Formats**: datetime, at-identifier, did, handle 62 + 63 + ### Lexicon Namespacing 64 + 65 + Lexicons follow reverse domain naming: 66 + 67 + - `com.example.post` - A post in the example.com namespace 68 + - `social.slices.slice` - Core slice record type 69 + - `app.bsky.actor.profile` - Bluesky profile (external) 70 + 71 + ## Collections 72 + 73 + Collections are groups of records with the same lexicon type. They map directly 74 + to XRPC endpoints. 75 + 76 + ### Primary Collections 77 + 78 + Collections that match your slice's domain namespace. For example, if your slice 79 + domain is `com.example`, then `com.example.post` would be a primary collection. 80 + 81 + ### External Collections 82 + 83 + Collections from other namespaces that you've synced into your slice. For 84 + example: 85 + 86 + - Bluesky profiles (`app.bsky.actor.profile`) 87 + - Bluesky posts (`app.bsky.feed.post`) 88 + - Collections from other slices 89 + 90 + ### Collection Operations 91 + 92 + Both primary and external collections support the same operations: 93 + 94 + - `*.listRecords` - List with pagination and filtering 95 + - `*.getRecord` - Get single record by URI 96 + - `*.createRecord` - Create new record 97 + - `*.updateRecord` - Update existing record 98 + - `*.deleteRecord` - Remove record 99 + - `*.searchRecords` - Search within collection 100 + 101 + The key difference is conceptual: primary collections are "native" to your 102 + slice's domain, while external collections are imported from other namespaces. 103 + 104 + ## Records 105 + 106 + Records are individual data items stored in collections. 107 + 108 + ### Record Properties 109 + 110 + - **URI**: Unique AT Protocol URI 111 + - **CID**: Content identifier (hash) 112 + - **DID**: Owner's decentralized identifier 113 + - **Collection**: Lexicon type 114 + - **Value**: Actual record data 115 + - **IndexedAt**: When record was indexed 116 + 117 + ### Record Keys (rkeys) 118 + 119 + Records use keys for identification: 120 + 121 + - **Self**: Special key for singleton records (e.g., profiles) 122 + - **TID**: Timestamp-based identifiers 123 + - **Custom**: User-defined keys 124 + 125 + ### Record Lifecycle 126 + 127 + 1. **Creation**: Via API or sync 128 + 2. **Indexing**: Stored in PostgreSQL 129 + 3. **Updates**: New versions with new CIDs 130 + 4. **Deletion**: Soft or hard delete 131 + 132 + ## Sync Engine 133 + 134 + The sync engine imports AT Protocol data into your slice using multiple 135 + strategies for optimal performance and reliability. 136 + 137 + ### Sync Types 138 + 139 + **Bulk Sync**: One-time import of historical data 140 + 141 + - Specify collections to sync 142 + - Filter by repositories (DIDs) 143 + - Set limits per repository 144 + - Uses optimized bulk database operations 145 + 146 + **User Sync**: Sync data for authenticated user 147 + 148 + - Automatic on login 149 + - Timeout protection (30 seconds default) 150 + - External collection discovery 151 + - Synchronous operation for immediate feedback 152 + 153 + **Jetstream Sync**: Real-time updates via WebSocket 154 + 155 + - Subscribe to AT Protocol firehose 156 + - Filter relevant events by slice collections 157 + - Automatic record updates and deletions 158 + - Built-in reconnection with exponential backoff 159 + 160 + ### Sync Process 161 + 162 + 1. **Discovery**: Find available records via AT Protocol relay 163 + 2. **Filtering**: Apply slice and collection filters 164 + 3. **Validation**: Check lexicon compliance against slice schemas 165 + 4. **Storage**: Index in database using bulk operations 166 + 5. **Deduplication**: Skip existing records (by CID comparison) 167 + 168 + ### Performance Optimizations 169 + 170 + **CID-Based Deduplication** 171 + 172 + - Compare Content Identifiers (CIDs) before processing 173 + - Skip records that haven't changed since last sync 174 + - Reduces unnecessary database operations and validation overhead 175 + 176 + **Actor Caching** 177 + 178 + - Pre-load actor lookup cache to avoid database hits during Jetstream processing 179 + - Cache (DID, slice_uri) mappings for external collection filtering 180 + - Periodic cache refresh every 5 minutes 181 + 182 + ### Jetstream Reliability 183 + 184 + **Automatic Recovery** 185 + 186 + - Infinite retry loop with exponential backoff (5 seconds → 5 minutes max) 187 + - Fresh consumer instance creation on each retry 188 + - Database connectivity monitoring and recovery 189 + - Connection status tracking via atomic flags 190 + 191 + **Error Handling** 192 + 193 + - Graceful degradation when database connections fail 194 + - Validation fallback with fresh lexicon loading from database 195 + - Separate error handling for primary vs external collections 196 + 197 + **Configuration Reloading** 198 + 199 + - Automatic slice configuration refresh every 5 minutes 200 + - Dynamic collection filtering based on slice lexicons 201 + - Actor cache updates to reflect new slice membership 202 + 203 + ## XRPC Handlers 204 + 205 + XRPC (Cross-Protocol Remote Procedure Call) handlers provide the API layer. 206 + 207 + ### Dynamic Handlers 208 + 209 + Automatically generated from lexicons: 210 + 211 + - No manual endpoint creation 212 + - Type-safe request/response 213 + - Automatic validation 214 + - OAuth integration 215 + 216 + ### Core Handlers 217 + 218 + Built-in endpoints for slice management: 219 + 220 + - `social.slices.slice.stats` - Slice statistics 221 + - `social.slices.slice.records` - Browse records 222 + - `social.slices.slice.codegen` - Generate SDKs 223 + - `social.slices.slice.sync` - Trigger sync 224 + 225 + ### Handler Authentication 226 + 227 + - **Read Operations**: Optional auth (public by default) 228 + - **Write Operations**: Require OAuth tokens 229 + - **Admin Operations**: Require slice ownership 230 + 231 + ## Generated SDKs 232 + 233 + Type-safe client libraries generated from lexicons. 234 + 235 + ### SDK Features 236 + 237 + - **Type Safety**: Full TypeScript types 238 + - **Nested Structure**: Matches lexicon namespacing 239 + - **OAuth Integration**: Automatic token handling 240 + - **Error Handling**: Retry logic and graceful failures 241 + 242 + ### SDK Generation Process 243 + 244 + 1. Parse slice lexicons 245 + 2. Generate TypeScript interfaces 246 + 3. Create client classes 247 + 4. Add utility functions 248 + 5. Format and validate 249 + 250 + ### Using Generated SDKs 251 + 252 + ```typescript 253 + // Initialize client 254 + const client = new AtProtoClient(apiUrl, sliceUri, oauthClient); 255 + 256 + // Use nested structure matching lexicons 257 + await client.com.example.post.listRecords(); 258 + await client.social.slices.slice.stats(); 259 + await client.app.bsky.actor.profile.getRecord({ uri }); 260 + ``` 261 + 262 + ## Authentication 263 + 264 + OAuth 2.0 with PKCE for secure authentication. 265 + 266 + ### OAuth Flow 267 + 268 + 1. **Authorization**: Redirect to AT Protocol provider 269 + 2. **Callback**: Exchange code for tokens 270 + 3. **Token Storage**: Secure client-side storage 271 + 4. **Refresh**: Automatic token renewal 272 + 273 + ### Session Management 274 + 275 + - Encrypted cookies for web sessions 276 + - Token refresh before expiration 277 + - Graceful degradation for read-only access 278 + 279 + ## Blob Handling 280 + 281 + Media files use blob references with CDN URLs. 282 + 283 + ### Blob Structure 284 + 285 + ```json 286 + { 287 + "$type": "blob", 288 + "ref": { "$link": "bafkreig5bcb..." }, 289 + "mimeType": "image/jpeg", 290 + "size": 127198 291 + } 292 + ``` 293 + 294 + ### CDN URL Generation 295 + 296 + Convert blob references to CDN URLs using Bluesky's CDN: 297 + 298 + ```typescript 299 + recordBlobToCdnUrl(record, blobRef, "avatar"); 300 + // -> https://cdn.bsky.app/img/avatar/plain/did:plc:abc/bafkrei...@jpeg 301 + ``` 302 + 303 + ### Bluesky CDN Presets 304 + 305 + - `avatar` - Profile pictures 306 + - `banner` - Cover images 307 + - `feed_thumbnail` - Small previews 308 + - `feed_fullsize` - Full resolution 309 + 310 + ## Next Steps 311 + 312 + - [API Reference](./api-reference.md) - Detailed endpoint documentation 313 + - [SDK Usage](./sdk-usage.md) - Advanced client patterns 314 + - [Getting Started](./getting-started.md) - Build your first slice
+23
docs/deployment.md
··· 1 + # Deployment Guide 2 + 3 + Documentation for deploying Slices to production is coming soon. 4 + 5 + ## Basic Requirements 6 + 7 + - [AIP server](https://github.com/graze-social/aip) running and accessible 8 + - Registered OAuth client with AIP 9 + - PostgreSQL database 10 + 11 + ## Environment Setup 12 + 13 + See the [environment variables](../README.md#environment-variables) section in the README for required configuration. 14 + 15 + ## Docker 16 + 17 + Docker images can be built using the provided Dockerfile in each directory. 18 + 19 + ## More Information 20 + 21 + For now, refer to: 22 + - [Getting Started](./getting-started.md) for local setup 23 + - [README](../README.md) for environment configuration
+223
docs/getting-started.md
··· 1 + # Getting Started with Slices 2 + 3 + This guide will help you set up Slices and create your first slice. 4 + 5 + ## Prerequisites 6 + 7 + - Docker and Docker Compose 8 + - PostgreSQL (or use Docker) 9 + - Deno (for frontend) 10 + - Rust and Cargo (for API development) 11 + - An AT Protocol account (for OAuth) 12 + 13 + ## Initial Setup 14 + 15 + ### 1. Clone the Repository 16 + 17 + ```bash 18 + git clone https://github.com/your-org/slice 19 + cd slice 20 + ``` 21 + 22 + ### 2. Set Up the Database 23 + 24 + Start PostgreSQL using Docker: 25 + 26 + ```bash 27 + docker-compose up -d postgres 28 + ``` 29 + 30 + Or use an existing PostgreSQL instance and create a database: 31 + 32 + ```sql 33 + CREATE DATABASE slices; 34 + ``` 35 + 36 + ### 3. Configure Environment Variables 37 + 38 + Create `.env` files for both API and frontend: 39 + 40 + **API (`/api/.env`)**: 41 + ```bash 42 + DATABASE_URL=postgres://user:password@localhost:5432/slices 43 + AUTH_BASE_URL=https://aip.your-domain.com 44 + PORT=3000 45 + ``` 46 + 47 + **Frontend (`/frontend/.env`)**: 48 + ```bash 49 + OAUTH_CLIENT_ID=your-client-id 50 + OAUTH_CLIENT_SECRET=your-client-secret 51 + OAUTH_REDIRECT_URI=http://localhost:8000/oauth/callback 52 + OAUTH_AIP_BASE_URL=https://aip.your-domain.com 53 + SESSION_ENCRYPTION_KEY=your-32-char-key 54 + API_URL=http://localhost:3000 55 + SLICE_URI=at://did:plc:your-did/social.slices.slice/your-slice-id 56 + DATABASE_URL=slices.db 57 + ``` 58 + 59 + ### 4. Register OAuth Client 60 + 61 + Register your application with the AIP server: 62 + 63 + ```bash 64 + cd frontend 65 + ./scripts/register-oauth-client.sh 66 + ``` 67 + 68 + Save the client ID and secret to your `.env` file. 69 + 70 + ### 5. Start the Services 71 + 72 + Start the API server: 73 + ```bash 74 + cd api 75 + cargo run 76 + ``` 77 + 78 + Start the frontend: 79 + ```bash 80 + cd frontend 81 + deno task dev 82 + ``` 83 + 84 + Visit `http://localhost:8000` to access the web interface. 85 + 86 + ## Creating Your First Slice 87 + 88 + ### 1. Log In 89 + 90 + Click "Login" and authenticate with your AT Protocol account. 91 + 92 + ### 2. Create a Slice 93 + 94 + Click "Create Slice" and provide: 95 + - **Name**: A friendly name for your slice 96 + - **Domain**: Your namespace (e.g., `com.example`) 97 + 98 + ### 3. Define a Lexicon 99 + 100 + Navigate to your slice and go to the Lexicon tab. Create a lexicon for your first record type: 101 + 102 + ```json 103 + { 104 + "lexicon": 1, 105 + "id": "com.example.post", 106 + "defs": { 107 + "main": { 108 + "type": "record", 109 + "description": "A blog post", 110 + "record": { 111 + "type": "object", 112 + "properties": { 113 + "title": { 114 + "type": "string", 115 + "description": "Post title" 116 + }, 117 + "content": { 118 + "type": "string", 119 + "description": "Post content" 120 + }, 121 + "createdAt": { 122 + "type": "string", 123 + "format": "datetime", 124 + "description": "Creation timestamp" 125 + }, 126 + "tags": { 127 + "type": "array", 128 + "items": { 129 + "type": "string" 130 + }, 131 + "description": "Post tags" 132 + } 133 + }, 134 + "required": ["title", "content", "createdAt"] 135 + } 136 + } 137 + } 138 + } 139 + ``` 140 + 141 + ### 4. Generate TypeScript Client 142 + 143 + Navigate to the Code Generation tab and click "Generate TypeScript Client". This creates a type-safe client library for your slice. 144 + 145 + ### 5. Use the Generated Client 146 + 147 + In your application: 148 + 149 + ```typescript 150 + import { AtProtoClient } from "./generated-client.ts"; 151 + 152 + const client = new AtProtoClient( 153 + 'http://localhost:3000', 154 + 'at://did:plc:your-did/social.slices.slice/your-slice-id' 155 + ); 156 + 157 + // List posts 158 + const posts = await client.com.example.post.listRecords(); 159 + 160 + // Create a post 161 + const newPost = await client.com.example.post.createRecord({ 162 + title: "My First Post", 163 + content: "Hello from Slices!", 164 + createdAt: new Date().toISOString(), 165 + tags: ["introduction", "slices"] 166 + }); 167 + 168 + // Get a specific post 169 + const post = await client.com.example.post.getRecord({ 170 + uri: newPost.uri 171 + }); 172 + ``` 173 + 174 + ## Syncing External Data 175 + 176 + To import data from other AT Protocol repositories: 177 + 178 + ### 1. Navigate to Sync 179 + 180 + Go to your slice and click the Sync tab. 181 + 182 + ### 2. Configure Sync 183 + 184 + Choose collections to sync: 185 + - **Primary Collections**: Your slice's lexicons 186 + - **External Collections**: Bluesky or other AT Protocol collections 187 + 188 + ### 3. Start Sync 189 + 190 + Specify repositories (DIDs) to sync from, or leave empty to sync all available data. 191 + 192 + ### 4. Monitor Progress 193 + 194 + The sync will run in the background. Check the status in the UI or via API. 195 + 196 + ## Next Steps 197 + 198 + - [Core Concepts](./concepts.md) - Understand slices, lexicons, and collections 199 + - [API Reference](./api-reference.md) - Explore available endpoints 200 + - [SDK Usage](./sdk-usage.md) - Advanced SDK patterns 201 + - [Examples](./examples/) - Sample applications 202 + 203 + ## Troubleshooting 204 + 205 + ### Database Connection Issues 206 + - Verify PostgreSQL is running: `docker ps` 207 + - Check DATABASE_URL format 208 + - Ensure database exists 209 + 210 + ### OAuth Errors 211 + - Verify client ID and secret 212 + - Check redirect URI matches configuration 213 + - Ensure AIP server is accessible 214 + 215 + ### Sync Not Working 216 + - Check user has necessary permissions 217 + - Verify lexicons are valid 218 + - Check API server logs for errors 219 + 220 + ### Generated Client Issues 221 + - Regenerate client after lexicon changes 222 + - Ensure API server is running 223 + - Check for TypeScript compilation errors
+95
docs/intro.md
··· 1 + # Introduction to Slices 2 + 3 + Slices is an AT Protocol appview platform that enables developers to create 4 + custom data slices (appviews) with their own lexicons, sync AT Protocol data, 5 + and generate type-safe SDKs. 6 + 7 + ## What is a Slice? 8 + 9 + A slice is a custom appview within the AT Protocol ecosystem. Each slice: 10 + 11 + - Has its own domain namespace (e.g., `social.grain`, `xyz.statusphere`) 12 + - Defines custom lexicons (schemas) for record types 13 + - Can sync both internal and external AT Protocol collections 14 + - Provides automatically generated type-safe SDKs 15 + 16 + ## Why Slices? 17 + 18 + Building AT Protocol applications typically requires: 19 + 20 + - Setting up infrastructure to index and query AT Protocol data 21 + - Managing OAuth authentication flows 22 + - Implementing XRPC handlers for CRUD operations 23 + - Creating client libraries for frontend integration 24 + 25 + Slices provides all of this infrastructure out of the box, letting you focus on 26 + your application logic. 27 + 28 + ## Key Features 29 + 30 + ### Dynamic API Generation 31 + 32 + Define a lexicon, and Slices automatically creates REST endpoints for: 33 + 34 + - Listing records with filtering and sorting 35 + - Getting individual records by URI 36 + - Creating new records with OAuth authentication 37 + - Updating existing records 38 + - Deleting records 39 + - Searching within collections 40 + 41 + ### Data Synchronization 42 + 43 + Sync AT Protocol data into your slice: 44 + 45 + - Import external collections (e.g., Bluesky profiles, posts) 46 + - Filter data by repository or collection 47 + - Maintain slice-specific data isolation 48 + - Real-time sync via Jetstream (coming soon) 49 + 50 + ### Type-Safe SDK Generation 51 + 52 + Automatically generate TypeScript clients with: 53 + 54 + - Full type safety for all record types 55 + - OAuth authentication integration 56 + - Nested API structure matching your lexicons 57 + - Blob/CDN URL utilities for media handling 58 + 59 + ### Multi-Tenant Architecture 60 + 61 + Each slice operates independently: 62 + 63 + - Isolated data storage 64 + - Custom lexicon definitions 65 + - Separate sync configurations 66 + - Per-slice statistics and analytics 67 + 68 + ## Use Cases 69 + 70 + - **Custom Social Apps**: Build specialized social networks with custom record 71 + types 72 + - **Data Aggregators**: Collect and organize AT Protocol data for analysis 73 + - **Specialized Feeds**: Create curated views of AT Protocol content 74 + - **Research Tools**: Index and query specific subsets of AT Protocol data 75 + - **Creative Applications**: Blogs, galleries, portfolios built on AT Protocol 76 + 77 + ## Architecture Overview 78 + 79 + Slices consists of two main components: 80 + 81 + - **API Server** (Rust): Handles AT Protocol integration, database operations, 82 + and API endpoints 83 + - **Frontend** (Deno): Provides web UI for slice management and user 84 + authentication 85 + 86 + Both components work together to provide a complete AT Protocol appview 87 + platform. The API server can also be run standalone as a headless service and 88 + interactive with via it's XRPC apis. 89 + 90 + ## Next Steps 91 + 92 + - [Getting Started](./getting-started.md) - Set up your first slice 93 + - [Core Concepts](./concepts.md) - Understand the key concepts 94 + - [API Reference](./api-reference.md) - Explore the API endpoints 95 + - [SDK Usage](./sdk-usage.md) - Learn to use generated clients
+397
docs/sdk-usage.md
··· 1 + # SDK Usage Guide 2 + 3 + This guide covers how to use the generated TypeScript SDK for your slice. 4 + 5 + ## Installation 6 + 7 + After generating your TypeScript client, you can use it directly in your project: 8 + 9 + ```typescript 10 + import { AtProtoClient } from "./generated-client.ts"; 11 + import { OAuthClient } from "@slices/oauth"; 12 + ``` 13 + 14 + ## Basic Setup 15 + 16 + ### Without Authentication (Read-Only) 17 + 18 + ```typescript 19 + const client = new AtProtoClient( 20 + 'https://api.your-domain.com', 21 + 'at://did:plc:abc/social.slices.slice/your-slice-id' 22 + ); 23 + 24 + // Read operations work without auth 25 + const records = await client.com.example.post.listRecords(); 26 + ``` 27 + 28 + ### With Authentication (Full Access) 29 + 30 + ```typescript 31 + import { OAuthClient } from "@slices/oauth"; 32 + 33 + // Set up OAuth client 34 + const oauthClient = new OAuthClient({ 35 + clientId: 'your-client-id', 36 + clientSecret: 'your-client-secret', 37 + authBaseUrl: 'https://aip.your-domain.com', 38 + redirectUri: 'https://your-app.com/oauth/callback', 39 + scopes: ['atproto:atproto', 'atproto:transition:generic'] 40 + }); 41 + 42 + // Initialize API client with OAuth 43 + const client = new AtProtoClient( 44 + 'https://api.your-domain.com', 45 + 'at://did:plc:abc/social.slices.slice/your-slice-id', 46 + oauthClient 47 + ); 48 + ``` 49 + 50 + ## CRUD Operations 51 + 52 + ### Listing Records 53 + 54 + ```typescript 55 + // List all records 56 + const posts = await client.com.example.post.listRecords(); 57 + 58 + // With pagination 59 + const page1 = await client.com.example.post.listRecords({ limit: 20 }); 60 + const page2 = await client.com.example.post.listRecords({ 61 + limit: 20, 62 + cursor: page1.cursor 63 + }); 64 + 65 + // With filtering 66 + const userPosts = await client.com.example.post.listRecords({ 67 + author: 'did:plc:user123' 68 + }); 69 + 70 + // With sorting 71 + const recentPosts = await client.com.example.post.listRecords({ 72 + sort: 'createdAt:desc' 73 + }); 74 + 75 + // Multiple sort fields 76 + const sortedPosts = await client.com.example.post.listRecords({ 77 + sort: 'createdAt:desc,title:asc' 78 + }); 79 + ``` 80 + 81 + ### Getting a Single Record 82 + 83 + ```typescript 84 + const post = await client.com.example.post.getRecord({ 85 + uri: 'at://did:plc:abc/com.example.post/3xyz' 86 + }); 87 + 88 + console.log(post.value.title); 89 + console.log(post.value.content); 90 + ``` 91 + 92 + ### Creating Records 93 + 94 + ```typescript 95 + // Create with auto-generated key 96 + const newPost = await client.com.example.post.createRecord({ 97 + title: "My New Post", 98 + content: "This is the content", 99 + createdAt: new Date().toISOString(), 100 + tags: ["typescript", "atproto"] 101 + }); 102 + 103 + console.log(`Created: ${newPost.uri}`); 104 + 105 + // Create with custom key 106 + const customPost = await client.com.example.post.createRecord( 107 + { 108 + title: "Custom Key Post", 109 + content: "Using a custom record key", 110 + createdAt: new Date().toISOString() 111 + }, 112 + true // useSelfRkey for singleton records like profiles 113 + ); 114 + ``` 115 + 116 + ### Updating Records 117 + 118 + ```typescript 119 + // Get the record key from the URI 120 + const uri = 'at://did:plc:abc/com.example.post/3xyz'; 121 + const rkey = uri.split('/').pop(); // '3xyz' 122 + 123 + const updated = await client.com.example.post.updateRecord( 124 + rkey, 125 + { 126 + title: "Updated Title", 127 + content: "Updated content", 128 + createdAt: new Date().toISOString(), 129 + updatedAt: new Date().toISOString() 130 + } 131 + ); 132 + 133 + console.log(`Updated: ${updated.cid}`); 134 + ``` 135 + 136 + ### Deleting Records 137 + 138 + ```typescript 139 + const rkey = '3xyz'; 140 + await client.com.example.post.deleteRecord(rkey); 141 + ``` 142 + 143 + ### Searching Records 144 + 145 + ```typescript 146 + // Basic search 147 + const results = await client.com.example.post.searchRecords({ 148 + query: "typescript" 149 + }); 150 + 151 + // Search specific field 152 + const titleResults = await client.com.example.post.searchRecords({ 153 + query: "guide", 154 + field: "title" 155 + }); 156 + 157 + // Search with pagination 158 + const searchPage = await client.com.example.post.searchRecords({ 159 + query: "tutorial", 160 + limit: 10, 161 + cursor: previousCursor 162 + }); 163 + ``` 164 + 165 + ## Working with External Collections 166 + 167 + Access synced external collections like Bluesky profiles: 168 + 169 + ```typescript 170 + // List Bluesky profiles in your slice 171 + const profiles = await client.app.bsky.actor.profile.listRecords(); 172 + 173 + // Get a specific profile 174 + const profile = await client.app.bsky.actor.profile.getRecord({ 175 + uri: 'at://did:plc:user/app.bsky.actor.profile/self' 176 + }); 177 + 178 + // Access profile data 179 + console.log(profile.value.displayName); 180 + console.log(profile.value.description); 181 + ``` 182 + 183 + ## Blob Handling 184 + 185 + ### Uploading Blobs 186 + 187 + ```typescript 188 + // Read file as ArrayBuffer 189 + const file = await Deno.readFile('./image.jpg'); 190 + 191 + // Upload blob 192 + const blobResponse = await client.uploadBlob({ 193 + data: file, 194 + mimeType: 'image/jpeg' 195 + }); 196 + 197 + // Use blob in a record 198 + const postWithImage = await client.com.example.post.createRecord({ 199 + title: "Post with Image", 200 + content: "Check out this image!", 201 + image: blobResponse.blob, 202 + createdAt: new Date().toISOString() 203 + }); 204 + ``` 205 + 206 + ### Converting Blobs to CDN URLs 207 + 208 + ```typescript 209 + import { recordBlobToCdnUrl } from "./generated-client.ts"; 210 + 211 + // Get a record with a blob 212 + const profile = await client.app.bsky.actor.profile.getRecord({ 213 + uri: 'at://did:plc:user/app.bsky.actor.profile/self' 214 + }); 215 + 216 + // Convert avatar blob to CDN URL 217 + if (profile.value.avatar) { 218 + const avatarUrl = recordBlobToCdnUrl( 219 + profile, 220 + profile.value.avatar, 221 + 'avatar' // Size preset 222 + ); 223 + console.log(`Avatar URL: ${avatarUrl}`); 224 + } 225 + 226 + // Available presets: 227 + // - 'avatar': Small square images 228 + // - 'banner': Wide header images 229 + // - 'feed_thumbnail': Small feed previews 230 + // - 'feed_fullsize': Full resolution images 231 + ``` 232 + 233 + ## Slice Operations 234 + 235 + ### Get Slice Statistics 236 + 237 + ```typescript 238 + const stats = await client.social.slices.slice.stats({ 239 + slice: 'at://your-slice-uri' 240 + }); 241 + 242 + console.log(`Total records: ${stats.totalRecords}`); 243 + console.log(`Total actors: ${stats.totalActors}`); 244 + 245 + stats.collectionStats.forEach(stat => { 246 + console.log(`${stat.collection}: ${stat.recordCount} records`); 247 + }); 248 + ``` 249 + 250 + ### Browse Slice Records 251 + 252 + ```typescript 253 + const records = await client.social.slices.slice.records({ 254 + slice: 'at://your-slice-uri', 255 + collection: 'com.example.post', 256 + limit: 50 257 + }); 258 + 259 + records.records.forEach(record => { 260 + console.log(`${record.uri}: ${JSON.stringify(record.value)}`); 261 + }); 262 + ``` 263 + 264 + ### Sync User Collections 265 + 266 + ```typescript 267 + // Sync current user's data (requires auth) 268 + const syncResult = await client.social.slices.slice.syncUserCollections({ 269 + timeoutSeconds: 30 270 + }); 271 + 272 + console.log(`Synced ${syncResult.recordsSynced} records`); 273 + ``` 274 + 275 + ## Error Handling 276 + 277 + ```typescript 278 + try { 279 + const post = await client.com.example.post.getRecord({ 280 + uri: 'at://invalid-uri' 281 + }); 282 + } catch (error) { 283 + if (error.message.includes('404')) { 284 + console.log('Record not found'); 285 + } else if (error.message.includes('401')) { 286 + console.log('Authentication required'); 287 + } else { 288 + console.error('Unexpected error:', error); 289 + } 290 + } 291 + ``` 292 + 293 + ## OAuth Authentication Flow 294 + 295 + ### 1. Initialize OAuth 296 + 297 + ```typescript 298 + const oauthClient = new OAuthClient({ 299 + clientId: process.env.OAUTH_CLIENT_ID, 300 + clientSecret: process.env.OAUTH_CLIENT_SECRET, 301 + authBaseUrl: process.env.OAUTH_AIP_BASE_URL, 302 + redirectUri: 'https://your-app.com/oauth/callback' 303 + }); 304 + ``` 305 + 306 + ### 2. Start Authorization 307 + 308 + ```typescript 309 + const authResult = await oauthClient.authorize({ 310 + loginHint: 'user.bsky.social' 311 + }); 312 + 313 + // Redirect user to authorization URL 314 + window.location.href = authResult.authorizationUrl; 315 + ``` 316 + 317 + ### 3. Handle Callback 318 + 319 + ```typescript 320 + // In your callback handler 321 + const urlParams = new URLSearchParams(window.location.search); 322 + const code = urlParams.get('code'); 323 + const state = urlParams.get('state'); 324 + 325 + await oauthClient.handleCallback({ code, state }); 326 + ``` 327 + 328 + ### 4. Use Authenticated Client 329 + 330 + ```typescript 331 + const client = new AtProtoClient(apiUrl, sliceUri, oauthClient); 332 + 333 + // OAuth tokens are automatically managed 334 + const profile = await client.social.slices.actor.profile.createRecord({ 335 + displayName: "New User", 336 + description: "My profile" 337 + }, true); // useSelfRkey for profile 338 + ``` 339 + 340 + ## Type Safety 341 + 342 + The generated SDK provides full TypeScript type safety: 343 + 344 + ```typescript 345 + // TypeScript knows the shape of your records 346 + const post = await client.com.example.post.getRecord({ uri }); 347 + 348 + // Type error: property 'unknownField' does not exist 349 + // post.value.unknownField 350 + 351 + // Autocomplete works for all fields 352 + post.value.title; // string 353 + post.value.tags; // string[] 354 + post.value.createdAt; // string 355 + 356 + // Creating records is type-checked 357 + await client.com.example.post.createRecord({ 358 + title: "Valid", 359 + content: "Also valid", 360 + createdAt: new Date().toISOString(), 361 + // Type error: 'invalidField' is not assignable 362 + // invalidField: "This will error" 363 + }); 364 + ``` 365 + 366 + ## Advanced Patterns 367 + 368 + ### Batch Operations 369 + 370 + ```typescript 371 + // Process records in batches 372 + async function* getAllRecords() { 373 + let cursor: string | undefined; 374 + 375 + do { 376 + const batch = await client.com.example.post.listRecords({ 377 + limit: 100, 378 + cursor 379 + }); 380 + 381 + yield* batch.records; 382 + cursor = batch.cursor; 383 + } while (cursor); 384 + } 385 + 386 + // Use the generator 387 + for await (const record of getAllRecords()) { 388 + console.log(record.value.title); 389 + } 390 + ``` 391 + 392 + 393 + ## Next Steps 394 + 395 + - [API Reference](./api-reference.md) - Complete endpoint documentation 396 + - [Concepts](./concepts.md) - Understand the architecture 397 + - [Getting Started](./getting-started.md) - Initial setup guide