데이터베이스

어댑터

Better Auth는 데이터를 저장하기 위해 데이터베이스 연결이 필요합니다. 데이터베이스는 사용자, 세션 등의 데이터를 저장하는 데 사용됩니다. 플러그인도 데이터를 저장하기 위해 자체 데이터베이스 테이블을 정의할 수 있습니다.

database 옵션에 지원되는 데이터베이스 인스턴스를 전달하여 Better Auth에 데이터베이스 연결을 전달할 수 있습니다. 지원되는 데이터베이스 어댑터에 대한 자세한 내용은 기타 관계형 데이터베이스 문서에서 확인할 수 있습니다.

CLI

Better Auth는 데이터베이스 마이그레이션 관리와 스키마 생성을 위한 CLI 도구를 제공합니다.

마이그레이션 실행하기

CLI는 데이터베이스를 확인하고 누락된 테이블을 추가하거나 새로운 컬럼으로 기존 테이블을 업데이트하도록 안내합니다. 이는 내장 Kysely 어댑터에만 지원됩니다. 다른 어댑터의 경우 generate 명령을 사용하여 스키마를 생성하고 ORM을 통해 마이그레이션을 처리할 수 있습니다.

npx @better-auth/cli migrate

스키마 생성하기

Better Auth는 Better Auth에 필요한 스키마를 생성하는 generate 명령도 제공합니다. generate 명령은 Better Auth에 필요한 스키마를 생성합니다. Prisma나 Drizzle 같은 데이터베이스 어댑터를 사용하는 경우 이 명령은 ORM에 맞는 스키마를 생성합니다. 내장 Kysely 어댑터를 사용하는 경우 데이터베이스에서 직접 실행할 수 있는 SQL 파일을 생성합니다.

npx @better-auth/cli generate

CLI에 대한 자세한 내용은 CLI 문서를 참조하세요.

테이블을 수동으로 추가하는 것을 선호하는 경우 그렇게 할 수 있습니다. Better Auth에 필요한 코어 스키마는 아래에 설명되어 있으며 플러그인에 필요한 추가 스키마는 플러그인 문서에서 찾을 수 있습니다.

보조 스토리지

Better Auth의 보조 스토리지를 사용하면 세션 데이터, rate limiting 카운터 등을 관리하기 위해 키-값 저장소를 사용할 수 있습니다. 이는 이러한 집약적인 레코드의 저장을 고성능 스토리지나 RAM으로 오프로드하려는 경우에 유용할 수 있습니다.

구현

보조 스토리지를 사용하려면 SecondaryStorage 인터페이스를 구현하세요:

interface SecondaryStorage {
  get: (key: string) => Promise<unknown>;
  set: (key: string, value: string, ttl?: number) => Promise<void>;
  delete: (key: string) => Promise<void>;
}

그런 다음 betterAuth 함수에 구현을 제공하세요:

betterAuth({
  // ... 기타 옵션
  secondaryStorage: {
    // 여기에 구현
  },
});

예제: Redis 구현

다음은 Redis를 사용하는 기본 예제입니다:

import { createClient } from "redis";
import { betterAuth } from "better-auth";

const redis = createClient();
await redis.connect();

export const auth = betterAuth({
	// ... 기타 옵션
	secondaryStorage: {
		get: async (key) => {
			return await redis.get(key);
		},
		set: async (key, value, ttl) => {
			if (ttl) await redis.set(key, value, { EX: ttl });
			// 또는 ioredis의 경우:
			// if (ttl) await redis.set(key, value, 'EX', ttl)
			else await redis.set(key, value);
		},
		delete: async (key) => {
			await redis.del(key);
		}
	}
});

이 구현을 통해 Better Auth는 Redis를 사용하여 세션 데이터와 rate limiting 카운터를 저장할 수 있습니다. 키 이름에 접두사를 추가할 수도 있습니다.

코어 스키마

Better Auth는 데이터베이스에 다음 테이블이 있어야 합니다. 타입은 typescript 형식입니다. 데이터베이스에서 해당 타입을 사용할 수 있습니다.

User

테이블 이름: user

Field NameTypeKeyDescription
idstring각 사용자의 고유 식별자
namestring-사용자가 선택한 표시 이름
emailstring-통신 및 로그인을 위한 사용자의 이메일 주소
emailVerifiedboolean-사용자의 이메일이 검증되었는지 여부
imagestring사용자의 이미지 URL
createdAtDate-사용자 계정이 생성된 시간의 타임스탬프
updatedAtDate-사용자 정보의 마지막 업데이트 타임스탬프

