[Feature]: Make supportsArrays configurable in DrizzleAdapterConfig to avoid double serialization when querying directly with Drizzle #2908

Open
opened 2026-03-13 10:27:11 -05:00 by GiteaMirror · 0 comments
Owner

Originally created by @cnfatal on GitHub (Feb 23, 2026).

Description

When using the Drizzle adapter with provider: "sqlite" (or "mysql"), string[] fields (such as redirect_uris, grant_types, response_types in the OAuth provider plugin) are double-serialized in the database.

This does not cause issues when accessing data through better-auth's API — the adapter's transformOutput reverses the double serialization on read, so you get correct arrays. However, if you query the database directly with Drizzle (e.g. db.select().from(oauthClient) for an admin panel), the data appears corrupted because Drizzle only does one JSON.parse, leaving you with a JSON string instead of an array.

Why This Happens

The CLI schema generator and the runtime adapter handle JSON serialization independently, causing the data to be serialized twice:

1. CLI generator (packages/cli/src/generators/drizzle.ts):

"string[]": {
    sqlite: `text('${name}', { mode: "json" })`,  // Drizzle auto-serializes
    pg: `text('${name}').array()`,
    mysql: `text('${name}', { mode: "json" })`,    // Drizzle auto-serializes
},

The generated schema uses text(..., { mode: "json" }), which means Drizzle automatically calls JSON.stringify() on write and JSON.parse() on read.

