···55## Base URL
6677```
88-https://your-api-domain.com/xrpc/
88+https://api.slices.network/xrpc/
99```
10101111## Authentication
···19192020Read operations typically work without authentication.
21212222+## Dynamic Collection Endpoints
2323+2424+For each collection in your slice, the following endpoints are automatically
2525+generated:
2626+2727+### `[collection].getRecords`
2828+2929+Get records in a collection.
3030+3131+**Method**: GET
3232+3333+**Parameters**:
3434+3535+- `slice` (string, required): Slice URI
3636+- `limit` (number, optional): Maximum records (default: 50)
3737+- `cursor` (string, optional): Pagination cursor
3838+- `where` (object, optional): Filter conditions using field-specific queries
3939+- `sortBy` (array, optional): Sort specification with field and direction objects
4040+4141+### `[collection].getRecord`
4242+4343+Get a single record.
4444+4545+**Method**: GET
4646+4747+**Parameters**:
4848+4949+- `slice` (string, required): Slice URI
5050+- `uri` (string, required): Record URI
5151+5252+### `[collection].countRecords`
5353+5454+Count records in a collection.
5555+5656+**Method**: GET
5757+5858+**Parameters**:
5959+6060+- `slice` (string, required): Slice URI
6161+- `where` (object, optional): Filter conditions using field-specific queries
6262+- Other filter parameters (no limit/cursor)
6363+6464+**Response**:
6565+6666+```json
6767+{
6868+ "count": 150
6969+}
7070+```
7171+7272+### `[collection].createRecord`
7373+7474+Create a new record.
7575+7676+**Method**: POST
7777+7878+**Authentication**: Required
7979+8080+**Body**:
8181+8282+```json
8383+{
8484+ "slice": "at://your-slice-uri",
8585+ "record": {
8686+ "$type": "com.recordcollector.album",
8787+ "title": "Superunknown",
8888+ "artist": "Soundgarden",
8989+ "releaseDate": "1994-03-08",
9090+ "condition": "Near Mint",
9191+ "genre": ["grunge", "alternative metal"]
9292+ },
9393+ "rkey": "3jklmno456"
9494+}
9595+```
9696+9797+### `[collection].updateRecord`
9898+9999+Update an existing record.
100100+101101+**Method**: POST
102102+103103+**Authentication**: Required
104104+105105+**Body**:
106106+107107+```json
108108+{
109109+ "slice": "at://your-slice-uri",
110110+ "rkey": "3xyz789abc",
111111+ "record": {
112112+ "$type": "com.recordcollector.album",
113113+ "title": "Dirt",
114114+ "artist": "Alice in Chains",
115115+ "releaseDate": "1992-09-29",
116116+ "condition": "Very Good Plus",
117117+ "notes": "Minor sleeve wear, vinyl plays perfectly"
118118+ }
119119+}
120120+```
121121+122122+### `[collection].deleteRecord`
123123+124124+Delete a record.
125125+126126+**Method**: POST
127127+128128+**Authentication**: Required
129129+130130+**Body**:
131131+132132+```json
133133+{
134134+ "rkey": "3abc123xyz"
135135+}
136136+```
137137+22138## Core Endpoints
2313924140### Slice Management
251412626-#### `network.slices.slice.getRecords`
142142+### `network.slices.slice.getRecords`
2714328144Get all slices.
29145···3314934150- `limit` (number, optional): Maximum records to return (default: 50)
35151- `cursor` (string, optional): Pagination cursor
3636-- `sort` (string, optional): Sort field and order (e.g., `createdAt:desc`)
3737-- `author` (string, optional): Filter by author DID
3838-- `authors` (string[], optional): Filter by multiple author DIDs
152152+- `where` (object, optional): Filter conditions using field-specific queries
153153+- `sortBy` (array, optional): Sort specification with field and direction objects
3915440155**Response**:
41156···59174}
60175```
611766262-#### `network.slices.slice.getRecord`
177177+### `network.slices.slice.getRecord`
6317864179Get a specific slice by URI.
65180···7118672187**Response**: Single record object (same structure as getRecords item)
731887474-#### `network.slices.slice.createRecord`
189189+### `network.slices.slice.createRecord`
7519076191Create a new slice.
77192···105220106221### Slice Operations
107222108108-#### `network.slices.slice.stats`
223223+### `network.slices.slice.stats`
109224110225Get statistics for a slice.
111226···124239```json
125240{
126241 "success": true,
127127- "collections": ["com.example.post", "app.bsky.actor.profile"],
242242+ "collections": ["com.recordcollector.album", "com.recordcollector.review"],
128243 "collectionStats": [
129244 {
130130- "collection": "com.example.post",
131131- "recordCount": 150,
132132- "uniqueActors": 10
245245+ "collection": "com.recordcollector.album",
246246+ "recordCount": 427,
247247+ "uniqueActors": 23
133248 }
134249 ],
135250 "totalLexicons": 5,
···139254}
140255```
141256142142-#### `network.slices.slice.listSliceRecords`
257257+### `network.slices.slice.listSliceRecords`
143258144259List records across multiple collections in a slice.
145260···150265```json
151266{
152267 "slice": "at://your-slice-uri",
153153- "collections": ["com.example.post", "app.bsky.actor.profile"],
268268+ "collections": ["com.recordcollector.album", "com.recordcollector.review"],
154269 "authors": ["did:plc:optional-filter"],
155270 "limit": 20,
156271 "cursor": "pagination-cursor"
···164279 "success": true,
165280 "records": [
166281 {
167167- "uri": "at://did:plc:abc/com.example.post/xyz",
282282+ "uri": "at://did:plc:abc/com.recordcollector.album/xyz",
168283 "cid": "bafyrei...",
169284 "did": "did:plc:abc",
170170- "collection": "com.example.post",
285285+ "collection": "com.recordcollector.album",
171286 "value": {/* record data */},
172287 "indexedAt": "2024-01-01T00:00:00Z"
173288 }
···176291}
177292```
178293179179-#### `network.slices.slice.searchSliceRecords`
294294+### `network.slices.slice.searchSliceRecords`
180295181296Search records across multiple collections in a slice by content.
182297···187302```json
188303{
189304 "slice": "at://your-slice-uri",
190190- "collections": ["com.example.post", "app.bsky.actor.profile"],
305305+ "collections": ["com.recordcollector.album", "com.recordcollector.review"],
191306 "search": "search term",
192307 "authors": ["did:plc:optional-filter"],
193308 "limit": 20,
···202317 "success": true,
203318 "records": [
204319 {
205205- "uri": "at://did:plc:abc/com.example.post/xyz",
320320+ "uri": "at://did:plc:abc/com.recordcollector.album/xyz",
206321 "cid": "bafyrei...",
207322 "did": "did:plc:abc",
208208- "collection": "com.example.post",
323323+ "collection": "com.recordcollector.album",
209324 "value": {/* record data */},
210325 "indexedAt": "2024-01-01T00:00:00Z"
211326 }
···214329}
215330```
216331217217-#### `network.slices.slice.syncUserCollections`
332332+### `network.slices.slice.syncUserCollections`
218333219334Synchronously sync collections for the authenticated user.
220335···243358}
244359```
245360246246-#### `network.slices.slice.startSync`
361361+### `network.slices.slice.startSync`
247362248363Start an asynchronous bulk sync job.
249364···256371```json
257372{
258373 "slice": "at://your-slice-uri",
259259- "collections": ["com.example.post"],
374374+ "collections": ["com.recordcollector.album"],
260375 "externalCollections": ["app.bsky.actor.profile"],
261376 "repos": ["did:plc:abc", "did:plc:xyz"],
262377 "limitPerRepo": 100
···273388}
274389```
275390276276-#### `network.slices.slice.codegen`
391391+### `network.slices.slice.codegen`
277392278393Generate TypeScript client code.
279394···297412}
298413```
299414300300-## Dynamic Collection Endpoints
301301-302302-For each collection in your slice, the following endpoints are automatically
303303-generated:
304304-305305-### `[collection].getRecords`
306306-307307-Get records in a collection.
308308-309309-**Method**: GET
310310-311311-**Parameters**:
312312-313313-- `slice` (string, required): Slice URI
314314-- `limit` (number, optional): Maximum records (default: 50)
315315-- `cursor` (string, optional): Pagination cursor
316316-- `sort` (string, optional): Sort specification
317317-- `author` (string, optional): Filter by author DID
318318-- `authors` (string[], optional): Filter by multiple DIDs
319319-320320-### `[collection].getRecord`
321321-322322-Get a single record.
323323-324324-**Method**: GET
325325-326326-**Parameters**:
327327-328328-- `slice` (string, required): Slice URI
329329-- `uri` (string, required): Record URI
330330-331331-### `[collection].countRecords`
332332-333333-Count records in a collection.
334334-335335-**Method**: GET
336336-337337-**Parameters**:
338338-339339-- `slice` (string, required): Slice URI
340340-- `author` (string, optional): Filter by author DID
341341-- `authors` (string[], optional): Filter by multiple DIDs
342342-- Other filter parameters (no limit/cursor)
343343-344344-**Response**:
345345-346346-```json
347347-{
348348- "count": 150
349349-}
350350-```
351351-352352-### `[collection].createRecord`
353353-354354-Create a new record.
355355-356356-**Method**: POST
357357-358358-**Authentication**: Required
359359-360360-**Body**:
361361-362362-```json
363363-{
364364- "slice": "at://your-slice-uri",
365365- "record": {
366366- "$type": "collection.name"
367367- /* record fields */
368368- },
369369- "rkey": "optional-key"
370370-}
371371-```
372372-373373-### `[collection].updateRecord`
374374-375375-Update an existing record.
376376-377377-**Method**: POST
378378-379379-**Authentication**: Required
380380-381381-**Body**:
382382-383383-```json
384384-{
385385- "slice": "at://your-slice-uri",
386386- "rkey": "record-key",
387387- "record": {
388388- "$type": "collection.name"
389389- /* updated fields */
390390- }
391391-}
392392-```
393393-394394-### `[collection].deleteRecord`
395395-396396-Delete a record.
397397-398398-**Method**: POST
399399-400400-**Authentication**: Required
401401-402402-**Body**:
403403-404404-```json
405405-{
406406- "rkey": "record-key"
407407-}
408408-```
409409-410415## Lexicon Management
411416412417### `network.slices.lexicon.getRecords`
···448453 "slice": "at://your-slice-uri",
449454 "record": {
450455 "$type": "network.slices.lexicon",
451451- "nsid": "com.example.post",
456456+ "nsid": "com.recordcollector.album",
452457 "definitions": "{\"lexicon\": 1, ...}",
453458 "createdAt": "2024-01-01T00:00:00Z",
454459 "slice": "at://your-slice-uri"
···557562} while (cursor);
558563```
559564565565+## Filtering
566566+567567+List endpoints support filtering using the `where` parameter with field-specific query operators:
568568+569569+### Filter Operators
570570+571571+- `eq`: Exact match
572572+- `contains`: Partial text match (case-insensitive)
573573+- `in`: Match any value in array
574574+575575+### Examples
576576+577577+**Exact match filtering:**
578578+579579+```json
580580+{
581581+ "where": {
582582+ "artist": { "eq": "Nirvana" },
583583+ "condition": { "eq": "Mint" }
584584+ }
585585+}
586586+```
587587+588588+**Text search filtering:**
589589+590590+```json
591591+{
592592+ "where": {
593593+ "title": { "contains": "nevermind" },
594594+ "genre": { "contains": "grunge" }
595595+ }
596596+}
597597+```
598598+599599+**Array filtering:**
600600+601601+```json
602602+{
603603+ "where": {
604604+ "condition": { "in": ["Mint", "Near Mint", "Very Good Plus"] },
605605+ "artist": { "in": ["Nirvana", "Pearl Jam", "Soundgarden"] }
606606+ }
607607+}
608608+```
609609+610610+**Global search across all fields:**
611611+612612+```json
613613+{
614614+ "where": {
615615+ "json": { "contains": "grunge" }
616616+ }
617617+}
618618+```
619619+560620## Sorting
561621562562-Sort parameter format: `field:order` or `field1:order1,field2:order2`
622622+Sort parameter uses an array format with field and direction:
623623+624624+```json
625625+{
626626+ "sortBy": [
627627+ { "field": "releaseDate", "direction": "desc" },
628628+ { "field": "title", "direction": "asc" }
629629+ ]
630630+}
631631+```
563632564633Examples:
565634566566-- `createdAt:desc` - Newest first
567567-- `name:asc` - Alphabetical
568568-- `createdAt:desc,name:asc` - Newest first, then alphabetical
635635+- `[{ "field": "releaseDate", "direction": "desc" }]` - Newest releases first
636636+- `[{ "field": "artist", "direction": "asc" }]` - Alphabetical by artist
637637+- `[{ "field": "releaseDate", "direction": "desc" }, { "field": "title", "direction": "asc" }]` - Newest first, then alphabetical by title
569638570639## Next Steps
571640
+10-8
docs/concepts.md
···3434```json
3535{
3636 "lexicon": 1,
3737- "id": "com.example.blogPost",
3737+ "id": "com.recordcollector.album",
3838 "defs": {
3939 "main": {
4040 "type": "record",
4141- "description": "A blog post record",
4141+ "description": "A vinyl album record",
4242 "record": {
4343 "type": "object",
4444 "properties": {
4545 "title": { "type": "string" },
4646- "content": { "type": "string" },
4747- "publishedAt": { "type": "string", "format": "datetime" }
4646+ "artist": { "type": "string" },
4747+ "releaseDate": { "type": "string", "format": "datetime" },
4848+ "condition": { "type": "string" }
4849 },
4949- "required": ["title", "content"]
5050+ "required": ["title", "artist"]
5051 }
5152 }
5253 }
···64656566Lexicons follow reverse domain naming:
66676767-- `com.example.post` - A post in the example.com namespace
6868+- `com.recordcollector.album` - An album in the recordcollector.com namespace
6969+- `com.recordcollector.review` - A vinyl review record
6870- `network.slices.slice` - Core slice record type
6971- `app.bsky.actor.profile` - Bluesky profile (external)
7072···7678### Primary Collections
77797880Collections 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.
8181+domain is `com.recordcollector`, then `com.recordcollector.album` would be a primary collection.
80828183### External Collections
8284···254256const client = new AtProtoClient(apiUrl, sliceUri, oauthClient);
255257256258// Use nested structure matching lexicons
257257-await client.com.example.post.getRecords();
259259+await client.com.recordcollector.album.getRecords();
258260await client.app.bsky.actor.profile.getRecord({ uri });
259261```
260262
+33-23
docs/getting-started.md
···1515### 1. Clone the Repository
16161717```bash
1818-git clone https://github.com/your-org/slice
1818+git clone https://tangled.sh/@slices.network/slices
1919cd slice
2020```
2121···9898Click "Create Slice" and provide:
9999100100- **Name**: A friendly name for your slice
101101-- **Domain**: Your namespace (e.g., `com.example`)
101101+- **Domain**: Your namespace (e.g., `com.recordcollector`)
102102103103### 3. Define a Lexicon
104104···108108```json
109109{
110110 "lexicon": 1,
111111- "id": "com.example.post",
111111+ "id": "com.recordcollector.album",
112112 "defs": {
113113 "main": {
114114 "type": "record",
115115- "description": "A blog post",
115115+ "description": "A vinyl album record",
116116 "record": {
117117 "type": "object",
118118 "properties": {
119119 "title": {
120120 "type": "string",
121121- "description": "Post title"
121121+ "description": "Album title"
122122 },
123123- "content": {
123123+ "artist": {
124124 "type": "string",
125125- "description": "Post content"
125125+ "description": "Artist or band name"
126126 },
127127- "createdAt": {
127127+ "releaseDate": {
128128 "type": "string",
129129 "format": "datetime",
130130- "description": "Creation timestamp"
130130+ "description": "Original release date"
131131 },
132132- "tags": {
132132+ "genre": {
133133 "type": "array",
134134 "items": {
135135 "type": "string"
136136 },
137137- "description": "Post tags"
137137+ "description": "Music genres"
138138+ },
139139+ "condition": {
140140+ "type": "string",
141141+ "description": "Vinyl condition (Mint, Near Mint, Very Good, etc.)"
142142+ },
143143+ "notes": {
144144+ "type": "string",
145145+ "description": "Collector notes"
138146 }
139147 },
140140- "required": ["title", "content", "createdAt"]
148148+ "required": ["title", "artist", "releaseDate"]
141149 }
142150 }
143151 }
···161169 "at://did:plc:your-did/network.slices.slice/your-slice-id",
162170);
163171164164-// Get posts
165165-const posts = await client.com.example.post.getRecords();
172172+// Get albums
173173+const albums = await client.com.recordcollector.album.getRecords();
166174167167-// Create a post
168168-const newPost = await client.com.example.post.createRecord({
169169- title: "My First Post",
170170- content: "Hello from Slices!",
171171- createdAt: new Date().toISOString(),
172172- tags: ["introduction", "slices"],
175175+// Add a new album to your collection
176176+const newAlbum = await client.com.recordcollector.album.createRecord({
177177+ title: "Nevermind",
178178+ artist: "Nirvana",
179179+ releaseDate: "1991-09-24",
180180+ genre: ["grunge", "alternative rock"],
181181+ condition: "Near Mint",
182182+ notes: "Original pressing, includes poster",
173183});
174184175175-// Get a specific post
176176-const post = await client.com.example.post.getRecord({
177177- uri: newPost.uri,
185185+// Get a specific album
186186+const album = await client.com.recordcollector.album.getRecord({
187187+ uri: newAlbum.uri,
178188});
179189```
180190
+147-114
docs/sdk-usage.md
···2323);
24242525// Read operations work without auth
2626-const records = await client.com.example.post.getRecords();
2626+const albums = await client.com.recordcollector.album.getRecords();
2727```
28282929### With Authentication (Full Access)
···50505151## CRUD Operations
52525353-### Getting Records (Unified API)
5353+### Getting Records
54545555-The SDK uses a unified `getRecords` method that replaces the previous
5656-`listRecords` and `searchRecords` methods:
5555+The SDK uses `getRecords` for retrieving records:
57565857```typescript
5959-// Get all records
6060-const posts = await client.com.example.post.getRecords();
5858+// Get all vinyl records
5959+const albums = await client.com.recordcollector.album.getRecords();
61606261// With pagination
6363-const page1 = await client.com.example.post.getRecords({ limit: 20 });
6464-const page2 = await client.com.example.post.getRecords({
6262+const page1 = await client.com.recordcollector.album.getRecords({ limit: 20 });
6363+const page2 = await client.com.recordcollector.album.getRecords({
6564 limit: 20,
6665 cursor: page1.cursor,
6766});
68676968// With filtering using where clause
7070-const userPosts = await client.com.example.post.getRecords({
6969+const nirvanaAlbums = await client.com.recordcollector.album.getRecords({
7170 where: {
7272- did: { eq: "did:plc:user123" },
7171+ artist: { eq: "Nirvana" },
7372 },
7473});
75747675// Text search in specific fields
7777-const searchResults = await client.com.example.post.getRecords({
7676+const searchResults = await client.com.recordcollector.album.getRecords({
7877 where: {
7979- title: { contains: "typescript" },
7878+ title: { contains: "nevermind" },
8079 },
8180});
82818382// Global text search across ALL fields using 'json'
8484-const globalSearch = await client.com.example.post.getRecords({
8383+const globalSearch = await client.com.recordcollector.album.getRecords({
8584 where: {
8686- json: { contains: "typescript" },
8585+ json: { contains: "grunge" },
8786 },
8887});
89889089// Combine multiple filters
9191-const filteredPosts = await client.com.example.post.getRecords({
9090+const seattleGrunge = await client.com.recordcollector.album.getRecords({
9291 where: {
9393- did: { eq: "did:plc:user123" },
9494- text: { contains: "guide" },
9292+ city: { eq: "Seattle" },
9393+ genre: { contains: "grunge" },
9594 },
9695 limit: 50,
9796});
98979898+// Advanced filtering with multiple conditions
9999+const complexFilter = await client.com.recordcollector.album.getRecords({
100100+ where: {
101101+ artist: { contains: "alice" },
102102+ releaseDate: { gte: "1990-01-01" },
103103+ condition: { in: ["Mint", "Near Mint"] },
104104+ },
105105+ limit: 25,
106106+});
107107+108108+// Filtering with exact matches
109109+const exactMatch = await client.com.recordcollector.album.getRecords({
110110+ where: {
111111+ artist: { eq: "Soundgarden" },
112112+ genre: { contains: "grunge" },
113113+ },
114114+});
115115+116116+// Date range filtering
117117+const nineties = await client.com.recordcollector.album.getRecords({
118118+ where: {
119119+ releaseDate: {
120120+ gte: "1990-01-01",
121121+ lte: "1999-12-31"
122122+ },
123123+ },
124124+});
125125+99126// With sorting
100100-const recentPosts = await client.com.example.post.getRecords({
101101- sortBy: [{ field: "createdAt", direction: "desc" }],
127127+const recentAlbums = await client.com.recordcollector.album.getRecords({
128128+ sortBy: [{ field: "releaseDate", direction: "desc" }],
102129});
103130104131// Multiple sort fields
105105-const sortedPosts = await client.com.example.post.getRecords({
132132+const sortedAlbums = await client.com.recordcollector.album.getRecords({
106133 sortBy: [
107107- { field: "createdAt", direction: "desc" },
134134+ { field: "releaseDate", direction: "desc" },
108135 { field: "title", direction: "asc" },
109136 ],
110137});
···117144118145```typescript
119146// Count all records
120120-const total = await client.com.example.post.countRecords();
121121-console.log(`Total posts: ${total.count}`);
147147+const total = await client.com.recordcollector.album.countRecords();
148148+console.log(`Total albums: ${total.count}`);
122149123150// Count with filtering
124124-const userPostCount = await client.com.example.post.countRecords({
151151+const nirvanaCount = await client.com.recordcollector.album.countRecords({
125152 where: {
126126- did: { eq: "did:plc:user123" },
153153+ artist: { eq: "Nirvana" },
127154 },
128155});
129156130157// Count with text search
131131-const searchCount = await client.com.example.post.countRecords({
158158+const searchCount = await client.com.recordcollector.album.countRecords({
132159 where: {
133133- title: { contains: "typescript" },
160160+ title: { contains: "nevermind" },
134161 },
135162});
136163137164// Count with multiple filters
138138-const filteredCount = await client.com.example.post.countRecords({
165165+const filteredCount = await client.com.recordcollector.album.countRecords({
139166 where: {
140140- did: { eq: "did:plc:user123" },
141141- json: { contains: "guide" },
167167+ artist: { eq: "Alice in Chains" },
168168+ json: { contains: "grunge" },
142169 },
143170});
144171145172// Count with OR conditions
146146-const orCount = await client.com.example.post.countRecords({
173173+const orCount = await client.com.recordcollector.album.countRecords({
147174 where: {
148148- createdAt: { eq: "2025-09-03" },
175175+ releaseDate: { eq: "1991-09-24" },
149176 },
150177 orWhere: {
151151- title: { contains: "typescript" },
152152- did: { eq: "did:plc:author" },
178178+ title: { contains: "nevermind" },
179179+ artist: { eq: "Pearl Jam" },
153180 },
154181});
155182156156-console.log(`Found ${filteredCount.count} matching posts`);
157157-console.log(`Found ${orCount.count} posts with OR conditions`);
183183+console.log(`Found ${filteredCount.count} matching albums`);
184184+console.log(`Found ${orCount.count} albums with OR conditions`);
158185```
159186160187### Getting a Single Record
161188162189```typescript
163163-const post = await client.com.example.post.getRecord({
164164- uri: "at://did:plc:abc/com.example.post/3xyz",
190190+const album = await client.com.recordcollector.album.getRecord({
191191+ uri: "at://did:plc:abc/com.recordcollector.album/3jklmno456",
165192});
166193167167-console.log(post.value.title);
168168-console.log(post.value.content);
194194+console.log(album.value.title);
195195+console.log(album.value.artist);
169196```
170197171198### Creating Records
172199173200```typescript
174201// Create with auto-generated key
175175-const newPost = await client.com.example.post.createRecord({
176176- title: "My New Post",
177177- content: "This is the content",
178178- createdAt: new Date().toISOString(),
179179- tags: ["typescript", "atproto"],
202202+const newAlbum = await client.com.recordcollector.album.createRecord({
203203+ title: "In Utero",
204204+ artist: "Nirvana",
205205+ releaseDate: "1993-09-21",
206206+ genre: ["grunge", "alternative rock"],
180207});
181208182182-console.log(`Created: ${newPost.uri}`);
209209+console.log(`Created: ${newAlbum.uri}`);
183210184211// Create with custom key
185185-const customPost = await client.com.example.post.createRecord(
212212+const customAlbum = await client.com.recordcollector.album.createRecord(
186213 {
187187- title: "Custom Key Post",
188188- content: "Using a custom record key",
189189- createdAt: new Date().toISOString(),
214214+ title: "Badmotorfinger",
215215+ artist: "Soundgarden",
216216+ releaseDate: "1991-10-08",
190217 },
191218 true, // useSelfRkey for singleton records like profiles
192219);
···196223197224```typescript
198225// Get the record key from the URI
199199-const uri = "at://did:plc:abc/com.example.post/3xyz";
200200-const rkey = uri.split("/").pop(); // '3xyz'
226226+const uri = "at://did:plc:abc/com.recordcollector.album/3jklmno456";
227227+const rkey = uri.split("/").pop(); // '3jklmno456'
201228202202-const updated = await client.com.example.post.updateRecord(
229229+const updated = await client.com.recordcollector.album.updateRecord(
203230 rkey,
204231 {
205205- title: "Updated Title",
206206- content: "Updated content",
207207- createdAt: new Date().toISOString(),
232232+ title: "Nevermind (Remastered)",
233233+ artist: "Nirvana",
234234+ releaseDate: "1991-09-24",
208235 updatedAt: new Date().toISOString(),
209236 },
210237);
···215242### Deleting Records
216243217244```typescript
218218-const rkey = "3xyz";
219219-await client.com.example.post.deleteRecord(rkey);
245245+const rkey = "3jklmno456";
246246+await client.com.recordcollector.album.deleteRecord(rkey);
220247```
221248222249## Working with External Collections
···243270244271```typescript
245272// Read file as ArrayBuffer
246246-const file = await Deno.readFile("./image.jpg");
273273+const file = await Deno.readFile("./nevermind-cover.jpg");
247274248275// Upload blob
249276const blobResponse = await client.uploadBlob({
···252279});
253280254281// Use blob in a record
255255-const postWithImage = await client.com.example.post.createRecord({
256256- title: "Post with Image",
257257- content: "Check out this image!",
258258- image: blobResponse.blob,
259259- createdAt: new Date().toISOString(),
282282+const albumWithArt = await client.com.recordcollector.album.createRecord({
283283+ title: "Nevermind",
284284+ artist: "Nirvana",
285285+ releaseDate: "1991-09-24",
286286+ genre: ["grunge", "alternative rock"],
287287+ condition: "Near Mint",
288288+ albumArt: blobResponse.blob,
260289});
261290```
262291···351380352381### Browse Slice Records
353382354354-#### Get Records from Multiple Collections (Unified API)
383383+#### Get Records from Multiple Collections
355384356356-The `getSliceRecords` method uses the same unified `where` clause approach:
385385+The `getSliceRecords` method uses the same `where` clause approach:
357386358387```typescript
359388// Get records from specific collections
···412441413442```typescript
414443// Search in title field only
415415-const titleSearch = await client.com.example.post.getRecords({
444444+const titleSearch = await client.com.recordcollector.album.getRecords({
416445 where: {
417417- title: { contains: "javascript" },
446446+ title: { contains: "nevermind" },
418447 },
419448});
420449421421-// Search in description field
422422-const descSearch = await client.com.example.post.getRecords({
450450+// Search in notes field
451451+const notesSearch = await client.com.recordcollector.album.getRecords({
423452 where: {
424424- description: { contains: "tutorial" },
453453+ notes: { contains: "original pressing" },
425454 },
426455});
427456```
···431460Use the special `json` field to search across **all fields** in a record:
432461433462```typescript
434434-// Finds records containing "react" anywhere in their data
435435-const globalSearch = await client.com.example.post.getRecords({
463463+// Finds records containing "grunge" anywhere in their data
464464+const globalSearch = await client.com.recordcollector.album.getRecords({
436465 where: {
437437- json: { contains: "react" },
466466+ json: { contains: "grunge" },
438467 },
439468});
440469441441-// This will match records where "react" appears in:
442442-// - title: "Learning React"
443443-// - content: "This post is about React development"
444444-// - tags: ["javascript", "react", "frontend"]
470470+// This will match records where "grunge" appears in:
471471+// - title: "Nevermind"
472472+// - artist: "Nirvana"
473473+// - genre: ["grunge", "alternative rock"]
474474+// - notes: "Classic grunge album from Seattle"
445475// - or any other field in the record
446476```
447477···450480When using `getSliceRecords`, you can search across multiple collections:
451481452482```typescript
453453-// Search for "tutorial" across all collections
483483+// Search for "seattle" across all collections
454484const crossCollectionSearch = await client.network.slices.slice.getSliceRecords(
455485 {
456486 where: {
457457- json: { contains: "tutorial" },
487487+ json: { contains: "seattle" },
458488 },
459489 },
460490);
···462492// Limit to specific collections
463493const specificSearch = await client.network.slices.slice.getSliceRecords({
464494 where: {
465465- collection: { in: ["com.example.post", "com.example.article"] },
466466- json: { contains: "guide" },
495495+ collection: { in: ["com.recordcollector.album", "com.recordcollector.review"] },
496496+ json: { contains: "grunge" },
467497 },
468498});
469499```
···475505autocomplete for field names:
476506477507```typescript
478478-// Find posts by either user1 OR user2
479479-const posts = await client.com.example.post.getRecords({
508508+// Find albums by either Nirvana OR Alice in Chains
509509+const albums = await client.com.recordcollector.album.getRecords({
480510 orWhere: {
481481- did: { in: ["did:plc:user1", "did:plc:user2"] },
511511+ artist: { in: ["Nirvana", "Alice in Chains"] },
482512 },
483513});
484514485485-// Find posts that either have "typescript" in title OR are by a specific user
486486-const posts = await client.com.example.post.getRecords({
515515+// Find albums that either have "nevermind" in title OR are by Soundgarden
516516+const albums = await client.com.recordcollector.album.getRecords({
487517 orWhere: {
488488- title: { contains: "typescript" },
489489- did: { eq: "did:plc:alice" },
518518+ title: { contains: "nevermind" },
519519+ artist: { eq: "Soundgarden" },
490520 },
491521});
492522493523// Combining OR with regular AND conditions
494494-const posts = await client.com.example.post.getRecords({
524524+const albums = await client.com.recordcollector.album.getRecords({
495525 where: {
496496- createdAt: { eq: "2025-09-03" }, // AND conditions
526526+ releaseDate: { eq: "1991-09-24" }, // AND conditions
497527 },
498528 orWhere: { // OR conditions
499499- title: { contains: "guide" },
500500- did: { eq: "did:plc:user1" },
529529+ artist: { contains: "nirvana" },
530530+ genre: { contains: "grunge" },
501531 },
502532});
503503-// SQL: WHERE created_at = '2025-09-03' AND (title LIKE '%guide%' OR did = 'did:plc:user1')
533533+// SQL: WHERE release_date = '1991-09-24' AND (artist LIKE '%nirvana%' OR genre LIKE '%grunge%')
504534505535// OR queries work with cross-collection searches too
506536const crossCollectionOrSearch = await client.network.slices.slice
507537 .getSliceRecords({
508538 where: {
509509- collection: { eq: "com.example.post" },
539539+ collection: { eq: "com.recordcollector.album" },
510540 },
511541 orWhere: {
512512- title: { contains: "javascript" },
513513- tags: { contains: "tutorial" },
542542+ artist: { contains: "pearl jam" },
543543+ genre: { contains: "alternative rock" },
514544 },
515545 });
516546517547// You get full autocomplete and type safety for field names in both where and orWhere
518518-const typedSearch = await client.com.example.post.getRecords({
548548+const typedSearch = await client.com.recordcollector.album.getRecords({
519549 where: {
520550 // TypeScript autocompletes valid field names here
521521- title: { contains: "react" },
551551+ condition: { contains: "mint" },
522552 },
523553 orWhere: {
524554 // And also provides autocomplete here
525525- description: { contains: "tutorial" },
526526- tags: { contains: "guide" },
555555+ artist: { contains: "soundgarden" },
556556+ genre: { contains: "grunge" },
527557 },
528558});
529559```
···610640611641```typescript
612642// TypeScript knows the shape of your records
613613-const post = await client.com.example.post.getRecord({ uri });
643643+const album = await client.com.recordcollector.album.getRecord({ uri });
614644615645// Type error: property 'unknownField' does not exist
616616-// post.value.unknownField
646646+// album.value.unknownField
617647618648// Autocomplete works for all fields
619619-post.value.title; // string
620620-post.value.tags; // string[]
621621-post.value.createdAt; // string
649649+album.value.title; // string
650650+album.value.artist; // string
651651+album.value.genre; // string[]
652652+album.value.releaseDate; // string
622653623654// Creating records is type-checked
624624-await client.com.example.post.createRecord({
625625- title: "Valid",
626626- content: "Also valid",
627627- createdAt: new Date().toISOString(),
655655+await client.com.recordcollector.album.createRecord({
656656+ title: "Dirt",
657657+ artist: "Alice in Chains",
658658+ releaseDate: "1992-09-29",
659659+ genre: ["grunge", "alternative metal"],
660660+ condition: "Very Good Plus",
628661 // Type error: 'invalidField' is not assignable
629662 // invalidField: "This will error"
630663});
···636669637670```typescript
638671// Process records in batches
639639-async function* getAllRecords() {
672672+async function* getAllAlbums() {
640673 let cursor: string | undefined;
641674642675 do {
643643- const batch = await client.com.example.post.getRecords({
676676+ const batch = await client.com.recordcollector.album.getRecords({
644677 limit: 100,
645678 cursor,
646679 });
···651684}
652685653686// Use the generator
654654-for await (const record of getAllRecords()) {
655655- console.log(record.value.title);
687687+for await (const album of getAllAlbums()) {
688688+ console.log(`${album.value.artist} - ${album.value.title}`);
656689}
657690```
658691
+1
frontend/CLAUDE.md
···8080- `src/client.ts` - Generated AT Protocol client for API communication
8181- `src/config.ts` - Centralized configuration and service setup
8282- Environment variables required: `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, `OAUTH_REDIRECT_URI`, `OAUTH_AIP_BASE_URL`, `API_URL`, `SLICE_URI`
8383+- Optional variables: `DOCS_PATH` (path to markdown documentation files, defaults to "../docs")
83848485#### Rendering System
8586- `src/utils/render.tsx` - Unified HTML rendering with proper headers