@alchemy.run/better-auth

中文文档
2.0.0-beta.85发布于 2026/11/03Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 13.5万

Better Auth integration for Alchemy.

原始使用文档
文档目录12 节
  1. Cloudflare D1
  2. Neon
  3. Aurora (RDS Data API)
  4. Cloudflare Hyperdrive
  5. Postgres (generic TCP)
  6. MySQL (generic TCP)
  7. AWS Lambda
  8. SQLite (local development)
  9. Memory (tests)
  10. Drizzle
  11. Handling errors
  12. Migrations

@alchemy.run/better-auth

Better Auth for alchemy — an Effect-native wrapper plus a pluggable database layer per platform.

Follow the six-part tutorial to build browser sign-in, Effect HTTP API middleware, and GitHub authentication. See sign-in providers and database guides for other integrations.

import { BetterAuth } from "@alchemy.run/better-auth";
import { CloudflareD1 } from "@alchemy.run/better-auth/CloudflareD1";
import * as Cloudflare from "@alchemy.run/cloudflare";

export const AuthDb = Cloudflare.D1.Database("AuthDb");

export default class Api extends Cloudflare.Worker<Api>()(
  "Api",
  { main: import.meta.url, compatibility: { flags: ["nodejs_compat"] } },
  Effect.gen(function* () {
    const auth = yield* BetterAuth({
      basePath: "/auth",
      emailAndPassword: { enabled: true },
    });

    return {
      fetch: Effect.gen(function* () {
        const request = yield* HttpServerRequest;
        if (request.url.startsWith("/auth")) {
          return yield* auth.fetch; // the Better Auth HTTP surface
        }
        const session = yield* auth.getSession(); // null when anonymous
        return yield* HttpServerResponse.json({ user: session?.user ?? null });
      }),
    };
  }).pipe(Effect.provide(CloudflareD1(AuthDb))),
) {}

What you get:

  • yield* BetterAuth(options) — full Auth<Options> type inference: plugins you pass show up on auth.api, Session types flow from your options. No tags, no casts.
  • auth.api.* — every Better Auth endpoint as an Effect with a typed BetterAuthApiError failure (status, statusCode, body.code, and headers including the set-cookie headers better-call hides on a symbol). Non-APIError throws are defects.
  • auth.fetch — an alchemy HttpEffect serving the Better Auth routes; works on Cloudflare Workers and AWS Lambda unchanged.
  • auth.getSession() — typed session lookup from the ambient request (or explicit Headers); resolves null for anonymous.
  • auth.auth — the raw per-execution Auth instance as an escape hatch.
  • Automatic schema migrations at deploy — an internal alchemy Action applies Better Auth's schema (plugins included) during alchemy deploy, re-running only when the schema or target database changes. Opt out with migrate: false.
  • Managed secret — secret defaults to a stable auto-provisioned Alchemy.Random value bound into the host environment.

The database is a Layer you provide on the surrounding impl effect. Pick the optimal layer for your environment → database pair — HTTP/serverless drivers where they exist, TCP drivers as the fallback:

RuntimeDatabaseLayerTransport
Cloudflare WorkerD1CloudflareD1native binding
Cloudflare Worker / AWS LambdaNeonNeonserverless driver (WebSocket)
AWS LambdaAuroraAuroraDataApiRDS Data API (HTTPS, IAM)
Cloudflare Workerany TCP Postgres/MySQLCloudflareHyperdrivepooled TCP via Hyperdrive
anywhere with socketsany PostgresPostgrespg TCP
anywhere with socketsany MySQL (e.g. PlanetScale)MySQLmysql2 TCP
bun (dev/tests)local fileSQLitebun:sqlite
testsin-memoryMemory—
bring-your-ownyour Drizzle dbDrizzleyours

Every layer routes runtime access through alchemy's binding system — native bindings (D1, Hyperdrive, RDS Data API IAM grants) or host-environment Output bindings (connection strings) — never hand-wired env vars.

Cloudflare D1

Native D1 binding at runtime; migrations run over the D1 HTTP API at deploy (and against the local simulator under alchemy dev).

import { CloudflareD1 } from "@alchemy.run/better-auth/CloudflareD1";
import * as Cloudflare from "@alchemy.run/cloudflare";

export const AuthDb = Cloudflare.D1.Database("AuthDb");

Effect.gen(function* () {
  const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
  // ...
}).pipe(Effect.provide(CloudflareD1(AuthDb)));

Neon

Neon's serverless driver speaks WebSocket from the edge — no Hyperdrive, no pg, no TCP sockets. The optimal pairing for both Workers and Lambda (@neondatabase/serverless is an optional peer).

import { Neon as NeonDatabase } from "@alchemy.run/better-auth/Neon";
import * as Neon from "@alchemy.run/neon";