Session

테이블 이름: session

Field NameTypeKeyDescription
idstring각 세션의 고유 식별자
userIdstring사용자의 ID
tokenstring-고유한 세션 토큰
expiresAtDate-세션이 만료되는 시간
ipAddressstring기기의 IP 주소
userAgentstring기기의 user agent 정보
createdAtDate-세션이 생성된 시간의 타임스탬프
updatedAtDate-세션이 업데이트된 시간의 타임스탬프

Account

테이블 이름: account

Field NameTypeKeyDescription
idstring각 계정의 고유 식별자
userIdstring사용자의 ID
accountIdstring-SSO에서 제공한 계정의 ID 또는 credential 계정의 경우 userId와 동일
providerIdstring-제공자의 ID
accessTokenstring계정의 access token. 제공자가 반환
refreshTokenstring계정의 refresh token. 제공자가 반환
accessTokenExpiresAtDateaccess token이 만료되는 시간
refreshTokenExpiresAtDaterefresh token이 만료되는 시간
scopestring계정의 scope. 제공자가 반환
idTokenstring제공자로부터 반환된 ID token
passwordstring계정의 비밀번호. 주로 이메일과 비밀번호 인증에 사용됨
createdAtDate-계정이 생성된 시간의 타임스탬프
updatedAtDate-계정이 업데이트된 시간의 타임스탬프

Verification

테이블 이름: verification

Field NameTypeKeyDescription
idstring각 검증의 고유 식별자
identifierstring-검증 요청의 식별자
valuestring-검증될 값
expiresAtDate-검증 요청이 만료되는 시간
createdAtDate-검증 요청이 생성된 시간의 타임스탬프
updatedAtDate-검증 요청이 업데이트된 시간의 타임스탬프

커스텀 테이블

Better Auth를 사용하면 코어 스키마의 테이블 이름과 컬럼 이름을 커스터마이징할 수 있습니다. 또한 user와 session 테이블에 추가 필드를 추가하여 코어 스키마를 확장할 수도 있습니다.

커스텀 테이블 이름

auth 설정에서 modelNamefields 속성을 사용하여 코어 스키마의 테이블 이름과 컬럼 이름을 커스터마이징할 수 있습니다:

auth.ts
export const auth = betterAuth({
  user: {
    modelName: "users",
    fields: {
      name: "full_name",
      email: "email_address",
    },
  },
  session: {
    modelName: "user_sessions",
    fields: {
      userId: "user_id",
    },
  },
});

코드의 타입 추론은 여전히 원래 필드 이름을 사용합니다 (예: user.full_name이 아닌 user.name).

플러그인의 테이블 이름과 컬럼 이름을 커스터마이징하려면 플러그인 설정에서 schema 속성을 사용할 수 있습니다:

auth.ts
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";

export const auth = betterAuth({
  plugins: [
    twoFactor({
      schema: {
        user: {
          fields: {
            twoFactorEnabled: "two_factor_enabled",
            secret: "two_factor_secret",
          },
        },
      },
    }),
  ],
});

코어 스키마 확장하기

Better Auth는 usersession 스키마를 확장하는 타입 안전한 방법을 제공합니다. auth 설정에 커스텀 필드를 추가할 수 있으며, CLI가 자동으로 데이터베이스 스키마를 업데이트합니다. 이러한 추가 필드는 useSession, signUp.email 및 user 또는 session 객체로 작업하는 다른 엔드포인트에서 적절히 추론됩니다.

커스텀 필드를 추가하려면 auth 설정의 user 또는 session 객체에서 additionalFields 속성을 사용하세요. additionalFields 객체는 필드 이름을 키로 사용하며, 각 값은 다음을 포함하는 FieldAttributes 객체입니다:

  • type: 필드의 데이터 타입 (예: "string", "number", "boolean").
  • required: 필드가 필수인지 여부를 나타내는 부울 값.
  • defaultValue: 필드의 기본값 (참고: 이는 JavaScript 레이어에만 적용됩니다. 데이터베이스에서 필드는 선택 사항입니다).
  • input: 새 레코드를 생성할 때 값을 제공할 수 있는지 여부를 결정합니다 (기본값: true). 사용자가 가입 중에 제공해서는 안 되는 role 같은 추가 필드가 있는 경우 이를 false로 설정할 수 있습니다.

