家計簿アプリを作る #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.tssrc/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.

schemaauthSchema を両方スプレッドして渡します。

ハマりポイント④: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 generatenpx better-auth generate ではない)
CLI 用設定 auth という名前で export する必要がある
ファイル分離 CLI 用(auth.cli.ts)とランタイム用(auth.ts)を別ファイルに
スキーマ auth-schema.ts を Drizzle 管理フォルダに移動して取り込む
アダプター schemadrizzleAdapter に渡さないとエラーになる

次回はログイン・ログアウトを実装します。

← トップページに戻る