本記事では、Better Auth を使って Next.js 16 アプリケーションに Google OAuth 認証を実装する方法を解説します。データベースには Turso、ORM には Drizzle ORM を使用し、環境変数の管理には t3-env を使用します。
Better Auth とは
Better Auth は、TypeScript 向けのフレームワーク非依存な認証・認可ライブラリです。
主な特徴
- フレームワーク非依存: Next.js、SvelteKit、Remix など様々なフレームワークに対応
- TypeScript ファースト: 型安全な API を提供
- 豊富なプロバイダー: Google、GitHub、Discord など多数の OAuth プロバイダーをサポート
- プラグインシステム: 2FA、パスキー、マジックリンクなど拡張機能を追加可能
- Drizzle ORM 対応: ネイティブアダプターで簡単に統合
- CLI によるスキーマ自動生成: 手動でスキーマを書く必要なし
前提条件
- Next.js 16 プロジェクトがセットアップ済み
- Turso データベースと Drizzle ORM が導入済み
- t3-env が導入済み
- Google Cloud Console で OAuth 認証情報(Client ID、Client Secret)を取得済み
ステップ1: パッケージのインストール
# npm
npm install better-auth
# pnpm
pnpm add better-auth
# bun
bun add better-authステップ2: 環境変数の設定
.env.local に環境変数を追加
# Better Auth
BETTER_AUTH_SECRET=your-random-secret-key-here
BETTER_AUTH_URL=http://localhost:3000
# Google OAuth
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
# Turso Database(既存)
TURSO_DATABASE_URL=libsql://your-database.turso.io
TURSO_AUTH_TOKEN=your-auth-token
# サイト URL(既存)
NEXT_PUBLIC_SITE_URL=http://localhost:3000環境変数の説明
| 変数名 | 説明 |
|---|---|
BETTER_AUTH_SECRET | 暗号化・署名・ハッシュに使用する秘密鍵。32文字以上の高エントロピー文字列が必要。本番環境で未設定の場合はエラーになります。 |
BETTER_AUTH_URL | アプリのベース URL。OAuth コールバック URL の構築に使用されます。この設定がないと redirect_uri_mismatch エラーが発生します。 |
シークレットキーの生成
BETTER_AUTH_SECRET には安全なランダム文字列を使用します:
# OpenSSL で生成
openssl rand -base64 32
# または Node.js で生成
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"src/env.ts に環境変数のスキーマを追加
t3-env を使用して環境変数を型安全に管理します。既存の src/env.ts に Better Auth 用の環境変数を追加します:
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
/**
* サーバーサイド専用の環境変数
*/
server: {
// Turso Database
TURSO_DATABASE_URL: z
.string()
.refine((url) => url.startsWith("file:") || url.startsWith("libsql://"), {
message: "TURSO_DATABASE_URL must start with 'file:' or 'libsql://'",
}),
TURSO_AUTH_TOKEN: z.string(),
// Better Auth
BETTER_AUTH_SECRET: z.string().min(32, {
message: "BETTER_AUTH_SECRET must be at least 32 characters",
}),
BETTER_AUTH_URL: z.string().url(),
// Google OAuth
GOOGLE_CLIENT_ID: z.string().min(1),
GOOGLE_CLIENT_SECRET: z.string().min(1),
},
/**
* クライアントサイドでも使用可能な環境変数
* NEXT_PUBLIC_ プレフィックスが必須
*/
client: {
NEXT_PUBLIC_SITE_URL: z.string().url(),
},
/**
* Next.js 13.4.4 以降では experimental__runtimeEnv を使用
* クライアント変数のみ指定すれば OK
*/
experimental__runtimeEnv: {
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
},
/**
* 静的解析ツール(knip など)で環境変数が未設定の場合にバリデーションをスキップ
*/
skipValidation: process.env.SKIP_ENV_VALIDATION === "true",
});この設定により、以下のメリットが得られます:
- 型安全:
env.GOOGLE_CLIENT_IDのように型付きでアクセス可能 - バリデーション: アプリ起動時に環境変数の形式をチェック
- IDE 補完: 環境変数名の補完が効く
ステップ3: Better Auth の設定
src/lib/auth.ts を作成
t3-env の env オブジェクトを使用して環境変数にアクセスします:
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "@/db";
import { env } from "@/env";
export const auth = betterAuth({
// データベース設定
database: drizzleAdapter(db, {
provider: "sqlite", // Turso は SQLite 互換
}),
// ソーシャルプロバイダー設定
socialProviders: {
google: {
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
},
},
});
// 型のエクスポート
export type Auth = typeof auth;
export type Session = typeof auth.$Infer.Session;Note:
process.env.GOOGLE_CLIENT_ID!のような Non-null assertion を使用する代わりに、t3-env のenvオブジェクトを使用することで、環境変数が確実に存在することが保証されます。
ステップ4: スキーマの自動生成
Better Auth CLI を使用して、Drizzle 用のスキーマファイルを自動生成します。
スキーマファイルの生成
npx @better-auth/cli generateこのコマンドを実行すると、以下のようなプロンプトが表示されます:
? Where would you like to save the schema? › src/db/schema/auth.ts出力先を指定すると、Better Auth が必要とするテーブル定義(user、session、account、verification)が自動生成されます。
CLI オプション
# 出力先を指定
npx @better-auth/cli generate --output src/db/schema/auth.ts
# 確認プロンプトをスキップ
npx @better-auth/cli generate --yes
# 設定ファイルのパスを指定
npx @better-auth/cli generate --config src/lib/auth.ts生成されるスキーマの例
CLI により、以下のようなスキーマが自動生成されます:
// src/db/schema/auth.ts(自動生成)
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
export const user = sqliteTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: integer("email_verified", { mode: "boolean" }).notNull(),
image: text("image"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
});
export const session = sqliteTable("session", {
id: text("id").primaryKey(),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
token: text("token").notNull().unique(),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id),
});
export const account = sqliteTable("account", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: integer("access_token_expires_at", { mode: "timestamp" }),
refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: "timestamp" }),
scope: text("scope"),
password: text("password"),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
});
export const verification = sqliteTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
createdAt: integer("created_at", { mode: "timestamp" }),
updatedAt: integer("updated_at", { mode: "timestamp" }),
});スキーマのエクスポート
生成されたスキーマを src/db/schema/index.ts でエクスポートします:
export * from "./auth";
// 他のスキーマがあればここに追加ステップ5: データベース接続の更新
src/db/index.ts を更新
t3-env の env オブジェクトを使用します:
import { createClient } from "@libsql/client";
import { drizzle } from "drizzle-orm/libsql";
import { env } from "@/env";
import * as schema from "./schema";
const client = createClient({
url: env.TURSO_DATABASE_URL,
authToken: env.TURSO_AUTH_TOKEN,
});
export const db = drizzle(client, { schema });ステップ6: マイグレーションの実行
スキーマ生成後、Drizzle Kit でマイグレーションを実行します:
# マイグレーションファイルを生成
npx drizzle-kit generate
# マイグレーションを実行
npx drizzle-kit migrateまたは、package.json にスクリプトが定義されている場合:
npm run db:generate
npm run db:migrateステップ7: API ルートの作成
src/app/api/auth/[...all]/route.ts を作成
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "@/lib/auth";
export const { GET, POST } = toNextJsHandler(auth);この設定により、/api/auth/* パスで認証関連のエンドポイントが自動的に作成されます。
ステップ8: クライアントの設定
src/lib/auth-client.ts を作成
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient();
// 便利なエイリアスをエクスポート
export const { signIn, signOut, useSession } = authClient;Note: 認証サーバーとクライアントが同一ドメインで動作している場合、
baseURLは省略可能です。異なるドメインで動作している場合は、baseURL: "http://localhost:3000"のように指定してください。
ステップ9: ナビゲーションバーに認証ボタンを追加
ハイブリッドアプローチ
認証ボタンの実装には、サーバーサイドでセッションを取得し、クライアントコンポーネントに渡すハイブリッドアプローチを採用します。
このアプローチのメリット:
- 初回レンダリング時のローディング表示が不要
- SSRでセッション状態が確定した状態でHTMLを生成
- クライアントへの初期JavaScriptバンドルサイズ削減
認証ボタンコンポーネントを作成
src/components/auth-button.tsx:
"use client";
import { Button } from "@/components/ui/button";
import type { Session } from "@/lib/auth";
import { authClient } from "@/lib/auth-client";
type Props = {
session: Session | null;
};
export function AuthButton({ session }: Props) {
if (session) {
return (
<Button
variant="ghost"
onClick={() =>
authClient.signOut({
fetchOptions: {
onSuccess: () => {
window.location.reload();
},
},
})
}
>
ログアウト
</Button>
);
}
return (
<Button
variant="ghost"
onClick={() =>
authClient.signIn.social({
provider: "google",
callbackURL: "/",
})
}
>
ログイン
</Button>
);
}ポイント:
sessionを props として受け取る(サーバーで取得済み)useSessionフックを使用しない(ローディング状態が不要)Buttonコンポーネントで統一されたUI
signIn.social のオプション
await authClient.signIn.social({
provider: "google", // プロバイダー ID
callbackURL: "/", // 認証成功後のリダイレクト先
errorCallbackURL: "/error", // エラー時のリダイレクト先(オプション)
newUserCallbackURL: "/welcome", // 新規ユーザー登録時のリダイレクト先(オプション)
});ナビゲーションバーを更新(サーバーコンポーネント)
src/components/navbar.tsx をサーバーコンポーネントとして実装し、セッションを取得します:
import { Search } from "lucide-react";
import { headers } from "next/headers";
import Link from "next/link";
import { AuthButton } from "@/components/auth-button";
import { ThemeToggle } from "@/components/theme-toggle";
import { auth } from "@/lib/auth";
export async function Navbar() {
const session = await auth.api.getSession({
headers: await headers(),
});
return (
<nav className="border-b bg-background">
<div className="max-w-7xl mx-auto px-4 h-16 flex items-center justify-between">
<div className="flex items-center gap-6">
<Link
href="/"
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
>
{/* ロゴ */}
<span className="font-bold tracking-tight font-logo">linto</span>
</Link>
<Link
href="/articles"
className="text-sm font-medium hover:text-foreground/80 transition-colors"
>
Articles
</Link>
</div>
<div className="flex items-center gap-4">
<AuthButton session={session} />
<Link
href="/tags"
className="p-2 hover:bg-accent rounded-md transition-colors"
aria-label="Search by tags"
>
<Search size={18} aria-hidden="true" />
</Link>
<ThemeToggle />
</div>
</div>
</nav>
);
}ポイント:
async function Navbar()としてサーバーコンポーネントに変更auth.api.getSession({ headers: await headers() })でサーバーサイドでセッション取得AuthButtonにsessionを props として渡す
Google Cloud Console の設定
OAuth 同意画面の設定
- Google Cloud Console にアクセス
- 「APIとサービス」→「OAuth 同意画面」を選択
- ユーザーの種類を選択(外部 or 内部)
- アプリ情報を入力
- スコープで
email、profile、openidを追加
認証情報の作成
- 「APIとサービス」→「認証情報」を選択
- 「認証情報を作成」→「OAuth クライアント ID」
- アプリケーションの種類:「ウェブ アプリケーション」
- 承認済みの JavaScript 生成元を追加:
http://localhost:3000(開発用)https://your-domain.com(本番用)
- 承認済みのリダイレクト URI を追加:
http://localhost:3000/api/auth/callback/google(開発用)https://your-domain.com/api/auth/callback/google(本番用)
Important: リダイレクト URI は
BETTER_AUTH_URL+/api/auth/callback/googleの形式になります。
完成したプロジェクト構造
src/
├── app/
│ └── api/
│ └── auth/
│ └── [...all]/
│ └── route.ts # Better Auth API ハンドラー
├── components/
│ ├── navbar.tsx # ナビゲーションバー(AuthButton を追加)
│ └── auth-button.tsx # 認証ボタンコンポーネント
├── db/
│ ├── index.ts # DB 接続設定(env を使用)
│ └── schema/
│ ├── auth.ts # 認証スキーマ(CLI で自動生成)
│ └── index.ts # スキーマエクスポート
├── lib/
│ ├── auth.ts # Better Auth サーバー設定(env を使用)
│ └── auth-client.ts # Better Auth クライアント設定
└── env.ts # t3-env 環境変数定義セットアップ手順のまとめ
# 1. パッケージインストール
npm install better-auth
# 2. 環境変数を設定(.env.local)
# 3. src/env.ts に環境変数のスキーマを追加
# 4. src/lib/auth.ts を作成
# 5. スキーマを自動生成
npx @better-auth/cli generate --output src/db/schema/auth.ts
# 6. マイグレーションを実行
npm run db:generate
npm run db:migrate
# 7. API ルート、クライアント、UIコンポーネントを作成トラブルシューティング
環境変数のバリデーションエラー
t3-env を使用している場合、環境変数が不足しているとアプリ起動時にエラーが発生します:
❌ Invalid environment variables: { GOOGLE_CLIENT_ID: [ 'Required' ] }解決方法:
.env.localに必要な環境変数がすべて設定されているか確認- 環境変数の形式が正しいか確認(例:
BETTER_AUTH_SECRETは32文字以上)
「redirect_uri_mismatch」エラー
このエラーは、Google に送信されるリダイレクト URI と Google Cloud Console に登録された URI が一致しない場合に発生します。
解決方法:
.env.localにBETTER_AUTH_URLが正しく設定されているか確認:
# 開発環境
BETTER_AUTH_URL=http://localhost:3000
# 本番環境
BETTER_AUTH_URL=https://your-domain.com- Google Cloud Console のリダイレクト URI が正確か確認:
http://localhost:3000/api/auth/callback/google「Invalid redirect URI」エラー
Google Cloud Console で設定したリダイレクト URI が正確か確認してください。末尾のスラッシュの有無にも注意が必要です。
CLI でスキーマが生成されない
auth.ts ファイルが正しい場所にあるか確認してください。CLI はデフォルトで以下の場所を検索します:
./auth.ts./lib/auth.ts./utils/auth.ts./src/auth.ts./src/lib/auth.ts./src/utils/auth.ts
セッションが取得できない
BETTER_AUTH_SECRETが設定されているか確認(本番環境では必須)- Cookie が正しく設定されているか確認(DevTools → Application → Cookies)
BETTER_AUTH_URLが正しく設定されているか確認
CORS エラー
異なるドメイン間で通信する場合は、src/lib/auth.ts で trustedOrigins を設定:
import { env } from "@/env";
export const auth = betterAuth({
// ...
trustedOrigins: [
"http://localhost:3000",
env.NEXT_PUBLIC_SITE_URL,
],
});本番環境でエラーが発生する
BETTER_AUTH_SECRET が設定されていないと、本番環境ではエラーが発生します。t3-env を使用している場合、アプリ起動時にバリデーションエラーとして検出されます。
まとめ
Better Auth と t3-env を組み合わせることで、型安全で堅牢な Google 認証を実装できます。
主なポイント:
- t3-env で型安全な環境変数管理: バリデーションと型補完で設定ミスを防止
- CLI でスキーマ自動生成: 手動でスキーマを書く必要なし
- Drizzle 統合: ネイティブアダプターで型安全なデータベース操作
- ハイブリッドアプローチ: サーバーでセッション取得、クライアントで操作を実行
- SSR 対応: 初回レンダリングからセッション状態が確定