···32 run: nix flake check --refresh
3334 - name: Build Rust binary
35- run: nix build .#slice
3637 - name: Build Docker image
38- run: nix build .#sliceImg
3940 - name: Load Docker image
41 run: docker load < result
···58 echo "Checking PostgreSQL status..."
59 docker ps | grep postgres-test
60 docker logs postgres-test
61-62 # Test PostgreSQL connection
63 echo "Testing PostgreSQL connection..."
64 docker exec postgres-test psql -U slice -d slice_test -c "SELECT version();"
65-66 # Start the Slice container with PostgreSQL backend
67 echo "Starting Slice container..."
68 docker run -d --name slice-test -p 8080:8080 \
···74 # Wait for the service to start and show logs
75 echo "Waiting for service to start..."
76 sleep 10
77-78 echo "Checking all containers (including exited)..."
79 docker ps -a | grep -E "(slice-test|postgres-test)" || true
80-81 echo "Checking Slice container status..."
82 docker ps | grep slice-test || echo "Slice container not running!"
83-84 echo "Slice container logs:"
85 docker logs slice-test 2>&1 || echo "Failed to get logs"
86-87 echo "Checking if slice container exited:"
88 docker inspect slice-test --format='{{.State.Status}}' || echo "Container not found"
89 docker inspect slice-test --format='{{.State.ExitCode}}' || echo "No exit code"
90-91 # Additional wait
92 sleep 10
93-94 echo "Final Slice container logs:"
95 docker logs slice-test 2>&1 || echo "Failed to get final logs"
96
···32 run: nix flake check --refresh
3334 - name: Build Rust binary
35+ run: nix build .#slices
3637 - name: Build Docker image
38+ run: nix build .#slicesImg
3940 - name: Load Docker image
41 run: docker load < result
···58 echo "Checking PostgreSQL status..."
59 docker ps | grep postgres-test
60 docker logs postgres-test
61+62 # Test PostgreSQL connection
63 echo "Testing PostgreSQL connection..."
64 docker exec postgres-test psql -U slice -d slice_test -c "SELECT version();"
65+66 # Start the Slice container with PostgreSQL backend
67 echo "Starting Slice container..."
68 docker run -d --name slice-test -p 8080:8080 \
···74 # Wait for the service to start and show logs
75 echo "Waiting for service to start..."
76 sleep 10
77+78 echo "Checking all containers (including exited)..."
79 docker ps -a | grep -E "(slice-test|postgres-test)" || true
80+81 echo "Checking Slice container status..."
82 docker ps | grep slice-test || echo "Slice container not running!"
83+84 echo "Slice container logs:"
85 docker logs slice-test 2>&1 || echo "Failed to get logs"
86+87 echo "Checking if slice container exited:"
88 docker inspect slice-test --format='{{.State.Status}}' || echo "Container not found"
89 docker inspect slice-test --format='{{.State.ExitCode}}' || echo "No exit code"
90+91 # Additional wait
92 sleep 10
93+94 echo "Final Slice container logs:"
95 docker logs slice-test 2>&1 || echo "Failed to get final logs"
96
+4-1
api/.env.example
···1# Database configuration
2-DATABASE_URL=postgresql://slice:slice@localhost:5432/slice
34# Server configuration
5PORT=3000
···910# AT Protocol relay endpoint for syncing data
11RELAY_ENDPOINT=https://relay1.us-west.bsky.network
0001213# Logging level
14RUST_LOG=debug
···1# Database configuration
2+DATABASE_URL=postgresql://slices:slices@localhost:5432/slices
34# Server configuration
5PORT=3000
···910# AT Protocol relay endpoint for syncing data
11RELAY_ENDPOINT=https://relay1.us-west.bsky.network
12+13+# System slice URI
14+SYSTEM_SLICE_URI=at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z
1516# Logging level
17RUST_LOG=debug
+103-29
api/CLAUDE.md
···1-### OAuth 2.0 Endpoints with AIP
0000000000023-The AIP server implements the following OAuth 2.0 endpoints:
45-- `GET ${AIP_BASE_URL}/oauth/authorize` - Authorization endpoint for OAuth flows
6-- `POST ${AIP_BASE_URL}/oauth/token` - Token endpoint for exchanging
7- authorization codes for access tokens
8-- `POST ${AIP_BASE_URL}/oauth/par` - Pushed Authorization Request endpoint
9- (RFC 9126)
10-- `POST ${AIP_BASE_URL}/oauth/clients/register` - Dynamic Client Registration
11- endpoint (RFC 7591)
12-- `GET ${AIP_BASE_URL}/oauth/atp/callback` - ATProtocol OAuth callback handler
13-- `GET ${AIP_BASE_URL}/.well-known/oauth-authorization-server` - OAuth server
14- metadata discovery (RFC 8414)
15-- `GET ${AIP_BASE_URL}/.well-known/oauth-protected-resource` - Protected
16- resource metadata
17-- `GET ${AIP_BASE_URL}/.well-known/jwks.json` - JSON Web Key Set for token
18- verification
19-- `GET ${AIP_BASE_URL}/oauth/userinfo` - introspection endpoint returning claims
20- info where sub is the user's atproto did
21-- `GET ${AIP_BASE_URL}/api/atproto/session` - returns atproto session data
0000000000000000000000000000000000000000000000000000000000000000002223## Error Handling
2425All error strings must use this format:
2627- error-aip-<domain>-<number> <message>: <details>
2829Example errors:
3031-- error-slice-resolve-1 Multiple DIDs resolved for method
32-- error-slice-plc-1 HTTP request failed: https://google.com/ Not Found
33-- error-slice-key-1 Error decoding key: invalid
3435Errors should be represented as enums using the `thiserror` library when
36possible using `src/errors.rs` as a reference and example.
···4849## HTTP Handler Organization
5051-HTTP handlers should be organized as Rust source files in the `src/http`
52-directory and should have the `handler_` prefix. Each handler should have it's
53-own request and response types and helper functionality.
54-55-Example handler: `handler_index.rs`
5657- After updating, run `cargo check` to fix errors and warnings
58- Don't use dead code, if it's not used remove it
59-- Ise htmx and hyperscript when possible, if not javascript in script tag is ok
···1+# CLAUDE.md
2+3+This file provides guidance to Claude Code (claude.ai/code) when working with
4+code in this repository.
5+6+## Project Overview
7+8+Slices is an AT Protocol (ATProto) indexing and querying service that allows
9+developers to create custom slices (subsets) of the ATProto network data. It
10+indexes records from the Bluesky/ATProto network via Jetstream, validates them
11+against Lexicon schemas, and provides flexible querying capabilities through an
12+XRPC API.
1314+## Development Setup
1516+### Database Connection
17+18+The application uses PostgreSQL. You can connect to the database using:
19+20+1. **Docker Compose** (recommended for local development):
21+ ```bash
22+ docker-compose up postgres
23+ ```
24+ This starts PostgreSQL on port 5432 with:
25+ - Database: `slices`
26+ - User: `slices`
27+ - Password: `slices`
28+29+2. **Environment Variables** (.env file): Create an `api/.env` file (copy from
30+ `api/.env.example`):
31+ ```
32+ DATABASE_URL=postgresql://slices:slices@localhost:5432/slices
33+ SYSTEM_SLICE_URI=at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z
34+ AUTH_BASE_URL=http://localhost:8081
35+ RELAY_ENDPOINT=https://relay1.us-west.bsky.network
36+ ```
37+38+## Common Development Commands
39+40+```bash
41+# Type checking and validation
42+cargo check
43+44+# Run development server
45+cargo run
46+47+# Run sync script
48+./scripts/sync.sh http://localhost:3000 <token> # Local dev
49+./scripts/sync.sh https://api.slices.network <token> # Production
50+51+# Database setup
52+sqlx database create
53+54+# Database migrations
55+sqlx migrate run
56+sqlx migrate add <migration_name>
57+58+# sqlx query cache (run after changing queries)
59+cargo sqlx prepare
60+61+# Build for production
62+cargo build --release
63+```
64+65+## High-Level Architecture
66+67+### Data Flow
68+69+1. **Real-time Indexing:** Jetstream → JetstreamConsumer → Lexicon Validation →
70+ Database → Index
71+2. **XRPC Query:** HTTP Request → OAuth Verification → Dynamic Handler →
72+ Database Query → Response
73+3. **Background Sync:** Trigger → Job Queue → SyncService → ATProto Relay →
74+ Validation → Database
75+76+### Key Architectural Decisions
77+78+- **Single-table design** for records using PostgreSQL with JSONB for
79+ flexibility across arbitrary lexicons
80+- **Dynamic XRPC endpoint generation** - routes like
81+ `/{collection}.createRecord` are generated at runtime
82+- **Dual indexing strategy** - real-time via Jetstream and bulk sync via
83+ background jobs
84+- **Cursor-based pagination** using `base64(sort_value::indexed_at::cid)` for
85+ stable pagination
86+- **OAuth DPoP authentication** integrated with AIP server for ATProto
87+ authentication
88+89+### Module Organization
90+91+- `src/api/` - HTTP handlers for XRPC endpoints (actors, records, oauth, sync,
92+ etc.)
93+- `src/main.rs` - Application entry point, server setup, Jetstream startup
94+- `src/database.rs` - All database operations, query building, cursor pagination
95+- `src/jetstream.rs` - Real-time event processing from ATProto firehose
96+- `src/sync.rs` - Bulk synchronization operations with ATProto relay
97+- `src/auth.rs` - OAuth verification and DPoP authentication setup
98+- `src/errors.rs` - Error type definitions (reference for new errors)
99100## Error Handling
101102All error strings must use this format:
103104+ error-slices-<domain>-<number> <message>: <details>
105106Example errors:
107108+- error-slices-resolve-1 Multiple DIDs resolved for method
109+- error-slices-plc-1 HTTP request failed: https://google.com/ Not Found
110+- error-slices-key-1 Error decoding key: invalid
111112Errors should be represented as enums using the `thiserror` library when
113possible using `src/errors.rs` as a reference and example.
···125126## HTTP Handler Organization
127128+HTTP handlers should be organized as Rust source files in the `src/api`
129+directory. Each handler should have its own request and response types and
130+helper functionality.
00131132- After updating, run `cargo check` to fix errors and warnings
133- Don't use dead code, if it's not used remove it
0
···568569 // For network.slices.lexicon collection, validate against the system slice
570 let validation_slice_uri = if collection == "network.slices.lexicon" {
571- "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z"
572 } else {
573 &slice_uri
574 };
···684685 // For network.slices.lexicon collection, validate against the system slice
686 let validation_slice_uri = if collection == "network.slices.lexicon" {
687- "at://did:plc:bcgltzqazw5tb6k2g3ttenbj/network.slices.slice/3lymhd4jhrd2z"
688 } else {
689 &slice_uri
690 };
···568569 // For network.slices.lexicon collection, validate against the system slice
570 let validation_slice_uri = if collection == "network.slices.lexicon" {
571+ &state.config.system_slice_uri
572 } else {
573 &slice_uri
574 };
···684685 // For network.slices.lexicon collection, validate against the system slice
686 let validation_slice_uri = if collection == "network.slices.lexicon" {
687+ &state.config.system_slice_uri
688 } else {
689 &slice_uri
690 };