Better Auth
Delightful authentication and authorization
newt-app uses Better Auth for authentication, configured across both the Next.js frontend and NestJS backend.
Client
Use the auth client in your React components:
'use client';
import { authClient } from '@/lib/auth-client';
export function UserGreeting() {
const { data: session } = authClient.useSession();
if (!session) return <p>Not signed in</p>;
return <p>Hello, {session.user.name}</p>;
}Sign Up
'use client';
import { useForm } from '@tanstack/react-form';
import { useState } from 'react';
import { authClient } from '@/lib/auth-client';
export function SignUpForm() {
const [error, setError] = useState('');
const form = useForm({
defaultValues: { name: '', email: '', password: '' },
onSubmit: async ({ value }) => {
setError('');
const { error } = await authClient.signUp.email(value);
if (error) setError(error.message ?? 'Sign up failed');
},
});
return (
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
<form.Field name="name">
{(field) => (
<input
type="text"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
required
/>
)}
</form.Field>
<form.Field name="email">
{(field) => (
<input
type="email"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
required
/>
)}
</form.Field>
<form.Field name="password">
{(field) => (
<input
type="password"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
required
minLength={8}
/>
)}
</form.Field>
{error && <p>{error}</p>}
<form.Subscribe selector={(s) => s.isSubmitting}>
{(isSubmitting) => (
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Loading…' : 'Create account'}
</button>
)}
</form.Subscribe>
</form>
);
}Sign In
'use client';
import { useForm } from '@tanstack/react-form';
import { useState } from 'react';
import { authClient } from '@/lib/auth-client';
export function SignInForm() {
const [error, setError] = useState('');
const form = useForm({
defaultValues: { email: '', password: '' },
onSubmit: async ({ value }) => {
setError('');
const { error } = await authClient.signIn.email(value);
if (error) setError(error.message ?? 'Sign in failed');
},
});
return (
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
<form.Field name="email">
{(field) => (
<input
type="email"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
required
/>
)}
</form.Field>
<form.Field name="password">
{(field) => (
<input
type="password"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
required
/>
)}
</form.Field>
{error && <p>{error}</p>}
<form.Subscribe selector={(s) => s.isSubmitting}>
{(isSubmitting) => (
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Loading…' : 'Sign in'}
</button>
)}
</form.Subscribe>
</form>
);
}NestJS Integration
The NestJS integration uses @thallesp/nestjs-better-auth, a community-maintained package that wires Better Auth into NestJS with minimal setup.
Protecting Routes
Use the @Session decorator to access the current user, and @AllowAnonymous or @OptionalAuth to control access per route:
import { Session, UserSession, AllowAnonymous, OptionalAuth } from '@thallesp/nestjs-better-auth';
@Controller('users')
export class UserController {
@Get('me')
getProfile(@Session() session: UserSession) {
return { user: session.user };
}
@Get('public')
@AllowAnonymous()
getPublic() {
return { message: 'Public route' };
}
@Get('optional')
@OptionalAuth()
getOptional(@Session() session: UserSession) {
return { authenticated: !!session };
}
}Database
Pick SQLite or Postgres at scaffold time; only that driver is installed. Better Auth and your app share one Kysely connection in packages/db.
- SQLite (default): a local
dev.db, zero configuration. - Postgres: set
DATABASE_URLin.env.
Run migrations with:
pnpm db:migratepnpm dev runs this for you. Add a migration with pnpm db:make <name>.