Upgrading to 0.12
0.12 stores array fields natively. Convert rows written as JSON strings once, verify in SQL, and only then remove your own workarounds.
Use 0.12.1 or later
On 0.12.0 the migration below was a silent no-op on Postgres. If you ran it on 0.12.0, run it again.
Releases up to 0.11.3 reported supportsArrays: false to Better Auth, so every string[] / number[] value was JSON.stringify'd on its way into Payload. Those columns hold '["a","b"]' where an array belongs. From 0.12.0 the adapter stores arrays natively, so existing rows need converting once.
If you don't use the oauth-provider plugin and have no array-typed additionalFields, there is nothing to do. Nothing else in Better Auth uses an array field.
Otherwise: migrate, then verify, then remove any workaround of your own, in that order.
Migrate
import { migrateStringifiedArrays } from '@delmaredigital/payload-better-auth'
import { betterAuthOptions } from './lib/auth/config'
const results = await migrateStringifiedArrays({
payload,
betterAuthOptions,
dryRun: true, // drop this once the report looks right
})
console.table(results)
// [{ collection: 'oauthClients', field: 'redirectUris', scanned: 12,
// converted: 12, skipped: 0, observedVia: 'stored-shape' }, …]It derives the fields to convert from your Better Auth schema rather than a hardcoded list, so it covers whatever plugins you run. It is safe to re-run: values already stored as arrays are left alone, and a string that doesn't parse to an array is skipped and counted rather than guessed at.
Other options: usePlural (must match the adapter; default true), batchSize (rows per page; default 100, honoured on Postgres since 0.12.2) and onProgress (called once per converted document). See migrateStringifiedArrays.
Read observedVia alongside converted. On Postgres it must say stored-shape; local-api there would mean the count came from the laundered value and is not trustworthy. If the stored shape can't be inspected at all, the migration throws rather than reporting a clean database.
Verify against the database
converted: 0 is only good news if the census could see the stored shape. Confirm independently:
SELECT jsonb_typeof(scopes) AS shape, count(*)
FROM oauth_access_tokens GROUP BY 1;You want only array (and null). Any string rows are unconverted. To fix them outside the plugin:
UPDATE oauth_access_tokens
SET scopes = (scopes #>> '{}')::jsonb
WHERE jsonb_typeof(scopes) = 'string';Only then, remove your own workarounds
If you were writing JSON.stringify(uris) into these columns, or parsing them back out on read, drop that. After migrating, the column holds one shape.
Do this last, and only after the verification reads clean. Removing a tolerant parse while unconverted rows remain is what turns this into an incident: raw SQL reads carry no column type, so they bypass drizzle's mapper and still see the string. Code that reads redirect_uris through drizzle.execute(sql`…`), such as a DCR connector gate or a consent screen, would get [] from an unconverted row. For a redirect-URI allowlist, that locks out a legitimate connector.
Why 0.12.0 reported converted: 0
Payload writes these fields to a jsonb column, so a stringified value is stored as a jsonb string. On read, node-postgres parses the jsonb and hands drizzle a JS string, and drizzle's PgJsonb.mapFromDriverValue parses it a second time. The stored string becomes an array before Payload sees it, so a stringified row and a native one are indistinguishable through payload.find(). 0.12.1 censuses the stored shape in SQL instead.
That raw-SQL path is also the real exposure of leaving rows unmigrated. Better Auth's own reads go through Payload, where the double parse launders the value, so /oauth2/authorize keeps working. The damage is confined to code that queries these columns directly.
Upgrading to 0.13
0.13 needs no code, config or data changes. It fixes sliding sessions through Payload and changes when payload.auth refreshes a session.
Upgrading to 0.11
0.11 requires Better Auth 1.7. Upgrade the peers together, then handle the account issuer change, which Better Auth 1.7.3 later reverted.

