@slashclick/auth
v0.3.0
Published
Shared auth UI components, hooks, schemas, and better-auth client factory
Readme
@slashclick/auth
This package provides shared auth UI (SigninFormCard) and a better-auth client
factory. It handles the UI and client-side flow. Each consuming app is responsible
for its own better-auth server configuration, database connection, and — critically —
rate limiting.
Required: Rate Limiting
Without rate limiting, auth endpoints are open to brute-force attacks.
This package does not bundle rate limiting because it belongs on the server in each consuming app. You must implement it before going to production.
Option A: better-auth Rate Limit Plugin (Recommended)
better-auth ships a built-in rate limit plugin. Add it to your server config:
// apps/your-app/auth.ts
import { betterAuth } from 'better-auth'
import { rateLimit } from 'better-auth/plugins'
export const auth = betterAuth({
database: {
/* your db config */
},
plugins: [
rateLimit({
window: 60,
max: 5,
customRules: {
'/sign-in/email': { window: 60, max: 5 },
'/sign-up/email': { window: 60, max: 3 },
'/forget-password': { window: 3600, max: 3 },
'/resend-verification-email': { window: 3600, max: 3 },
},
}),
],
})Option B: Server Action Wrapper (Manual)
If you use Next.js Server Actions for auth, apply rate limiting before calling
auth.api.signInEmail. Write a small rateLimit helper for your app (e.g. backed by
Redis or an in-memory token bucket) and wrap each action with it:
// apps/your-app/actions/signIn.ts
'use server'
import { headers } from 'next/headers'
import { auth } from '../auth'
import { rateLimit, AuthRateLimits } from '../lib/rate-limit'
export async function signInAction(values: {
emailOrUsername: string
password: string
}) {
const headersList = await headers()
const req = new Request('http://localhost', { headers: headersList })
const limited = await rateLimit(req, AuthRateLimits.signIn)
if (limited) return { error: 'Too many attempts. Please try again later.' }
// ... rest of sign-in logic
}The AuthRateLimits config should be:
export const AuthRateLimits = {
signIn: {
tokensPerInterval: 5,
interval: 'minute' as const,
failClosed: true,
},
signUp: {
tokensPerInterval: 3,
interval: 'minute' as const,
failClosed: true,
},
resend: { tokensPerInterval: 3, interval: 'hour' as const, failClosed: true },
reset: { tokensPerInterval: 3, interval: 'hour' as const, failClosed: true },
}failClosed: true is critical — if the rate limiter errors, it returns 503
instead of letting the request through.
Pre-Production Checklist
- [ ] Rate limiting on sign-in (max 5/min per IP, fail closed)
- [ ] Rate limiting on sign-up (max 3/min per IP, fail closed)
- [ ] Rate limiting on password reset (max 3/hr per IP, fail closed)
- [ ] Rate limiting on resend verification (max 3/hr per IP, fail closed)
- [ ]
emailVerificationRequired: truein better-auth server config - [ ]
NEXT_PUBLIC_APP_URLenv var set correctly in production - [ ] No hardcoded secrets — all credentials via environment variables
- [ ]
.envin.gitignore
Usage in a New App
1. Install
npm install @slashclick/auth2. Create the auth client
// lib/auth-client.ts
import { createSharedAuthClient } from '@slashclick/auth'
export const authClient = createSharedAuthClient({
baseURL: process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000',
})
export const { signIn, signUp, signOut, useSession } = authClient3a. Use with a Server Action (supports username resolution)
// app/(auth)/signin/page.tsx
import { SigninFormCard } from '@slashclick/auth'
import { signInAction } from '../../actions/signIn' // your rate-limited action
import { resendAction } from '../../actions/resend'
export default function SigninPage() {
const handleSignOut = async () => {
'use server'
// call your auth.signOut()
}
return (
<SigninFormCard
providers={[]}
credentials
credentialsSignin={signInAction}
resendVerificationEmail={resendAction}
signOut={handleSignOut}
/>
)
}3b. Use with the default hook (email only, no username resolution)
// app/(auth)/signin/SigninWrapper.tsx
'use client'
import { useSignIn, SigninFormCard } from '@slashclick/auth'
import { authClient } from '../../lib/auth-client'
export function SigninWrapper() {
const { credentialsSignin } = useSignIn(authClient)
return (
<SigninFormCard
providers={[]}
credentials
credentialsSignin={credentialsSignin}
signOut={async () => { /* call authClient.signOut() */ }}
/>
)
}What Lives Where
| Concern | Lives in | Why |
| -------------------------------- | ------------------ | ------------------------ |
| UI components (SigninFormCard) | @slashclick/auth | Shared |
| Validation schemas | @slashclick/auth | Shared |
| useSignIn hook | @slashclick/auth | Shared (email only) |
| better-auth server config | Each consuming app | Needs DB, email, secrets |
| Rate limiting | Each consuming app | Needs server context |
| Username→email resolution | Each consuming app | App-specific DB query |
| Email service (Resend etc.) | Each consuming app | App-specific credentials |
Notes
SigninFormCardusesnext/navigation(useSearchParams,useRouter) and targets Next.js apps. If you need a non-Next.js consumer, these can be abstracted behind props.- The
credentialsSigninprop accepts any function matchingCredentialsSigninFn— pass a Server Action for username support, or useuseSignIn(authClient)for email-only apps. - Register, reset-password, and verify screens follow the same pattern and will be added to this package in a follow-up.
