BA
Better Auth•3mo ago
Aruthoth

How to set up the database with drizzleAdapter + D1 using Hono on Cloudflare Workers

Hey folks, what's up? I'm creating an API with authentication that runs on Cloudflare Workers. Initially, I configured Hono, Drizzle, and D1. But now I'm facing some errors when implementing authentication with better-auth. Briefly, I'd like to know if anyone has successfully implemented something similar with these technologies. Initially, I couldn't set up the database because the Cloudflare Workers environment only provides access to D1 through the context of a request (as far as I could understand from the Cloudflare documentation). I tried to work around the problem by creating a function to encapsulate the creation of the auth object and pass the D1 through the context.
// src/lib/auth.ts
import type { Context } from 'hono'

import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { openAPI } from 'better-auth/plugins'

import type { AppBindings } from '@/lib/types'

export function getAuth(c: Context<AppBindings>) {
return betterAuth({
plugins: [
openAPI(),
],
database: drizzleAdapter(c.env.DB, {
provider: 'sqlite',
usePlural: true,
}),
socialProviders: {
google: {
clientId: c.env.AUTH_GOOGLE_CLIENT_ID,
clientSecret: c.env.AUTH_GOOGLE_CLIENT_SECRET,
},
},
})
}
// src/lib/auth.ts
import type { Context } from 'hono'

import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { openAPI } from 'better-auth/plugins'

import type { AppBindings } from '@/lib/types'

