a a vibe-coded abomination experiment of a fragrance review platform built on the atmosphere. drydown.social

Adding AT Protocol Services to Drydown#

This guide explains how to add support for new AT Protocol services (like Bluesky, Blacksky, Northsky, etc.) to Drydown.

Overview#

Drydown 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.

Quick Start#

To add a new service, you only need to edit one file: src/config/services.ts

Step 1: Add Service Configuration#

Open src/config/services.ts and add a new entry to the KNOWN_SERVICES array:

export const KNOWN_SERVICES: ServiceConfig[] = [
  // ... existing services ...
  {
    id: 'yourservice',
    name: 'YourService',
    composeUrl: (text) => `https://yourservice.app/intent/compose?text=${encodeURIComponent(text)}`,
    profileUrl: (handle) => `https://yourservice.app/profile/${handle}`,
    issuerPatterns: ['yourservice.app', 'yourservice.com'],
    pdsPatterns: ['yourservice.app', 'pds.yourservice.com'],
  },
]

Step 2: Test#

That's it! No other code changes needed. The app will now:

  • Detect when users log in via your service
  • Generate correct share links for users on your service
  • Link to user profiles on your service
  • Show your service name in UI elements

ServiceConfig Reference#

Each service configuration has the following fields:

id (string)#

Unique identifier for the service. Use lowercase, no spaces.

id: 'bluesky'

name (string)#

Display name shown to users in tooltips and UI.

name: 'Bluesky'

composeUrl (function)#

Generates the URL for creating a new post on this service. Takes the post text as a parameter.

Important: This should be a compose intent URL, not the main compose page. Users should be able to click "post" immediately.

// Standard pattern for most AT Protocol services:
composeUrl: (text) => `https://bsky.app/intent/compose?text=${encodeURIComponent(text)}`

// If the service uses a different parameter name:
composeUrl: (text) => `https://example.app/share?content=${encodeURIComponent(text)}`

Note: Always use encodeURIComponent() to ensure special characters are properly encoded.

profileUrl (function)#

Generates the URL for a user's profile page. Takes the AT Protocol handle as a parameter.

// Standard pattern:
profileUrl: (handle) => `https://bsky.app/profile/${handle}`

// Some services might use @:
profileUrl: (handle) => `https://example.app/@${handle}`

issuerPatterns (string[])#

Array of domain patterns that appear in OAuth issuer URLs for this service. Used to detect which service a user logged in with.

// Check the OAuth session's issuer field (session.server.serverMetadata.issuer)
// Add all domains that might appear in that URL
issuerPatterns: ['bsky.social', 'bsky.app']

// Example: Blacksky uses https://blacksky.app as issuer
issuerPatterns: ['blacksky.app']

How to find this:

  1. Log in to the service via OAuth
  2. Open browser DevTools Console
  3. Look for the log: [ServiceContext] Detecting service for issuer: <url>
  4. Add the domain from that URL to issuerPatterns

pdsPatterns (string[])#

Array of domain patterns for Personal Data Server URLs associated with this service. Used as a fallback when OAuth issuer detection doesn't match.

// Common PDS domains for this service
pdsPatterns: ['bsky.social', 'bsky.network']

How to find this:

  1. Visit https://plc.directory/{did} with a user's DID
  2. Look for the service array with type: "AtprotoPersonalDataServer"
  3. Add the domain from serviceEndpoint

Complete Example#

Here's a complete example adding support for "Northsky":

{
  id: 'northsky',
  name: 'Northsky',
  composeUrl: (text) => `https://northsky.app/intent/compose?text=${encodeURIComponent(text)}`,
  profileUrl: (handle) => `https://northsky.app/profile/${handle}`,
  issuerPatterns: ['northsky.app'],
  pdsPatterns: ['northsky.app'],
}

How Service Detection Works#

Drydown detects services in the following order:

1. OAuth Issuer Detection (Primary)#

When a user logs in, Drydown checks session.server.serverMetadata.issuer against all issuerPatterns:

// Example issuer: "https://bsky.social"
// Matches issuerPatterns: ['bsky.social', 'bsky.app']

2. PDS URL Detection (Fallback)#

If the issuer doesn't match any known service, Drydown fetches the user's DID document and checks their PDS URL against pdsPatterns.

3. Default to Bluesky#

If no match is found, the app defaults to Bluesky (per product decision: unknown/custom PDS instances should work with Bluesky URLs).

Where Services Are Used#

Once configured, service information is automatically used in:

Share Buttons#

  • Location: Review pages (SingleReviewPage, EditReview)
  • Behavior: Uses the logged-in user's service (not the review author's)
  • Example: User on Blacksky sees "Share on Blacksky" and links open Blacksky compose
  • Location: Profile page headers (ProfilePage, ProfileHousesPage)
  • Behavior: Detects the profile owner's service (each user can be on a different service)
  • Example: Viewing a Northsky user's profile shows a link to their Northsky profile
  • Location: App footer (Footer)
  • Behavior: Links to @drydown.social on the logged-in user's service
  • Example: User on Blacksky sees a link to Blacksky's drydown.social profile

Testing Your Service Configuration#

Manual Testing Checklist#

  1. Login Detection

    • Log in with an account on your service
    • Check browser console: session.server.serverMetadata.issuer should match your patterns
    • Verify no errors in console about service detection
  2. Share Buttons

    • Create or view a review
    • Click a share button
    • Verify it opens your service's compose page with pre-filled text
    • Verify the URL structure matches your composeUrl pattern
  3. Profile Links

    • Visit a profile page
    • Check the external link icon next to the profile name
    • Verify hovering shows "View on YourService"
    • Click the link and verify it goes to the correct profile
  4. Footer Link

    • Check the footer link to @drydown.social
    • Verify it points to your service's drydown.social profile

Testing with Mock Issuers#

If you don't have access to an account on the service, you can temporarily mock the issuer for testing:

// In src/contexts/ServiceContext.tsx, temporarily modify:
useEffect(() => {
  if (session) {
    // Original:
    // const issuer = session.server.serverMetadata.issuer

    // Test mock:
    const issuer = 'https://yourservice.app' // Your test issuer

    const detected = detectService(issuer)
    setUserService(detected)
  }
}, [session])

Remember to remove the mock before committing!

Edge Cases & Considerations#

Multiple Domains#

Some services might use different domains for OAuth vs profiles:

{
  id: 'example',
  name: 'Example',
  composeUrl: (text) => `https://app.example.com/compose?text=${encodeURIComponent(text)}`,
  profileUrl: (handle) => `https://example.com/u/${handle}`,
  issuerPatterns: ['auth.example.com', 'oauth.example.com'],
  pdsPatterns: ['pds.example.com', 'data.example.com'],
}

Services Without Compose Intents#

Some AT Protocol services might not have a /intent/compose endpoint yet. In this case:

  1. Use the regular compose page URL
  2. Document this limitation in comments
  3. Users will need to manually paste the text
{
  id: 'example',
  name: 'Example',
  // No intent URL available, falls back to main compose
  composeUrl: (text) => `https://example.app/compose`,
  // ... rest of config
}

Custom PDS Instances#

Individual 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.

Troubleshooting#

Service Not Detected After Login#

  1. Check session.server.serverMetadata.issuer in browser console
  2. Verify the issuer domain matches your issuerPatterns
  3. Check for typos in pattern strings (must be exact substring matches)
  1. Open browser devtools Network tab
  2. Look for requests to https://plc.directory/did:plc:...
  3. Check if the response includes a PDS service endpoint
  4. Verify that endpoint domain matches your pdsPatterns

Share Button Opens Wrong Service#

  1. Verify you're checking the logged-in user's service, not the author's
  2. Check useShareButton() hook is imported and used correctly
  3. Confirm ServiceProvider wraps your component tree in app.tsx

TypeScript Errors After Adding Service#

  1. Run npx tsc -b to check for type errors
  2. Ensure all required fields are present in your config
  3. Verify function signatures match ServiceConfig interface

Build Verification#

After adding a service, always verify the build:

# Type check
npx tsc -b

# Production build
npm run build

# Dev server (test manually)
npm run dev

All commands should complete without errors.

Contributing Service Configurations#

When contributing a new service configuration to Drydown:

  1. Research: Verify the service actually exists and has a public web interface
  2. Test: Log in with a real account if possible, or coordinate with service maintainers
  3. Document: Add a comment above your config explaining any quirks
  4. Verify: Test all three use cases (share, profile links, footer)
  5. PR: Submit with a clear description of which service you're adding

Future Enhancements#

Potential improvements to the service system:

  • Service icons: Add logos/icons for each service
  • Service discovery: Automatically detect services from a registry
  • User preferences: Let users override detected service
  • Federation status: Show which services support which AT Protocol features

For questions or issues, please open an issue on the Drydown GitHub repository.