Better Auth logoBetter 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_URL in .env.

Run migrations with:

pnpm db:migrate

pnpm dev runs this for you. Add a migration with pnpm db:make <name>.