Complete in-house authentication system built with tRPC, Prisma, and Next.js 16. Includes email/password, 2FA/TOTP, magic links, OAuth foundation, sessions, rate limiting, and email system.
Add to your .env:
# Core Auth
SESSION_SECRET="your-32-character-secret-here"
APP_URL="http://localhost:3000"
# Email (Resend - optional for development)
RESEND_API_KEY="re_xxxxx"
RESEND_FROM_EMAIL="auth@yourdomain.com"
# OAuth (optional)
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""Generate SESSION_SECRET: openssl rand -base64 32
npx prisma migrate dev --name init
npx prisma generateAuth Utilities (lib/auth/):
password.ts- bcrypt hashing & validationsession.ts- HttpOnly cookie sessions (8h default, 30d with remember-me)tokens.ts- Secure token generation & verificationtotp.ts- 2FA TOTP & recovery codesrate-limit.ts- In-memory rate limiting per routeaudit.ts- Authentication event logging
Email System (lib/email/):
resend.ts- Resend API client (gracefully degrades without API key)templates.ts- HTML email templates (verify, reset, magic link, 2FA, etc.)index.ts- Send functions with EmailMessage logging
tRPC Router (lib/trpc/router/auth.ts):
- All auth procedures (sign-up, sign-in, 2FA, password management, etc.)
- Rate limiting per action
- Audit logging for security events
Middleware (lib/trpc/trpc.ts):
publicProcedure- No auth requiredprotectedProcedure- Requires valid sessionadminProcedure- Requires ADMIN rolecuratorProcedure- Requires PROBLEM_CURATOR role
Context (lib/trpc/context.ts):
- Automatically loads session from cookies
- Available in all tRPC procedures as
ctx.userandctx.session
Auth Routes (app/(auth)/):
/sign-in- Email/password + 2FA challenge/sign-up- Registration with email verification/auth/forgot-password- Request password reset/auth/reset-password- Reset with token/auth/verify-email- Email verification handler
Settings Routes (app/(platform)/settings/):
/settings/account- Profile, sessions, account deletion/settings/security- Password, email, 2FA management
- HttpOnly cookies (secure, SameSite=Lax)
- Dual token: session (8h) + refresh (30d)
- Device tracking (IP, user agent)
- Per-device revocation
- Automatic rotation on privilege change
- TOTP via authenticator apps (Google Auth, Authy, etc.)
- QR code + manual secret display
- 10 single-use recovery codes (hashed)
- Recovery code regeneration on 2FA disable
- Per-IP + per-action limits
- Configured in
lib/auth/rate-limit.ts:- Signup: 3/hour
- Login: 5/15min
- Password reset: 3/hour
- Email verify: 3/hour
- Magic link: 3/hour
- 2FA attempts: 5/5min
- All auth events logged to
AuthAuditLog - IP address, user agent, timestamps
- Queryable per user
- Visible in admin/user dashboards
- Templates: verify, reset, magic link, 2FA enabled, email changed
- Sends via Resend (dev mode: console.log only)
EmailMessagetable tracks delivery status- Link tracking disabled for security
- 15-minute TTL for all tokens
- Passwords: bcrypt (12 rounds), strength validation
- Sessions: HttpOnly + Secure + SameSite
- Tokens: single-use, short-lived, HMAC-signed
- No email enumeration (consistent error messages)
- CSRF protection via SameSite cookies
- Rate limiting prevents brute force
import { trpc } from "@/lib/trpc/client";
const { data: session } = trpc.auth.getSession.useQuery();
if (!session) return <SignInForm />;
return <div>Welcome {session.user.email}</div>;import { protectedProcedure } from "@/lib/trpc/trpc";
export const myRouter = router({
myProtectedRoute: protectedProcedure.query(({ ctx }) => {
// ctx.user is guaranteed to exist
return { message: `Hello ${ctx.user.email}` };
}),
});Session is auto-loaded in tRPC context. Use appropriate procedure type:
publicProcedure- anyoneprotectedProcedure- authenticated usersadminProcedure- admin onlycuratorProcedure- curator or admin
User: id, email, handle, hashedPassword, role, twoFactorEnabled, emailVerified, timestamps Session: id, userId, sessionToken, refreshToken, expires, ipAddress, userAgent, twoFactorVerified TwoFactorSecret: userId, secret TwoFactorRecoveryCode: userId, codeHash, consumedAt AuthAuditLog: userId, action, ipAddress, userAgent, metadata, createdAt EmailMessage: userId, type, templateVersion, resendMessageId, status, providerReason, timestamps VerificationToken: identifier (email), token, type, expires
- Update
lib/validators/auth.tswith OAuth schemas - Add provider routes in
lib/trpc/router/auth.ts - Use
Accountmodel to link external accounts - Handle token exchange & account linking
- Add roles to
UserRoleenum inschema.prisma - Create custom middleware in
lib/trpc/trpc.ts - Example:
moderatorProcedure,premiumProcedure
- Implement
/api/webhooks/resendroute handler - Update
EmailMessagestatus on delivery/bounce/complaint - Handle suppression list for bounced emails
Build fails with env validation: Set SKIP_ENV_VALIDATION=true npm run build
Emails not sending: Check RESEND_API_KEY or check console logs (dev mode)
Database errors: Run npx prisma migrate dev
Session not persisting: Check cookie settings (Secure flag requires HTTPS in production)
- Passwords hashed with bcrypt
- Sessions in HttpOnly cookies
- CSRF protection via SameSite
- Rate limiting per action
- Email verification required
- 2FA available for all users
- Recovery codes for 2FA
- Audit log for sensitive actions
- No email enumeration
- Single-use tokens
- Account deletion (GDPR)
- Per-device session management
- Set
SESSION_SECRETto strong random value - Configure
APP_URLto production domain - Set up Resend account & verify domain
- Configure DKIM/SPF/DMARC for email domain
- Enable HTTPS (Secure cookie flag)
- Set up database backups
- Monitor rate limit hits
- Review audit logs regularly
- Test 2FA enrollment & recovery flows
- Test email deliverability across providers