Skip to main content

Overview

Bounty uses Better Auth for authentication, providing a modern, type-safe authentication system with support for multiple providers and advanced features.
The authentication system is implemented in packages/auth/src/server.ts:1 and configured in packages/auth/src/config.ts:1.

Authentication Methods

Bounty supports multiple authentication methods to accommodate different user preferences and use cases.

Email & Password

Traditional email/password authentication with email verification.
// Sign up with email and password
await auth.signUp.email({
  email: '[email protected]',
  password: 'secure-password',
  name: 'John Doe'
});

// Sign in
await auth.signIn.email({
  email: '[email protected]',
  password: 'secure-password'
});
Features:
  • Email verification on sign-up
  • Password reset via email
  • Auto sign-in after email verification

Email OTP (One-Time Password)

Passwordless authentication using one-time codes sent via email.
// Request OTP
await auth.emailOTP.sendOTP({
  email: '[email protected]',
  type: 'sign-in'
});

// Verify OTP
await auth.emailOTP.verifyOTP({
  email: '[email protected]',
  otp: '123456'
});
OTP Types:
  • email-verification - Verify email address
  • sign-in - Passwordless sign-in
  • forget-password - Password reset

OAuth Providers

Bounty integrates with multiple OAuth providers for social authentication.

GitHub

Primary OAuth provider with handle auto-syncScopes: read:user, public_repo, read:org

Google

Google OAuth authenticationScopes: openid, email, profile

Discord

Discord authentication with guild accessScopes: identify, email, guilds

Linear

Linear workspace integrationScopes: read, write

GitHub Integration

GitHub authentication includes automatic handle synchronization. When a user authenticates with GitHub:
  1. Their GitHub username is extracted and lowercased
  2. If the user doesn’t have a handle, it’s set to their GitHub username
  3. The handle is used for personal team creation and profile URLs
See packages/auth/src/server.ts:104 for implementation details.

Account Linking

Users can link multiple OAuth providers to a single account:
const accountLinking = {
  enabled: true,
  trustedProviders: ['github', 'google', 'discord', 'linear'],
  allowDifferentEmails: true
};

Device Authorization

For CLI tools and mobile apps, Bounty supports OAuth 2.0 Device Authorization Flow.
// 1. Device requests authorization
const { deviceCode, userCode, verificationUri } = 
  await auth.deviceAuthorization.request({
    clientId: 'cli-client-id'
  });

// 2. User visits verificationUri and enters userCode
console.log(`Visit ${verificationUri} and enter: ${userCode}`);

// 3. Device polls for authorization
const token = await auth.deviceAuthorization.poll({
  deviceCode,
  interval: 5 // seconds
});
Configuration:
  • Expires in: 30 minutes
  • Poll interval: 5 seconds
  • Client IDs validated via DEVICE_AUTH_ALLOWED_CLIENT_IDS environment variable
Device authorization uses a fail-closed security model. If no client IDs are configured, all device authorization requests are rejected.

Session Management

Session Configuration

Sessions are configured for security and performance. See packages/auth/src/config.ts:12 for details.
const session = {
  expiresIn: 60 * 60 * 24 * 7,  // 7 days
  updateAge: 60 * 5,             // Refresh every 5 minutes
  cookieCache: {
    enabled: true,
    maxAge: 60 * 5               // 5 minute cookie cache
  }
};
Key Features:
  • 7-day session expiration
  • Database refresh every 5 minutes for up-to-date user data
  • Cookie caching to reduce database queries

Multi-Session Support

Users can maintain multiple active sessions across devices:
const multiSession = {
  maximumSessions: 5
};
When the limit is exceeded, the oldest session is automatically revoked.

Session Context

The API context includes session information from cookies or Bearer tokens. See packages/api/src/context.ts:38 for implementation.
// Session extraction supports both methods
const session = await auth.api.getSession({
  headers: req.headers  // Checks cookies AND Authorization header
});
Session Object:
interface Session {
  session: {
    id: string;
    userId: string;
    expiresAt: Date;
    activeOrganizationId?: string;
    impersonatedBy?: string;
  };
  user: {
    id: string;
    email: string;
    name: string;
    role: 'user' | 'early_access' | 'admin';
    handle?: string;
    emailVerified: boolean;
  };
}

