## Problem

MySQL apps cannot use the documented composed `AuthPersistence.make(auth).managed(...)` or `.map(...)` path. `@yielded/auth-persistence` rejects the `mysql` dialect, and `/Mysql2` exports the explicit factories but no `AuthPersistence` facade. Apps that want the same managed/mapped API as PostgreSQL/SQLite must hand-wire the persistence graph or carry a fork.

This is a feature request, not a regression: the docs currently say MySQL uses explicit persistence services. The request is to make the composed path available for MySQL too.

## Evidence

Verified against upstream `main` (`bc4df04`) and the published `0.1.0-beta.47` artifacts.

`@yielded/auth-persistence-drizzle/Mysql2` exports 52 symbols; `/Postgres` and `/SqliteBun` export 54. The missing two are `AuthPersistence` and `OAuthProxyPersistence`. Published `dist/Mysql2.mjs` has no `AuthPersistence` reference.

```ts
import { AuthPersistence } from "@yielded/auth-persistence-drizzle/Mysql2";
// TS2305: Module '"@yielded/auth-persistence-drizzle/Mysql2"'
// has no exported member 'AuthPersistence'
```

The composed Drizzle layer only recognises `pg` and `sqlite`:

```ts
// packages/auth-persistence/src/internal/persistence.ts
const dialect = client.onDialectOrElse({
  pg: () => "pg",
  sqlite: () => "sqlite",
  orElse: () => undefined,
});
```

So this composition fails:

```ts
export const Persistence = AuthPersistence.make(AppAuth);
export const storage = Persistence.managed({
  subjects: { table: users, id: "id", status: "disabled", ... },
  prefix: "auth",
});
```

There are **two** gates, not one:

- `packages/auth-persistence/src/internal/persistence.ts` for the Drizzle composed layer.
- `packages/auth-persistence/src/internal/sql-persistence.ts` for the direct Effect SQL facade.

Both need to recognise MySQL.

### There is no public escape hatch today

`createPersistence` and `Backend` are exported from `@yielded/auth-persistence/Adapter`, but the dialect check is hardcoded inside `createPersistence`, so a custom MySQL backend still fails. A Drizzle MySQL backend also needs `makeDrizzleSqlTables` and `nativeDatabase`, which are not exported from any public entrypoint.

The non-Drizzle path is not merely gate-blocked either: it emits ANSI double-quoted identifiers (`"name"`), types `Dialect` as only `"pg" | "sqlite"`, and special-cases boolean encoding only for SQLite. Even with `sql_mode=ANSI_QUOTES`, it still needs MySQL identifier/boolean support.

### Affected versions

The missing export is present in every published `@yielded/auth-persistence-drizzle` beta we checked, starting at `0.1.0-beta.0`. `@beta` is currently `0.1.0-beta.47`; upstream `main` is two commits newer and does not add it. The npm `latest` tags point at older betas.

The only active MySQL upstream work is #292, which adds MySQL/MariaDB to `OAuthServerPersistence`. That is a different surface. PR #195 explicitly left MySQL on the explicit persistence APIs; the storage docs also say composed `AuthPersistence` is available on PostgreSQL, PGlite, libSQL, SQLite, and D1, while “MySQL and Durable Object SQLite expose explicit persistence services.”

## Impact

This affects any MySQL/MariaDB/PlanetScale/Hyperdrive MySQL app that wants the managed/mapped composed API. It blocks all strategies on that path, not only password sign-in and stateful sessions.

The alternatives today are:

1. Hand-wire the explicit MySQL factories. For password sign-in + stateful sessions + registration this is roughly 7 tables / ~52 columns and ~9 factories/coordinators, plus typed mappings and your own DDL, transactions, and migrations. No MySQL example exists in the repo.
2. Use `Persistence.map` from `@yielded/auth-persistence` with `@effect/sql-mysql2`. Not viable because of the second gate and the SQL/identifier dialect assumptions above.
3. Carry local patches. This is the only way to get the same composed API today, but it forks a security package and is not a long-term answer.

I have a working patch and a real-world composition in https://github.com/oscartbeaumont/mattraxe/pull/3 (Mattrax on Cloudflare Workers + Hyperdrive MySQL). It currently typechecks, builds, and generates MySQL DDL. It has **not** been run against MySQL/MariaDB or Hyperdrive; runtime, concurrency, and DDL validation are still open. Happy to turn the patch into an upstream PR.

## What the composed MySQL adapter needs

- Export `AuthPersistence` from `/Mysql2`, mirroring `/Postgres`.
- Recognise `mysql` in both composed dialect gates.
- Add a MySQL backend for managed/mapped tables.
- Add a commit-owner hook. `Backend` only exposes `batch` today, so the composed path falls back to `sql.withTransaction` and MySQL’s default isolation. The explicit MySQL adapters use `mysqlNativeCommit` (reserved connection, one-shot `READ COMMITTED`). `REPEATABLE READ` also changes gap-locking behaviour, so this is a concurrency decision, not just snapshot freshness.
- Validate `validateStorageBatch("mysql", ...)` against a live MySQL 8 schema.

## MySQL-specific design constraints

These are why this is more than “add a backend”:

- **Index length.** With bounded `VARCHAR` keys, the widest current unique key is `oauthConnectedGrants(moduleId, subjectId, profileKey, identityKey)` at 4 × 191 chars × 4 bytes = **3056 bytes**, only 16 bytes under InnoDB’s 3072-byte limit for `utf8mb4`. `identifiers(subjectId, namespace, value)` is 2808 bytes. There is essentially no headroom.
- **Index/table identifier limit.** MySQL rejects identifiers over 64 characters. `managed` allows prefixes up to 101 characters and names tables `${prefix}_${role}`, then adds `_key_${i}` to index names, so a long prefix can produce valid-looking config that MySQL rejects.
- **Column sizing.** The current patch uses a name-based rule (`value` = 320, other keyed text = 191). That sizes `emailCredentials.identifierValue` at 191, but an email address can be 320 characters. It needs column-specific sizing, not key-name guessing.
- **Large text.** Non-keyed PostgreSQL `text` maps to MySQL `TEXT` at 65,535 bytes. `sessions.record`, `pending.snapshot`, and OAuth snapshots are unbounded on PostgreSQL/SQLite; they need `MEDIUMTEXT`/`LONGTEXT` or a documented cap.
- **Collation.** MySQL’s default `utf8mb4` collation is commonly case-insensitive and can be PAD SPACE. PostgreSQL/SQLite are byte-exact. Unique indexes need an explicit binary collation (as #292 chose for its tables) or two distinct app values can collide.
- **Timestamps.** Generated integer columns should become `BIGINT` epoch milliseconds, not `DATETIME`/`TIMESTAMP`. Application timestamps can use `DATETIME(3)` with UTC handling if they need date semantics.
- **Failure timing.** `managed(...)` and `layer` construction succeed; the dialect gate fires inside the lazily built persistence services on the first operation. Callers see the relevant `*Unavailable` error plus a `persistence-initialization` diagnostic, not the raw `PersistenceConfigurationError`.

## Related

- #292 — MySQL/MariaDB support for `OAuthServerPersistence` (different surface).
- #195 — explicitly kept MySQL on the explicit persistence APIs.
- Storage docs: `AuthPersistence` available on PostgreSQL/PGlite/libSQL/SQLite/D1; MySQL uses explicit services.

<sub>Generated on _MrBeast_ using _opencode-go/deepseek-v4.1-flash_ in _ses_e40ce88e1ffeF7Lm7SQ9QEOKIr_</sub>
