-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Description
Describe the Bug
Description:
When using @payloadcms/db-d1-sqlite deployed to Cloudflare Workers via OpenNext, DELETE operations return success but do not actually delete records from the D1 database. The issue only occurs in production Workers—local development with wrangler works correctly.
Hypothesis:
The D1 database binding is captured during module initialization and becomes stale by the time HTTP requests are processed. However, the issue is more nuanced than simply needing a fresh binding reference:
- Getting a fresh binding via getCloudflareContext() at property access time (e.g., when Drizzle accesses .prepare) is not sufficient
- The fresh binding must be obtained at method call time — the exact moment .prepare(), .batch(), etc. are actually invoked
- Using JavaScript's .bind() on D1 native methods does not resolve the issue
Suggestion:
The sqliteD1Adapter should accept a function or getter for the binding that is invoked at query execution time, rather than capturing the binding at initialization.
Temporary Workaround:
A Proxy wrapper that returns a function which calls getCloudflareContext() inside the function body at call time:
function createLazyD1Binding(initialBinding: any) {
return new Proxy({}, {
get(target, prop) {
if (typeof prop === 'symbol' || prop === 'then' || prop === 'catch') return undefined
const isWorkers = typeof globalThis.navigator !== 'undefined’ &&
globalThis.navigator.userAgent === 'Cloudflare-Workers’
if (!isWorkers) {
const value = initialBinding[prop]
return typeof value === 'function' ? value.bind(initialBinding) : value
}
// Key: get fresh binding at CALL TIME, not property access time
return function(...args: any[]) {
const ctx = getCloudflareContext()
const binding = (ctx as any)?.env?.D1 || initialBinding
return binding[prop](...args)
}
}
})
}
Link to the code that reproduces this issue
https://github.com/heatloss/chimera-d1/tree/payload-d1-bug-repro
Reproduction Steps
Reproduction:
- Configure Payload with sqliteD1Adapter using a D1 binding obtained at module initialization
- Deploy to Cloudflare Workers
- Delete a record via the Payload API or Admin UI
- Response indicates success, but record still exists in D1
Expectation:
Records should be deleted from D1 when Payload returns a successful delete response.
Which area(s) are affected?
db: d1-sqlite
Environment Info
Payload Info:
Binaries:
Node: 20.9.0
npm: 10.1.0
Yarn: N/A
pnpm: 10.23.0
Relevant Packages:
payload: 3.69.0
next: 16.0.3
@payloadcms/db-d1-sqlite: 3.69.0
@payloadcms/drizzle: 3.69.0
@payloadcms/graphql: 3.69.0
@payloadcms/next/utilities: 3.69.0
@payloadcms/plugin-cloud-storage: 3.69.0
@payloadcms/richtext-lexical: 3.69.0
@payloadcms/storage-r2: 3.69.0
@payloadcms/translations: 3.69.0
@payloadcms/ui/shared: 3.69.0
react: 19.2.3
react-dom: 19.2.3