Active Organization

Sessions track the user’s active organization for team-scoped operations:
// Set active organization
await auth.api.setActiveOrganization({
  session,
  organizationId: 'org_123'
});

// Access in procedures
const orgProcedure = protectedProcedure.use(async ({ ctx }) => {
  const activeOrgId = ctx.session.session.activeOrganizationId;
  // ...
});
Auto-Assignment: If no active organization is set, the user’s personal team is automatically assigned on session creation. See packages/auth/src/server.ts:384 for implementation.

Authorization Patterns

Role-Based Access Control (RBAC)

Bounty implements RBAC with three user roles:
Default role for all users.Permissions:
  • Create and manage own bounties
  • Apply to bounties
  • Comment and vote
  • Join organizations

Organization-Based Access Control

Organization members have role-based permissions within teams:
Standard team member.Permissions:
  • View organization bounties
  • Collaborate on team projects
  • Access organization resources

Impersonation

Admins can impersonate users for support purposes:
// Start impersonation
await auth.admin.impersonate({
  userId: 'user_123'
});

// Admin procedures block impersonation
const adminProcedure = protectedProcedure.use(({ ctx }) => {
  if (ctx.session.impersonatedBy) {
    throw new TRPCError({
      code: 'FORBIDDEN',
      message: 'Stop impersonating to view the admin panel'
    });
  }
});

Personal Teams

Every user automatically receives a personal team (organization) upon registration.

Auto-Creation

Personal teams are created in the user.create.after database hook. See packages/auth/src/server.ts:181 for implementation. Team Naming:
  1. If user has a GitHub handle: {handle}'s team
  2. Otherwise: {name}'s team or My's team
Slug Generation:
  • Format: {handle}-{random8chars} or {userId}
  • Random suffix prevents collisions
  • Reserved slugs are avoided

Self-Healing

For users created before the personal teams feature, teams are auto-created on first login:
session.create.before: async (session) => {
  if (!session.activeOrganizationId) {
    let personalTeamId = await getPersonalTeamId(session.userId);
    
    if (!personalTeamId) {
      // Auto-create personal team
      await createPersonalTeam(user);
      personalTeamId = await getPersonalTeamId(session.userId);
    }
    
    session.activeOrganizationId = personalTeamId;
  }
}

Email Notifications

Authentication events trigger email notifications using React Email templates.

Email Types

Sent when a user signs up with email/password.Template: OTPVerificationContains: Verification link or OTP codeAuto sign-in: Enabled after verification
Sent when a user requests a password reset.Template: ForgotPasswordContains: Password reset linkExpires: Link expires after configured time
Sent when a user is invited to join a team.Template: OrgInvitationContains: Invitation link, inviter name, organization name, role

Email Configuration

const emailConfig = {
  from: 'Bounty.new <[email protected]>',
  provider: 'resend' // or your email provider
};
See packages/auth/src/config.ts:42 for email sending implementation.

Security Features

Trusted Origins

CORS is configured with trusted origins for security:
const trustedOrigins = [
  'https://bounty.new',
  'https://*.bounty.new',
  // Development only:
  'http://localhost:3000',
  'http://localhost:3001'
];

Bearer Token Authentication

API endpoints support Bearer token authentication for programmatic access:
curl -H "Authorization: Bearer <token>" \
  https://api.bounty.new/trpc/user.profile
Session cookies are configured with security best practices:
  • httpOnly: Prevents JavaScript access
  • secure: HTTPS only in production
  • sameSite: CSRF protection
  • path: Limited to auth routes

Best Practices

1

Use the appropriate procedure type

Choose publicProcedure, protectedProcedure, orgProcedure, etc. based on your authorization requirements.
2

Check permissions early

Validate user permissions at the start of your procedure to fail fast.
3

Handle missing sessions gracefully

The session may be null in public procedures. Always check before accessing user data.
4

Use organization context

When working with teams, use orgProcedure to automatically validate membership and provide context.
5

Monitor rate limits

Implement appropriate rate limiting for sensitive operations like authentication and payments.

Next Steps

API Overview

Learn about tRPC architecture and available routers

Better Auth Docs

Explore Better Auth documentation for advanced features

Build docs developers (and LLMs) love