A comprehensive authentication system built with Next.js and Better Auth, featuring email/password authentication, passkeys (WebAuthn), magic links, OAuth integration, and robust security features.
sequenceDiagram
participant User
participant Client as Client/UI
participant Middleware as API Middleware
participant Route as Auth Route
participant BetterAuth as Better Auth
participant DB as Database
participant Resend as Resend Email
note over User,Resend: Sign-Up with Email Verification
User->>Client: Enter email & password
Client->>Route: POST /api/auth/signup
Middleware->>Middleware: Check rate limit & bot
Middleware->>Route: Allow request
Route->>BetterAuth: Sign up with email/password
BetterAuth->>DB: Create user, generate token
BetterAuth->>Resend: Send verification email
Resend->>User: Verification link email
Client->>Client: Show "Check your email"
note over User,Resend: Passkey Registration
User->>Client: Click "Register Passkey"
Client->>Route: GET /api/passkey/check-returning-user
Middleware->>Middleware: Check IP rate limit
Route->>DB: Query session & user
Route->>Client: Return { hasPasskey, user }
Client->>BetterAuth: Add passkey
BetterAuth->>DB: Store passkey credentials
Client->>Client: Redirect to home
note over User,Middleware: Rate Limit & Bot Detection
User->>Client: Make API request
Middleware->>Middleware: Extract user-agent
Middleware->>Middleware: Check against bot patterns
alt Bot detected
Middleware->>Client: Return 403 Forbidden
else Rate limit exceeded
Middleware->>Client: Return 429 Too Many Requests
else Allowed
Middleware->>Route: Add headers, forward
Route->>Client: Response + x-ratelimit-remaining
end
- Email/Password Authentication - Secure user registration and login with bcrypt password hashing
- Email Verification - Automatic email verification on signup with customizable expiration
- Password Reset - Secure password reset flow with time-limited tokens
- Magic Link Authentication - Passwordless authentication via email links
- Passkey Support (WebAuthn) - Modern passwordless authentication using WebAuthn standards
- Google OAuth Integration - Social authentication with Google
- Rate Limiting & Bot Detection - Built-in middleware for API protection
- Session Management - Secure session handling with JWT tokens and cookie-based sessions
- CORS Support - Configurable trusted origins for cross-origin requests
Before you begin, ensure you have the following installed:
- Node.js (v18 or higher recommended)
- PostgreSQL (v14 or higher) - Can be run via Docker
- Docker Desktop (for local development database)
- Yarn package manager
You'll need to set up accounts for the following services:
-
Resend - For sending transactional emails
- Sign up at resend.com
- Set up a domain at resend.com/domains
- Get your API key from resend.com/api-keys
-
Google OAuth (Optional, for production)
- Create OAuth credentials at console.cloud.google.com/auth
- Configure OAuth consent screen
- Create OAuth 2.0 Client ID
Follow these steps to get Authly up and running:
git clone https://github.com/ChristopherAlphonse/authly.git
cd authlyyarn installStart the PostgreSQL database using Docker:
yarn docker:upThis will start a PostgreSQL container on port 5433 with:
- Database:
authly - User:
authly - Password:
authly_dev_password
Create a .env file in the root directory with the following variables:
Important: Replace placeholder values with your actual credentials. See Environment Variables for detailed descriptions.
Set up the database schema:
yarn db:setupThis command will:
- Start Docker containers (if not already running)
- Push the database schema to PostgreSQL
yarn devThis will start:
- Next.js app on http://localhost:5173
- Drizzle Studio for database management at https://local.drizzle.studio
- Open http://localhost:5173 in your browser
- Try signing up with a test email
- Check your email for the verification link
- Access Drizzle Studio to view database records
Complete reference of all environment variables:
| Variable | Required | Description | Example |
|---|---|---|---|
BETTER_AUTH_SECRET |
✅ Yes | Secret key for JWT token signing and encryption | openssl rand -base64 32 |
DATABASE_URL |
✅ Yes | PostgreSQL connection string | postgresql://user:pass@host:port/db |
RESEND_API_KEY |
✅ Yes | Resend API key for sending emails | re_xxxxxxxxxxxxx |
EMAIL_SENDER_NAME |
✅ Yes | Display name for email sender | Authly |
EMAIL_SENDER_ADDRESS |
✅ Yes | Email address for sending (must be verified domain) | noreply@yourdomain.com |
GOOGLE_CLIENT_ID |
Google OAuth client ID | xxxxx.apps.googleusercontent.com |
|
GOOGLE_CLIENT_SECRET |
Google OAuth client secret | xxxxx |
|
TELEMETRY_ENABLED |
❌ No | Enable Better Auth telemetry (default: true in dev) |
true or false |
Generate a secure secret key:
# Using OpenSSL
openssl rand -base64 32
# Or using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"- Create Account: Sign up at resend.com
- Add Domain: Go to resend.com/domains and add your domain
- Verify DNS: Add the required DNS records to verify domain ownership
- Get API Key: Navigate to resend.com/api-keys and create an API key
- Configure Sender: Use a verified email address from your domain for
EMAIL_SENDER_ADDRESS
- Create Project: Go to Google Cloud Console
- Enable OAuth: Navigate to APIs & Services > Credentials
- Create OAuth Client: Create OAuth 2.0 Client ID
- Configure Consent Screen: Set up OAuth consent screen
- Add Redirect URIs: Add your callback URLs (e.g.,
http://localhost:5173/api/auth/callback/google) - Copy Credentials: Copy Client ID and Client Secret to your
.envfile
| Script | Description |
|---|---|
yarn dev |
Start Next.js app and Drizzle Studio concurrently |
yarn dev:app |
Start only the Next.js development server (port 5173) |
yarn dev:studio |
Start only Drizzle Studio for database management |
| Script | Description |
|---|---|
yarn db:generate |
Generate database migration files |
yarn db:migrate |
Run database migrations |
yarn db:push |
Push schema changes to database (dev) |
yarn db:push:prod |
Push schema changes to database (production) |
yarn db:setup |
Start Docker and push database schema |
yarn db:reset |
Drop and recreate database schema |
yarn db:studio |
Open Drizzle Studio (dev) |
yarn db:studio:prod |
Open Drizzle Studio (production) |
| Script | Description |
|---|---|
yarn docker:up |
Start Docker containers |
yarn docker:down |
Stop Docker containers |
yarn docker:logs |
View Docker container logs |
yarn docker:restart |
Restart Docker containers |
| Script | Description |
|---|---|
yarn build |
Build the application for production |
yarn start |
Start the production server |
yarn lint |
Run ESLint to check code quality |
| Script | Description |
|---|---|
yarn biome:check |
Check code with Biome (must return 0 errors) |
yarn biome:fix |
Auto-fix code issues with Biome |
yarn format |
Format code with Biome |
authly/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── api/ # API routes
│ │ │ ├── auth/ # Authentication endpoints
│ │ │ │ └── [...all]/ # Better Auth catch-all route
│ │ │ └── passkey/ # Passkey management endpoints
│ │ ├── login/ # Login page
│ │ ├── signup/ # Signup page
│ │ ├── magic-link/ # Magic link page
│ │ ├── passkey/ # Passkey pages
│ │ ├── forgot-password/ # Password reset request
│ │ ├── reset-password/ # Password reset form
│ │ └── resend-verification/ # Resend email verification
│ ├── components/ # React components
│ │ ├── ui/ # UI components (Button, Input, Card)
│ │ └── auth-status.tsx # Authentication status component
│ ├── constants/ # Application constants
│ │ ├── app_constants.ts # General constants
│ │ └── auth_constant.ts # Authentication constants
│ ├── db/ # Database configuration
│ │ ├── index.ts # Database connection
│ │ └── schema.ts # Drizzle ORM schema
│ ├── email/ # Email templates
│ │ ├── magic-link.tsx # Magic link email template
│ │ ├── reset-password.tsx # Password reset email template
│ │ └── verify-email.tsx # Email verification template
│ └── lib/ # Core libraries
│ ├── auth.ts # Better Auth configuration
│ ├── auth-client.ts # Client-side auth utilities
│ └── utils.ts # Utility functions
├── public/ # Static assets
├── middleware.ts # Next.js middleware (rate limiting, bot detection)
├── docker-compose.yml # Docker configuration for PostgreSQL
├── drizzle.config.ts # Drizzle ORM configuration
└── package.json # Dependencies and scripts
src/lib/auth.ts- Main Better Auth configuration with all authentication providers and settingsmiddleware.ts- API middleware for rate limiting and bot detectionsrc/app/api/auth/[...all]/route.ts- Better Auth catch-all route handler with CORS supportsrc/db/schema.ts- Database schema definitions using Drizzle ORMdocker-compose.yml- PostgreSQL database configuration for local development
All authentication endpoints are handled by Better Auth through the catch-all route /api/auth/[...all].
POST /api/auth/signup
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword123",
"name": "John Doe"
}Response:
{
"user": {
"id": "user_id",
"email": "user@example.com",
"name": "John Doe"
}
}POST /api/auth/sign-in
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword123"
}POST /api/auth/sign-outGET /api/auth/verify-email?token=verification_tokenPOST /api/auth/forget-password
Content-Type: application/json
{
"email": "user@example.com"
}POST /api/auth/reset-password
Content-Type: application/json
{
"token": "reset_token",
"password": "newpassword123"
}POST /api/auth/sign-in/email
Content-Type: application/json
{
"email": "user@example.com"
}GET /api/auth/sign-in/googleRedirects to Google OAuth consent screen.
GET /api/passkey/check-returning-userChecks if the current session user has a registered passkey.
Response:
{
"hasPasskey": true,
"isReturningUser": true,
"user": {
"id": "user_id",
"email": "user@example.com",
"name": "John Doe"
}
}Rate Limit: 30 requests per minute per IP
POST /api/passkey/has-passkeys
Content-Type: application/json
{
"email": "user@example.com"
}Response:
{
"hasPasskey": true,
"user": {
"id": "user_id",
"email": "user@example.com",
"name": "John Doe"
}
}Admin Endpoint (requires PASSKEY_ADMIN_TOKEN):
POST /api/passkey/has-passkeys
Content-Type: application/json
{
"adminToken": "your-admin-token"
}Response:
{
"users": [
{
"id": "user_id",
"email": "user@example.com",
"name": "John Doe"
}
]
}Rate Limit: 30 requests per minute per IP
All API endpoints are protected by rate limiting middleware:
- API Routes: 20 requests per 10 seconds per IP
- Passkey Endpoints: 30 requests per minute per IP
- Bot Detection: Automatic blocking of known bot user-agents
Rate limit headers are included in responses:
x-ratelimit-remaining: Number of requests remaining in the current window
-
Start Development Environment
yarn dev
-
Access Drizzle Studio
- Open https://local.drizzle.studio
- View and edit database records
- Test queries
-
Make Database Changes
# Edit src/db/schema.ts # Then push changes yarn db:push
-
Run Code Quality Checks
yarn biome:check yarn biome:fix # Auto-fix issues -
Test Authentication Flows
- Sign up → Email verification → Login
- Password reset flow
- Magic link authentication
- Passkey registration and login
- Google OAuth (if configured)
View Database:
yarn db:studioReset Database:
yarn db:resetGenerate Migrations:
yarn db:generateAuthly uses a layered architecture:
- Client Layer - Next.js pages and React components
- API Layer - Next.js API routes with middleware
- Auth Layer - Better Auth library handling authentication logic
- Database Layer - PostgreSQL with Drizzle ORM
- Email Layer - Resend for transactional emails
- Rate Limiting: Prevents brute force attacks and API abuse
- Bot Detection: Blocks known bot user-agents automatically
- Secure Cookies: HttpOnly, Secure, SameSite cookies for sessions
- JWT Tokens: Secure token-based authentication with rotation
- Password Hashing: bcrypt with salt rounds for password storage
- CORS Protection: Configurable trusted origins
- Email Verification: Required for account activation
- Token Expiration: Time-limited tokens for password reset and email verification
-
Set Environment Variables
- Use your hosting platform's environment variable configuration
- Ensure all required variables are set (see Environment Variables)
- Use strong, randomly generated secrets
-
Database Migration
NODE_ENV=production yarn db:push:prod
-
Build Application
yarn build
-
Start Production Server
yarn start
- Database: Use a managed PostgreSQL service (e.g., Vercel Postgres, Supabase, AWS RDS)
- Email: Ensure your Resend domain is verified and configured
- OAuth: Configure production redirect URIs in Google Cloud Console
- CORS: Update
TRUSTED_ORIGINSinsrc/lib/utils.tswith your production domain - Secrets: Use secure secret management (e.g., Vercel Environment Variables, AWS Secrets Manager)
- HTTPS: Always use HTTPS in production
- Rate Limiting: Consider using a distributed rate limiter (e.g., Redis) for multi-instance deployments
- All environment variables are set and secure
-
BETTER_AUTH_SECRETis a strong, randomly generated value - Database connection uses SSL in production
- CORS is configured with only trusted origins
- Email sender domain is verified in Resend
- Google OAuth redirect URIs are configured correctly
- HTTPS is enabled
- Rate limiting is configured appropriately
- Bot detection is enabled
- Session cookies are secure (HttpOnly, Secure, SameSite)
- Monitor authentication success/failure rates
- Track rate limit violations
- Log bot detection events
- Monitor email delivery rates
- Set up alerts for database connection issues
- Better Auth Documentation
- Next.js Documentation
- Drizzle ORM Documentation
- Resend Documentation
- WebAuthn/Passkeys Guide
Need Help? Check the troubleshooting section above or review the error logs for specific error messages.