···11-# Slices Social - A social app for sharing/creating AT Protocol Appviews
11+# Slices
2233-Slices Social is an AT Protocol appview that allows users to create and share
44-slices (appviews).
33+An open-source platform for building AT Protocol appviews with custom data
44+schemas, automatic SDK generation, and built-in sync capabilities.
5566-## Architecture
66+## Overview
7788-- **API** (`/api`) - Rust backend with AT Protocol integration, PostgreSQL
99- database, and dynamic XRPC handlers
1010-- **Frontend** (`/frontend`) - Deno server-side rendered application with OAuth
1111- authentication
88+Slices enables developers to create "slices" - custom appviews within the AT
99+Protocol ecosystem. Each slice can define its own lexicons (schemas), sync data
1010+from other AT Protocol services, and automatically generate type-safe SDKs.
1111+Think of it as your own customizable view into the AT Protocol network.
12121313-## ✅ Completed Features
1313+### Key Features
14141515-### Core Infrastructure
1515+- **Custom Lexicons**: Define your own data schemas using AT Protocol lexicons
1616+- **Automatic SDK Generation**: Get type-safe TypeScript clients generated from
1717+ your lexicons
1818+- **Data Synchronization**: Import and index data from Bluesky and other AT
1919+ Protocol services
2020+- **OAuth Integration**: Built-in AT Protocol authentication
2121+- **Multi-tenant Architecture**: Each slice operates independently with its own
2222+ data validated against its lexicons
2323+- **Dynamic API Endpoints**: CRUD operations automatically created for each
2424+ lexicon record type (collection)
16251717-- [x] AT Protocol OAuth integration with AIP server
1818-- [x] PostgreSQL database with slice-aware queries
1919-- [x] Docker containerization with Nix flakes
2020-- [x] Production deployment pipeline
2626+## Documentation
21272222-### Slice Management
2828+- [Introduction](./docs/intro.md) - What is Slices and why use it
2929+- [Getting Started](./docs/getting-started.md) - Set up your first slice
3030+- [Core Concepts](./docs/concepts.md) - Understand slices, lexicons, and
3131+ collections
3232+- [API Reference](./docs/api-reference.md) - Complete API documentation
3333+- [SDK Usage](./docs/sdk-usage.md) - Using generated TypeScript clients
23342424-- [x] Create and manage slice records
2525-- [x] Define custom lexicons for slices (appviews)
2626-- [x] Slice-specific data filtering
2727-- [x] Slice statistics (records, collections, actors)
3535+## Quick Start
28362929-### Data Synchronization
3737+### Prerequisites
30383131-- [x] Bulk sync from AT Protocol repositories based on domain specific lexicons
3232-- [x] Collection-specific sync with repo filtering
3333-- [x] Indexing of records into local database
3434-- [x] Slice-aware record filtering during sync
3939+- Docker and Docker Compose
4040+- PostgreSQL
4141+- Rust (for API development)
4242+- Deno (for frontend)
35433636-### TypeScript Client Generation
4444+### Installation
4545+4646+1. Clone the repository:
4747+4848+```bash
4949+git clone https://tangled.sh/justslices.net/core
5050+cd core
5151+```
5252+5353+2. Set up environment variables:
37543838-- [x] Dynamic TypeScript client generation from lexicons
3939-- [x] Nested collection structure (e.g.,
4040- `client.social.slices.slice.listRecords()`)
4141-- [x] CRUD operations (create, read, update, delete)
4242-- [x] OAuth client with PKCE authentication flow
4343-- [x] Slice-aware collection operations
4444-- [x] Optional authentication for read-only operations (e.g., listRecords,
4545- getRecord)
5555+Create `.env` files in both `/api` and `/frontend` directories (see
5656+[Getting Started](./docs/getting-started.md) for details).
5757+5858+3. Start the services:
46594747-### Frontend
6060+```bash
6161+# Start the API
6262+cd api
6363+cargo run
48644949-- [x] Server-side rendered pages with HTMX
5050-- [x] User authentication and session management
5151-- [x] Slice overview with index statistics
5252-- [x] Records browser with collection/author filtering
5353-- [x] Sync interface with collection prefilling from lexicons
5454-- [x] Settings page to edit profile
5555-- [x] Encrypted cookie-based sessions
6565+# In another terminal, start the frontend
6666+cd frontend
6767+deno task dev
6868+```
56695757-### API Endpoints
7070+4. Visit `http://localhost:8000` to access the web interface.
58715959-- [x] `/xrpc/social.slices.slice.sync` - Bulk synchronization
6060-- [x] `/xrpc/social.slices.slice.stats` - Slice statistics
6161-- [x] `/xrpc/social.slices.slice.records` - Slice records with filtering
6262-- [x] `/xrpc/social.slices.slice.codegen` - Client code generation (currently
6363- TypeScript only, but designed for extensibility)
6464-- [x] Dynamic collection XRPC endpoints (`*.list`, `*.get`, `*.create`, etc.)
6565-- [x] OAuth endpoints for authentication flow
7272+## Project Structure
66736767-## 🚧 In Progress/Next Up
7474+### API (`/api`)
68756969-- [ ] Connect to Jetstream
7070-- [ ] Add search and filtering functionality to records browser, and generated
7171- client
7272-- [ ] Support more complex lexicon types (e.g., unions, arrays, refs)
7373-- [ ] Lexicon validation
7474-- [ ] Lexicon verification
7575-- [ ] SDK examples and tutorials, implement Statusphere Slice
7676-- [ ] Pagination for large record sets, maybe even a table view would be cool
7777-- [ ] Slice timeline and user profile pages
7878-- [ ] Display xprc docs for dynamic endpoints (looking at
7979- [Scalar](https://github.com/scalar/scalar))
7676+The backend is built in Rust and serves as the core AT Protocol integration
7777+layer. It provides:
80788181-## ❌ The Future
7979+- **AT Protocol XRPC Handlers**: Dynamic endpoints for slice-specific
8080+ collections with full CRUD operations
8181+- **Sync Engine**: Bulk synchronization from AT Protocol repositories
8282+- **Jetstream Integration**: Real-time data streaming from AT Protocol firehose
8383+- **Database Layer**: PostgreSQL integration with slice-aware queries
8484+- **SDK Generation**: Automatically generates type-safe TypeScript clients and
8585+ OpenAPI specifications
8686+- **OAuth Integration**: Handles AT Protocol OAuth flows and token management
82878383-- [ ] Integrated labeler service for basic moderation
8484-- [ ] Auto view hydration strategies, maybe configureable in the UI
8585-- [ ] Add pre-defined lexicons straight from Lexicon Community or other sources
8686-- [ ] Support more languages for client code generation (e.g., Python,
8787- Flutter/Dart, Swift)
8888-- [ ] Integrate/interop with various Microcosm services
8989-- [ ] Cli tool for codegen and other utilities
9090-- [ ] Background job processing queue for syncs
9191-- [ ] Rate limiting and API quotas
9292-- [ ] API docs
9393-- [ ] Appview bug tracking, waitlists, feature flags, analytics, etc.
9494-- [ ] Fork a slice!
9595-- [ ] Strategies for managing/migrating lexicon changes over time
8888+### Frontend (`/frontend`)
96899797-## 🛠️ Development
9090+A server-side rendered web application built with Deno that provides:
98919999-### Prerequisites
9292+- **User Interface**: Web-based slice management, records browsing, and sync
9393+ controls
9494+- **Authentication**: OAuth integration with session management
9595+- **Server-Side Rendering**: HTMX-powered interactive UI with minimal
9696+ client-side JavaScript
9797+- **Generated Client Integration**: Uses auto-generated TypeScript clients for
9898+ API communication
10099101101-- Nix with flakes enabled (not required, used for deployment)
102102-- Docker
103103-- PostgreSQL
100100+## Development
104101105105-### Getting Started
102102+### Running Tests
106103107104```bash
108108-# Start API development
105105+# API tests
109106cd api
110110-cargo run
107107+cargo test
111108112112-# Start frontend development
109109+# Frontend tests
113110cd frontend
114114-deno task dev
111111+deno test
115112```
116113117117-### Useful Scripts
114114+### Database Migrations
118115119119-- `api/scripts/test_sync.sh` - Test local sync endpoint (pre-seed all slices
120120- from the atmosphere)
121121-- `frontend/scripts/register-oauth-client.sh` - Register OAuth client with AIP
116116+```bash
117117+cd api
118118+sqlx migrate run
119119+120120+# If you modify database queries, update the query cache
121121+cargo sqlx prepare
122122+```
123123+124124+### Building for Production
125125+126126+```bash
127127+# Build the API
128128+cd api
129129+cargo build --release
130130+```
131131+132132+## Contributing
122133123123-## 🚀 Deployment
134134+### How to Contribute
124135125125-The service is deployed using Nix-built Docker containers with:
136136+1. Fork the repository
137137+2. Create a feature branch (`git checkout -b feature/amazing-feature`)
138138+3. Commit your changes (`git commit -m 'Add amazing feature'`)
139139+4. Push to the branch (`git push origin feature/amazing-feature`)
140140+5. Open a Pull Request
126141127127-- Fly.io for hosting (move to Upscale?)
128128-- PostgreSQL for data storage
129129-- Environment-based configuration
142142+### Development Guidelines
130143131131-## 📝 Environment Variables
144144+- Follow Rust conventions for API code
145145+- Use Deno formatting for TypeScript/JavaScript
146146+- Write tests for new features following existing patterns
147147+- Update documentation as needed
148148+- Keep commits focused and descriptive
132149133133-### API
150150+### Areas for Contribution
134151135135-- `DATABASE_URL` - PostgreSQL connection string
136136-- `AUTH_BASE_URL` - AIP OAuth service URL
137137-- `PORT` - Server port (default: 3000)
152152+- Lexicon validation and verification
153153+- UI/UX improvements
154154+- Documentation and examples
155155+- Bug fixes and performance improvements
138156139139-### Frontend
157157+## Deployment
140158141141-- `OAUTH_CLIENT_ID` - OAuth application client ID
142142-- `OAUTH_CLIENT_SECRET` - OAuth application client secret
143143-- `OAUTH_REDIRECT_URI` - OAuth callback URL
144144-- `OAUTH_AIP_BASE_URL` - AIP OAuth service URL
145145-- `SESSION_ENCRYPTION_KEY` - Session cookie encryption key
146146-- `API_URL` - Backend API base URL
147147-- `SLICE_URI` - Filters collection based queries by slice
159159+The service can be deployed using Docker containers. We provide:
160160+161161+- Docker Compose configuration for local development
162162+- Nix flakes for reproducible builds
163163+- Fly.io configuration for cloud deployment
164164+165165+See the [deployment guide](./docs/deployment.md) for detailed instructions.
166166+167167+## Environment Variables
168168+169169+### API Configuration
170170+171171+| Variable | Description | Required |
172172+| --------------- | ---------------------------- | -------- |
173173+| `DATABASE_URL` | PostgreSQL connection string | Yes |
174174+| `AUTH_BASE_URL` | AIP OAuth service URL | Yes |
175175+| `PORT` | Server port (default: 3000) | No |
176176+177177+### Frontend Configuration
178178+179179+| Variable | Description | Required |
180180+| --------------------- | ------------------------------- | -------- |
181181+| `OAUTH_CLIENT_ID` | OAuth application client ID | Yes |
182182+| `OAUTH_CLIENT_SECRET` | OAuth application client secret | Yes |
183183+| `OAUTH_REDIRECT_URI` | OAuth callback URL | Yes |
184184+| `OAUTH_AIP_BASE_URL` | AIP OAuth service URL | Yes |
185185+| `API_URL` | Backend API base URL | Yes |
186186+| `SLICE_URI` | Default slice URI for queries | Yes |
187187+188188+## Roadmap
189189+190190+### In Progress
191191+192192+- Documentation
193193+- Frontend UX improvements/social features
194194+- Support more search and filtering params in collection xrpx handlers and SDK
195195+- Surface jetstream and sync logs in the UI
196196+- Improve sync and jetstream reliability
197197+- Monitor api container performance and resource usage
198198+199199+### Planned Features
200200+201201+- Labeler service integration
202202+- CLI tool
203203+- API rate limiting
204204+- Enhanced lexicon management UI
205205+- Lexicon discovery and sharing
206206+207207+## Community
208208+209209+- **Bluesky**: [@justslices.net](https://bsky.app/profile/justslices.net)
210210+- **Discord**: [Join our server](https://discord.gg/your-invite)
211211+212212+## Support
213213+214214+- [Documentation](./docs/)
215215+- [Discord Community](https://discord.gg/your-invite)
216216+217217+## License
218218+219219+This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
220220+for details.
221221+222222+## Acknowledgments
223223+224224+- Built on the [AT Protocol](https://atproto.com)
225225+- Inspired by the AT Protocol community
226226+- Thanks to all contributors
227227+228228+## Status
229229+230230+This project is in active development. APIs may change as we approach v1.0.
231231+232232+---
233233+234234+Made with love for the AT Protocol ecosystem ❤️
-91
api/README.md
···11-# Slice - AT Protocol Indexer
22-33-A Rust-based AT Protocol indexer service with HTMX web interface for syncing and viewing AT Protocol records.
44-55-## Features
66-77-- 📚 **Bulk Collection Sync**: Efficiently sync entire AT Protocol collections
88-- 🔄 **Smart Discovery**: Automatically find repositories with target collections
99-- 🌐 **Web Interface**: HTMX-powered UI for easy bulk operations
1010-- 🚀 **XRPC API**: Native AT Protocol XRPC endpoints
1111-- 🗄️ **PostgreSQL Storage**: Efficient JSONB storage with smart indexing
1212-1313-## Quick Start
1414-1515-### Prerequisites
1616-1717-- Rust 1.70+
1818-- PostgreSQL 12+
1919-2020-### Setup
2121-2222-1. **Clone and setup**:
2323- ```bash
2424- git clone <repo>
2525- cd slice
2626- ```
2727-2828-2. **Database setup**:
2929- ```bash
3030- createdb slice
3131- export DATABASE_URL="postgresql://localhost/slice"
3232- ```
3333-3434-3. **Run the server**:
3535- ```bash
3636- cargo run
3737- ```
3838-3939-4. **Open web interface**: http://127.0.0.1:3000
4040-4141-## Usage
4242-4343-### Web Interface
4444-4545-- **Home**: Overview and quick links
4646-- **Records**: Browse indexed records by collection
4747-- **Sync**: Manually sync individual records
4848-4949-### API Endpoints
5050-5151-- `GET /xrpc/social.slices.records.list?collection=app.bsky.feed.post` - List records
5252-- `POST /xrpc/social.slices.collections.bulkSync` - Bulk sync collections
5353-5454-### Example: Bulk Sync Collections
5555-5656-```bash
5757-curl -X POST "http://127.0.0.1:3000/xrpc/social.slices.collections.bulkSync" \
5858- -H "Content-Type: application/json" \
5959- -d '{"collections": ["app.bsky.feed.post", "app.bsky.actor.profile"]}'
6060-```
6161-6262-### Popular Collections to Sync
6363-6464-```
6565-app.bsky.feed.post # Bluesky posts
6666-app.bsky.actor.profile # User profiles
6767-app.bsky.feed.like # Likes
6868-app.bsky.feed.repost # Reposts
6969-app.bsky.graph.follow # Follows
7070-```
7171-7272-## Architecture
7373-7474-Built following the [AT Protocol Indexer Specification](docs/atproto_indexer_spec.md):
7575-7676-- **Single Table Design**: All records in one `record` table with JSONB for flexibility
7777-- **Smart Syncing**: Hybrid approach supporting both individual record fetch and bulk operations
7878-- **Future CAR Support**: Architecture ready for CAR file import for efficient bulk syncing
7979-8080-## Development
8181-8282-```bash
8383-# Run with auto-reload
8484-cargo watch -x run
8585-8686-# Run tests
8787-cargo test
8888-8989-# Check code
9090-cargo clippy
9191-```
-907
api/docs/atproto_indexer_spec.md
···11-# AT Protocol Indexing Service - Technical Specification
22-33-## Project Overview
44-55-Build a high-performance, scalable indexing service for AT Protocol that
66-automatically generates typed APIs for any lexicon, with intelligent data
77-fetching strategies and real-time synchronization.
88-99-### Core Goals
1010-1111-- **Universal Lexicon Support**: Automatically handle any AT Protocol lexicon
1212- without manual configuration
1313-- **Multi-Language Client Generation**: Generate typed API clients for
1414- TypeScript, Rust, Python, Go, etc.
1515-- **High Performance**: Handle millions of records efficiently with smart
1616- caching and batching
1717-- **Real-time Sync**: Support both bulk imports and live firehose updates
1818-- **Developer Experience**: Hasura-style auto-generated APIs with full type
1919- safety
2020-2121-## Architecture Overview
2222-2323-### Data Storage Strategy
2424-2525-**Primary Database: PostgreSQL**
2626-2727-- Single source of truth for all indexed records
2828-- Single table approach for maximum flexibility across arbitrary lexicons
2929-- JSONB for complete record storage and sophisticated querying
3030-- Optional partitioning by collection for very high volume deployments
3131-3232-```sql
3333--- Single table for all AT Protocol records
3434-CREATE TABLE IF NOT EXISTS "record" (
3535- "uri" TEXT PRIMARY KEY NOT NULL,
3636- "cid" TEXT NOT NULL,
3737- "did" TEXT NOT NULL,
3838- "collection" TEXT NOT NULL,
3939- "json" JSONB NOT NULL, -- Use JSONB for performance and querying
4040- "indexedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
4141-);
4242-4343--- Essential indexes for performance
4444-CREATE INDEX IF NOT EXISTS idx_record_collection ON "record"("collection");
4545-CREATE INDEX IF NOT EXISTS idx_record_did ON "record"("did");
4646-CREATE INDEX IF NOT EXISTS idx_record_indexed_at ON "record"("indexedAt");
4747-CREATE INDEX IF NOT EXISTS idx_record_json_gin ON "record" USING GIN("json");
4848-4949--- Collection-specific indexes for common queries
5050-CREATE INDEX IF NOT EXISTS idx_record_collection_did ON "record"("collection", "did");
5151-CREATE INDEX IF NOT EXISTS idx_record_cid ON "record"("cid");
5252-```
5353-5454-**Caching Strategy**
5555-5656-- **Redis**: Hot data caching, query result caching, rate limiting
5757-- **Application-level**: Compiled lexicon handlers, parsed schemas
5858-- **CDN**: Public API endpoints with appropriate cache headers
5959-6060-**PostgreSQL JSONB Advantages**
6161-6262-- **GIN indexes**: Fast querying on JSON content with `@>`, `?`, `?&`, `?|`
6363- operators
6464-- **JSON operators**: Rich querying with `->`, `->>`, `#>`, `#>>` for nested
6565- access
6666-- **JSON path queries**: Complex nested field access and filtering
6767-- **Performance**: JSONB stored in optimized binary format for fast access
6868-- **Flexibility**: Handle arbitrary lexicon schemas without schema migrations
6969-7070-### Search Implementation
7171-7272-**Hybrid Approach**:
7373-7474-- **PostgreSQL**: Primary queries, exact matches, admin operations, complex
7575- joins
7676-- **Optional Search Engine**: User-facing search, fuzzy matching, aggregations,
7777- analytics
7878-7979-**Search Engine Options**:
8080-8181-- **Typesense**: Easy setup, good performance for smaller deployments
8282-- **Meilisearch**: Excellent for instant search experiences
8383-- **Elasticsearch/OpenSearch**: Full-featured for large-scale deployments
8484-8585-## Record Fetching Strategies
8686-8787-### Decision Matrix
8888-8989-| Scenario | Strategy | Reasoning |
9090-| -------------------- | ----------------------- | -------------------------------------------- |
9191-| Initial sync | CAR file download | Most efficient for bulk data |
9292-| Real-time updates | Firehose stream | Live updates as they happen |
9393-| Catch-up sync (<24h) | List + individual fetch | Good for small gaps |
9494-| Catch-up sync (>24h) | CAR file re-download | More efficient than many individual requests |
9595-| Single record update | Individual fetch | Targeted and fast |
9696-9797-### Implementation Strategy
9898-9999-```rust
100100-async fn smart_sync(&self, did: &str) -> Result<()> {
101101- let last_sync = self.get_last_sync_time(did).await?;
102102-103103- match last_sync {
104104- None => self.sync_repo_car(did).await?, // Initial: CAR file
105105- Some(last) if Utc::now() - last > Duration::hours(24) => {
106106- self.sync_repo_car(did).await? // Full resync: CAR file
107107- }
108108- Some(last) => {
109109- self.incremental_sync(did, last).await? // Incremental: List + fetch
110110- }
111111- }
112112-113113- Ok(())
114114-}
115115-```
116116-117117-## Dynamic Lexicon System
118118-119119-### Why Single Table Works Better for AT Protocol
120120-121121-**Lexicon characteristics that favor single table:**
122122-123123-- **Runtime schema definition**: Lexicons can be arbitrary and defined by any
124124- developer
125125-- **Shared metadata**: All records have common fields (CID, timestamp, author,
126126- etc.)
127127-- **Flexible querying**: Query across different record types seamlessly
128128-- **Unknown schema count**: Could have hundreds of different lexicons
129129-130130-### Unified Query Interface
131131-132132-**Cross-lexicon querying capabilities:**
133133-134134-```sql
135135--- Posts with specific hashtags
136136-SELECT * FROM "record"
137137-WHERE "collection" = 'app.bsky.feed.post'
138138-AND "json"->>'text' ILIKE '%#atproto%';
139139-140140--- All records by author across all lexicons
141141-SELECT "collection", COUNT(*) FROM "record"
142142-WHERE "did" = 'did:plc:example'
143143-GROUP BY "collection";
144144-145145--- Cross-lexicon search for any record with text content
146146-SELECT * FROM "record"
147147-WHERE "json" ? 'text'
148148-AND "json"->>'text' ILIKE '%search term%';
149149-150150--- Recent records across all collections
151151-SELECT "uri", "collection", "json"->>'$type' as record_type, "indexedAt"
152152-FROM "record"
153153-WHERE "indexedAt" > NOW() - INTERVAL '24 hours'
154154-ORDER BY "indexedAt" DESC;
155155-```
156156-157157-### Schema Management
158158-159159-**Components**:
160160-161161-1. **Lexicon Registry**: Parse and store lexicon definitions for validation
162162-2. **Indexer Lexicons**: Define the indexer's own XRPC procedures with proper
163163- lexicons
164164-3. **Validation Layer**: Ensure records conform to their lexicon schemas
165165-4. **XRPC Server**: Serve both indexed AT Protocol data and indexer's own
166166- procedures
167167-5. **Type Generator**: Generate typed interfaces for all lexicons (AT Protocol +
168168- indexer)
169169-170170-### Dynamic Index Creation
171171-172172-```sql
173173--- Add lexicon-specific indexes as needed for performance
174174-CREATE INDEX IF NOT EXISTS idx_posts_text ON "record" USING GIN(("json"->'text'))
175175-WHERE "collection" = 'app.bsky.feed.post';
176176-177177-CREATE INDEX IF NOT EXISTS idx_profiles_handle ON "record"(("json"->>'handle'))
178178-WHERE "collection" = 'app.bsky.actor.profile';
179179-180180--- For very high volume, consider partitioning by collection
181181-CREATE TABLE "record_posts" PARTITION OF "record"
182182-FOR VALUES IN ('app.bsky.feed.post');
183183-184184--- Composite indexes for common query patterns
185185-CREATE INDEX IF NOT EXISTS idx_record_collection_created_at ON "record"("collection", ("json"->>'createdAt'))
186186-WHERE "json" ? 'createdAt';
187187-```
188188-189189-### Implementation Strategy
190190-191191-```rust
192192-async fn register_lexicon(lexicon: LexiconDoc) -> Result<()> {
193193- // 1. Store lexicon definition for validation
194194- self.store_lexicon_schema(lexicon).await?;
195195-196196- // 2. Create collection-specific indexes if needed
197197- self.create_performance_indexes(&lexicon.id).await?;
198198-199199- // 3. Register XRPC handlers for core AT Protocol lexicons
200200- if lexicon.id.starts_with("com.atproto.") {
201201- self.register_atproto_handlers(&lexicon.id).await?;
202202- }
203203-204204- // 4. Generate TypeScript types for all lexicons (AT Protocol + indexer)
205205- self.generate_client_types(&lexicon.id).await?;
206206-207207- Ok(())
208208-}
209209-210210-async fn initialize_indexer_lexicons(&self) -> Result<()> {
211211- // Define and register the indexer's own XRPC procedures
212212- let indexer_lexicons = vec![
213213- self.create_list_records_lexicon(),
214214- self.create_search_records_lexicon(),
215215- self.create_get_record_lexicon(),
216216- // ... other indexer procedures
217217- ];
218218-219219- for lexicon in indexer_lexicons {
220220- self.register_indexer_procedure(lexicon).await?;
221221- }
222222-223223- Ok(())
224224-}
225225-```
226226-227227-### Record Validation
228228-229229-**Validation Layer**: Ensure data integrity with lexicon schema validation
230230-231231-```rust
232232-async fn insert_record(&self, record: ATProtoRecord) -> Result<()> {
233233- // 1. Validate against lexicon schema
234234- let lexicon = self.get_lexicon_schema(&record.collection).await?;
235235- self.validate_record_against_schema(&record.json, &lexicon)?;
236236-237237- // 2. Insert with proper indexing
238238- sqlx::query!(
239239- r#"INSERT INTO "record" ("uri", "cid", "did", "collection", "json", "indexedAt")
240240- VALUES ($1, $2, $3, $4, $5, $6)
241241- ON CONFLICT ("uri")
242242- DO UPDATE SET
243243- "cid" = EXCLUDED."cid",
244244- "json" = EXCLUDED."json",
245245- "indexedAt" = EXCLUDED."indexedAt""#,
246246- record.uri,
247247- record.cid,
248248- record.did,
249249- record.collection,
250250- record.json,
251251- record.indexed_at
252252- ).execute(&self.db).await?;
253253-254254- Ok(())
255255-}
256256-257257-// Batch processing for CAR file imports
258258-async fn batch_insert_records(&self, records: &[ATProtoRecord]) -> Result<()> {
259259- let mut tx = self.db.begin().await?;
260260-261261- for record in records {
262262- sqlx::query!(
263263- r#"INSERT INTO "record" ("uri", "cid", "did", "collection", "json", "indexedAt")
264264- VALUES ($1, $2, $3, $4, $5, $6)
265265- ON CONFLICT ("uri")
266266- DO UPDATE SET
267267- "cid" = EXCLUDED."cid",
268268- "json" = EXCLUDED."json",
269269- "indexedAt" = EXCLUDED."indexedAt""#,
270270- record.uri,
271271- record.cid,
272272- record.did,
273273- record.collection,
274274- record.json,
275275- record.indexed_at
276276- ).execute(&mut *tx).await?;
277277- }
278278-279279- tx.commit().await?;
280280- Ok(())
281281-}
282282-```
283283-284284-### API Generation Strategy
285285-286286-**XRPC Endpoints** with proper lexicon definitions:
287287-288288-```
289289-GET /xrpc/social.slices.records.list # List records for collection
290290-GET /xrpc/social.slices.records.get # Get specific record
291291-POST /xrpc/social.slices.records.create # Create record
292292-POST /xrpc/social.slices.records.update # Update record
293293-POST /xrpc/social.slices.records.delete # Delete record
294294-295295-# Advanced query procedures
296296-GET /xrpc/social.slices.records.search # Full-text search on record content
297297-GET /xrpc/social.slices.records.filter # JSON field filtering
298298-GET /xrpc/social.slices.author.listRecords # All records by author (cross-collection)
299299-GET /xrpc/social.slices.search.global # Global search across all collections
300300-```
301301-302302-**Lexicon Definitions** for indexer procedures:
303303-304304-```json
305305-{
306306- "lexicon": 1,
307307- "id": "social.slices.records.list",
308308- "defs": {
309309- "main": {
310310- "type": "query",
311311- "description": "List records for a specific collection",
312312- "parameters": {
313313- "collection": {
314314- "type": "string",
315315- "description": "Collection/lexicon ID (e.g. app.bsky.feed.post)",
316316- "required": true
317317- },
318318- "author": {
319319- "type": "string",
320320- "description": "Filter by author DID"
321321- },
322322- "limit": {
323323- "type": "integer",
324324- "minimum": 1,
325325- "maximum": 100,
326326- "default": 25
327327- },
328328- "cursor": {
329329- "type": "string",
330330- "description": "Pagination cursor"
331331- }
332332- },
333333- "output": {
334334- "encoding": "application/json",
335335- "schema": {
336336- "type": "object",
337337- "required": ["records"],
338338- "properties": {
339339- "records": {
340340- "type": "array",
341341- "items": { "$ref": "#/defs/indexedRecord" }
342342- },
343343- "cursor": { "type": "string" }
344344- }
345345- }
346346- }
347347- },
348348- "indexedRecord": {
349349- "type": "object",
350350- "required": ["uri", "cid", "value", "indexedAt"],
351351- "properties": {
352352- "uri": { "type": "string", "format": "at-uri" },
353353- "cid": { "type": "string" },
354354- "value": { "type": "unknown" },
355355- "indexedAt": { "type": "string", "format": "datetime" },
356356- "collection": { "type": "string" },
357357- "rkey": { "type": "string" },
358358- "authorDid": { "type": "string", "format": "did" }
359359- }
360360- }
361361- }
362362-}
363363-```
364364-365365-**Benefits of XRPC + Lexicons**:
366366-367367-- **Native AT Protocol**: Indexer becomes a proper AT Protocol service
368368-- **Discoverable APIs**: Lexicons can be fetched and introspected
369369-- **Type Generation**: Same code generation works for indexer APIs
370370-- **Consistent**: Uses established AT Protocol patterns
371371-- **Composable**: Can be mixed with other AT Protocol services
372372-373373-**XRPC Implementation Examples**:
374374-375375-```rust
376376-// XRPC query handler for listing records
377377-async fn handle_list_records(&self, params: ListRecordsParams) -> Result<ListRecordsOutput> {
378378- let records = sqlx::query!(
379379- r#"SELECT "uri", "cid", "did", "collection", "json", "indexedAt"
380380- FROM "record"
381381- WHERE "collection" = $1
382382- AND ($2::text IS NULL OR "did" = $2)
383383- ORDER BY "indexedAt" DESC
384384- LIMIT $3"#,
385385- params.collection,
386386- params.author,
387387- params.limit.unwrap_or(25) as i32
388388- ).fetch_all(&self.db).await?;
389389-390390- let indexed_records: Vec<IndexedRecord> = records.into_iter().map(|row| {
391391- IndexedRecord {
392392- uri: row.uri,
393393- cid: row.cid,
394394- did: row.did,
395395- collection: row.collection,
396396- value: serde_json::from_str(&row.json.to_string()).unwrap_or_default(),
397397- indexed_at: row.indexedAt.to_rfc3339(),
398398- }
399399- }).collect();
400400-401401- Ok(ListRecordsOutput {
402402- records: indexed_records,
403403- cursor: self.generate_cursor(&records).await?,
404404- })
405405-}
406406-407407-// XRPC search handler with JSONB queries
408408-async fn handle_search_records(&self, params: SearchParams) -> Result<SearchOutput> {
409409- let records = sqlx::query!(
410410- r#"SELECT "uri", "cid", "did", "collection", "json", "indexedAt"
411411- FROM "record"
412412- WHERE ($1::text IS NULL OR "collection" = $1)
413413- AND "json"->>'text' ILIKE $2
414414- ORDER BY "indexedAt" DESC
415415- LIMIT $3"#,
416416- params.collection,
417417- format!("%{}%", params.query),
418418- params.limit.unwrap_or(25) as i32
419419- ).fetch_all(&self.db).await?;
420420-421421- Ok(SearchOutput {
422422- records: records.into_iter().map(|row| IndexedRecord {
423423- uri: row.uri,
424424- cid: row.cid,
425425- did: row.did,
426426- collection: row.collection,
427427- value: serde_json::from_str(&row.json.to_string()).unwrap_or_default(),
428428- indexed_at: row.indexedAt.to_rfc3339(),
429429- }).collect()
430430- })
431431-}
432432-```
433433-434434-## Multi-Language Client Generation
435435-436436-### Initial Target: TypeScript
437437-438438-**Primary focus**: Generate fully typed TypeScript clients for web applications
439439-and Node.js services
440440-441441-- **Type Safety**: Complete interfaces for all request/response objects
442442-- **Auto-completion**: Full IDE support with generated types
443443-- **Runtime Validation**: Optional runtime type checking
444444-- **Documentation**: Auto-generated JSDoc comments from lexicon descriptions
445445-446446-### Future Language Support
447447-448448-**Planned targets** for multi-language expansion:
449449-450450-- **Rust**: High-performance services, CLI tools
451451-- **Python**: Data analysis, ML workflows, web backends
452452-- **Go**: Microservices, system tools
453453-454454-### Code Generation Pipeline
455455-456456-**Extensible architecture** designed for multiple languages:
457457-458458-```rust
459459-trait CodeGenerator {
460460- fn generate_client(&self, lexicons: &[LexiconDoc]) -> Result<String>;
461461- fn generate_types(&self, lexicon: &LexiconDoc) -> Result<String>;
462462- fn generate_method(&self, nsid: &str, def: &LexiconDef) -> Result<String>;
463463-}
464464-465465-// Initial implementation: TypeScript
466466-impl CodeGenerator for TypeScriptGenerator {
467467- fn generate_client(&self, lexicons: &[LexiconDoc]) -> Result<String> {
468468- // Generate TypeScript client with full type safety
469469- }
470470-}
471471-472472-// Future implementations:
473473-// impl CodeGenerator for RustGenerator { /* ... */ }
474474-// impl CodeGenerator for PythonGenerator { /* ... */ }
475475-// impl CodeGenerator for GoGenerator { /* ... */ }
476476-```
477477-478478-### TypeScript Client Generation
479479-480480-**Type-Safe Generic XRPC Client with Auto-Discovery:**
481481-482482-```typescript
483483-// Registry of all known collections -> their record types
484484-interface CollectionRecordMap {
485485- // Core AT Protocol (always included)
486486- "app.bsky.feed.post": PostRecord;
487487- "app.bsky.actor.profile": ProfileRecord;
488488- "app.bsky.feed.like": LikeRecord;
489489-490490- // Dynamically discovered custom lexicons
491491- "recipes.cooking-app.com": RecipeRecord;
492492- "tasks.productivity-tool.io": TaskRecord;
493493- "photos.gallery-app.net": PhotoRecord;
494494- "someRecord.something-cool.indexer.com": SomeCustomRecord;
495495-}
496496-497497-// Generic input/output types with conditional typing
498498-interface CreateRecordInput<T extends keyof CollectionRecordMap> {
499499- collection: T;
500500- repo: string; // The DID that will become the 'did' field
501501- rkey?: string; // Used to construct the URI
502502- record: CollectionRecordMap[T]; // Type depends on collection!
503503-}
504504-505505-interface ListRecordsParams<T extends keyof CollectionRecordMap> {
506506- collection: T;
507507- author?: string;
508508- limit?: number;
509509- cursor?: string;
510510-}
511511-512512-interface ListRecordsOutput<T extends keyof CollectionRecordMap> {
513513- records: Array<{
514514- uri: string;
515515- cid: string;
516516- did: string; // Author DID
517517- collection: T;
518518- value: CollectionRecordMap[T]; // Typed based on collection (parsed from json field)
519519- indexedAt: string;
520520- }>;
521521- cursor?: string;
522522-}
523523-524524-// Generated client class with conditional types
525525-export class ATProtoIndexerClient {
526526- private client: AxiosInstance;
527527-528528- constructor(baseURL: string, accessToken?: string) {
529529- this.client = axios.create({
530530- baseURL,
531531- headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : {},
532532- });
533533- }
534534-535535- // Generic method - fully typed based on collection parameter
536536- async createRecord<T extends keyof CollectionRecordMap>(
537537- input: CreateRecordInput<T>,
538538- ): Promise<CreateRecordOutput>;
539539-540540- // Fallback for unknown collections
541541- async createRecord(input: {
542542- collection: string;
543543- repo: string;
544544- rkey?: string;
545545- record: unknown;
546546- }): Promise<CreateRecordOutput>;
547547-548548- // Implementation handles both cases
549549- async createRecord(input: any): Promise<CreateRecordOutput> {
550550- const response = await this.client.post(
551551- "/xrpc/social.slices.records.create",
552552- input,
553553- );
554554- return response.data;
555555- }
556556-557557- // Generic typed list method
558558- async listRecords<T extends keyof CollectionRecordMap>(
559559- params: ListRecordsParams<T>,
560560- ): Promise<ListRecordsOutput<T>>;
561561-562562- // Fallback for unknown collections
563563- async listRecords(params: {
564564- collection: string;
565565- author?: string;
566566- limit?: number;
567567- cursor?: string;
568568- }): Promise<ListRecordsOutput<string>>;
569569-570570- async listRecords(params: any): Promise<any> {
571571- const response = await this.client.get("/xrpc/social.slices.records.list", {
572572- params,
573573- });
574574- return response.data;
575575- }
576576-577577- // Convenience methods for popular collections
578578- async createPost(
579579- input: Omit<CreateRecordInput<"app.bsky.feed.post">, "collection">,
580580- ) {
581581- return this.createRecord({ ...input, collection: "app.bsky.feed.post" });
582582- }
583583-584584- async listPosts(
585585- params: Omit<ListRecordsParams<"app.bsky.feed.post">, "collection">,
586586- ) {
587587- return this.listRecords({ ...params, collection: "app.bsky.feed.post" });
588588- }
589589-590590- // Auto-generated convenience methods for custom lexicons
591591- async createRecipe(
592592- input: Omit<CreateRecordInput<"recipes.cooking-app.com">, "collection">,
593593- ) {
594594- return this.createRecord({
595595- ...input,
596596- collection: "recipes.cooking-app.com",
597597- });
598598- }
599599-600600- async searchRecords(params: SearchRecordsParams): Promise<SearchOutput> {
601601- const response = await this.client.get("/xrpc/social.slices.records.search", {
602602- params,
603603- });
604604- return response.data;
605605- }
606606-}
607607-```
608608-609609-**Usage Examples with Full Type Safety:**
610610-611611-```typescript
612612-const indexer = new ATProtoIndexerClient("https://indexer.example.com");
613613-614614-// ✅ Fully typed for known collections
615615-await indexer.createPost({
616616- repo: "did:plc:user123",
617617- record: {
618618- $type: "app.bsky.feed.post",
619619- text: "Hello!",
620620- createdAt: new Date().toISOString(),
621621- // TypeScript knows this must be a PostRecord
622622- },
623623-});
624624-625625-// ✅ Custom lexicon with full typing
626626-await indexer.createRecord({
627627- collection: "recipes.cooking-app.com",
628628- repo: "did:plc:chef456",
629629- record: {
630630- $type: "recipes.cooking-app.com",
631631- title: "Pizza",
632632- ingredients: ["dough", "sauce", "cheese"],
633633- difficulty: "easy",
634634- // TypeScript enforces RecipeRecord structure
635635- },
636636-});
637637-638638-// ✅ Query with same type safety - returns typed results
639639-const posts = await indexer.listPosts({
640640- author: "did:plc:user123",
641641- limit: 50,
642642-});
643643-// posts.records[0].value is typed as PostRecord!
644644-645645-// ✅ Unknown collection - falls back gracefully
646646-await indexer.createRecord({
647647- collection: "new-app.startup.xyz",
648648- repo: "did:plc:user789",
649649- record: {
650650- customField: "value", // No type checking, but still works
651651- },
652652-});
653653-```
654654-655655-**Auto-Discovery Implementation:**
656656-657657-```rust
658658-// Indexer discovers and registers custom lexicons dynamically
659659-impl ATProtoIndexer {
660660- async fn discover_lexicons(&self) -> Result<Vec<LexiconDoc>> {
661661- let mut lexicons = Vec::new();
662662-663663- // Core AT Protocol lexicons
664664- lexicons.extend(self.load_core_lexicons().await?);
665665-666666- // Custom lexicons from indexed records
667667- let custom_collections = sqlx::query!(
668668- r#"SELECT DISTINCT "collection" FROM "record"
669669- WHERE "collection" NOT LIKE 'app.bsky.%'
670670- AND "collection" NOT LIKE 'com.atproto.%'"#
671671- ).fetch_all(&self.db).await?;
672672-673673- for row in custom_collections {
674674- if let Ok(lexicon) = self.fetch_lexicon_definition(&row.collection).await {
675675- lexicons.push(lexicon);
676676- }
677677- }
678678-679679- Ok(lexicons)
680680- }
681681-682682- async fn fetch_lexicon_definition(&self, nsid: &str) -> Result<LexiconDoc> {
683683- // Fetch from domain's well-known endpoint
684684- let domain = nsid.split('.').last().unwrap_or("");
685685- let lexicon_url = format!("https://{}/.well-known/atproto/lexicon/{}", domain, nsid);
686686-687687- let response = self.client.get(&lexicon_url).send().await?;
688688- let lexicon: LexiconDoc = response.json().await?;
689689- Ok(lexicon)
690690- }
691691-692692- async fn regenerate_typescript_client(&self) -> Result<()> {
693693- let all_lexicons = self.discover_lexicons().await?;
694694- let typescript_code = self.typescript_generator.generate_client(&all_lexicons)?;
695695-696696- // Write to file or serve via API endpoint
697697- self.write_client_code("typescript", &typescript_code).await?;
698698- Ok(())
699699- }
700700-701701- // Get statistics about indexed collections
702702- async fn get_collection_stats(&self) -> Result<Vec<CollectionStats>> {
703703- let stats = sqlx::query!(
704704- r#"SELECT "collection",
705705- COUNT(*) as record_count,
706706- COUNT(DISTINCT "did") as unique_authors,
707707- MIN("indexedAt") as first_indexed,
708708- MAX("indexedAt") as last_indexed
709709- FROM "record"
710710- GROUP BY "collection"
711711- ORDER BY record_count DESC"#
712712- ).fetch_all(&self.db).await?;
713713-714714- Ok(stats.into_iter().map(|row| CollectionStats {
715715- collection: row.collection,
716716- record_count: row.record_count.unwrap_or(0) as u64,
717717- unique_authors: row.unique_authors.unwrap_or(0) as u64,
718718- first_indexed: row.first_indexed,
719719- last_indexed: row.last_indexed,
720720- }).collect())
721721- }
722722-}
723723-```
724724-725725-**Lexicon Discovery Protocol:**
726726-727727-```json
728728-// GET https://cooking-app.com/.well-known/atproto/lexicon/recipes.cooking-app.com
729729-{
730730- "lexicon": 1,
731731- "id": "recipes.cooking-app.com",
732732- "description": "Recipe sharing lexicon",
733733- "defs": {
734734- "main": {
735735- "type": "record",
736736- "record": {
737737- "type": "object",
738738- "required": ["$type", "title", "ingredients"],
739739- "properties": {
740740- "$type": { "const": "recipes.cooking-app.com" },
741741- "title": { "type": "string" },
742742- "ingredients": { "type": "array", "items": { "type": "string" } },
743743- "cookingTime": { "type": "integer" },
744744- "difficulty": { "type": "string", "enum": ["easy", "medium", "hard"] }
745745- }
746746- }
747747- }
748748- }
749749-}
750750-```
751751-752752-**Generated CLI with Discovery:**
753753-754754-```bash
755755-# Generate TypeScript client with auto-discovered lexicons
756756-npx atproto-codegen typescript \
757757- --discover \
758758- --output ./src/generated/indexer-client.ts \
759759- --endpoint https://your-indexer.com
760760-761761-# Or specify additional custom lexicons
762762-npx atproto-codegen typescript \
763763- --lexicons recipes.cooking-app.com,tasks.productivity-tool.io \
764764- --output ./src/generated/indexer-client.ts \
765765- --endpoint https://your-indexer.com
766766-```
767767-768768-## Implementation Technology Stack
769769-770770-### Backend: Rust
771771-772772-**Rationale**:
773773-774774-- Zero-copy parsing of CAR files and CBOR data
775775-- Memory safety for long-running indexing processes
776776-- High-performance concurrent processing
777777-- Strong type system prevents runtime errors
778778-- Excellent async ecosystem (Tokio)
779779-780780-### Client Generation: TypeScript (Initial Target)
781781-782782-**Rationale**:
783783-784784-- **Primary ecosystem**: Most AT Protocol developers use JavaScript/TypeScript
785785-- **Immediate value**: Web apps and Node.js services are common use cases
786786-- **Type safety**: Excellent TypeScript support for generated interfaces
787787-- **Developer experience**: Full IDE support with auto-completion
788788-- **Ecosystem compatibility**: Works with React, Next.js, Express, etc.
789789-790790-### Key Dependencies
791791-792792-```toml
793793-[dependencies]
794794-tokio = { version = "1.0", features = ["full"] }
795795-sqlx = { version = "0.7", features = ["postgres", "chrono", "serde_json"] }
796796-serde = { version = "1.0", features = ["derive"] }
797797-reqwest = { version = "0.11", features = ["json", "stream"] }
798798-libipld = { version = "0.16", features = ["dag-cbor", "car"] }
799799-tokio-tungstenite = "0.20" # WebSocket for firehose
800800-redis = { version = "0.23", features = ["tokio-comp"] }
801801-tracing = "0.1"
802802-803803-# Code generation dependencies
804804-handlebars = "4.0" # Template engine for TypeScript generation
805805-```
806806-807807-## Performance Optimizations
808808-809809-### Concurrent Processing
810810-811811-- **Bounded concurrency**: Limit simultaneous CAR file processing
812812-- **Streaming**: Process large CAR files without loading entirely into memory
813813-- **Batching**: Group database operations for better throughput
814814-- **Connection pooling**: Efficient database connection management
815815-816816-### Rate Limiting
817817-818818-```rust
819819-// Token bucket implementation for API rate limiting
820820-struct RateLimiter {
821821- tokens: Arc<Mutex<f64>>,
822822- max_tokens: f64,
823823- refill_rate: f64, // tokens per second
824824-}
825825-```
826826-827827-### Memory Management
828828-829829-- **Streaming CAR processing**: Avoid loading entire repos into memory
830830-- **LRU caches**: Intelligent caching of frequently accessed data
831831-- **Pagination**: Cursor-based pagination for large result sets
832832-833833-## Real-Time Synchronization
834834-835835-### Firehose Integration
836836-837837-```rust
838838-async fn start_firehose_listener(&self) -> Result<()> {
839839- let (ws_stream, _) = connect_async(
840840- "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos"
841841- ).await?;
842842-843843- // Process commits in real-time
844844- while let Some(msg) = read.next().await {
845845- if let Ok(commit) = self.parse_commit(&msg) {
846846- self.process_commit(commit).await?;
847847- }
848848- }
849849-850850- Ok(())
851851-}
852852-```
853853-854854-### Sync Strategies
855855-856856-1. **Initial Bootstrap**: Download existing data via CAR files
857857-2. **Real-time Updates**: Process firehose stream for live changes
858858-3. **Periodic Reconciliation**: Compare local state with remote to catch missed
859859- updates
860860-4. **Backfill**: Handle gaps in data due to downtime
861861-862862-## API Design
863863-864864-### Core Principles
865865-866866-- **RESTful**: Follow REST conventions where applicable
867867-- **Lexicon-Agnostic**: Work with any current or future AT Protocol lexicon
868868-- **Type-Safe**: Generate strongly typed clients
869869-- **Cacheable**: Design for HTTP caching and CDN distribution
870870-- **Paginated**: Support cursor-based pagination for large datasets
871871-872872-### Authentication
873873-874874-- **Optional**: Support authenticated requests for private data
875875-- **Bearer tokens**: Standard AT Protocol authentication
876876-- **Rate limiting**: Per-user and global rate limits
877877-878878-### Response Format
879879-880880-```json
881881-{
882882- "data": [...],
883883- "cursor": "next_page_token",
884884- "count": 42,
885885- "total": 1337
886886-}
887887-```
888888-889889-## Risk Mitigation
890890-891891-### Data Consistency
892892-893893-- **Idempotent operations**: Safe to retry any indexing operation
894894-- **Checksum validation**: Verify CAR file integrity
895895-- **Reconciliation**: Periodic comparison with authoritative sources
896896-897897-### Scalability
898898-899899-- **Horizontal scaling**: Design for multiple indexer instances
900900-- **Database sharding**: Partition by lexicon type or DID prefix
901901-- **Caching layers**: Multiple levels of caching for performance
902902-903903-### Operational
904904-905905-- **Circuit breakers**: Prevent cascade failures
906906-- **Graceful degradation**: Continue operating with reduced functionality
907907-- **Monitoring**: Comprehensive observability for quick issue detection
+483
docs/api-reference.md
···11+# API Reference
22+33+Complete reference for Slices API endpoints.
44+55+## Base URL
66+77+```
88+https://your-api-domain.com/xrpc/
99+```
1010+1111+## Authentication
1212+1313+Most write operations require OAuth 2.0 authentication. Include the access token in the Authorization header:
1414+1515+```
1616+Authorization: Bearer YOUR_ACCESS_TOKEN
1717+```
1818+1919+Read operations typically work without authentication.
2020+2121+## Core Endpoints
2222+2323+### Slice Management
2424+2525+#### `social.slices.slice.listRecords`
2626+2727+List all slices.
2828+2929+**Method**: GET
3030+3131+**Parameters**:
3232+- `limit` (number, optional): Maximum records to return (default: 50)
3333+- `cursor` (string, optional): Pagination cursor
3434+- `sort` (string, optional): Sort field and order (e.g., `createdAt:desc`)
3535+- `author` (string, optional): Filter by author DID
3636+- `authors` (string[], optional): Filter by multiple author DIDs
3737+3838+**Response**:
3939+```json
4040+{
4141+ "records": [
4242+ {
4343+ "uri": "at://did:plc:abc/social.slices.slice/xyz",
4444+ "cid": "bafyrei...",
4545+ "did": "did:plc:abc",
4646+ "collection": "social.slices.slice",
4747+ "value": {
4848+ "name": "My Slice",
4949+ "domain": "com.example",
5050+ "createdAt": "2024-01-01T00:00:00Z"
5151+ },
5252+ "indexedAt": "2024-01-01T00:00:00Z"
5353+ }
5454+ ],
5555+ "cursor": "next-page-cursor"
5656+}
5757+```
5858+5959+#### `social.slices.slice.getRecord`
6060+6161+Get a specific slice by URI.
6262+6363+**Method**: GET
6464+6565+**Parameters**:
6666+- `uri` (string, required): AT Protocol URI of the slice
6767+6868+**Response**: Single record object (same structure as listRecords item)
6969+7070+#### `social.slices.slice.createRecord`
7171+7272+Create a new slice.
7373+7474+**Method**: POST
7575+7676+**Authentication**: Required
7777+7878+**Body**:
7979+```json
8080+{
8181+ "slice": "at://your-slice-uri",
8282+ "record": {
8383+ "$type": "social.slices.slice",
8484+ "name": "My New Slice",
8585+ "domain": "com.example",
8686+ "createdAt": "2024-01-01T00:00:00Z"
8787+ },
8888+ "rkey": "optional-record-key"
8989+}
9090+```
9191+9292+**Response**:
9393+```json
9494+{
9595+ "uri": "at://did:plc:abc/social.slices.slice/xyz",
9696+ "cid": "bafyrei..."
9797+}
9898+```
9999+100100+### Slice Operations
101101+102102+#### `social.slices.slice.stats`
103103+104104+Get statistics for a slice.
105105+106106+**Method**: POST
107107+108108+**Body**:
109109+```json
110110+{
111111+ "slice": "at://your-slice-uri"
112112+}
113113+```
114114+115115+**Response**:
116116+```json
117117+{
118118+ "success": true,
119119+ "collections": ["com.example.post", "app.bsky.actor.profile"],
120120+ "collectionStats": [
121121+ {
122122+ "collection": "com.example.post",
123123+ "recordCount": 150,
124124+ "uniqueActors": 10
125125+ }
126126+ ],
127127+ "totalLexicons": 5,
128128+ "totalRecords": 500,
129129+ "totalActors": 25,
130130+ "message": "Statistics retrieved successfully"
131131+}
132132+```
133133+134134+#### `social.slices.slice.records`
135135+136136+Browse records in a slice.
137137+138138+**Method**: POST
139139+140140+**Body**:
141141+```json
142142+{
143143+ "slice": "at://your-slice-uri",
144144+ "collection": "com.example.post",
145145+ "repo": "did:plc:optional-filter",
146146+ "limit": 20,
147147+ "cursor": "pagination-cursor"
148148+}
149149+```
150150+151151+**Response**:
152152+```json
153153+{
154154+ "success": true,
155155+ "records": [
156156+ {
157157+ "uri": "at://did:plc:abc/com.example.post/xyz",
158158+ "cid": "bafyrei...",
159159+ "did": "did:plc:abc",
160160+ "collection": "com.example.post",
161161+ "value": { /* record data */ },
162162+ "indexedAt": "2024-01-01T00:00:00Z"
163163+ }
164164+ ],
165165+ "cursor": "next-page-cursor"
166166+}
167167+```
168168+169169+#### `social.slices.slice.syncUserCollections`
170170+171171+Synchronously sync collections for the authenticated user.
172172+173173+**Method**: POST
174174+175175+**Authentication**: Required
176176+177177+**Body**:
178178+```json
179179+{
180180+ "slice": "at://your-slice-uri",
181181+ "timeoutSeconds": 30
182182+}
183183+```
184184+185185+**Response**:
186186+```json
187187+{
188188+ "success": true,
189189+ "reposProcessed": 1,
190190+ "recordsSynced": 45,
191191+ "timedOut": false,
192192+ "message": "Sync completed successfully"
193193+}
194194+```
195195+196196+#### `social.slices.slice.startSync`
197197+198198+Start an asynchronous bulk sync job.
199199+200200+**Method**: POST
201201+202202+**Authentication**: Required
203203+204204+**Body**:
205205+```json
206206+{
207207+ "slice": "at://your-slice-uri",
208208+ "collections": ["com.example.post"],
209209+ "externalCollections": ["app.bsky.actor.profile"],
210210+ "repos": ["did:plc:abc", "did:plc:xyz"],
211211+ "limitPerRepo": 100
212212+}
213213+```
214214+215215+**Response**:
216216+```json
217217+{
218218+ "success": true,
219219+ "jobId": "job-uuid",
220220+ "message": "Sync job started"
221221+}
222222+```
223223+224224+#### `social.slices.slice.codegen`
225225+226226+Generate TypeScript client code.
227227+228228+**Method**: POST
229229+230230+**Body**:
231231+```json
232232+{
233233+ "target": "typescript",
234234+ "slice": "at://your-slice-uri"
235235+}
236236+```
237237+238238+**Response**:
239239+```json
240240+{
241241+ "success": true,
242242+ "generatedCode": "// Generated TypeScript client code..."
243243+}
244244+```
245245+246246+## Dynamic Collection Endpoints
247247+248248+For each collection in your slice, the following endpoints are automatically generated:
249249+250250+### `[collection].listRecords`
251251+252252+List records in a collection.
253253+254254+**Method**: GET
255255+256256+**Parameters**:
257257+- `slice` (string, required): Slice URI
258258+- `limit` (number, optional): Maximum records (default: 50)
259259+- `cursor` (string, optional): Pagination cursor
260260+- `sort` (string, optional): Sort specification
261261+- `author` (string, optional): Filter by author DID
262262+- `authors` (string[], optional): Filter by multiple DIDs
263263+264264+### `[collection].getRecord`
265265+266266+Get a single record.
267267+268268+**Method**: GET
269269+270270+**Parameters**:
271271+- `slice` (string, required): Slice URI
272272+- `uri` (string, required): Record URI
273273+274274+### `[collection].searchRecords`
275275+276276+Search within a collection.
277277+278278+**Method**: GET
279279+280280+**Parameters**:
281281+- `slice` (string, required): Slice URI
282282+- `query` (string, required): Search query
283283+- `field` (string, optional): Specific field to search
284284+- `limit` (number, optional): Maximum results
285285+- `cursor` (string, optional): Pagination cursor
286286+- `sort` (string, optional): Sort specification
287287+288288+### `[collection].createRecord`
289289+290290+Create a new record.
291291+292292+**Method**: POST
293293+294294+**Authentication**: Required
295295+296296+**Body**:
297297+```json
298298+{
299299+ "slice": "at://your-slice-uri",
300300+ "record": {
301301+ "$type": "collection.name",
302302+ /* record fields */
303303+ },
304304+ "rkey": "optional-key"
305305+}
306306+```
307307+308308+### `[collection].updateRecord`
309309+310310+Update an existing record.
311311+312312+**Method**: POST
313313+314314+**Authentication**: Required
315315+316316+**Body**:
317317+```json
318318+{
319319+ "slice": "at://your-slice-uri",
320320+ "rkey": "record-key",
321321+ "record": {
322322+ "$type": "collection.name",
323323+ /* updated fields */
324324+ }
325325+}
326326+```
327327+328328+### `[collection].deleteRecord`
329329+330330+Delete a record.
331331+332332+**Method**: POST
333333+334334+**Authentication**: Required
335335+336336+**Body**:
337337+```json
338338+{
339339+ "rkey": "record-key"
340340+}
341341+```
342342+343343+## Lexicon Management
344344+345345+### `social.slices.lexicon.listRecords`
346346+347347+List lexicons in a slice.
348348+349349+**Method**: GET
350350+351351+**Parameters**: Same as collection.listRecords
352352+353353+### `social.slices.lexicon.createRecord`
354354+355355+Add a lexicon to a slice.
356356+357357+**Method**: POST
358358+359359+**Authentication**: Required
360360+361361+**Body**:
362362+```json
363363+{
364364+ "slice": "at://your-slice-uri",
365365+ "record": {
366366+ "$type": "social.slices.lexicon",
367367+ "nsid": "com.example.post",
368368+ "definitions": "{\"lexicon\": 1, ...}",
369369+ "createdAt": "2024-01-01T00:00:00Z",
370370+ "slice": "at://your-slice-uri"
371371+ }
372372+}
373373+```
374374+375375+## Actor Management
376376+377377+### `social.slices.slice.getActors`
378378+379379+Get actors (users) in a slice.
380380+381381+**Method**: GET
382382+383383+**Parameters**:
384384+- `slice` (string, required): Slice URI
385385+- `search` (string, optional): Search query
386386+- `dids` (string[], optional): Filter by DIDs
387387+- `limit` (number, optional): Maximum results
388388+- `cursor` (string, optional): Pagination cursor
389389+390390+**Response**:
391391+```json
392392+{
393393+ "actors": [
394394+ {
395395+ "did": "did:plc:abc",
396396+ "handle": "user.bsky.social",
397397+ "sliceUri": "at://slice-uri",
398398+ "indexedAt": "2024-01-01T00:00:00Z"
399399+ }
400400+ ],
401401+ "cursor": "next-page"
402402+}
403403+```
404404+405405+## Blob Upload
406406+407407+### `com.atproto.repo.uploadBlob`
408408+409409+Upload a blob (image, file).
410410+411411+**Method**: POST
412412+413413+**Authentication**: Required
414414+415415+**Headers**:
416416+- `Content-Type`: MIME type of the blob
417417+418418+**Body**: Raw binary data
419419+420420+**Response**:
421421+```json
422422+{
423423+ "blob": {
424424+ "$type": "blob",
425425+ "ref": { "$link": "bafkrei..." },
426426+ "mimeType": "image/jpeg",
427427+ "size": 127198
428428+ }
429429+}
430430+```
431431+432432+## Error Responses
433433+434434+All endpoints may return error responses:
435435+436436+```json
437437+{
438438+ "error": "InvalidRequest",
439439+ "message": "Detailed error message"
440440+}
441441+```
442442+443443+Common HTTP status codes:
444444+- `200`: Success
445445+- `400`: Bad request
446446+- `401`: Authentication required
447447+- `403`: Forbidden
448448+- `404`: Not found
449449+- `500`: Internal server error
450450+451451+## Pagination
452452+453453+List endpoints support cursor-based pagination:
454454+455455+1. Make initial request without cursor
456456+2. Use returned cursor for next page
457457+3. Continue until no cursor returned
458458+459459+Example:
460460+```javascript
461461+let cursor = undefined;
462462+do {
463463+ const response = await fetch(`/xrpc/collection.listRecords?cursor=${cursor}`);
464464+ const data = await response.json();
465465+ // Process records
466466+ cursor = data.cursor;
467467+} while (cursor);
468468+```
469469+470470+## Sorting
471471+472472+Sort parameter format: `field:order` or `field1:order1,field2:order2`
473473+474474+Examples:
475475+- `createdAt:desc` - Newest first
476476+- `name:asc` - Alphabetical
477477+- `createdAt:desc,name:asc` - Newest first, then alphabetical
478478+479479+## Next Steps
480480+481481+- [SDK Usage](./sdk-usage.md) - Using generated TypeScript clients
482482+- [Getting Started](./getting-started.md) - Build your first application
483483+- [Concepts](./concepts.md) - Understand the architecture
+314
docs/concepts.md
···11+# Core Concepts
22+33+Understanding these core concepts will help you effectively use Slices.
44+55+## Slices
66+77+A slice is an independent appview within the AT Protocol ecosystem. Think of it
88+as your own data universe with custom schemas and records.
99+1010+### Key Properties
1111+1212+- **URI**: Unique AT Protocol URI (e.g.,
1313+ `at://did:plc:abc123/social.slices.slice/3xyz`)
1414+- **Name**: Human-readable identifier
1515+- **Domain**: Namespace for lexicons (e.g., `com.example`, `social.grain`)
1616+- **Creation Date**: When the slice was created
1717+1818+### Slice Isolation
1919+2020+Each slice maintains complete data isolation:
2121+2222+- Records are filtered by slice URI in all queries
2323+- Sync operations respect slice boundaries
2424+- Statistics are calculated per-slice
2525+- Users can have different data in different slices
2626+2727+## Lexicons
2828+2929+Lexicons are JSON schemas that define record types in AT Protocol. They specify
3030+the structure, validation rules, and metadata for records.
3131+3232+### Lexicon Structure
3333+3434+```json
3535+{
3636+ "lexicon": 1,
3737+ "id": "com.example.blogPost",
3838+ "defs": {
3939+ "main": {
4040+ "type": "record",
4141+ "description": "A blog post record",
4242+ "record": {
4343+ "type": "object",
4444+ "properties": {
4545+ "title": { "type": "string" },
4646+ "content": { "type": "string" },
4747+ "publishedAt": { "type": "string", "format": "datetime" }
4848+ },
4949+ "required": ["title", "content"]
5050+ }
5151+ }
5252+ }
5353+}
5454+```
5555+5656+### Supported Types
5757+5858+- **Primitives**: string, number, integer, boolean
5959+- **Complex**: object, array, union, ref
6060+- **Special**: blob (for media), cid-link, at-uri
6161+- **Formats**: datetime, at-identifier, did, handle
6262+6363+### Lexicon Namespacing
6464+6565+Lexicons follow reverse domain naming:
6666+6767+- `com.example.post` - A post in the example.com namespace
6868+- `social.slices.slice` - Core slice record type
6969+- `app.bsky.actor.profile` - Bluesky profile (external)
7070+7171+## Collections
7272+7373+Collections are groups of records with the same lexicon type. They map directly
7474+to XRPC endpoints.
7575+7676+### Primary Collections
7777+7878+Collections that match your slice's domain namespace. For example, if your slice
7979+domain is `com.example`, then `com.example.post` would be a primary collection.
8080+8181+### External Collections
8282+8383+Collections from other namespaces that you've synced into your slice. For
8484+example:
8585+8686+- Bluesky profiles (`app.bsky.actor.profile`)
8787+- Bluesky posts (`app.bsky.feed.post`)
8888+- Collections from other slices
8989+9090+### Collection Operations
9191+9292+Both primary and external collections support the same operations:
9393+9494+- `*.listRecords` - List with pagination and filtering
9595+- `*.getRecord` - Get single record by URI
9696+- `*.createRecord` - Create new record
9797+- `*.updateRecord` - Update existing record
9898+- `*.deleteRecord` - Remove record
9999+- `*.searchRecords` - Search within collection
100100+101101+The key difference is conceptual: primary collections are "native" to your
102102+slice's domain, while external collections are imported from other namespaces.
103103+104104+## Records
105105+106106+Records are individual data items stored in collections.
107107+108108+### Record Properties
109109+110110+- **URI**: Unique AT Protocol URI
111111+- **CID**: Content identifier (hash)
112112+- **DID**: Owner's decentralized identifier
113113+- **Collection**: Lexicon type
114114+- **Value**: Actual record data
115115+- **IndexedAt**: When record was indexed
116116+117117+### Record Keys (rkeys)
118118+119119+Records use keys for identification:
120120+121121+- **Self**: Special key for singleton records (e.g., profiles)
122122+- **TID**: Timestamp-based identifiers
123123+- **Custom**: User-defined keys
124124+125125+### Record Lifecycle
126126+127127+1. **Creation**: Via API or sync
128128+2. **Indexing**: Stored in PostgreSQL
129129+3. **Updates**: New versions with new CIDs
130130+4. **Deletion**: Soft or hard delete
131131+132132+## Sync Engine
133133+134134+The sync engine imports AT Protocol data into your slice using multiple
135135+strategies for optimal performance and reliability.
136136+137137+### Sync Types
138138+139139+**Bulk Sync**: One-time import of historical data
140140+141141+- Specify collections to sync
142142+- Filter by repositories (DIDs)
143143+- Set limits per repository
144144+- Uses optimized bulk database operations
145145+146146+**User Sync**: Sync data for authenticated user
147147+148148+- Automatic on login
149149+- Timeout protection (30 seconds default)
150150+- External collection discovery
151151+- Synchronous operation for immediate feedback
152152+153153+**Jetstream Sync**: Real-time updates via WebSocket
154154+155155+- Subscribe to AT Protocol firehose
156156+- Filter relevant events by slice collections
157157+- Automatic record updates and deletions
158158+- Built-in reconnection with exponential backoff
159159+160160+### Sync Process
161161+162162+1. **Discovery**: Find available records via AT Protocol relay
163163+2. **Filtering**: Apply slice and collection filters
164164+3. **Validation**: Check lexicon compliance against slice schemas
165165+4. **Storage**: Index in database using bulk operations
166166+5. **Deduplication**: Skip existing records (by CID comparison)
167167+168168+### Performance Optimizations
169169+170170+**CID-Based Deduplication**
171171+172172+- Compare Content Identifiers (CIDs) before processing
173173+- Skip records that haven't changed since last sync
174174+- Reduces unnecessary database operations and validation overhead
175175+176176+**Actor Caching**
177177+178178+- Pre-load actor lookup cache to avoid database hits during Jetstream processing
179179+- Cache (DID, slice_uri) mappings for external collection filtering
180180+- Periodic cache refresh every 5 minutes
181181+182182+### Jetstream Reliability
183183+184184+**Automatic Recovery**
185185+186186+- Infinite retry loop with exponential backoff (5 seconds → 5 minutes max)
187187+- Fresh consumer instance creation on each retry
188188+- Database connectivity monitoring and recovery
189189+- Connection status tracking via atomic flags
190190+191191+**Error Handling**
192192+193193+- Graceful degradation when database connections fail
194194+- Validation fallback with fresh lexicon loading from database
195195+- Separate error handling for primary vs external collections
196196+197197+**Configuration Reloading**
198198+199199+- Automatic slice configuration refresh every 5 minutes
200200+- Dynamic collection filtering based on slice lexicons
201201+- Actor cache updates to reflect new slice membership
202202+203203+## XRPC Handlers
204204+205205+XRPC (Cross-Protocol Remote Procedure Call) handlers provide the API layer.
206206+207207+### Dynamic Handlers
208208+209209+Automatically generated from lexicons:
210210+211211+- No manual endpoint creation
212212+- Type-safe request/response
213213+- Automatic validation
214214+- OAuth integration
215215+216216+### Core Handlers
217217+218218+Built-in endpoints for slice management:
219219+220220+- `social.slices.slice.stats` - Slice statistics
221221+- `social.slices.slice.records` - Browse records
222222+- `social.slices.slice.codegen` - Generate SDKs
223223+- `social.slices.slice.sync` - Trigger sync
224224+225225+### Handler Authentication
226226+227227+- **Read Operations**: Optional auth (public by default)
228228+- **Write Operations**: Require OAuth tokens
229229+- **Admin Operations**: Require slice ownership
230230+231231+## Generated SDKs
232232+233233+Type-safe client libraries generated from lexicons.
234234+235235+### SDK Features
236236+237237+- **Type Safety**: Full TypeScript types
238238+- **Nested Structure**: Matches lexicon namespacing
239239+- **OAuth Integration**: Automatic token handling
240240+- **Error Handling**: Retry logic and graceful failures
241241+242242+### SDK Generation Process
243243+244244+1. Parse slice lexicons
245245+2. Generate TypeScript interfaces
246246+3. Create client classes
247247+4. Add utility functions
248248+5. Format and validate
249249+250250+### Using Generated SDKs
251251+252252+```typescript
253253+// Initialize client
254254+const client = new AtProtoClient(apiUrl, sliceUri, oauthClient);
255255+256256+// Use nested structure matching lexicons
257257+await client.com.example.post.listRecords();
258258+await client.social.slices.slice.stats();
259259+await client.app.bsky.actor.profile.getRecord({ uri });
260260+```
261261+262262+## Authentication
263263+264264+OAuth 2.0 with PKCE for secure authentication.
265265+266266+### OAuth Flow
267267+268268+1. **Authorization**: Redirect to AT Protocol provider
269269+2. **Callback**: Exchange code for tokens
270270+3. **Token Storage**: Secure client-side storage
271271+4. **Refresh**: Automatic token renewal
272272+273273+### Session Management
274274+275275+- Encrypted cookies for web sessions
276276+- Token refresh before expiration
277277+- Graceful degradation for read-only access
278278+279279+## Blob Handling
280280+281281+Media files use blob references with CDN URLs.
282282+283283+### Blob Structure
284284+285285+```json
286286+{
287287+ "$type": "blob",
288288+ "ref": { "$link": "bafkreig5bcb..." },
289289+ "mimeType": "image/jpeg",
290290+ "size": 127198
291291+}
292292+```
293293+294294+### CDN URL Generation
295295+296296+Convert blob references to CDN URLs using Bluesky's CDN:
297297+298298+```typescript
299299+recordBlobToCdnUrl(record, blobRef, "avatar");
300300+// -> https://cdn.bsky.app/img/avatar/plain/did:plc:abc/bafkrei...@jpeg
301301+```
302302+303303+### Bluesky CDN Presets
304304+305305+- `avatar` - Profile pictures
306306+- `banner` - Cover images
307307+- `feed_thumbnail` - Small previews
308308+- `feed_fullsize` - Full resolution
309309+310310+## Next Steps
311311+312312+- [API Reference](./api-reference.md) - Detailed endpoint documentation
313313+- [SDK Usage](./sdk-usage.md) - Advanced client patterns
314314+- [Getting Started](./getting-started.md) - Build your first slice
+23
docs/deployment.md
···11+# Deployment Guide
22+33+Documentation for deploying Slices to production is coming soon.
44+55+## Basic Requirements
66+77+- [AIP server](https://github.com/graze-social/aip) running and accessible
88+- Registered OAuth client with AIP
99+- PostgreSQL database
1010+1111+## Environment Setup
1212+1313+See the [environment variables](../README.md#environment-variables) section in the README for required configuration.
1414+1515+## Docker
1616+1717+Docker images can be built using the provided Dockerfile in each directory.
1818+1919+## More Information
2020+2121+For now, refer to:
2222+- [Getting Started](./getting-started.md) for local setup
2323+- [README](../README.md) for environment configuration
+223
docs/getting-started.md
···11+# Getting Started with Slices
22+33+This guide will help you set up Slices and create your first slice.
44+55+## Prerequisites
66+77+- Docker and Docker Compose
88+- PostgreSQL (or use Docker)
99+- Deno (for frontend)
1010+- Rust and Cargo (for API development)
1111+- An AT Protocol account (for OAuth)
1212+1313+## Initial Setup
1414+1515+### 1. Clone the Repository
1616+1717+```bash
1818+git clone https://github.com/your-org/slice
1919+cd slice
2020+```
2121+2222+### 2. Set Up the Database
2323+2424+Start PostgreSQL using Docker:
2525+2626+```bash
2727+docker-compose up -d postgres
2828+```
2929+3030+Or use an existing PostgreSQL instance and create a database:
3131+3232+```sql
3333+CREATE DATABASE slices;
3434+```
3535+3636+### 3. Configure Environment Variables
3737+3838+Create `.env` files for both API and frontend:
3939+4040+**API (`/api/.env`)**:
4141+```bash
4242+DATABASE_URL=postgres://user:password@localhost:5432/slices
4343+AUTH_BASE_URL=https://aip.your-domain.com
4444+PORT=3000
4545+```
4646+4747+**Frontend (`/frontend/.env`)**:
4848+```bash
4949+OAUTH_CLIENT_ID=your-client-id
5050+OAUTH_CLIENT_SECRET=your-client-secret
5151+OAUTH_REDIRECT_URI=http://localhost:8000/oauth/callback
5252+OAUTH_AIP_BASE_URL=https://aip.your-domain.com
5353+SESSION_ENCRYPTION_KEY=your-32-char-key
5454+API_URL=http://localhost:3000
5555+SLICE_URI=at://did:plc:your-did/social.slices.slice/your-slice-id
5656+DATABASE_URL=slices.db
5757+```
5858+5959+### 4. Register OAuth Client
6060+6161+Register your application with the AIP server:
6262+6363+```bash
6464+cd frontend
6565+./scripts/register-oauth-client.sh
6666+```
6767+6868+Save the client ID and secret to your `.env` file.
6969+7070+### 5. Start the Services
7171+7272+Start the API server:
7373+```bash
7474+cd api
7575+cargo run
7676+```
7777+7878+Start the frontend:
7979+```bash
8080+cd frontend
8181+deno task dev
8282+```
8383+8484+Visit `http://localhost:8000` to access the web interface.
8585+8686+## Creating Your First Slice
8787+8888+### 1. Log In
8989+9090+Click "Login" and authenticate with your AT Protocol account.
9191+9292+### 2. Create a Slice
9393+9494+Click "Create Slice" and provide:
9595+- **Name**: A friendly name for your slice
9696+- **Domain**: Your namespace (e.g., `com.example`)
9797+9898+### 3. Define a Lexicon
9999+100100+Navigate to your slice and go to the Lexicon tab. Create a lexicon for your first record type:
101101+102102+```json
103103+{
104104+ "lexicon": 1,
105105+ "id": "com.example.post",
106106+ "defs": {
107107+ "main": {
108108+ "type": "record",
109109+ "description": "A blog post",
110110+ "record": {
111111+ "type": "object",
112112+ "properties": {
113113+ "title": {
114114+ "type": "string",
115115+ "description": "Post title"
116116+ },
117117+ "content": {
118118+ "type": "string",
119119+ "description": "Post content"
120120+ },
121121+ "createdAt": {
122122+ "type": "string",
123123+ "format": "datetime",
124124+ "description": "Creation timestamp"
125125+ },
126126+ "tags": {
127127+ "type": "array",
128128+ "items": {
129129+ "type": "string"
130130+ },
131131+ "description": "Post tags"
132132+ }
133133+ },
134134+ "required": ["title", "content", "createdAt"]
135135+ }
136136+ }
137137+ }
138138+}
139139+```
140140+141141+### 4. Generate TypeScript Client
142142+143143+Navigate to the Code Generation tab and click "Generate TypeScript Client". This creates a type-safe client library for your slice.
144144+145145+### 5. Use the Generated Client
146146+147147+In your application:
148148+149149+```typescript
150150+import { AtProtoClient } from "./generated-client.ts";
151151+152152+const client = new AtProtoClient(
153153+ 'http://localhost:3000',
154154+ 'at://did:plc:your-did/social.slices.slice/your-slice-id'
155155+);
156156+157157+// List posts
158158+const posts = await client.com.example.post.listRecords();
159159+160160+// Create a post
161161+const newPost = await client.com.example.post.createRecord({
162162+ title: "My First Post",
163163+ content: "Hello from Slices!",
164164+ createdAt: new Date().toISOString(),
165165+ tags: ["introduction", "slices"]
166166+});
167167+168168+// Get a specific post
169169+const post = await client.com.example.post.getRecord({
170170+ uri: newPost.uri
171171+});
172172+```
173173+174174+## Syncing External Data
175175+176176+To import data from other AT Protocol repositories:
177177+178178+### 1. Navigate to Sync
179179+180180+Go to your slice and click the Sync tab.
181181+182182+### 2. Configure Sync
183183+184184+Choose collections to sync:
185185+- **Primary Collections**: Your slice's lexicons
186186+- **External Collections**: Bluesky or other AT Protocol collections
187187+188188+### 3. Start Sync
189189+190190+Specify repositories (DIDs) to sync from, or leave empty to sync all available data.
191191+192192+### 4. Monitor Progress
193193+194194+The sync will run in the background. Check the status in the UI or via API.
195195+196196+## Next Steps
197197+198198+- [Core Concepts](./concepts.md) - Understand slices, lexicons, and collections
199199+- [API Reference](./api-reference.md) - Explore available endpoints
200200+- [SDK Usage](./sdk-usage.md) - Advanced SDK patterns
201201+- [Examples](./examples/) - Sample applications
202202+203203+## Troubleshooting
204204+205205+### Database Connection Issues
206206+- Verify PostgreSQL is running: `docker ps`
207207+- Check DATABASE_URL format
208208+- Ensure database exists
209209+210210+### OAuth Errors
211211+- Verify client ID and secret
212212+- Check redirect URI matches configuration
213213+- Ensure AIP server is accessible
214214+215215+### Sync Not Working
216216+- Check user has necessary permissions
217217+- Verify lexicons are valid
218218+- Check API server logs for errors
219219+220220+### Generated Client Issues
221221+- Regenerate client after lexicon changes
222222+- Ensure API server is running
223223+- Check for TypeScript compilation errors
+95
docs/intro.md
···11+# Introduction to Slices
22+33+Slices is an AT Protocol appview platform that enables developers to create
44+custom data slices (appviews) with their own lexicons, sync AT Protocol data,
55+and generate type-safe SDKs.
66+77+## What is a Slice?
88+99+A slice is a custom appview within the AT Protocol ecosystem. Each slice:
1010+1111+- Has its own domain namespace (e.g., `social.grain`, `xyz.statusphere`)
1212+- Defines custom lexicons (schemas) for record types
1313+- Can sync both internal and external AT Protocol collections
1414+- Provides automatically generated type-safe SDKs
1515+1616+## Why Slices?
1717+1818+Building AT Protocol applications typically requires:
1919+2020+- Setting up infrastructure to index and query AT Protocol data
2121+- Managing OAuth authentication flows
2222+- Implementing XRPC handlers for CRUD operations
2323+- Creating client libraries for frontend integration
2424+2525+Slices provides all of this infrastructure out of the box, letting you focus on
2626+your application logic.
2727+2828+## Key Features
2929+3030+### Dynamic API Generation
3131+3232+Define a lexicon, and Slices automatically creates REST endpoints for:
3333+3434+- Listing records with filtering and sorting
3535+- Getting individual records by URI
3636+- Creating new records with OAuth authentication
3737+- Updating existing records
3838+- Deleting records
3939+- Searching within collections
4040+4141+### Data Synchronization
4242+4343+Sync AT Protocol data into your slice:
4444+4545+- Import external collections (e.g., Bluesky profiles, posts)
4646+- Filter data by repository or collection
4747+- Maintain slice-specific data isolation
4848+- Real-time sync via Jetstream (coming soon)
4949+5050+### Type-Safe SDK Generation
5151+5252+Automatically generate TypeScript clients with:
5353+5454+- Full type safety for all record types
5555+- OAuth authentication integration
5656+- Nested API structure matching your lexicons
5757+- Blob/CDN URL utilities for media handling
5858+5959+### Multi-Tenant Architecture
6060+6161+Each slice operates independently:
6262+6363+- Isolated data storage
6464+- Custom lexicon definitions
6565+- Separate sync configurations
6666+- Per-slice statistics and analytics
6767+6868+## Use Cases
6969+7070+- **Custom Social Apps**: Build specialized social networks with custom record
7171+ types
7272+- **Data Aggregators**: Collect and organize AT Protocol data for analysis
7373+- **Specialized Feeds**: Create curated views of AT Protocol content
7474+- **Research Tools**: Index and query specific subsets of AT Protocol data
7575+- **Creative Applications**: Blogs, galleries, portfolios built on AT Protocol
7676+7777+## Architecture Overview
7878+7979+Slices consists of two main components:
8080+8181+- **API Server** (Rust): Handles AT Protocol integration, database operations,
8282+ and API endpoints
8383+- **Frontend** (Deno): Provides web UI for slice management and user
8484+ authentication
8585+8686+Both components work together to provide a complete AT Protocol appview
8787+platform. The API server can also be run standalone as a headless service and
8888+interactive with via it's XRPC apis.
8989+9090+## Next Steps
9191+9292+- [Getting Started](./getting-started.md) - Set up your first slice
9393+- [Core Concepts](./concepts.md) - Understand the key concepts
9494+- [API Reference](./api-reference.md) - Explore the API endpoints
9595+- [SDK Usage](./sdk-usage.md) - Learn to use generated clients
+397
docs/sdk-usage.md
···11+# SDK Usage Guide
22+33+This guide covers how to use the generated TypeScript SDK for your slice.
44+55+## Installation
66+77+After generating your TypeScript client, you can use it directly in your project:
88+99+```typescript
1010+import { AtProtoClient } from "./generated-client.ts";
1111+import { OAuthClient } from "@slices/oauth";
1212+```
1313+1414+## Basic Setup
1515+1616+### Without Authentication (Read-Only)
1717+1818+```typescript
1919+const client = new AtProtoClient(
2020+ 'https://api.your-domain.com',
2121+ 'at://did:plc:abc/social.slices.slice/your-slice-id'
2222+);
2323+2424+// Read operations work without auth
2525+const records = await client.com.example.post.listRecords();
2626+```
2727+2828+### With Authentication (Full Access)
2929+3030+```typescript
3131+import { OAuthClient } from "@slices/oauth";
3232+3333+// Set up OAuth client
3434+const oauthClient = new OAuthClient({
3535+ clientId: 'your-client-id',
3636+ clientSecret: 'your-client-secret',
3737+ authBaseUrl: 'https://aip.your-domain.com',
3838+ redirectUri: 'https://your-app.com/oauth/callback',
3939+ scopes: ['atproto:atproto', 'atproto:transition:generic']
4040+});
4141+4242+// Initialize API client with OAuth
4343+const client = new AtProtoClient(
4444+ 'https://api.your-domain.com',
4545+ 'at://did:plc:abc/social.slices.slice/your-slice-id',
4646+ oauthClient
4747+);
4848+```
4949+5050+## CRUD Operations
5151+5252+### Listing Records
5353+5454+```typescript
5555+// List all records
5656+const posts = await client.com.example.post.listRecords();
5757+5858+// With pagination
5959+const page1 = await client.com.example.post.listRecords({ limit: 20 });
6060+const page2 = await client.com.example.post.listRecords({
6161+ limit: 20,
6262+ cursor: page1.cursor
6363+});
6464+6565+// With filtering
6666+const userPosts = await client.com.example.post.listRecords({
6767+ author: 'did:plc:user123'
6868+});
6969+7070+// With sorting
7171+const recentPosts = await client.com.example.post.listRecords({
7272+ sort: 'createdAt:desc'
7373+});
7474+7575+// Multiple sort fields
7676+const sortedPosts = await client.com.example.post.listRecords({
7777+ sort: 'createdAt:desc,title:asc'
7878+});
7979+```
8080+8181+### Getting a Single Record
8282+8383+```typescript
8484+const post = await client.com.example.post.getRecord({
8585+ uri: 'at://did:plc:abc/com.example.post/3xyz'
8686+});
8787+8888+console.log(post.value.title);
8989+console.log(post.value.content);
9090+```
9191+9292+### Creating Records
9393+9494+```typescript
9595+// Create with auto-generated key
9696+const newPost = await client.com.example.post.createRecord({
9797+ title: "My New Post",
9898+ content: "This is the content",
9999+ createdAt: new Date().toISOString(),
100100+ tags: ["typescript", "atproto"]
101101+});
102102+103103+console.log(`Created: ${newPost.uri}`);
104104+105105+// Create with custom key
106106+const customPost = await client.com.example.post.createRecord(
107107+ {
108108+ title: "Custom Key Post",
109109+ content: "Using a custom record key",
110110+ createdAt: new Date().toISOString()
111111+ },
112112+ true // useSelfRkey for singleton records like profiles
113113+);
114114+```
115115+116116+### Updating Records
117117+118118+```typescript
119119+// Get the record key from the URI
120120+const uri = 'at://did:plc:abc/com.example.post/3xyz';
121121+const rkey = uri.split('/').pop(); // '3xyz'
122122+123123+const updated = await client.com.example.post.updateRecord(
124124+ rkey,
125125+ {
126126+ title: "Updated Title",
127127+ content: "Updated content",
128128+ createdAt: new Date().toISOString(),
129129+ updatedAt: new Date().toISOString()
130130+ }
131131+);
132132+133133+console.log(`Updated: ${updated.cid}`);
134134+```
135135+136136+### Deleting Records
137137+138138+```typescript
139139+const rkey = '3xyz';
140140+await client.com.example.post.deleteRecord(rkey);
141141+```
142142+143143+### Searching Records
144144+145145+```typescript
146146+// Basic search
147147+const results = await client.com.example.post.searchRecords({
148148+ query: "typescript"
149149+});
150150+151151+// Search specific field
152152+const titleResults = await client.com.example.post.searchRecords({
153153+ query: "guide",
154154+ field: "title"
155155+});
156156+157157+// Search with pagination
158158+const searchPage = await client.com.example.post.searchRecords({
159159+ query: "tutorial",
160160+ limit: 10,
161161+ cursor: previousCursor
162162+});
163163+```
164164+165165+## Working with External Collections
166166+167167+Access synced external collections like Bluesky profiles:
168168+169169+```typescript
170170+// List Bluesky profiles in your slice
171171+const profiles = await client.app.bsky.actor.profile.listRecords();
172172+173173+// Get a specific profile
174174+const profile = await client.app.bsky.actor.profile.getRecord({
175175+ uri: 'at://did:plc:user/app.bsky.actor.profile/self'
176176+});
177177+178178+// Access profile data
179179+console.log(profile.value.displayName);
180180+console.log(profile.value.description);
181181+```
182182+183183+## Blob Handling
184184+185185+### Uploading Blobs
186186+187187+```typescript
188188+// Read file as ArrayBuffer
189189+const file = await Deno.readFile('./image.jpg');
190190+191191+// Upload blob
192192+const blobResponse = await client.uploadBlob({
193193+ data: file,
194194+ mimeType: 'image/jpeg'
195195+});
196196+197197+// Use blob in a record
198198+const postWithImage = await client.com.example.post.createRecord({
199199+ title: "Post with Image",
200200+ content: "Check out this image!",
201201+ image: blobResponse.blob,
202202+ createdAt: new Date().toISOString()
203203+});
204204+```
205205+206206+### Converting Blobs to CDN URLs
207207+208208+```typescript
209209+import { recordBlobToCdnUrl } from "./generated-client.ts";
210210+211211+// Get a record with a blob
212212+const profile = await client.app.bsky.actor.profile.getRecord({
213213+ uri: 'at://did:plc:user/app.bsky.actor.profile/self'
214214+});
215215+216216+// Convert avatar blob to CDN URL
217217+if (profile.value.avatar) {
218218+ const avatarUrl = recordBlobToCdnUrl(
219219+ profile,
220220+ profile.value.avatar,
221221+ 'avatar' // Size preset
222222+ );
223223+ console.log(`Avatar URL: ${avatarUrl}`);
224224+}
225225+226226+// Available presets:
227227+// - 'avatar': Small square images
228228+// - 'banner': Wide header images
229229+// - 'feed_thumbnail': Small feed previews
230230+// - 'feed_fullsize': Full resolution images
231231+```
232232+233233+## Slice Operations
234234+235235+### Get Slice Statistics
236236+237237+```typescript
238238+const stats = await client.social.slices.slice.stats({
239239+ slice: 'at://your-slice-uri'
240240+});
241241+242242+console.log(`Total records: ${stats.totalRecords}`);
243243+console.log(`Total actors: ${stats.totalActors}`);
244244+245245+stats.collectionStats.forEach(stat => {
246246+ console.log(`${stat.collection}: ${stat.recordCount} records`);
247247+});
248248+```
249249+250250+### Browse Slice Records
251251+252252+```typescript
253253+const records = await client.social.slices.slice.records({
254254+ slice: 'at://your-slice-uri',
255255+ collection: 'com.example.post',
256256+ limit: 50
257257+});
258258+259259+records.records.forEach(record => {
260260+ console.log(`${record.uri}: ${JSON.stringify(record.value)}`);
261261+});
262262+```
263263+264264+### Sync User Collections
265265+266266+```typescript
267267+// Sync current user's data (requires auth)
268268+const syncResult = await client.social.slices.slice.syncUserCollections({
269269+ timeoutSeconds: 30
270270+});
271271+272272+console.log(`Synced ${syncResult.recordsSynced} records`);
273273+```
274274+275275+## Error Handling
276276+277277+```typescript
278278+try {
279279+ const post = await client.com.example.post.getRecord({
280280+ uri: 'at://invalid-uri'
281281+ });
282282+} catch (error) {
283283+ if (error.message.includes('404')) {
284284+ console.log('Record not found');
285285+ } else if (error.message.includes('401')) {
286286+ console.log('Authentication required');
287287+ } else {
288288+ console.error('Unexpected error:', error);
289289+ }
290290+}
291291+```
292292+293293+## OAuth Authentication Flow
294294+295295+### 1. Initialize OAuth
296296+297297+```typescript
298298+const oauthClient = new OAuthClient({
299299+ clientId: process.env.OAUTH_CLIENT_ID,
300300+ clientSecret: process.env.OAUTH_CLIENT_SECRET,
301301+ authBaseUrl: process.env.OAUTH_AIP_BASE_URL,
302302+ redirectUri: 'https://your-app.com/oauth/callback'
303303+});
304304+```
305305+306306+### 2. Start Authorization
307307+308308+```typescript
309309+const authResult = await oauthClient.authorize({
310310+ loginHint: 'user.bsky.social'
311311+});
312312+313313+// Redirect user to authorization URL
314314+window.location.href = authResult.authorizationUrl;
315315+```
316316+317317+### 3. Handle Callback
318318+319319+```typescript
320320+// In your callback handler
321321+const urlParams = new URLSearchParams(window.location.search);
322322+const code = urlParams.get('code');
323323+const state = urlParams.get('state');
324324+325325+await oauthClient.handleCallback({ code, state });
326326+```
327327+328328+### 4. Use Authenticated Client
329329+330330+```typescript
331331+const client = new AtProtoClient(apiUrl, sliceUri, oauthClient);
332332+333333+// OAuth tokens are automatically managed
334334+const profile = await client.social.slices.actor.profile.createRecord({
335335+ displayName: "New User",
336336+ description: "My profile"
337337+}, true); // useSelfRkey for profile
338338+```
339339+340340+## Type Safety
341341+342342+The generated SDK provides full TypeScript type safety:
343343+344344+```typescript
345345+// TypeScript knows the shape of your records
346346+const post = await client.com.example.post.getRecord({ uri });
347347+348348+// Type error: property 'unknownField' does not exist
349349+// post.value.unknownField
350350+351351+// Autocomplete works for all fields
352352+post.value.title; // string
353353+post.value.tags; // string[]
354354+post.value.createdAt; // string
355355+356356+// Creating records is type-checked
357357+await client.com.example.post.createRecord({
358358+ title: "Valid",
359359+ content: "Also valid",
360360+ createdAt: new Date().toISOString(),
361361+ // Type error: 'invalidField' is not assignable
362362+ // invalidField: "This will error"
363363+});
364364+```
365365+366366+## Advanced Patterns
367367+368368+### Batch Operations
369369+370370+```typescript
371371+// Process records in batches
372372+async function* getAllRecords() {
373373+ let cursor: string | undefined;
374374+375375+ do {
376376+ const batch = await client.com.example.post.listRecords({
377377+ limit: 100,
378378+ cursor
379379+ });
380380+381381+ yield* batch.records;
382382+ cursor = batch.cursor;
383383+ } while (cursor);
384384+}
385385+386386+// Use the generator
387387+for await (const record of getAllRecords()) {
388388+ console.log(record.value.title);
389389+}
390390+```
391391+392392+393393+## Next Steps
394394+395395+- [API Reference](./api-reference.md) - Complete endpoint documentation
396396+- [Concepts](./concepts.md) - Understand the architecture
397397+- [Getting Started](./getting-started.md) - Initial setup guide