家計簿アプリを作る #7:Better Auth のセットアップ + サインアップ実装
家計簿アプリ作成シリーズの第7回(前編)です。今回は Better Auth を使った認証の土台を作り、サインアップまでを実装します。
Better Auth とは
TypeScript 向けの認証ライブラリで、メール+パスワード認証や OAuth(Google・GitHub 等)をまとめて実装できます。Cloudflare D1 にも対応しています。
インストール
npm install better-auth
ファイル構成
src/
lib/server/
auth.ts # ランタイム用(D1)の認証設定
auth.cli.ts # CLI用(better-sqlite3)の認証設定
db/
auth-schema.ts # Better Auth のテーブルスキーマ
routes/
signup/
+page.server.ts
+page.svelte
app.d.ts # Locals の型定義
Step 1:Better Auth のスキーマを生成する
npx @better-auth/cli generate
ハマりポイント①:コマンド名が違う
npx better-auth generate を実行するとエラーになります。正しくは @better-auth/cli です。
# ❌
npx better-auth generate
# ✅
npx @better-auth/cli generate
ハマりポイント②:auth のエクスポート形式
CLI は auth という名前の変数か default export を期待します。createAuth() 関数形式だと以下のエラーが出ます。
[#better-auth]: Couldn't read your auth config.
Make sure to default export your auth instance or to export as a variable named auth.
CLI 用に auth.cli.ts を別ファイルとして作成します。
// src/lib/server/auth.cli.ts
import 'dotenv/config';
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
const sqlite = new Database(process.env.DATABASE_URL);
export const auth = betterAuth({
baseURL: process.env.BETTER_AUTH_URL,
database: drizzleAdapter(drizzle(sqlite), { provider: 'sqlite' }),
emailAndPassword: { enabled: true },
secret: process.env.BETTER_AUTH_SECRET
});
.env に必要な環境変数を追加します。
DATABASE_URL=local.db
BETTER_AUTH_URL=http://localhost:5173
BETTER_AUTH_SECRET=(openssl rand -base64 32 で生成)
CLI を実行すると auth-schema.ts が生成されます。
npx @better-auth/cli generate
✔ Do you want to generate the schema to ./auth-schema.ts? … yes
🚀 Schema was generated successfully!
Step 2:スキーマを Drizzle に取り込む
生成された auth-schema.ts を src/lib/server/db/ に移動します。
mv auth-schema.ts src/lib/server/db/auth-schema.ts
drizzle.config.ts のスキーマ指定をフォルダ全体に変更します。
export default defineConfig({
schema: './src/lib/server/db/*.ts', // ← * でまとめて読み込む
dialect: 'sqlite',
out: './migrations',
dbCredentials: { url: process.env.DATABASE_URL! },
});
マイグレーションを生成してローカル D1 に適用します。
npx drizzle-kit generate
npx wrangler d1 migrations apply kakeibo-app-db --local
Step 3:auth.ts を作成する(ランタイム用)
D1 を使うランタイム用の createAuth 関数を src/lib/server/auth.ts に作成します。
import { betterAuth } from 'better-auth';
import { drizzleAdapter } from 'better-auth/adapters/drizzle';
import { createDb } from '$lib/server/db';
import * as schema from '$lib/server/db/schema';
import * as authSchema from '$lib/server/db/auth-schema';
export function createAuth(d1: D1Database) {
const db = createDb(d1);
return betterAuth({
baseURL: process.env.BETTER_AUTH_URL,
database: drizzleAdapter(db, {
provider: 'sqlite',
schema: { ...schema, ...authSchema }
}),
emailAndPassword: { enabled: true },
secret: process.env.BETTER_AUTH_SECRET
});
}
ハマりポイント③:スキーマをアダプターに渡す
drizzleAdapter にスキーマを渡さないと以下のエラーが出ます。
[BetterAuthError]: The model "user" was not found in the schema object.
Please pass the schema directly to the adapter options.
schema と authSchema を両方スプレッドして渡します。
ハマりポイント④:better-sqlite3 を auth.ts に含めない
当初 CLI 用と ランタイム用を同じファイルにまとめていましたが、better-sqlite3 はネイティブ Node.js モジュールのため Vite 環境でインポートするとエラーになります。CLI 用は別ファイル(auth.cli.ts)に分離する必要があります。
Step 4:app.d.ts に型定義を追加する
import type { auth } from '$lib/server/auth.cli';
declare global {
namespace App {
interface Locals {
user: typeof auth.$Infer.Session.user | null;
session: typeof auth.$Infer.Session.session | null;
}
interface Platform {
env: {
DB: D1Database;
};
}
}
}
export {};
Step 5:サインアップページを作成する
src/routes/signup/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { createAuth } from '$lib/server/auth';
export const actions: Actions = {
signup: async ({ request, platform }) => {
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');
const name = formData.get('name');
if (!email || !password || !name) {
return fail(400, { error: 'メールアドレスとパスワードを名前を入力してください' });
}
const auth = createAuth(platform!.env.DB);
const response = await auth.api.signUpEmail({
body: {
email: String(email),
password: String(password),
name: String(name)
},
asResponse: true
});
if (!response.ok) {
return fail(400, { error: 'サインアップに失敗しました' });
}
redirect(303, '/login');
}
};
src/routes/signup/+page.svelte
<script lang="ts">
import { enhance } from "$app/forms";
let { form } = $props();
</script>
<form method="POST" action="?/signup" use:enhance>
<div>
<label for="name">名前:</label>
<input type="text" id="name" name="name" required />
</div>
<div>
<label for="email">メールアドレス:</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label for="password">パスワード:</label>
<input type="password" id="password" name="password" required />
</div>
<button type="submit">サインアップ</button>
</form>
{#if form?.error}
<p style="color: red">{form.error}</p>
{/if}
まとめ
| ポイント | 内容 |
|---|---|
| CLI コマンド | npx @better-auth/cli generate(npx better-auth generate ではない) |
| CLI 用設定 | auth という名前で export する必要がある |
| ファイル分離 | CLI 用(auth.cli.ts)とランタイム用(auth.ts)を別ファイルに |
| スキーマ | auth-schema.ts を Drizzle 管理フォルダに移動して取り込む |
| アダプター | schema を drizzleAdapter に渡さないとエラーになる |
次回はログイン・ログアウトを実装します。