better-auth AT Protocol (Bluesky) Plugin
This package provides an AT Protocol (Bluesky) OAuth plugin for the `better-auth` framework. It enables users to sign in with their Bluesky or any AT Protocol Personal Data Server (PDS) account, leveraging OAuth 2.0 with PKCE and DPoP for secure authentication. Currently at version 0.1.0 (initial release), its release cadence is expected to be event-driven as a new, specialized plugin. Key differentiators include built-in support for localhost development without tunnels via AT Protocol's loopback client, automatic hosting of necessary OAuth discovery endpoints (`client-metadata.json` and `jwks.json`), and persistence of OAuth state and session data through `better-auth`'s database adapter. It also features automatic profile synchronization, updating user details like DID, handle, and display name.
Common errors
-
TypeError: Cannot read properties of undefined (reading 'split') OR Invalid DPoP Private Key: The privateKey option must be a valid PEM-encoded ES256 private key.
cause Missing, invalid, or improperly formatted private key provided in a production environment.fixVerify that `process.env.BSKY_PRIVATE_KEY` contains the full PEM-encoded ES256 private key string and is correctly passed to the `privateKey` option in `atprotoAuth`. -
Error: Failed to initiate Bluesky sign-in: authClient.signIn.bsky is not a function
cause The client-side `atprotoAuthClient` plugin was not correctly configured or imported into `createAuthClient`.fixEnsure `import { atprotoAuthClient } from 'better-auth-atproto/client';` is used and `atprotoAuthClient()` is passed within the `plugins` array to `createAuthClient`. -
Database error: column "bskyDid" of relation "user" does not exist (or similar schema mismatch error)
cause Database migrations were not executed after the `better-auth-atproto` plugin was added to the server configuration.fixRun `npx auth migrate` in your project's root directory to apply the necessary database schema changes for the plugin.
Warnings
- gotcha The provided README snippet incorrectly suggests 'better-auth-bsky' for installation and client imports. The correct package name for installation and client-side imports is 'better-auth-atproto'.
- gotcha A PEM-encoded ES256 private key is mandatory for production deployments to secure OAuth interactions (DPoP). It is not needed for localhost development due to AT Protocol's loopback client support.
- gotcha Database migrations must be run after adding the plugin to your `better-auth` configuration. This extends the `user` table and creates new tables for OAuth state and sessions.
- gotcha The `baseURL` option is a required global configuration for the core `better-auth` instance. It is used by the plugin to construct callback URLs and auto-hosted discovery endpoint URLs.
Install
-
npm install better-auth-atproto -
yarn add better-auth-atproto -
pnpm add better-auth-atproto
Imports
- atprotoAuth
const atprotoAuth = require('better-auth-atproto');import { atprotoAuth } from 'better-auth-atproto'; - atprotoAuthClient
import { atprotoAuthClient } from 'better-auth-atproto';import { atprotoAuthClient } from 'better-auth-atproto/client'; - betterAuth
import betterAuth from 'better-auth';
import { betterAuth } from 'better-auth'; - createAuthClient
import { createAuthClient } from 'better-auth';import { createAuthClient } from 'better-auth/client';
Quickstart
import { betterAuth } from 'better-auth';
import { atprotoAuth } from 'better-auth-atproto';
import { createAuthClient } from 'better-auth/client';
import { atprotoAuthClient } from 'better-auth-atproto/client';
// 1. (Optional) Generate a private key for production:
// openssl ecparam -name prime256v1 -genkey -noout -out ec-private.pem
// 2. Server-side configuration
export const auth = betterAuth({
baseURL: process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000', // Required for callbacks and discovery endpoints
plugins: [
atprotoAuth({
privateKey: process.env.BSKY_PRIVATE_KEY ?? '', // Required for production, optional for localhost
clientMetadata: {
clientName: 'My Awesome App',
scope: 'atproto transition:generic'
},
mapProfileToUser: (profile) => ({
name: profile.displayName || `@${profile.handle}`,
image: profile.avatar,
}),
}),
],
});
// 3. Run migrations after adding plugin: `npx auth migrate` or `npx auth generate`
// 4. Client-side configuration
export const authClient = createAuthClient({
plugins: [atprotoAuthClient()],
});
// 5. Example client-side sign-in flow
async function initiateSignIn(handle: string, callbackURL: string) {
try {
await authClient.signIn.bsky({
handle: handle,
callbackURL: callbackURL,
});
console.log('Bluesky sign-in initiated successfully!');
} catch (error) {
console.error('Failed to initiate Bluesky sign-in:', error);
}
}
// Example usage (e.g., in a React component or server action)
// initiateSignIn('example.bsky.social', '/dashboard');