export function getAuth(c: Context<AppBindings>) {
return betterAuth({
plugins: [
openAPI(),
],
database: drizzleAdapter(c.env.DB, {
provider: 'sqlite',
usePlural: true,
}),
socialProviders: {
google: {
clientId: c.env.AUTH_GOOGLE_CLIENT_ID,
clientSecret: c.env.AUTH_GOOGLE_CLIENT_SECRET,
},
},
})
}
However, it only "works" when I use this function directly, for example: getAuth().api.someFunction(). When I try to authenticate through created routes, I get the error "Cannot read properties of undefined (reading 'DB')". It seems that just encapsulating the creation of the auth object was a terrible idea because somehow better-auth (I didn't get to see the internal code of better-auth) expects and searches for the reference to this object by the name auth from the auth.ts file.
12 Replies
Unknown User
Unknown User•3mo ago
Message Not Public
Sign In & Join Server To View
bekacru
bekacru•3mo ago
bascailly what you're doing is fine to use at runtime but to use the cli this won't be possible. One thing you could do is mount another endpoint where you call to migrate the schema. And call the migration manually by doing
import { getMigrations } from "better-auth/db";

const { runMigrations, toBeAdded, toBeCreated } = await getMigrations(getAuth(ctx).options);

if (toBeAdded.length || toBeCreated.length) {
await runMigrations(); //this will add the required schema for D1
}
import { getMigrations } from "better-auth/db";

const { runMigrations, toBeAdded, toBeCreated } = await getMigrations(getAuth(ctx).options);

if (toBeAdded.length || toBeCreated.length) {
await runMigrations(); //this will add the required schema for D1
}
Aruthoth
AruthothOP•3mo ago
Interesting, I did some tests and ended up populating the database manually. I'll test it this way. Except for populating the database, I think I managed to get the authentication flow working. At least I managed to create a new user and retrieve their session using Google. Revisiting the code more carefully, I realized the database is being initialized incorrectly.
// new code
import { drizzle } from 'drizzle-orm/d1'

// code...

return betterAuth({
database: drizzleAdapter(drizzle(c.env.DB), { // add drizzle

// code...
// new code
import { drizzle } from 'drizzle-orm/d1'

// code...

return betterAuth({
database: drizzleAdapter(drizzle(c.env.DB), { // add drizzle

// code...
It was missing the step of creating the client with Drizzle first. database: drizzleAdapter(c.env.DB, { -> database: drizzleAdapter(drizzle(c.env.DB), { I also adjusted the middleware for the session
import { createMiddleware } from 'hono/factory'

import type { AppBindings } from '@/lib/types'

import { getAuth } from '@/lib/auth'

const userSession = createMiddleware<AppBindings>(async (c, next) => {
const auth = getAuth(c) // only change <------------------
const session = await auth.api.getSession({ headers: c.req.raw.headers })

if (!session) {
c.set('user', null)
c.set('session', null)
return next()
}

c.set('user', session.user)
c.set('session', session.session)
return next()
})

export default userSession
import { createMiddleware } from 'hono/factory'

import type { AppBindings } from '@/lib/types'

import { getAuth } from '@/lib/auth'

const userSession = createMiddleware<AppBindings>(async (c, next) => {
const auth = getAuth(c) // only change <------------------
const session = await auth.api.getSession({ headers: c.req.raw.headers })

if (!session) {
c.set('user', null)
c.set('session', null)
return next()
}

c.set('user', session.user)
c.set('session', session.session)
return next()
})

export default userSession
Take a look to see if you're initializing Drizzle incorrectly, just like I did 😅 If I initialize it incorrectly, like the first time I did, I only receive a 500 error.
Aruthoth
AruthothOP•3mo ago
No description
Aruthoth
AruthothOP•3mo ago
I forgot to mention, but I also changed this part
app.on(['GET', 'POST'], '/api/auth/**', (c) => getAuth(c).handler(c.req.raw))
app.on(['GET', 'POST'], '/api/auth/**', (c) => getAuth(c).handler(c.req.raw))
Unknown User
Unknown User•3mo ago
Message Not Public
Sign In & Join Server To View
Aruthoth
AruthothOP•3mo ago
Look, I'm not sure if there's a more elegant way to do this, but I just created a few types
import type { OpenAPIHono, RouteConfig, RouteHandler } from '@hono/zod-openapi'
import type { Env } from 'hono'

import type { getAuth } from '@/lib/auth'

type Auth = ReturnType<typeof getAuth>
type VariableUser = Auth['$Infer']['Session']['user'] | null
type VariableSession = Auth['$Infer']['Session']['session'] | null

export interface AppBindings extends Env {
Bindings: {
AUTH_URL: string
AUTH_SECRET: string
AUTH_GOOGLE_CLIENT_ID: string
AUTH_GOOGLE_CLIENT_SECRET: string
DB: D1Database
}
Variables: {
user: VariableUser
session: VariableSession
}
}

export type AppOpenAPI = OpenAPIHono<AppBindings>

export type AppRouteHandler<R extends RouteConfig> = RouteHandler<R, AppBindings>
import type { OpenAPIHono, RouteConfig, RouteHandler } from '@hono/zod-openapi'
import type { Env } from 'hono'

import type { getAuth } from '@/lib/auth'

type Auth = ReturnType<typeof getAuth>
type VariableUser = Auth['$Infer']['Session']['user'] | null
type VariableSession = Auth['$Infer']['Session']['session'] | null

export interface AppBindings extends Env {
Bindings: {
AUTH_URL: string
AUTH_SECRET: string
AUTH_GOOGLE_CLIENT_ID: string
AUTH_GOOGLE_CLIENT_SECRET: string
DB: D1Database
}
Variables: {
user: VariableUser
session: VariableSession
}
}

export type AppOpenAPI = OpenAPIHono<AppBindings>

export type AppRouteHandler<R extends RouteConfig> = RouteHandler<R, AppBindings>
Unknown User
Unknown User•2mo ago
Message Not Public
Sign In & Join Server To View
bekacru
bekacru•2mo ago
yeah baseURL should be inferred from a request, if it's not behind a proxy but curious what was the value you provided?
Unknown User
Unknown User•2mo ago
Message Not Public
Sign In & Join Server To View
whistlecube
whistlecube•3w ago
Do you create this middleware in a separate file, and then import it to your Hono app? How do you use it within Hono? nvm i got it Ok I’m still having trouble, I think the auth instance isn’t persistent across requests. I can sign-in just fine, and accessing the session works, but on the next request using the middleware, I get a null session. How are you protecting your other routes using the middleware?
Aruthoth
AruthothOP•2w ago
Well, I haven't "got to that part yet" I did some initial tests to see what the authentication would be like and left it to finish after I finished the main part of the application I'm using Astro to develop, my middleware is like this for now
import { auth } from '@/lib/auth'
import { defineMiddleware, sequence } from 'astro:middleware'

const setSession = defineMiddleware(async (c, next) => {
const isAuthed = await auth(c).api.getSession({
headers: c.request.headers,
})

if (isAuthed) {
c.locals.user = isAuthed.user
c.locals.session = isAuthed.session
} else {
c.locals.user = null
c.locals.session = null
}

return next()
})

const protectsRoutes = defineMiddleware(async (c, next) => {
const noSession = c.locals.user === null || c.locals.session === null

if (noSession && c.url.pathname !== '/') {
return next(
new Request('/login', {
headers: {
'x-redirect-to': c.url.pathname,
},
})
)
}

return next()
})

export const onRequest = sequence(setSession, protectsRoutes)
import { auth } from '@/lib/auth'
import { defineMiddleware, sequence } from 'astro:middleware'

const setSession = defineMiddleware(async (c, next) => {
const isAuthed = await auth(c).api.getSession({
headers: c.request.headers,
})

if (isAuthed) {
c.locals.user = isAuthed.user
c.locals.session = isAuthed.session
} else {
c.locals.user = null
c.locals.session = null
}

return next()
})

const protectsRoutes = defineMiddleware(async (c, next) => {
const noSession = c.locals.user === null || c.locals.session === null

if (noSession && c.url.pathname !== '/') {
return next(
new Request('/login', {
headers: {
'x-redirect-to': c.url.pathname,
},
})
)
}

return next()
})

export const onRequest = sequence(setSession, protectsRoutes)
I still need to see how this part still works in astro doing an if for each route I need to reach doesn't seem like an elegant way to solve it, but it's what worked in my test

Did you find this page helpful?