2. Runtime adapter (packages/drizzle-adapter/src/drizzle-adapter.ts#L628):

supportsArrays: config.provider === "pg" ? true : false,

When supportsArrays is false (SQLite/MySQL), the adapter factory (packages/core/src/db/adapter/factory.ts#L252-L257) also calls JSON.stringify() before passing data to Drizzle:

if (config.supportsArrays === false && Array.isArray(newValue)) {
    newValue = JSON.stringify(newValue);
}

What's Stored in the Database

Step Value
Input ["https://example.com/callback"]
Adapter transformInput (supportsArrays: false) '["https://example.com/callback"]' (1st stringify)
Drizzle text({ mode: "json" }) on write '"[\"https://example.com/callback\"]"' (2nd stringify)
Stored in DB "[\"https://example.com/callback\"]" — a JSON string, not a JSON array
  • Reading through better-auth API: Drizzle parses once → adapter parses again → correct array. No issue.
  • Reading directly with Drizzle (e.g. db.select().from(oauthClient)): Drizzle parses once → returns '["https://example.com/callback"]' (a string, not an array).

Use Case

It's common to query better-auth managed tables directly with Drizzle for admin dashboards, analytics, or custom queries that better-auth's API doesn't cover. The double-serialized data makes this unnecessarily difficult — developers need to add an extra JSON.parse() step for every string[] field.

Proposal

Make supportsArrays configurable in DrizzleAdapterConfig, so users whose Drizzle schema already handles serialization via mode: "json" can let Drizzle handle it instead:

export interface DrizzleAdapterConfig {
    // ... existing fields ...

    /**
     * Whether the database natively supports array columns.
     *
     * When `false` (default for sqlite/mysql), the adapter will
     * JSON.stringify array values before writing and JSON.parse
     * after reading. Set to `true` if your Drizzle schema already
     * handles serialization (e.g. via `text(..., { mode: "json" })`).
     *
     * @default `true` for "pg", `false` for "sqlite" and "mysql"
     */
    supportsArrays?: boolean;
}

And wire it through in the adapter:

// drizzle-adapter.ts
supportsArrays: config.supportsArrays ?? (config.provider === "pg" ? true : false),

This is a non-breaking change — existing behavior is preserved by default, but users can set supportsArrays: true to let Drizzle handle serialization when the schema is set up for it. This also keeps the database content clean and consistent for direct queries.

Environment

  • better-auth: v1.4.x (latest)
  • @better-auth/oauth-provider: latest
  • drizzle-orm: 0.45.x
  • Database: SQLite (D1) / also affects MySQL
Originally created by @cnfatal on GitHub (Feb 23, 2026). ## Description When using the Drizzle adapter with `provider: "sqlite"` (or `"mysql"`), `string[]` fields (such as `redirect_uris`, `grant_types`, `response_types` in the OAuth provider plugin) are double-serialized in the database. **This does not cause issues when accessing data through better-auth's API** — the adapter's `transformOutput` reverses the double serialization on read, so you get correct arrays. However, if you **query the database directly with Drizzle** (e.g. `db.select().from(oauthClient)` for an admin panel), the data appears corrupted because Drizzle only does one `JSON.parse`, leaving you with a JSON string instead of an array. ## Why This Happens The CLI schema generator and the runtime adapter handle JSON serialization **independently**, causing the data to be serialized twice: **1. CLI generator** (`packages/cli/src/generators/drizzle.ts`): ```typescript "string[]": { sqlite: `text('${name}', { mode: "json" })`, // Drizzle auto-serializes pg: `text('${name}').array()`, mysql: `text('${name}', { mode: "json" })`, // Drizzle auto-serializes }, ``` The generated schema uses `text(..., { mode: "json" })`, which means **Drizzle automatically calls `JSON.stringify()` on write and `JSON.parse()` on read**. **2. Runtime adapter** (`packages/drizzle-adapter/src/drizzle-adapter.ts#L628`): ```typescript supportsArrays: config.provider === "pg" ? true : false, ``` When `supportsArrays` is `false` (SQLite/MySQL), the adapter factory (`packages/core/src/db/adapter/factory.ts#L252-L257`) **also calls `JSON.stringify()`** before passing data to Drizzle: ```typescript if (config.supportsArrays === false && Array.isArray(newValue)) { newValue = JSON.stringify(newValue); } ``` ## What's Stored in the Database | Step | Value | |------|-------| | Input | `["https://example.com/callback"]` | | Adapter `transformInput` (`supportsArrays: false`) | `'["https://example.com/callback"]'` (1st stringify) | | Drizzle `text({ mode: "json" })` on write | `'"[\"https://example.com/callback\"]"'` (2nd stringify) | | Stored in DB | `"[\"https://example.com/callback\"]"` — a JSON string, not a JSON array | - **Reading through better-auth API**: Drizzle parses once → adapter parses again → correct array. No issue. - **Reading directly with Drizzle** (e.g. `db.select().from(oauthClient)`): Drizzle parses once → returns `'["https://example.com/callback"]'` (a string, not an array). ## Use Case It's common to query better-auth managed tables directly with Drizzle for admin dashboards, analytics, or custom queries that better-auth's API doesn't cover. The double-serialized data makes this unnecessarily difficult — developers need to add an extra `JSON.parse()` step for every `string[]` field. ## Proposal **Make `supportsArrays` configurable in `DrizzleAdapterConfig`**, so users whose Drizzle schema already handles serialization via `mode: "json"` can let Drizzle handle it instead: ```typescript export interface DrizzleAdapterConfig { // ... existing fields ... /** * Whether the database natively supports array columns. * * When `false` (default for sqlite/mysql), the adapter will * JSON.stringify array values before writing and JSON.parse * after reading. Set to `true` if your Drizzle schema already * handles serialization (e.g. via `text(..., { mode: "json" })`). * * @default `true` for "pg", `false` for "sqlite" and "mysql" */ supportsArrays?: boolean; } ``` And wire it through in the adapter: ```typescript // drizzle-adapter.ts supportsArrays: config.supportsArrays ?? (config.provider === "pg" ? true : false), ``` This is a non-breaking change — existing behavior is preserved by default, but users can set `supportsArrays: true` to let Drizzle handle serialization when the schema is set up for it. This also keeps the database content clean and consistent for direct queries. ## Environment - better-auth: v1.4.x (latest) - @better-auth/oauth-provider: latest - drizzle-orm: 0.45.x - Database: SQLite (D1) / also affects MySQL
GiteaMirror added the enhancementadapter labels 2026-03-13 10:27:11 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#2908