export const AuthDb = Neon.Project("AuthDb");

Effect.gen(function* () {
  const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
  // ...
}).pipe(
  Effect.provide(
    Layer.unwrap(Effect.map(AuthDb, (db) => NeonDatabase(db.connectionUri))),
  ),
);

Aurora (RDS Data API)

SQL over HTTPS with IAM auth — no VPC attachment for the Lambda, no drivers, no pools. Runtime access flows through the AWS.RDSData.* bindings (which grant rds-data:* + secretsmanager:GetSecretValue on the host); deploy-time migrations use the same Data API with the stack's credentials. kysely and @distilled.cloud/aws are optional peers.

import { AuroraDataApi } from "@alchemy.run/better-auth/AuroraDataApi";
import * as AWS from "@alchemy.run/aws";

export const Db = AWS.RDS.Aurora("AuthDb", {
  subnetIds: network.privateSubnetIds,
  securityGroupIds: [securityGroup.groupId],
  // the Data API is enabled by default
});

Effect.gen(function* () {
  const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
  // ...
}).pipe(Effect.provide(AuroraDataApi(Db, { database: "postgres" })));

Cloudflare Hyperdrive

Pooled Postgres (or MySQL) through a Hyperdrive connection. Hyperdrive's connection string only exists inside the Worker, so pass the origin URL for deploy-time migrations (or omit migrate to skip them).

import { CloudflareHyperdrive } from "@alchemy.run/better-auth/CloudflareHyperdrive";

const branch = yield* Neon.Branch("auth-db", { project });
const hyperdrive = yield* Cloudflare.Hyperdrive.Connection("auth-hd", {
  origin: branch.origin,
});

Effect.gen(function* () {
  const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
  // ...
}).pipe(
  Effect.provide(
    CloudflareHyperdrive(hyperdrive, { migrate: branch.connectionUri }),
  ),
);

Postgres (generic TCP)

The fallback for any Postgres you have a connection string for: PlanetScale Postgres (role.connectionUrl), Prisma Postgres (connection.databaseUrl), RDS inside a VPC, or a literal URL. One pg pool per execution, closed when the event settles. pg is an optional peer dependency. (Prefer Neon/AuroraDataApi/CloudflareHyperdrive when they match your pair.)

import { Postgres } from "@alchemy.run/better-auth/Postgres";

const project = yield* Neon.Project("AuthDb");

Effect.gen(function* () {
  const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
  // ...
}).pipe(Effect.provide(Postgres(project.connectionUri)));

Resource Outputs are bound into the host environment at deploy and read back at runtime; the same source drives deploy-time migrations (override with migrate:, disable with migrate: false).

MySQL (generic TCP)

Same shape as Postgres over mysql2 (optional peer): PlanetScale MySQL, Hyperdrive MySQL origins, RDS MySQL. PlanetScale requires TLS — pass it in the URL's ssl query parameter.

import { MySQL } from "@alchemy.run/better-auth/MySQL";

const password = yield* Planetscale.MySQLPassword("auth-db", { database, role: "admin" });
const url = `mysql://${password.username}:...@${password.host}/${database.name}?ssl=${encodeURIComponent('{"rejectUnauthorized":true}')}`;

Effect.gen(function* () {
  const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
  // ...
}).pipe(Effect.provide(MySQL(url)));

AWS Lambda

No dedicated host layer needed — auth.fetch is host-portable. Serve it from a Lambda function URL with the database layer matching your target: AuroraDataApi for Aurora, Neon for Neon, or generic Postgres/MySQL (TCP drivers on Lambda need build: { install: ["pg"] } so the dynamically-imported driver ships with an npm layout).

import * as Lambda from "@alchemy.run/aws/Lambda";
import { Neon as NeonDatabase } from "@alchemy.run/better-auth/Neon";

export const AuthDb = Neon.Project("AuthDb");

export class AuthFunction extends Lambda.Function<Lambda.Function>()("AuthFunction") {}

export default AuthFunction.make(
  // scrypt password hashing needs headroom over the 128 MB default
  { main, url: true, memorySize: 512, timeout: Duration.seconds(30) },
  Effect.gen(function* () {
    const auth = yield* BetterAuth({
      basePath: "/auth",
      emailAndPassword: { enabled: true },
    });
    return { fetch: /* route /auth/* to auth.fetch */ };
  }).pipe(
    Effect.provide(
      Layer.unwrap(Effect.map(AuthDb, (db) => NeonDatabase(db.connectionUri))),
    ),
  ),
);

SQLite (local development)

bun:sqlite-backed layer for development and tests on the bun runtime.

import { SQLite } from "@alchemy.run/better-auth/SQLite";

