···11+# CLAUDE.md
22+33+This file provides guidance to Claude Code (claude.ai/code) when working with
44+code in this repository.
55+66+## Development Commands
77+88+```bash
99+# Start development server with hot reload
1010+deno task dev
1111+1212+# Start production server
1313+deno task start
1414+1515+# Format code
1616+deno fmt
1717+1818+# Check types
1919+deno check src/**/*.ts src/**/*.tsx
2020+```
2121+2222+## Architecture Overview
2323+2424+This is a Deno-based web application built with the Slices CLI. It provides
2525+server-side rendering with Preact, OAuth authentication, and AT Protocol
2626+integration for building applications on the decentralized web.
2727+2828+### Technology Stack
2929+3030+- **Runtime**: Deno with TypeScript
3131+- **Frontend**: Preact with server-side rendering
3232+- **Styling**: Tailwind CSS (via CDN)
3333+- **Interactivity**: HTMX + Hyperscript
3434+- **Routing**: Deno's standard HTTP routing
3535+- **Authentication**: OAuth with PKCE flow using `@slices/oauth`
3636+- **Sessions**: SQLite-based with `@slices/session`
3737+- **Database**: SQLite via OAuth and session libraries
3838+3939+### Core Architecture Patterns
4040+4141+#### Feature-Based Organization
4242+4343+The codebase is organized by features rather than technical layers:
4444+4545+```
4646+src/
4747+├── features/ # Feature modules
4848+│ └── auth/ # Authentication (login/logout)
4949+├── shared/ # Shared UI components
5050+├── routes/ # Route definitions and middleware
5151+├── utils/ # Utility functions
5252+└── config.ts # Core configuration
5353+```
5454+5555+#### Handler Pattern
5656+5757+Each feature follows a consistent pattern:
5858+5959+- `handlers.tsx` - Route handlers that return Response objects
6060+- `templates/` - Preact components for rendering
6161+- `templates/fragments/` - Reusable UI components
6262+6363+#### Authentication & Sessions
6464+6565+- OAuth integration with AT Protocol using `@slices/oauth`
6666+- PKCE flow for secure authentication
6767+- Session management with `@slices/session`
6868+- SQLite storage for OAuth state and sessions
6969+- Automatic token refresh capabilities
7070+7171+### Key Components
7272+7373+#### Route System
7474+7575+- All routes defined in `src/routes/mod.ts`
7676+- Feature routes exported from `src/features/*/handlers.tsx`
7777+- Middleware in `src/routes/middleware.ts` handles auth state
7878+7979+#### OAuth Integration
8080+8181+- `src/config.ts` - OAuth client and session store setup
8282+- Environment variables required: `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`,
8383+ `OAUTH_REDIRECT_URI`, `OAUTH_AIP_BASE_URL`, `API_URL`, `SLICE_URI`
8484+- PKCE flow implementation in auth handlers
8585+- SQLite storage for OAuth state and tokens
8686+8787+#### Rendering System
8888+8989+- `src/utils/render.tsx` - Unified HTML rendering with proper headers
9090+- Server-side rendering with Preact
9191+- HTMX for dynamic interactions without page reloads
9292+- Shared `Layout` component in `src/shared/fragments/Layout.tsx`
9393+9494+### Development Guidelines
9595+9696+#### Component Conventions
9797+9898+- Use `.tsx` extension for components with JSX
9999+- Preact components for all UI rendering
100100+- HTMX attributes for interactive behavior
101101+- Tailwind classes for styling
102102+103103+#### Feature Development
104104+105105+When adding new features:
106106+107107+1. Create feature directory under `src/features/`
108108+2. Add `handlers.tsx` with route definitions
109109+3. Create `templates/` directory with Preact components
110110+4. Export routes from feature and add to `src/routes/mod.ts`
111111+5. Follow existing authentication patterns using auth middleware
112112+113113+#### Environment Setup
114114+115115+The application requires a `.env` file with OAuth and API configuration.
116116+Copy `.env.example` and fill in your values. Missing environment variables
117117+will cause startup failures with descriptive error messages.
118118+119119+### Request/Response Flow
120120+121121+1. Request hits main server in `src/main.ts`
122122+2. Routes processed through `src/routes/mod.ts`
123123+3. Authentication middleware applies session state
124124+4. Feature handlers process requests and return rendered HTML
125125+5. HTMX handles partial page updates on client-side interactions
126126+127127+### OAuth Flow
128128+129129+1. User initiates login with handle/identifier
130130+2. OAuth client generates PKCE challenge and redirects to auth server
131131+3. User authenticates and is redirected back with authorization code
132132+4. Client exchanges code for tokens using PKCE verifier
133133+5. Session created with automatic token refresh
134134+6. Protected routes access user data through authenticated client
135135+136136+### Adding New Features
137137+138138+To add a new feature:
139139+140140+1. Create `src/features/feature-name/`
141141+2. Add `handlers.tsx` with route handlers
142142+3. Create `templates/` directory for UI components
143143+4. Export routes and add to main router
144144+5. Use existing patterns for authentication and rendering
+84
README.md
···11+# slice
22+33+A Deno SSR web application with AT Protocol integration, built with Preact,
44+HTMX, and OAuth authentication.
55+66+## Quick Start
77+88+```bash
99+# Start the development server
1010+deno task dev
1111+```
1212+1313+Visit your app at http://localhost:8080
1414+1515+> **Note:** Your slice and OAuth credentials were automatically configured
1616+> during project creation. The `.env` file is already set up with your
1717+> credentials.
1818+1919+## Features
2020+2121+- 🔐 **OAuth Authentication** with PKCE flow
2222+- ⚡ **Server-Side Rendering** with Preact
2323+- 🎯 **Interactive UI** with HTMX
2424+- 🎨 **Styling** with Tailwind CSS
2525+- 🗄️ **Session Management** with SQLite
2626+- 🔄 **Auto Token Refresh**
2727+- 🏗️ **Feature-Based Architecture**
2828+2929+## Development
3030+3131+```bash
3232+# Start development server with hot reload
3333+deno task dev
3434+3535+# Start production server
3636+deno task start
3737+3838+# Format code
3939+deno fmt
4040+4141+# Check types
4242+deno check src/**/*.ts src/**/*.tsx
4343+```
4444+4545+## Project Structure
4646+4747+```
4848+slices.json # Slices configuration file
4949+lexicons/ # AT Protocol lexicon definitions
5050+src/
5151+├── main.ts # Server entry point
5252+├── config.ts # OAuth & session configuration
5353+├── generated_client.ts # Generated TypeScript client from lexicons
5454+├── routes/ # Route definitions
5555+├── features/ # Feature modules
5656+│ └── auth/ # Authentication
5757+├── shared/fragments/ # Reusable UI components
5858+└── utils/ # Utility functions
5959+```
6060+6161+## OAuth Setup
6262+6363+Your OAuth application was automatically created during project initialization
6464+with:
6565+6666+- **Client ID & Secret**: Already configured in `.env`
6767+- **Redirect URI**: `http://localhost:8080/oauth/callback`
6868+- **Slice**: Automatically created and linked
6969+7070+To manage your OAuth clients or create additional ones:
7171+7272+1. Visit [Slices Network](https://slices.network)
7373+2. Use the `slices login` CLI command
7474+7575+## Documentation
7676+7777+- `CLAUDE.md` - Architecture guide for AI assistance
7878+- Feature directories contain handlers and templates
7979+- Components use Preact with server-side rendering
8080+- HTMX provides interactive behavior without page reloads
8181+8282+## License
8383+8484+MIT
···11+{
22+ "lexicon": 1,
33+ "id": "app.bsky.feed.postgate",
44+ "defs": {
55+ "main": {
66+ "key": "tid",
77+ "type": "record",
88+ "record": {
99+ "type": "object",
1010+ "required": [
1111+ "post",
1212+ "createdAt"
1313+ ],
1414+ "properties": {
1515+ "post": {
1616+ "type": "string",
1717+ "format": "at-uri",
1818+ "description": "Reference (AT-URI) to the post record."
1919+ },
2020+ "createdAt": {
2121+ "type": "string",
2222+ "format": "datetime"
2323+ },
2424+ "embeddingRules": {
2525+ "type": "array",
2626+ "items": {
2727+ "refs": [
2828+ "#disableRule"
2929+ ],
3030+ "type": "union"
3131+ },
3232+ "maxLength": 5,
3333+ "description": "List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed."
3434+ },
3535+ "detachedEmbeddingUris": {
3636+ "type": "array",
3737+ "items": {
3838+ "type": "string",
3939+ "format": "at-uri"
4040+ },
4141+ "maxLength": 50,
4242+ "description": "List of AT-URIs embedding this post that the author has detached from."
4343+ }
4444+ }
4545+ },
4646+ "description": "Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository."
4747+ },
4848+ "disableRule": {
4949+ "type": "object",
5050+ "properties": {},
5151+ "description": "Disables embedding of this post."
5252+ }
5353+ }
5454+}
+80
lexicons/app/bsky/feed/threadgate.json
···11+{
22+ "lexicon": 1,
33+ "id": "app.bsky.feed.threadgate",
44+ "defs": {
55+ "main": {
66+ "key": "tid",
77+ "type": "record",
88+ "record": {
99+ "type": "object",
1010+ "required": [
1111+ "post",
1212+ "createdAt"
1313+ ],
1414+ "properties": {
1515+ "post": {
1616+ "type": "string",
1717+ "format": "at-uri",
1818+ "description": "Reference (AT-URI) to the post record."
1919+ },
2020+ "allow": {
2121+ "type": "array",
2222+ "items": {
2323+ "refs": [
2424+ "#mentionRule",
2525+ "#followerRule",
2626+ "#followingRule",
2727+ "#listRule"
2828+ ],
2929+ "type": "union"
3030+ },
3131+ "maxLength": 5,
3232+ "description": "List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply."
3333+ },
3434+ "createdAt": {
3535+ "type": "string",
3636+ "format": "datetime"
3737+ },
3838+ "hiddenReplies": {
3939+ "type": "array",
4040+ "items": {
4141+ "type": "string",
4242+ "format": "at-uri"
4343+ },
4444+ "maxLength": 50,
4545+ "description": "List of hidden reply URIs."
4646+ }
4747+ }
4848+ },
4949+ "description": "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository."
5050+ },
5151+ "listRule": {
5252+ "type": "object",
5353+ "required": [
5454+ "list"
5555+ ],
5656+ "properties": {
5757+ "list": {
5858+ "type": "string",
5959+ "format": "at-uri"
6060+ }
6161+ },
6262+ "description": "Allow replies from actors on a list."
6363+ },
6464+ "mentionRule": {
6565+ "type": "object",
6666+ "properties": {},
6767+ "description": "Allow replies from actors mentioned in your post."
6868+ },
6969+ "followerRule": {
7070+ "type": "object",
7171+ "properties": {},
7272+ "description": "Allow replies from actors who follow you."
7373+ },
7474+ "followingRule": {
7575+ "type": "object",
7676+ "properties": {},
7777+ "description": "Allow replies from actors you follow."
7878+ }
7979+ }
8080+}
···11+{
22+ "lexicon": 1,
33+ "id": "app.bsky.richtext.facet",
44+ "defs": {
55+ "tag": {
66+ "type": "object",
77+ "required": [
88+ "tag"
99+ ],
1010+ "properties": {
1111+ "tag": {
1212+ "type": "string",
1313+ "maxLength": 640,
1414+ "maxGraphemes": 64
1515+ }
1616+ },
1717+ "description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags')."
1818+ },
1919+ "link": {
2020+ "type": "object",
2121+ "required": [
2222+ "uri"
2323+ ],
2424+ "properties": {
2525+ "uri": {
2626+ "type": "string",
2727+ "format": "uri"
2828+ }
2929+ },
3030+ "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL."
3131+ },
3232+ "main": {
3333+ "type": "object",
3434+ "required": [
3535+ "index",
3636+ "features"
3737+ ],
3838+ "properties": {
3939+ "index": {
4040+ "ref": "#byteSlice",
4141+ "type": "ref"
4242+ },
4343+ "features": {
4444+ "type": "array",
4545+ "items": {
4646+ "refs": [
4747+ "#mention",
4848+ "#link",
4949+ "#tag"
5050+ ],
5151+ "type": "union"
5252+ }
5353+ }
5454+ },
5555+ "description": "Annotation of a sub-string within rich text."
5656+ },
5757+ "mention": {
5858+ "type": "object",
5959+ "required": [
6060+ "did"
6161+ ],
6262+ "properties": {
6363+ "did": {
6464+ "type": "string",
6565+ "format": "did"
6666+ }
6767+ },
6868+ "description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID."
6969+ },
7070+ "byteSlice": {
7171+ "type": "object",
7272+ "required": [
7373+ "byteStart",
7474+ "byteEnd"
7575+ ],
7676+ "properties": {
7777+ "byteEnd": {
7878+ "type": "integer",
7979+ "minimum": 0
8080+ },
8181+ "byteStart": {
8282+ "type": "integer",
8383+ "minimum": 0
8484+ }
8585+ },
8686+ "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets."
8787+ }
8888+ }
8989+}
+191
lexicons/com/atproto/label/defs.json
···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+ ],
1212+ "properties": {
1313+ "cid": {
1414+ "type": "string",
1515+ "format": "cid",
1616+ "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to."
1717+ },
1818+ "cts": {
1919+ "type": "string",
2020+ "format": "datetime",
2121+ "description": "Timestamp when this label was created."
2222+ },
2323+ "exp": {
2424+ "type": "string",
2525+ "format": "datetime",
2626+ "description": "Timestamp at which this label expires (no longer applies)."
2727+ },
2828+ "neg": {
2929+ "type": "boolean",
3030+ "description": "If true, this is a negation label, overwriting a previous label."
3131+ },
3232+ "sig": {
3333+ "type": "bytes",
3434+ "description": "Signature of dag-cbor encoded label."
3535+ },
3636+ "src": {
3737+ "type": "string",
3838+ "format": "did",
3939+ "description": "DID of the actor who created this label."
4040+ },
4141+ "uri": {
4242+ "type": "string",
4343+ "format": "uri",
4444+ "description": "AT URI of the record, repository (account), or other resource that this label applies to."
4545+ },
4646+ "val": {
4747+ "type": "string",
4848+ "maxLength": 128,
4949+ "description": "The short string name of the value or type of this label."
5050+ },
5151+ "ver": {
5252+ "type": "integer",
5353+ "description": "The AT Protocol version of the label object."
5454+ }
5555+ },
5656+ "description": "Metadata tag on an atproto resource (eg, repo or record)."
5757+ },
5858+ "selfLabel": {
5959+ "type": "object",
6060+ "required": [
6161+ "val"
6262+ ],
6363+ "properties": {
6464+ "val": {
6565+ "type": "string",
6666+ "maxLength": 128,
6767+ "description": "The short string name of the value or type of this label."
6868+ }
6969+ },
7070+ "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel."
7171+ },
7272+ "labelValue": {
7373+ "type": "string",
7474+ "knownValues": [
7575+ "!hide",
7676+ "!no-promote",
7777+ "!warn",
7878+ "!no-unauthenticated",
7979+ "dmca-violation",
8080+ "doxxing",
8181+ "porn",
8282+ "sexual",
8383+ "nudity",
8484+ "nsfl",
8585+ "gore"
8686+ ]
8787+ },
8888+ "selfLabels": {
8989+ "type": "object",
9090+ "required": [
9191+ "values"
9292+ ],
9393+ "properties": {
9494+ "values": {
9595+ "type": "array",
9696+ "items": {
9797+ "ref": "#selfLabel",
9898+ "type": "ref"
9999+ },
100100+ "maxLength": 10
101101+ }
102102+ },
103103+ "description": "Metadata tags on an atproto record, published by the author within the record."
104104+ },
105105+ "labelValueDefinition": {
106106+ "type": "object",
107107+ "required": [
108108+ "identifier",
109109+ "severity",
110110+ "blurs",
111111+ "locales"
112112+ ],
113113+ "properties": {
114114+ "blurs": {
115115+ "type": "string",
116116+ "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.",
117117+ "knownValues": [
118118+ "content",
119119+ "media",
120120+ "none"
121121+ ]
122122+ },
123123+ "locales": {
124124+ "type": "array",
125125+ "items": {
126126+ "ref": "#labelValueDefinitionStrings",
127127+ "type": "ref"
128128+ }
129129+ },
130130+ "severity": {
131131+ "type": "string",
132132+ "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
133133+ "knownValues": [
134134+ "inform",
135135+ "alert",
136136+ "none"
137137+ ]
138138+ },
139139+ "adultOnly": {
140140+ "type": "boolean",
141141+ "description": "Does the user need to have adult content enabled in order to configure this label?"
142142+ },
143143+ "identifier": {
144144+ "type": "string",
145145+ "maxLength": 100,
146146+ "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
147147+ "maxGraphemes": 100
148148+ },
149149+ "defaultSetting": {
150150+ "type": "string",
151151+ "default": "warn",
152152+ "description": "The default setting for this label.",
153153+ "knownValues": [
154154+ "ignore",
155155+ "warn",
156156+ "hide"
157157+ ]
158158+ }
159159+ },
160160+ "description": "Declares a label value and its expected interpretations and behaviors."
161161+ },
162162+ "labelValueDefinitionStrings": {
163163+ "type": "object",
164164+ "required": [
165165+ "lang",
166166+ "name",
167167+ "description"
168168+ ],
169169+ "properties": {
170170+ "lang": {
171171+ "type": "string",
172172+ "format": "language",
173173+ "description": "The code of the language these strings are written in."
174174+ },
175175+ "name": {
176176+ "type": "string",
177177+ "maxLength": 640,
178178+ "description": "A short human-readable name for the label.",
179179+ "maxGraphemes": 64
180180+ },
181181+ "description": {
182182+ "type": "string",
183183+ "maxLength": 100000,
184184+ "description": "A longer description of what the label means and why it might be applied.",
185185+ "maxGraphemes": 10000
186186+ }
187187+ },
188188+ "description": "Strings which describe the label in the UI, localized into a specific language."
189189+ }
190190+ }
191191+}