a a vibe-coded abomination experiment of a fragrance review platform built on the atmosphere.
drydown.social
1# Adding AT Protocol Services to Drydown
2
3This guide explains how to add support for new AT Protocol services (like Bluesky, Blacksky, Northsky, etc.) to Drydown.
4
5## Overview
6
7Drydown uses a **configuration-driven approach** for AT Protocol service support. When you add a new service to the configuration file, all share buttons, profile links, and footer links automatically work with that service—no additional code changes required.
8
9## Quick Start
10
11To add a new service, you only need to edit **one file**: `src/config/services.ts`
12
13### Step 1: Add Service Configuration
14
15Open `src/config/services.ts` and add a new entry to the `KNOWN_SERVICES` array:
16
17```typescript
18export const KNOWN_SERVICES: ServiceConfig[] = [
19 // ... existing services ...
20 {
21 id: 'yourservice',
22 name: 'YourService',
23 composeUrl: (text) => `https://yourservice.app/intent/compose?text=${encodeURIComponent(text)}`,
24 profileUrl: (handle) => `https://yourservice.app/profile/${handle}`,
25 issuerPatterns: ['yourservice.app', 'yourservice.com'],
26 pdsPatterns: ['yourservice.app', 'pds.yourservice.com'],
27 },
28]
29```
30
31### Step 2: Test
32
33That's it! No other code changes needed. The app will now:
34- Detect when users log in via your service
35- Generate correct share links for users on your service
36- Link to user profiles on your service
37- Show your service name in UI elements
38
39## ServiceConfig Reference
40
41Each service configuration has the following fields:
42
43### `id` (string)
44Unique identifier for the service. Use lowercase, no spaces.
45
46```typescript
47id: 'bluesky'
48```
49
50### `name` (string)
51Display name shown to users in tooltips and UI.
52
53```typescript
54name: 'Bluesky'
55```
56
57### `composeUrl` (function)
58Generates the URL for creating a new post on this service. Takes the post text as a parameter.
59
60**Important:** This should be a compose intent URL, not the main compose page. Users should be able to click "post" immediately.
61
62```typescript
63// Standard pattern for most AT Protocol services:
64composeUrl: (text) => `https://bsky.app/intent/compose?text=${encodeURIComponent(text)}`
65
66// If the service uses a different parameter name:
67composeUrl: (text) => `https://example.app/share?content=${encodeURIComponent(text)}`
68```
69
70**Note:** Always use `encodeURIComponent()` to ensure special characters are properly encoded.
71
72### `profileUrl` (function)
73Generates the URL for a user's profile page. Takes the AT Protocol handle as a parameter.
74
75```typescript
76// Standard pattern:
77profileUrl: (handle) => `https://bsky.app/profile/${handle}`
78
79// Some services might use @:
80profileUrl: (handle) => `https://example.app/@${handle}`
81```
82
83### `issuerPatterns` (string[])
84Array of domain patterns that appear in OAuth issuer URLs for this service. Used to detect which service a user logged in with.
85
86```typescript
87// Check the OAuth session's issuer field (session.server.serverMetadata.issuer)
88// Add all domains that might appear in that URL
89issuerPatterns: ['bsky.social', 'bsky.app']
90
91// Example: Blacksky uses https://blacksky.app as issuer
92issuerPatterns: ['blacksky.app']
93```
94
95**How to find this:**
961. Log in to the service via OAuth
972. Open browser DevTools Console
983. Look for the log: `[ServiceContext] Detecting service for issuer: <url>`
994. Add the domain from that URL to `issuerPatterns`
100
101### `pdsPatterns` (string[])
102Array of domain patterns for Personal Data Server URLs associated with this service. Used as a fallback when OAuth issuer detection doesn't match.
103
104```typescript
105// Common PDS domains for this service
106pdsPatterns: ['bsky.social', 'bsky.network']
107```
108
109**How to find this:**
1101. Visit `https://plc.directory/{did}` with a user's DID
1112. Look for the `service` array with `type: "AtprotoPersonalDataServer"`
1123. Add the domain from `serviceEndpoint`
113
114## Complete Example
115
116Here's a complete example adding support for "Northsky":
117
118```typescript
119{
120 id: 'northsky',
121 name: 'Northsky',
122 composeUrl: (text) => `https://northsky.app/intent/compose?text=${encodeURIComponent(text)}`,
123 profileUrl: (handle) => `https://northsky.app/profile/${handle}`,
124 issuerPatterns: ['northsky.app'],
125 pdsPatterns: ['northsky.app'],
126}
127```
128
129## How Service Detection Works
130
131Drydown detects services in the following order:
132
133### 1. OAuth Issuer Detection (Primary)
134When a user logs in, Drydown checks `session.server.serverMetadata.issuer` against all `issuerPatterns`:
135
136```typescript
137// Example issuer: "https://bsky.social"
138// Matches issuerPatterns: ['bsky.social', 'bsky.app']
139```
140
141### 2. PDS URL Detection (Fallback)
142If the issuer doesn't match any known service, Drydown fetches the user's DID document and checks their PDS URL against `pdsPatterns`.
143
144### 3. Default to Bluesky
145If no match is found, the app defaults to Bluesky (per product decision: unknown/custom PDS instances should work with Bluesky URLs).
146
147## Where Services Are Used
148
149Once configured, service information is automatically used in:
150
151### Share Buttons
152- **Location:** Review pages (`SingleReviewPage`, `EditReview`)
153- **Behavior:** Uses the **logged-in user's service** (not the review author's)
154- **Example:** User on Blacksky sees "Share on Blacksky" and links open Blacksky compose
155
156### Profile Links
157- **Location:** Profile page headers (`ProfilePage`, `ProfileHousesPage`)
158- **Behavior:** Detects the **profile owner's service** (each user can be on a different service)
159- **Example:** Viewing a Northsky user's profile shows a link to their Northsky profile
160
161### Footer Links
162- **Location:** App footer (`Footer`)
163- **Behavior:** Links to `@drydown.social` on the **logged-in user's service**
164- **Example:** User on Blacksky sees a link to Blacksky's drydown.social profile
165
166## Testing Your Service Configuration
167
168### Manual Testing Checklist
169
1701. **Login Detection**
171 - [ ] Log in with an account on your service
172 - [ ] Check browser console: `session.server.serverMetadata.issuer` should match your patterns
173 - [ ] Verify no errors in console about service detection
174
1752. **Share Buttons**
176 - [ ] Create or view a review
177 - [ ] Click a share button
178 - [ ] Verify it opens your service's compose page with pre-filled text
179 - [ ] Verify the URL structure matches your `composeUrl` pattern
180
1813. **Profile Links**
182 - [ ] Visit a profile page
183 - [ ] Check the external link icon next to the profile name
184 - [ ] Verify hovering shows "View on YourService"
185 - [ ] Click the link and verify it goes to the correct profile
186
1874. **Footer Link**
188 - [ ] Check the footer link to `@drydown.social`
189 - [ ] Verify it points to your service's drydown.social profile
190
191### Testing with Mock Issuers
192
193If you don't have access to an account on the service, you can temporarily mock the issuer for testing:
194
195```typescript
196// In src/contexts/ServiceContext.tsx, temporarily modify:
197useEffect(() => {
198 if (session) {
199 // Original:
200 // const issuer = session.server.serverMetadata.issuer
201
202 // Test mock:
203 const issuer = 'https://yourservice.app' // Your test issuer
204
205 const detected = detectService(issuer)
206 setUserService(detected)
207 }
208}, [session])
209```
210
211**Remember to remove the mock before committing!**
212
213## Edge Cases & Considerations
214
215### Multiple Domains
216Some services might use different domains for OAuth vs profiles:
217
218```typescript
219{
220 id: 'example',
221 name: 'Example',
222 composeUrl: (text) => `https://app.example.com/compose?text=${encodeURIComponent(text)}`,
223 profileUrl: (handle) => `https://example.com/u/${handle}`,
224 issuerPatterns: ['auth.example.com', 'oauth.example.com'],
225 pdsPatterns: ['pds.example.com', 'data.example.com'],
226}
227```
228
229### Services Without Compose Intents
230Some AT Protocol services might not have a `/intent/compose` endpoint yet. In this case:
231
2321. Use the regular compose page URL
2332. Document this limitation in comments
2343. Users will need to manually paste the text
235
236```typescript
237{
238 id: 'example',
239 name: 'Example',
240 // No intent URL available, falls back to main compose
241 composeUrl: (text) => `https://example.app/compose`,
242 // ... rest of config
243}
244```
245
246### Custom PDS Instances
247Individual users can run their own PDS. These will not match any known patterns and will default to Bluesky. This is intentional—custom PDS users are advanced users who understand Bluesky URLs will work.
248
249## Troubleshooting
250
251### Service Not Detected After Login
2521. Check `session.server.serverMetadata.issuer` in browser console
2532. Verify the issuer domain matches your `issuerPatterns`
2543. Check for typos in pattern strings (must be exact substring matches)
255
256### Profile Links Not Appearing
2571. Open browser devtools Network tab
2582. Look for requests to `https://plc.directory/did:plc:...`
2593. Check if the response includes a PDS service endpoint
2604. Verify that endpoint domain matches your `pdsPatterns`
261
262### Share Button Opens Wrong Service
2631. Verify you're checking the **logged-in user's service**, not the author's
2642. Check `useShareButton()` hook is imported and used correctly
2653. Confirm `ServiceProvider` wraps your component tree in `app.tsx`
266
267### TypeScript Errors After Adding Service
2681. Run `npx tsc -b` to check for type errors
2692. Ensure all required fields are present in your config
2703. Verify function signatures match `ServiceConfig` interface
271
272## Build Verification
273
274After adding a service, always verify the build:
275
276```bash
277# Type check
278npx tsc -b
279
280# Production build
281npm run build
282
283# Dev server (test manually)
284npm run dev
285```
286
287All commands should complete without errors.
288
289## Contributing Service Configurations
290
291When contributing a new service configuration to Drydown:
292
2931. **Research**: Verify the service actually exists and has a public web interface
2942. **Test**: Log in with a real account if possible, or coordinate with service maintainers
2953. **Document**: Add a comment above your config explaining any quirks
2964. **Verify**: Test all three use cases (share, profile links, footer)
2975. **PR**: Submit with a clear description of which service you're adding
298
299## Future Enhancements
300
301Potential improvements to the service system:
302
303- **Service icons**: Add logos/icons for each service
304- **Service discovery**: Automatically detect services from a registry
305- **User preferences**: Let users override detected service
306- **Federation status**: Show which services support which AT Protocol features
307
308---
309
310For questions or issues, please open an issue on the Drydown GitHub repository.