···11+# Agents
22+33+This is a starter kit for building apps on the Atmosphere with Quickslice.
44+55+## Project Structure
66+77+- `index.html` - Main frontend with OAuth login flow
88+- `docker-compose.yml` - Quickslice server configuration
99+- `lexicons/` - AT Protocol lexicon schemas
1010+- `favicon.svg` - Slices logo
1111+1212+## Key Technologies
1313+1414+- **Quickslice** - Backend server for AT Protocol apps
1515+- **AT Protocol** - Decentralized social protocol (Bluesky)
1616+- **OAuth** - Authentication via Bluesky accounts
1717+- **Web Components** - `<qs-actor-autocomplete>` for handle input
1818+1919+## Configuration
2020+2121+In `index.html`:
2222+- `SERVER_URL` - Quickslice server (default: `http://127.0.0.1:8080`)
2323+- `CLIENT_ID` - OAuth client ID from Quickslice settings
2424+2525+## Development
2626+2727+```bash
2828+docker compose up # Start Quickslice server
2929+make serve # Serve frontend
3030+make format # Format HTML with Prettier
3131+make zip # Create lexicons.zip
3232+```
3333+3434+## MCP Server
3535+3636+Quickslice exposes an MCP server at `http://127.0.0.1:8080/mcp`. Add it to your AI assistant:
3737+3838+```bash
3939+claude mcp add --scope user quickslice http://127.0.0.1:8080/mcp
4040+```
4141+4242+Use MCP tools to:
4343+- Query the GraphQL API
4444+- Explore available lexicon schemas
4545+- Generate GraphQL queries/mutations
4646+- Understand record structures
4747+4848+## Lexicons
4949+5050+Custom lexicons go in `/lexicons` following AT Protocol naming conventions (e.g., `com/example/myrecord.json`). Run `make zip` and upload via the Quickslice settings page.
5151+5252+See the [Lexicon Style Guide](https://github.com/bluesky-social/atproto/discussions/4245) for best practices.
5353+5454+## Common Tasks
5555+5656+**Add a new feature**: Ask the Quickslice MCP about relevant lexicons, then update `index.html` with new GraphQL queries and UI.
5757+5858+**Add custom lexicons**: Create JSON schemas in `/lexicons`, run `make zip`, upload via settings page. If the lexicon already has published records across multiple PDSes, click "Trigger Backfill" on the Quickslice dashboard to sync existing data. Note: Avoid backfilling well-known lexicons like `app.bsky.*` as this will take days and require large amounts of disk space.
5959+6060+**Style the app**: CSS variables are defined in `:root` - modify colors, spacing as needed.
···11+.PHONY: format zip serve
22+33+format:
44+ npx prettier --write "**/*.html"
55+66+zip:
77+ zip -r lexicons.zip lexicons
88+99+serve:
1010+ npx serve .
+72
README.md
···11+# Slice Kit
22+33+A starter kit for building apps on the Atmosphere with [Quickslice](https://tangled.org/slices.network/quickslice).
44+55+## Prerequisites
66+77+- [Docker](https://docs.docker.com/get-docker/)
88+- [Node.js](https://nodejs.org/)
99+1010+## Quick Start
1111+1212+1. Start the server:
1313+1414+```bash
1515+docker compose up
1616+```
1717+1818+2. Login at http://127.0.0.1:8080 to create an admin account
1919+2020+3. Enter your domain authority (e.g., `com.example`) - this is the namespace for your app's lexicons. Leave blank if unsure; `app.bsky.actor.profile` will be treated as an external collection.
2121+2222+4. Upload `lexicons.zip` in the Lexicons section
2323+2424+5. Register an OAuth client at http://127.0.0.1:8080/oauth/clients
2525+2626+6. Copy the client ID and set it in `index.html`:
2727+2828+```javascript
2929+const CLIENT_ID = "your_client_id_here";
3030+```
3131+3232+7. Serve the frontend:
3333+3434+```bash
3535+make serve
3636+```
3737+3838+8. Open http://localhost:3000 and login with your Bluesky handle. You should see your Bluesky avatar and profile info after logging in, it's synced automatically after logging in.
3939+4040+9. Configure the Quickslice MCP server for your AI assistant. Example:
4141+4242+```bash
4343+claude mcp add --scope user quickslice http://127.0.0.1:8080/mcp
4444+```
4545+4646+10. Add your own custom lexicons to the `/lexicons` folder, run `make zip`, and upload on the settings page. Ask the Quickslice MCP about your lexicons and use it to help build your app.
4747+4848+## Configuration
4949+5050+Environment variables in `docker-compose.yml`:
5151+5252+| Variable | Description |
5353+|----------|-------------|
5454+| `DATABASE_URL` | SQLite database path |
5555+| `SECRET_KEY_BASE` | Session signing key (generate your own for production) |
5656+| `OAUTH_SIGNING_KEY` | OAuth token signing key (generate your own for production) |
5757+| `OAUTH_LOOPBACK_MODE` | Enables localhost OAuth redirects for development |
5858+| `EXTERNAL_BASE_URL` | Public URL of your server |
5959+6060+## Make Commands
6161+6262+| Command | Description |
6363+|---------|-------------|
6464+| `make serve` | Serve frontend locally |
6565+| `make format` | Format HTML files with Prettier |
6666+| `make zip` | Create `lexicons.zip` from lexicons directory |
6767+6868+## Production
6969+7070+**Backend:** See the [deployment guide](https://quickslice.slices.network/guides/deployment) for deploying your Quickslice instance.
7171+7272+**Frontend:** Deploy to a CDN or [wisp.place](https://wisp.place).
+20
docker-compose.yml
···11+services:
22+ quickslice:
33+ image: ghcr.io/bigmoves/quickslice:latest
44+ ports:
55+ - "8080:8080"
66+ volumes:
77+ - ./data:/data
88+ environment:
99+ - HOST=0.0.0.0
1010+ - PORT=8080
1111+ - DATABASE_URL=sqlite:/data/quickslice.db
1212+ # NOTE: Do NOT use in production - generate your own secure key
1313+ - SECRET_KEY_BASE=Xdb/9oovpIzYRKPjfTm45QSWYyYJi35GY3n4475SBVmcyxHS9tMoFJcOwPGfA0xW
1414+ # This disables cursor tracking in development so you're not always backfilling between server boots (for chattier lexicons)
1515+ - JETSTREAM_DISABLE_CURSOR=true
1616+ - EXTERNAL_BASE_URL=http://127.0.0.1:8080
1717+ # NOTE: Do NOT use in production - generate your own secure key
1818+ - OAUTH_SIGNING_KEY=z42tsNCXT8jZHj37qRd1D1vBE4qns8rp43DZsm1uez3cr8h6
1919+ - OAUTH_LOOPBACK_MODE=true
2020+ restart: on-failure:5
···11+{
22+ "lexicon": 1,
33+ "id": "com.atproto.label.defs",
44+ "defs": {
55+ "label": {
66+ "type": "object",
77+ "required": [
88+ "src",
99+ "uri",
1010+ "val",
1111+ "cts"
1212+ ],
1313+ "properties": {
1414+ "cid": {
1515+ "type": "string",
1616+ "format": "cid",
1717+ "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
1818+ },
1919+ "cts": {
2020+ "type": "string",
2121+ "format": "datetime",
2222+ "description": "Timestamp when this label was created."
2323+ },
2424+ "exp": {
2525+ "type": "string",
2626+ "format": "datetime",
2727+ "description": "Timestamp at which this label expires (no longer applies)."
2828+ },
2929+ "neg": {
3030+ "type": "boolean",
3131+ "description": "If true, this is a negation label, overwriting a previous label."
3232+ },
3333+ "sig": {
3434+ "type": "bytes",
3535+ "description": "Signature of dag-cbor encoded label."
3636+ },
3737+ "src": {
3838+ "type": "string",
3939+ "format": "did",
4040+ "description": "DID of the actor who created this label."
4141+ },
4242+ "uri": {
4343+ "type": "string",
4444+ "format": "uri",
4545+ "description": "AT URI of the record, repository (account), or other resource that this label applies to."
4646+ },
4747+ "val": {
4848+ "type": "string",
4949+ "maxLength": 128,
5050+ "description": "The short string name of the value or type of this label."
5151+ },
5252+ "ver": {
5353+ "type": "integer",
5454+ "description": "The AT Protocol version of the label object."
5555+ }
5656+ },
5757+ "description": "Metadata tag on an atproto resource (eg, repo or record)."
5858+ },
5959+ "selfLabel": {
6060+ "type": "object",
6161+ "required": [
6262+ "val"
6363+ ],
6464+ "properties": {
6565+ "val": {
6666+ "type": "string",
6767+ "maxLength": 128,
6868+ "description": "The short string name of the value or type of this label."
6969+ }
7070+ },
7171+ "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel."
7272+ },
7373+ "labelValue": {
7474+ "type": "string",
7575+ "knownValues": [
7676+ "!hide",
7777+ "!no-promote",
7878+ "!warn",
7979+ "!no-unauthenticated",
8080+ "dmca-violation",
8181+ "doxxing",
8282+ "porn",
8383+ "sexual",
8484+ "nudity",
8585+ "nsfl",
8686+ "gore"
8787+ ]
8888+ },
8989+ "selfLabels": {
9090+ "type": "object",
9191+ "required": [
9292+ "values"
9393+ ],
9494+ "properties": {
9595+ "values": {
9696+ "type": "array",
9797+ "items": {
9898+ "ref": "#selfLabel",
9999+ "type": "ref"
100100+ },
101101+ "maxLength": 10
102102+ }
103103+ },
104104+ "description": "Metadata tags on an atproto record, published by the author within the record."
105105+ },
106106+ "labelValueDefinition": {
107107+ "type": "object",
108108+ "required": [
109109+ "identifier",
110110+ "severity",
111111+ "blurs",
112112+ "locales"
113113+ ],
114114+ "properties": {
115115+ "blurs": {
116116+ "type": "string",
117117+ "description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
118118+ "knownValues": [
119119+ "content",
120120+ "media",
121121+ "none"
122122+ ]
123123+ },
124124+ "locales": {
125125+ "type": "array",
126126+ "items": {
127127+ "ref": "#labelValueDefinitionStrings",
128128+ "type": "ref"
129129+ }
130130+ },
131131+ "severity": {
132132+ "type": "string",
133133+ "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
134134+ "knownValues": [
135135+ "inform",
136136+ "alert",
137137+ "none"
138138+ ]
139139+ },
140140+ "adultOnly": {
141141+ "type": "boolean",
142142+ "description": "Does the user need to have adult content enabled in order to configure this label?"
143143+ },
144144+ "identifier": {
145145+ "type": "string",
146146+ "maxLength": 100,
147147+ "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
148148+ "maxGraphemes": 100
149149+ },
150150+ "defaultSetting": {
151151+ "type": "string",
152152+ "default": "warn",
153153+ "description": "The default setting for this label.",
154154+ "knownValues": [
155155+ "ignore",
156156+ "warn",
157157+ "hide"
158158+ ]
159159+ }
160160+ },
161161+ "description": "Declares a label value and its expected interpretations and behaviors."
162162+ },
163163+ "labelValueDefinitionStrings": {
164164+ "type": "object",
165165+ "required": [
166166+ "lang",
167167+ "name",
168168+ "description"
169169+ ],
170170+ "properties": {
171171+ "lang": {
172172+ "type": "string",
173173+ "format": "language",
174174+ "description": "The code of the language these strings are written in."
175175+ },
176176+ "name": {
177177+ "type": "string",
178178+ "maxLength": 640,
179179+ "description": "A short human-readable name for the label.",
180180+ "maxGraphemes": 64
181181+ },
182182+ "description": {
183183+ "type": "string",
184184+ "maxLength": 100000,
185185+ "description": "A longer description of what the label means and why it might be applied.",
186186+ "maxGraphemes": 10000
187187+ }
188188+ },
189189+ "description": "Strings which describe the label in the UI, localized into a specific language."
190190+ }
191191+ }
192192+}