다음은 추가 필드로 user 스키마를 확장하는 예제입니다:

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  user: {
    additionalFields: {
      role: {
        type: "string",
        required: false,
        defaultValue: "user",
        input: false, // 사용자가 role을 설정하지 못하도록 함
      },
      lang: {
        type: "string",
        required: false,
        defaultValue: "en",
      },
    },
  },
});

이제 애플리케이션 로직에서 추가 필드에 접근할 수 있습니다.

//가입 시
const res = await auth.api.signUpEmail({
  email: "test@example.com",
  password: "password",
  name: "John Doe",
  lang: "fr",
});

//user 객체
res.user.role; // > "admin"
res.user.lang; // > "fr"

클라이언트 측에서 추가 필드를 추론하는 방법에 대한 자세한 내용은 TypeScript 문서를 참조하세요.

소셜 / OAuth 제공자를 사용하는 경우 프로필 데이터를 user 객체에 매핑하기 위해 mapProfileToUser를 제공할 수 있습니다. 이렇게 하면 제공자의 프로필에서 추가 필드를 채울 수 있습니다.

예제: firstNamelastName에 대한 프로필 매핑

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  socialProviders: {
    github: {
      clientId: "YOUR_GITHUB_CLIENT_ID",
      clientSecret: "YOUR_GITHUB_CLIENT_SECRET",
      mapProfileToUser: (profile) => {
        return {
          firstName: profile.name.split(" ")[0],
          lastName: profile.name.split(" ")[1],
        };
      },
    },
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      mapProfileToUser: (profile) => {
        return {
          firstName: profile.given_name,
          lastName: profile.family_name,
        };
      },
    },
  },
});

ID 생성

Better Auth는 기본적으로 사용자, 세션 및 기타 엔티티에 대한 고유 ID를 생성합니다. ID 생성 방식을 커스터마이징하려면 auth 설정의 advanced.database.generateId 옵션에서 구성할 수 있습니다.

advanced.database.generateId 옵션을 false로 설정하여 ID 생성을 비활성화할 수도 있습니다. 이렇게 하면 데이터베이스가 자동으로 ID를 생성한다고 가정합니다.

예제: 자동 데이터베이스 ID

auth.ts
import { betterAuth } from "better-auth";
import { db } from "./db";

export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: false,
    },
  },
});

예제: 커스텀 ID 생성기 사용

auth.ts
import { betterAuth } from "better-auth";
import { db } from "./db";

export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      generateId: () => crypto.randomUUID(),
    },
  },
});

숫자 ID

자동 증가하는 숫자 ID를 선호하는 경우 advanced.database.useNumberId 옵션을 true로 설정할 수 있습니다. 이렇게 하면 Better-Auth가 모든 테이블에 대한 ID 생성을 비활성화하고 데이터베이스가 자동으로 숫자 ID를 생성한다고 가정합니다.

활성화하면 Better-Auth CLI는 데이터베이스에 대한 id 필드를 숫자 타입으로 생성하거나 마이그레이션하며 자동 증가 속성이 연결됩니다.

import { betterAuth } from "better-auth";
import { db } from "./db";

export const auth = betterAuth({
  database: db,
  advanced: {
    database: {
      useNumberId: true,
    },
  },
});

Better-Auth는 데이터베이스에 대한 id 필드의 타입을 string으로 계속 추론하지만, 데이터베이스에서 데이터를 가져오거나 삽입할 때 자동으로 숫자 타입으로 변환합니다.

Better-Auth에서 반환된 id 값을 가져올 때 숫자의 문자열 버전을 받을 가능성이 높으며, 이는 정상입니다. Better-Auth에 전달되는 모든 id 값(예: 엔드포인트 body를 통해)은 문자열이어야 합니다.

데이터베이스 훅

데이터베이스 훅을 사용하면 Better Auth의 코어 데이터베이스 작업 수명 주기 동안 실행될 수 있는 커스텀 로직을 정의할 수 있습니다. 다음 모델에 대한 훅을 만들 수 있습니다: user, session, account.

추가 필드가 지원되지만, 이러한 필드에 대한 완전한 타입 추론은 아직 지원되지 않습니다. 개선된 타입 지원이 계획되어 있습니다.

정의할 수 있는 두 가지 타입의 훅이 있습니다:

