How to use better-auth CLI in a Cloudflare workers + Cloudflare D1 + react-router + drizzle project?

I created a React Router 7 project using the Cloudflare workers template: pnpm create cloudflare@latest my-react-router-app --framework=react-router . I added D1 bindings and drizzle to this project. The Problem: I am struggling to find a pattern where I can somehow export auth in a way the CLI can pick up. The crux is that betterAuth() requires the db instance as a function argument. In Cloudlfare workers, however, the db instance is only available in the fetch() function. So logically the only spot I can instance the drizzle ORM db instance, and thus the betterAuth instance, is within the fetch function scope. The question is: is there a work around to this? I can't be the first to run into this little puzzle. Let me know any ideas! Note: I am aware of a way to import the env globally by import { env } from "cloudflare:workers";, however this package is only available in a Cloudflare or Miniflare environment. Ref: https://developers.cloudflare.com/changelog/2025-03-17-importable-env/
import { createRequestHandler } from "react-router";
import { drizzle, type DrizzleD1Database } from "drizzle-orm/d1";
import { betterAuth } from "better-auth";
import * as schema from '../database/schema'
import { drizzleAdapter } from "better-auth/adapters/drizzle";

declare module "react-router" {
export interface AppLoadContext {
db: DrizzleD1Database<typeof schema>,
cloudflare: {
env: Env;
ctx: ExecutionContext;
};
}
}

const requestHandler = createRequestHandler(
() => import("virtual:react-router/server-build"),
import.meta.env.MODE
);

export default {
async fetch(request, env, ctx) {
const db = drizzle(env.DB, { schema });
// Better Auth CLI cannot reach this:
const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "sqlite",
})
});
return requestHandler(request, {
db,
cloudflare: { env, ctx },
});
},
} satisfies ExportedHandler<Env>;
import { createRequestHandler } from "react-router";
import { drizzle, type DrizzleD1Database } from "drizzle-orm/d1";
import { betterAuth } from "better-auth";
import * as schema from '../database/schema'
import { drizzleAdapter } from "better-auth/adapters/drizzle";

declare module "react-router" {
export interface AppLoadContext {
db: DrizzleD1Database<typeof schema>,
cloudflare: {
env: Env;
ctx: ExecutionContext;
};
}
}

const requestHandler = createRequestHandler(
() => import("virtual:react-router/server-build"),
import.meta.env.MODE
);

export default {
async fetch(request, env, ctx) {
const db = drizzle(env.DB, { schema });
// Better Auth CLI cannot reach this:
const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "sqlite",
})
});
return requestHandler(request, {
db,
cloudflare: { env, ctx },
});
},
} satisfies ExportedHandler<Env>;
Solution:
I recommend making a fake auth file and a real auth file. The fake one is just your better-auth config except for any fields which require ENV vars. This file will be the one you would then use for the better-auth cli to read. The real one would be the one which includes all needed ENV variables for Better Auth to function....
Jump to solution
7 Replies
Solution
Ping
Ping5mo ago
I recommend making a fake auth file and a real auth file. The fake one is just your better-auth config except for any fields which require ENV vars. This file will be the one you would then use for the better-auth cli to read. The real one would be the one which includes all needed ENV variables for Better Auth to function. Tip: Make a config which you can export and use between the both of the real & fake auth files, to keep things consistent - and of course, this excludes the ENV parts in this config.
true
trueOP5mo ago
Gochya. Thanks for the response! I think that is a fine workaround. I simply did this to generate my tables:
// lib/auth.ts

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { drizzle } from "drizzle-orm/libsql";

const mockDb = drizzle("file:mock.db");

export const auth = betterAuth({
database: drizzleAdapter(mockDb, {
provider: "sqlite",
}),
});
// lib/auth.ts

import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { drizzle } from "drizzle-orm/libsql";

const mockDb = drizzle("file:mock.db");

export const auth = betterAuth({
database: drizzleAdapter(mockDb, {
provider: "sqlite",
}),
});
Raz
Raz5mo ago
I have this issue as well -- is there any chance to provide 1st class support for this, or at least dedicated docs?
EisenbergEffect
EisenbergEffect5mo ago
I'm also hitting this issue. I thought I'd get this thing setup real fast today...and then completely hit a roadblock because I can't scaffold the database. @Ping I'm not seeing how your recommendation would work, particularly since the betterAuth configuration requires a database, which can only come from the env in cloudflare. How should that be configured? Ok, to get the DB setup. I had to do what @true said above as a first step. That enabled me to get drizzle to create a schema file. I then had to use drizzle-kit with that schema file to create the SQL. Finally, I had to manually connect to the local cloudflare db and execute the sql container in that file. That gets the DB setup. It's not great though, because I have no desire to use drizzle. But, right now this seems the only way to get some SQL to actually use in setting up the DB. I'm on to the next in a string of problems now. When I try to do a POST /api/auth/sign-up/email I get a 422 UNPROCESSABLE_ENTITY error. So, no idea what's going on there. I'm using code just like the docs and have no fancy backend stuff. I'm just handing it straight to the better auth handler. I resolve this next set of issues. It turns out the Drizzle was snake_casing the properties, but Keysely was sending camelCase. Drizzle apparent has no way to not snake_case and Keysely has no way to snake_case. So, to fix this issue, I had to manually update the DB schema generated by Drizzle to use pascalCase column names. Then, once that was used to set up the DB, everything worked. NOTE: See the next message for a better (not silly) way to handle this, which occurred to me once I stepped away from the problem. Ok, I am silly. The simpler solution is to just use better-sqlite3 directly. For some reason I didn't realize this was an option. Didn't have my thinking cap on I guess. So, the solution I ended up with is having two auth configuration files. One is for the runtime setup, which uses the D1Dialect. No changes there. Then I have a second setup that changes the database property. I import Database from "better-sqlite3" directly and then set database: new Database("database.sqlite") in the config. This serves as a dummy database. I then run something like npx @better-auth/cli@latest generate --config ./build/auth.ts --output ./migrations/schema.sql --y to injest my custom build-time config and output the generated schema. Finally, using SQLLiteStudio, I open the local Cloudflare database, located at .wrangler/state/v3/d1. From the DB context menu, I can select "Execute SQL from file". I then select my schema and run it. Voila, the DB is now setup for local development with Cloudflare. Hopefully this helps folks in the future.
bekacru
bekacru5mo ago
Btw here you don't need to run generate migrate is enough. Generate is only required if you want to run migration on your own
EisenbergEffect
EisenbergEffect5mo ago
Well, I'll need to run it on my own too. Thanks!
Julian
Julian4mo ago
you have example code for your solution? i do not quite get it what you have done...

Did you find this page helpful?