Add better-auth to drizzle / D1 setup

For https://github.com/yusukebe/hono-vite-react-stack

@martinklepsch

Adding Authentication to the Project

Step 1: Install Dependencies

npm install better-auth

Step 2: Configure Database Schema

Add authentication tables to your schema file (src/lib/schema.ts):

  • users - Store user information
  • accounts - Manage login methods and credentials
  • sessions - Track active login sessions
  • verifications - Handle email verification tokens
import { sql } from 'drizzle-orm';
import { text, integer, sqliteTable } from 'drizzle-orm/sqlite-core';

export const users = sqliteTable("user", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull(),
  emailVerified: integer("email_verified", { mode: 'boolean' }).notNull(),
  image: text("image"),
  createdAt: integer("created_at", { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
  updatedAt: integer("updated_at", { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
});

export const accounts = sqliteTable("account", {
  id: text("id").primaryKey(),
  userId: text("user_id").notNull().references(() => users.id),
  accountId: text("account_id").notNull(),
  providerId: text("provider_id").notNull(),
  accessToken: text("access_token"),
  refreshToken: text("refresh_token"),
  accessTokenExpiresAt: integer("access_token_expires_at", { mode: 'timestamp' }),
  refreshTokenExpiresAt: integer("refresh_token_expires_at", { mode: 'timestamp' }),
  scope: text("scope"),
  idToken: text("id_token"),
  password: text("password"),
  createdAt: integer("created_at", { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
  updatedAt: integer("updated_at", { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
});

export const sessions = sqliteTable("session", {
  id: text("id").primaryKey(),
  userId: text("user_id").notNull().references(() => users.id),
  token: text("token").notNull(),
  expiresAt: integer("expires_at", { mode: 'timestamp' }).notNull(),
  ipAddress: text("ip_address"),
  userAgent: text("user_agent"),
  createdAt: integer("created_at", { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
  updatedAt: integer("updated_at", { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
});

export const verifications = 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' }).notNull().default(sql`CURRENT_TIMESTAMP`),
  updatedAt: integer("updated_at", { mode: 'timestamp' }).notNull().default(sql`CURRENT_TIMESTAMP`),
});

Step 3: Set Up Better Auth

The auth instance is created based on env.

  1. Create src/lib/auth.ts to configure the auth provider:

    import { betterAuth } from "better-auth";
    import { drizzleAdapter } from "better-auth/adapters/drizzle";
    import { Env, getDb } from "@/server/db";
    import { users, accounts, sessions, verifications } from "@/lib/schema";
    
    export const getAuth = (env: Env) => {
      return betterAuth({
        database: drizzleAdapter(getDb(env), {
          provider: "sqlite",
          schema: {
            user: users,
            account: accounts,
            session: sessions,
            verification: verifications
          }
        }),
        emailAndPassword: {
          enabled: true,
        },
        // Uncomment to enable social login
        // socialProviders: {
        //   github: {
        //     clientId: process.env.GITHUB_CLIENT_ID as string,
        //     clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
        //   },
        // }
      });
    };
    
  2. Create authentication middleware (src/server/middleware.ts):

    import { getAuth } from "@/lib/auth";
    
    export const authMiddleware = async (c, next) => {
      const auth = getAuth(c.env);
      const session = await auth.api.getSession(c.req.raw);
    
      if (!session && c.req.method === "POST") {
        return c.json({ error: "Unauthorized" }, 401);
      }
    
      c.set("session", session);
      await next();
    };
    
  3. Configure auth routes in main server file (src/server/index.tsx):

    app.on(["POST", "GET"], "/api/auth/*", async (c) => {
      const auth = getAuth(c.env);
      return auth.handler(c.req.raw);
    });
    
  4. Protect routes with middleware:

    app.use("/api/listings", authMiddleware);
    

Step 4: Set Up Client-Side Auth Utilities

Create src/lib/auth-client.ts:

import { createAuthClient } from "better-auth/react";

export const authClient = createAuthClient({});

export const {
  signIn,
  signUp,
  signOut,
  useSession
} = authClient;

Step 5: Implement Auth UI Components

  1. Create login and signup forms using the exported functions
  2. Add authentication state to your app layout
  3. Implement protected routes for authenticated content

Step 6: Update Environment Variables

Add any required auth-related environment variables (for social providers, etc.)

Add this context to your project via the ctxs command line integration: