build

Sharing Better Auth Configuration Between Astro Runtime and CLI

Part of Villa Owner Trust System

The Villa Owner Trust System uses the same Better Auth configuration in two different execution environments: the running Astro application and the Better Auth CLI used to generate its Drizzle auth schema.

Both need to describe the same Better Auth setup, but they do not load it under the same conditions.

The Astro application can use its normal server environment configuration and database client. The Better Auth CLI runs separately and needs a configuration entry point that it can load without depending on the application’s Astro-specific environment setup.

The implementation therefore shares the Better Auth options while keeping the Astro runtime and CLI dependencies separate.

The configuration boundary

The auth setup is split across three files:

src/lib/auth/options.ts
    shared Better Auth options

src/lib/auth/server.ts
    Astro runtime Better Auth instance

better-auth.config.ts
    Better Auth CLI instance

The relationship is:

      ┌─────────────────────────┐
      │       options.ts        │
      │                         │
      │ shared Better Auth      │
      │ options                 │
      └────────────┬────────────┘

      ┌────────────┴────────────┐
      │                         │
      ▼                         ▼
server.ts              better-auth.config.ts
Astro runtime          Better Auth CLI
      │                         │
Astro env                 dotenv / process.env
app database             CLI database

options.ts defines the Better Auth configuration that should remain consistent between the two environments.

The two entry points provide the environment-specific dependencies needed to create a Better Auth instance.

Shared Better Auth options

src/lib/auth/options.ts contains the Better Auth options shared by the runtime application and CLI configuration:

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

type AuthOptions = Parameters<typeof betterAuth>[0];
type DrizzleDatabase = Parameters<typeof drizzleAdapter>[0];

type CreateAuthOptionsInput = {
  db: DrizzleDatabase;
  baseURL: string;
  secret: string;
};

export function createAuthOptions({
  db,
  baseURL,
  secret,
}: CreateAuthOptionsInput): AuthOptions {
  return {
    baseURL,
    secret,

    database: drizzleAdapter(db, {
      provider: "pg",
      usePlural: true,
    }),

    emailAndPassword: {
      enabled: true,
      disableSignUp: true,
      minPasswordLength: 12,
    },
  };
}

The factory does not load environment variables or create a database connection.

Instead, it receives the dependencies that vary between environments:

database
base URL
Better Auth secret

and returns the Better Auth options.

That keeps the shared authentication configuration independent of how either environment obtains those values.

The relevant TypeScript types are derived from Better Auth and the Drizzle adapter:

type AuthOptions = Parameters<typeof betterAuth>[0];
type DrizzleDatabase = Parameters<typeof drizzleAdapter>[0];

This keeps the factory checked against the types accepted by the installed Better Auth integration instead of maintaining separate local definitions for them.

Astro runtime configuration

src/lib/auth/server.ts creates the Better Auth instance used by the running Astro application:

import { betterAuth } from "better-auth";

import { env } from "@/lib/config/env";
import { db } from "@/lib/db/client";

import { createAuthOptions } from "./options";

export const auth = betterAuth(
  createAuthOptions({
    db,
    baseURL: env.app.baseUrl,
    secret: env.auth.secret,
  }),
);

This entry point belongs to the Astro runtime.

It uses the application’s existing environment configuration and database client, then passes those values into the shared Better Auth options.

The runtime path is:

Astro request

Better Auth handler

src/lib/auth/server.ts

createAuthOptions(...)

application database

Runtime environment access therefore stays behind the application’s normal Astro environment boundary instead of introducing separate process.env access into the runtime auth configuration.

Better Auth CLI configuration

Better Auth schema generation follows a different execution path.

The Better Auth CLI loads an auth configuration to determine the database schema required by the configured Better Auth setup.

better-auth.config.ts provides the CLI-specific entry point:

import "dotenv/config";

import { Pool } from "@neondatabase/serverless";
import { betterAuth } from "better-auth";
import { drizzle } from "drizzle-orm/neon-serverless";

import { createAuthOptions } from "./src/lib/auth/options";

function requiredEnv(name: string): string {
  const value = process.env[name];

  if (!value) {
    throw new Error(
      `${name} is required for Better Auth CLI schema generation.`,
    );
  }

  return value;
}

const pool = new Pool({
  connectionString: requiredEnv("DATABASE_URL"),
});

const db = drizzle({ client: pool });

export const auth = betterAuth(
  createAuthOptions({
    db,
    baseURL: requiredEnv("APP_BASE_URL"),
    secret: requiredEnv("AUTH_SECRET"),
  }),
);

export default auth;

This file does not import the application’s Astro environment configuration.

It loads its environment with dotenv, reads the required values through process.env, and creates the database client needed by the Better Auth CLI configuration.

Its dependency path is:

Better Auth CLI

better-auth.config.ts

createAuthOptions(...)

CLI environment + database

The Better Auth CLI can then generate the Drizzle auth schema from this configuration explicitly:

pnpm dlx auth@latest generate \
  --config ./better-auth.config.ts \
  --output ./src/lib/db/schema/auth.ts \
  --yes

Why the entry points are separate

The split came from an actual schema-generation failure.

The original Better Auth CLI command pointed directly at the Astro runtime auth module:

Better Auth CLI

src/lib/auth/server.ts

src/lib/config/env.ts

astro:env/server

That import path reached the application’s Astro server environment dependency:

import { getSecret } from "astro:env/server";

When the Better Auth CLI attempted to load that runtime auth module, schema generation failed with:

Cannot find module 'astro:env/server'

The failure exposed a dependency boundary in the auth setup.

server.ts contained both:

the Better Auth configuration

and

the Astro runtime dependencies used to instantiate it

The Better Auth CLI needed the same configuration, but it did not need to inherit those Astro runtime dependencies.

Extracting the shared options made it possible for the Astro application and Better Auth CLI to instantiate the same authentication configuration through different entry points.

Why the shared options matter

The alternative would be to duplicate the Better Auth options between server.ts and better-auth.config.ts.

That would avoid the import problem, but it would create two copies of configuration that are expected to remain equivalent.

For example, the runtime configuration could contain:

database: drizzleAdapter(db, {
  provider: "pg",
  usePlural: true,
}),

while a later change accidentally leaves usePlural: true out of the CLI configuration.

The same problem applies to authentication options such as:

emailAndPassword: {
  enabled: true,
  disableSignUp: true,
  minPasswordLength: 12,
},

A change made to only one copy would allow the runtime Better Auth setup and the configuration used for schema generation to drift apart.

The shared factory keeps those responsibilities explicit:

options.ts
    shared Better Auth options

server.ts
    Astro runtime dependencies

better-auth.config.ts
    Better Auth CLI dependencies

The two Better Auth instances differ where their execution environments differ, while the configuration that is intended to be shared remains in one place.

Result

The Villa Owner Trust System now has separate Astro runtime and Better Auth CLI entry points built from the same Better Auth options.

The Astro application can use its existing environment configuration and application database client. The Better Auth CLI can independently load the configuration it needs for Drizzle schema generation.

The important boundary is:

shared Better Auth configuration

shared execution environment

The configuration can be shared without forcing the Astro runtime and Better Auth CLI through the same dependency path.