See demo at : https://better-auth-strava-demo.cristoper.dev/
Strava OAuth provider for Better Auth with automatic token refresh and seamless session management.
- 🔐 OAuth 2.0 authentication with Strava
- 🔄 Automatic token refresh (offline access)
- 📧 Smart email handling (placeholder generation for Strava's no-email limitation)
- 🎯 Type-safe session management
- ⚡ Zero-config setup with Better Auth
npm install better-auth better-auth-strava
# or
bun add better-auth better-auth-strava- Go to Strava API Settings
- Create a new application
- Set your Authorization Callback Domain (e.g.,
localhostfor development) - Copy your Client ID and Client Secret
// lib/auth.ts
import { betterAuth } from "better-auth";
import { strava } from "better-auth-strava";
export const auth = betterAuth({
database: {
provider: "postgres",
url: process.env.DATABASE_URL!,
},
socialProviders: {
strava: {
clientId: process.env.STRAVA_CLIENT_ID!,
clientSecret: process.env.STRAVA_CLIENT_SECRET!,
},
},
});// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: process.env.NEXT_PUBLIC_AUTH_URL,
});Next.js App Router:
// app/api/auth/[...all]/route.ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth);Next.js Pages Router:
// pages/api/auth/[...all].ts
import { auth } from "@/lib/auth";
import { toNextJsHandler } from "better-auth/next-js";
export default toNextJsHandler(auth);"use client";
import { authClient } from "@/lib/auth-client";
export default function LoginButton() {
const { data: session, isPending } = authClient.useSession();
const handleSignIn = async () => {
await authClient.signIn.social({
provider: "strava",
callbackURL: "/dashboard",
});
};
const handleSignOut = async () => {
await authClient.signOut();
};
if (isPending) {
return <div>Loading...</div>;
}
if (session) {
return (
<div>
<p>Welcome, {session.user.name}!</p>
<p>Athlete ID: {session.user.image}</p>
<button onClick={handleSignOut}>Sign Out</button>
</div>
);
}
return <button onClick={handleSignIn}>Connect with Strava</button>;
}const { data: session } = authClient.useSession();
if (session) {
console.log(session.user.name); // "John Doe"
console.log(session.user.image); // Profile photo URL
console.log(session.user.email); // "athlete-12345@strava.local" (placeholder)
}Strava doesn't provide email addresses. This package automatically generates placeholder emails:
const { data: session } = authClient.useSession();
if (session?.user.email.endsWith("@strava.local")) {
// Prompt user to provide a real email
console.log("Please add your email address");
}import { betterAuth } from "better-auth";
import { strava } from "better-auth-strava";
export const auth = betterAuth({
database: {
provider: "postgres",
url: process.env.DATABASE_URL!,
},
socialProviders: {
strava: {
clientId: process.env.STRAVA_CLIENT_ID!,
clientSecret: process.env.STRAVA_CLIENT_SECRET!,
scopes: ["read", "profile:read_all", "activity:read", "activity:write"],
},
},
});| Scope | Description |
|---|---|
read |
Read public profile data |
profile:read_all |
Read all profile information |
activity:read |
Read activity data |
activity:read_all |
Read all activity data (including private) |
activity:write |
Create and modify activities |
// app/api/user/route.ts
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
export async function GET() {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
return Response.json({
user: session.user,
});
}// middleware.ts
import { auth } from "@/lib/auth";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export async function middleware(request: NextRequest) {
const session = await auth.api.getSession({
headers: request.headers,
});
if (!session && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*"],
};strava({
clientId: string; // Required: Your Strava OAuth client ID
clientSecret: string; // Required: Your Strava OAuth client secret
redirectURI?: string; // Optional: Override redirect URI
scopes?: string[]; // Optional: Custom scopes (default: ["read", "profile:read_all", "activity:read_all"])
approvalPrompt?: "auto" | "force"; // Optional: Force re-authorization (default: "auto")
accessType?: "offline" | "online"; // Optional: Refresh token support (default: "offline")
})Important: Strava does not provide email addresses through their API.
This package automatically generates deterministic placeholder emails:
athlete-{athleteId}@strava.local
Example: athlete-34217575@strava.local
- ✅ Seamless: Works with Better Auth's required email field
- ✅ Deterministic: Same athlete always gets the same email
- ✅ Identifiable: Easy to detect with
.endsWith("@strava.local") - ✅ No Configuration: Zero setup required
// Check if user needs to provide email
if (session.user.email.endsWith("@strava.local")) {
// Show email collection form
}
// Filter real vs placeholder emails
const realUsers = users.filter((u) => !u.email.endsWith("@strava.local"));This repository includes a full-featured demo app showing:
- Complete authentication flow
- Session management
- User profile display
- Modern UI with Shadcn components
- PostgreSQL database integration
See Demo Setup Guide for installation instructions.
.
├── packages/
│ └── better-auth-strava/ # NPM package
│ ├── src/
│ │ └── index.ts # Strava provider implementation
│ └── README.md
├── apps/
│ └── web/ # Demo Next.js app
│ ├── app/
│ ├── lib/
│ └── components/
├── docker-compose.yml # PostgreSQL for demo
└── README.md # This file
Contributions are welcome! Please open an issue or submit a pull request.
MIT © Cristoper Anderson