Effect.gen(function* () {
  const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
  // ...
}).pipe(Effect.provide(SQLite(".alchemy/auth.sqlite")));

Memory (tests)

In-memory adapter; nothing persisted, no migrations needed.

import { Memory } from "@alchemy.run/better-auth";

Effect.gen(function* () {
  const auth = yield* BetterAuth({
    emailAndPassword: { enabled: true },
    secret: "test-secret",
  });
  // ...
}).pipe(Effect.provide(Memory()));

Drizzle

Bring your own Drizzle database via Better Auth's Relations v2 drizzleAdapter (optional peers drizzle-orm and @better-auth/drizzle-adapter). Schema is yours — there is no automatic migration for this layer. Configure the CLI's auth instance with @better-auth/drizzle-adapter/relations-v2, then run npx [email protected] generate --config ./auth.cli.ts. Passing --adapter drizzle selects the legacy generator instead of the configured adapter.

import { Drizzle } from "@alchemy.run/better-auth/Drizzle";
import { drizzle } from "drizzle-orm/node-postgres";
import * as schema from "./auth-schema.ts";
import { relations } from "./app-schema.ts";

const db = drizzle({
  client: pool,
  relations: { ...relations, ...schema.authRelations },
});

Effect.gen(function* () {
  const auth = yield* BetterAuth({ emailAndPassword: { enabled: true } });
  // ...
}).pipe(Effect.provide(Drizzle(db, { provider: "pg", schema })));

Handling errors

const result = yield* auth.api
  .signInEmail({ body: { email, password } })
  .pipe(
    Effect.catchTag("BetterAuthApiError", (error) =>
      error.statusCode === 401
        ? Effect.succeed(null)         // wrong credentials
        : Effect.fail(error),
    ),
  );

error.toResponse() renders the failure exactly as Better Auth would have (status + JSON body + merged headers, set-cookie included) for pass-through handlers.

Migrations

SQL layers with migration support apply schema changes during alchemy deploy via an internal alchemy Action:

  • runs only at apply (never at plan, never inside the deployed runtime — the migration code is dead-code-eliminated from bundles),
  • re-runs only when the auth schema (plugins, additional fields, indexes) or the target database changes,
  • applies missing tables, columns, and indexes through Better Auth's migrator; unsafe schema changes surface as BetterAuthMigrationError.

For a direct 1.6 → 1.7.5+ upgrade, the core account schema is unchanged: no issuer column or identity backfill is needed. Keep the same auth secret and upgrade Better Auth's synchronized packages together. Microsoft identifiers, legacy OIDC/MCP clients, SCIM, and device codes can require manual preparation; Alchemy does not rewrite identities or convert plugin data. See the upgrade guide before deploying an existing database.

Opt out with migrate: false and manage the schema yourself (npx [email protected] generate). Multiple BetterAuth instances in one stack: give each a distinct id to disambiguate the secret + migration resources.

版本与发布记录

@alchemy.run/better-auth 共发布 90 个版本,首次发布于 2026/04/10;过去 12 个月发布了 90 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.0.0-beta.85latest2026/11/033 天前
2.0.0-beta.842026/10/2413 天前
2.0.0-beta.832026/10/1324 天前
2.0.0-beta.822026/10/1324 天前
2.0.0-beta.812026/10/061 个月前
2.0.0-beta.802026/10/021 个月前

其他发布通道:beta-pkg-setup-rc → 2.0.0-beta.pkg-setup-rc,pipeline-v2-test → 2.0.0-pipeline-v2-test,cli-shim-1 → 2.0.0-cli-shim.1,cli-bundle-fix-1 → 2.0.0-cli-bundle-fix.1,fix-redacted → 2.0.0-fix-redacted,next → 2.0.0-beta.72。

关于 @alchemy.run/better-auth 的常见问题

@alchemy.run/better-auth 是什么?

Better Auth integration for Alchemy.

如何安装 @alchemy.run/better-auth?

在项目目录运行 npm install @alchemy.run/better-auth,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:effect(^4.1.0)、alchemy(2.0.0-beta.85)、better-auth(^1.7.5)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@alchemy.run/better-auth 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@alchemy.run/better-auth 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@alchemy.run/better-auth 可以商用吗?

@alchemy.run/better-auth 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@alchemy.run/better-auth 的下载量有多少?

根据 npm 官方下载统计,@alchemy.run/better-auth 在 2026/10/06 – 2026/11/04 这 30 天内被下载 135,215 次,日均约 4,507 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@alchemy.run/better-auth 还在维护吗?

最新版本 2.0.0-beta.85 发布于 2026/11/03(3 天前);至今共发布 90 个版本,其中过去 12 个月发布了 90 个;首次发布于 2026/04/10。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。