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.