···107107- **OAuth**: ATProto OAuth Client (Node implementation) - runs in SSR
108108- **Database**: SQLite for OAuth session state (server-side)
109109- **Protocol**: XRPC client via @atcute/client
110110+- **Identity Resolution**: @atcute/identity-resolver + @atcute/identity (DID/handle resolution)
110111- **UI Library**: React 19
111112- **Styling**: Tailwind CSS v4 + DaisyUI
112113- **Build Tool**: Vite
···260261- ✅ Session management with SQLite
261262- ✅ XRPC server and client
262263- ✅ Post creation and storage (public and private)
264264+- ✅ Post deletion (transactional, removes post and associated tags)
263265- ✅ Tag system for posts
264266- ✅ User authentication flow
265265-- ✅ Basic UI components (Header, PostFeed, UserMenu)
267267+- ✅ UI components
268268+ - Header, PostFeed, UserMenu
269269+ - GlobalSpinner (navigation state feedback)
270270+ - Delete post button (visible to post authors only)
266271- ✅ Lexicon type generation
267272- ✅ Social graph (follow relationships)
268273 - Follow/unfollow operations with transaction-safe count updates
+42-1
packages/client/CLAUDE.md
···5353- **OAuth**: @atproto/oauth-client-node (ATProto OAuth)
5454- **Database**: SQLite (OAuth state storage)
5555- **Protocol**: XRPC via @atcute/client
5656+- **Identity Resolution**: @atcute/identity-resolver + @atcute/identity (DID/handle resolution)
5657- **UI Library**: React 19
5758- **Styling**: Tailwind CSS v4 + DaisyUI
5859- **Build Tool**: Vite
···7374│ ├── components/ # React components
7475│ │ ├── Header.tsx # App header with navigation
7576│ │ ├── UserMenu.tsx # User dropdown menu
7676-│ │ ├── PostFeed.tsx # Post list component
7777+│ │ ├── PostFeed.tsx # Post list component with delete button
7878+│ │ ├── GlobalSpinner.tsx # Navigation state progress indicator
7779│ │ └── FlashMessages.tsx # Toast notifications
7880│ ├── lib/ # Server-side utilities (.server.ts)
7981│ │ ├── oauth.server.ts # OAuth client setup
···189191 )
190192}
191193```
194194+195195+### UI Components
196196+197197+**GlobalSpinner** (`components/GlobalSpinner.tsx`):
198198+- Displays a progress bar during navigation transitions
199199+- Uses React Router's `useNavigation()` hook to track navigation state
200200+- Styled with DaisyUI progress component
201201+- Integrated into root layout for global navigation feedback
202202+203203+**PostFeed** (`components/PostFeed.tsx`):
204204+- Displays a list of posts with author info, content, tags, and timestamps
205205+- Shows delete button for posts authored by the current user
206206+- Delete action handled via React Router Form with POST method
207207+- Uses XRPC `app.wafrn.content.deletePost` endpoint
208208+- Responsive card-based layout with DaisyUI styling
209209+210210+### Identity Resolution
211211+212212+**ATProto Identity Resolution** (`lib/idResolver.server.ts`):
213213+The client uses @atcute/identity-resolver and @atcute/identity for resolving ATProto identities:
214214+215215+- **Handle Resolution**: Composite resolver with DNS (via DoH) and HTTP (.well-known) methods
216216+ - Uses race strategy for fastest response
217217+ - DNS resolution via Cloudflare's DoH JSON API
218218+ - HTTP resolution via .well-known/atproto-did
219219+220220+- **DID Document Resolution**: Supports PLC and Web DIDs
221221+ - PLC DIDs: Resolved via PLC directory
222222+ - Web DIDs: Resolved via HTTPS .well-known endpoint
223223+224224+**Utility Functions**:
225225+- `atUriToDid(uri)` - Extract DID from AT-URI
226226+- `didToHandle(did)` - Resolve DID to handle via DID document
227227+228228+**Benefits of @atcute packages**:
229229+- More modular than @atproto/identity
230230+- Configurable resolution strategies
231231+- Better error handling
232232+- Consistent with other @atcute packages
192233193234### Authentication Flow
194235
+63
packages/server/CLAUDE.md
···7171│ │ ├── account.ts # Account management logic
7272│ │ ├── follow.ts # Follow relationship management
7373│ │ ├── profile.ts # Profile management (wafrn-specific + cache)
7474+│ │ ├── post.ts # Post operations (delete, etc.)
7575+│ │ ├── auth.ts # Service authentication (JWT generation)
7476│ │ ├── xrpcServer.ts # XRPC server instance and configuration
7577│ │ └── idResolver.ts # ATProto identity resolution
7678│ └── db/
···106108 - Create and store public posts
107109 - Create and store private posts
108110 - Retrieve posts by account or tag
111111+ - Delete posts (transactional, removes post and associated tags)
109112 - Tag management for posts
1101131111142. **Social Graph**:
···186189- `PORT` - Server port (optional, default: 3000)
187190- `DATABASE_URL` - SQLite database path (optional, default: ':memory:')
188191- `SERVICE_DID` - DID that points to this service (required)
192192+- `SERVER_SIGNING_PRIVATE_KEY` - Multikey-encoded P256 private key for service authentication (required)
189193190194## Key Implementation Details
191195···302306- Private key JWT authentication with ES256
303307- DPoP-bound access tokens enabled
304308309309+### Post Management Patterns
310310+311311+**Delete Post** (`lib/post.ts`):
312312+The `deletePost()` function handles transactional deletion of posts:
313313+314314+```typescript
315315+import { deletePost } from '@api/lib/post'
316316+317317+// Delete a post (checks authorization via authorDid)
318318+const deletedUri = await deletePost(
319319+ 'at://did:plc:abc123/app.wafrn.content.publicPost/tid123',
320320+ 'did:plc:abc123'
321321+)
322322+// Returns URI if deleted, null if not found or unauthorized
323323+```
324324+325325+**Implementation details**:
326326+- Validates URI format using `parseResourceUri()`
327327+- Checks post collection type (public or private)
328328+- Runs deletion in transaction (post + tags for public posts)
329329+- Verifies author ownership before deletion
330330+- Returns deleted URI or null
331331+332332+### Service Authentication
333333+334334+**JWT Generation** (`lib/auth.ts`):
335335+The server can authenticate itself to other services (like keyserver) using service JWTs:
336336+337337+```typescript
338338+import { generateServiceAuthToken } from '@api/lib/auth'
339339+340340+// Generate service auth token for calling another service
341341+const token = await generateServiceAuthToken(
342342+ 'did:plc:keyserver123', // audience DID
343343+ 'com.example.method' // optional XRPC method
344344+)
345345+346346+// Use in Authorization header
347347+const response = await fetch(serviceUrl, {
348348+ headers: {
349349+ 'Authorization': `Bearer ${token}`
350350+ }
351351+})
352352+```
353353+354354+**Key Functions**:
355355+- `getPrivateKeyFromEnv()` - Load P256 private key from environment
356356+- `generateServiceAuthToken(audience, method?)` - Create signed JWT
357357+ - Uses ES256 algorithm
358358+ - 60-second expiration
359359+ - Includes service DID as issuer
360360+ - Optional XRPC method in `lxm` claim
361361+362362+**Environment Requirements**:
363363+- `SERVER_SIGNING_PRIVATE_KEY` - Multikey-encoded P256 private key
364364+- `SERVICE_DID` - DID of this service (issuer)
365365+305366### Database Patterns
306367- Use `Insertable<TableName>` type for insert operations
307368- Upsert pattern: `.onConflict((oc) => oc.columns([...]).doUpdateSet(...))`
308369- Reference excluded values in upserts: `data.ref('excluded.column_name')`
370370+- Use transactions for operations that modify multiple tables (e.g., delete post + tags)
309371310372### Migration Workflow
3113731. Create migration: manually create timestamped file in `src/db/migrations/`
···324386- `watproto.post.create` - Create new public or private post
325387- `watproto.post.list` - List posts by account
326388- `watproto.post.listByTag` - List posts filtered by tag
389389+- `app.wafrn.content.deletePost` - Delete cached post from AppView (public or private)
327390328391**Account Management**:
329392- `watproto.account.create` - Create or update account record