1. Before 훅

  • 목적: 이 훅은 각 엔티티(user, session 또는 account)가 생성, 업데이트 또는 삭제되기 전에 호출됩니다.
  • 동작: 훅이 false를 반환하면 작업이 중단됩니다. 그리고 data 객체를 반환하면 원래 페이로드를 대체합니다.

2. After 훅

  • 목적: 이 훅은 각 엔티티가 생성되거나 업데이트된 후에 호출됩니다.
  • 동작: 엔티티가 성공적으로 생성되거나 업데이트된 후 추가 액션이나 수정을 수행할 수 있습니다.

사용 예제

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          // 생성되기 전에 user 객체 수정
          return {
            data: {
              // Better-Auth 명명 필드를 반환해야 하며, 데이터베이스의 원래 필드 이름이 아닙니다.
              ...user,
              firstName: user.name.split(" ")[0],
              lastName: user.name.split(" ")[1],
            },
          };
        },
        after: async (user) => {
          //추가 액션 수행, 예: stripe 고객 생성
        },
      },
      delete: {
        before: async (user, ctx) => {
          console.log(`사용자 ${user.email}이(가) 삭제되고 있습니다`);
          if (user.email.includes("admin")) {
            return false; // 삭제 중단
          }

          return true; // 삭제 허용
        },
        after: async (user) => {
          console.log(`사용자 ${user.email}이(가) 삭제되었습니다`);
        },
      },
    },
    session: {
      delete: {
        before: async (session, ctx) => {
          console.log(`세션 ${session.token}이(가) 삭제되고 있습니다`);
          if (session.userId === "admin-user-id") {
            return false; // 삭제 중단
          }
          return true; // 삭제 허용
        },
        after: async (session) => {
          console.log(`세션 ${session.token}이(가) 삭제되었습니다`);
        },
      },
    },
  },
});

오류 던지기

데이터베이스 훅을 진행하지 않도록 중지하려면 better-auth/api에서 가져온 APIError 클래스를 사용하여 오류를 던질 수 있습니다.

auth.ts
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";

export const auth = betterAuth({
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          if (user.isAgreedToTerms === false) {
            // 특별한 조건.
            // API 오류 전송.
            throw new APIError("BAD_REQUEST", {
              message: "가입하기 전에 약관에 동의해야 합니다.",
            });
          }
          return {
            data: user,
          };
        },
      },
    },
  },
});

Context 객체 사용하기

훅의 두 번째 인자로 전달되는 context 객체(ctx)에는 유용한 정보가 포함되어 있습니다. update 훅의 경우 현재 session이 포함되어 있으며, 이를 사용하여 로그인한 사용자의 세부 정보에 접근할 수 있습니다.

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  databaseHooks: {
    user: {
      update: {
        before: async (data, ctx) => {
          // context 객체에서 세션에 접근할 수 있습니다.
          if (ctx.context.session) {
            console.log("사용자 업데이트 시작:", ctx.context.session.userId);
          }
          return { data };
        },
      },
    },
  },
});

표준 훅과 마찬가지로 데이터베이스 훅도 다양한 유용한 속성을 제공하는 ctx 객체를 제공합니다. 자세한 내용은 훅 문서를 참조하세요.

플러그인 스키마

플러그인은 데이터베이스에서 추가 데이터를 저장하기 위해 자체 테이블을 정의할 수 있습니다. 또한 추가 데이터를 저장하기 위해 코어 테이블에 컬럼을 추가할 수도 있습니다. 예를 들어 2단계 인증 플러그인은 다음 컬럼을 user 테이블에 추가합니다:

  • twoFactorEnabled: 사용자에 대해 2단계 인증이 활성화되었는지 여부.
  • twoFactorSecret: TOTP 코드를 생성하는 데 사용되는 비밀 키.
  • twoFactorBackupCodes: 계정 복구를 위한 암호화된 백업 코드.

데이터베이스에 새 테이블과 컬럼을 추가하려면 두 가지 옵션이 있습니다:

CLI: migrate 또는 generate 명령을 사용합니다. 이러한 명령은 데이터베이스를 스캔하고 누락된 테이블이나 컬럼을 추가하도록 안내합니다. 수동 방법: 플러그인 문서의 지침에 따라 수동으로 테이블과 컬럼을 추가합니다.

두 방법 모두 데이터베이스 스키마가 플러그인의 요구 사항과 최신 상태를 유지하도록 보장합니다.