api.cms.storage exposes only list / create / update / delete (src/core/plugin-sdk/types/serverApi.ts:110-117). createPluginRecord inserts unconditionally (server/repositories/plugins.ts:422-437), and data_json fields carry no uniqueness constraint (migrations-sqlite.ts:358-368, migrations-pg.ts:379-389).
The consequence for any plugin that keys records by a data field — mine stores SEO metadata under key = <tableSlug>:<entryId> — is that two concurrent writers can both miss on read and both insert, leaving two records for one logical key. list ties on created_at are also nondeterministic, so which duplicate wins a later read is not stable.
There is no atomic fix available from inside a plugin.
What I do instead, in case it is useful to anyone hitting this: every read resolves newest-wins for a key and the write paths delete the stale duplicates they find (capped per request); delete removes all matches for the key; and the publish-time filter is strictly read-only, so a render never writes and can never create a duplicate under load. It self-heals, but it is a convention every plugin has to reinvent, and it cannot close the window — only narrow it.
Ask: either
collection(id).upsert(data, { uniqueBy: 'field' }), or
- a declarable unique field on
PluginResourceField, so the database enforces it.
Either one turns a race into an error the plugin can handle.
api.cms.storageexposes onlylist/create/update/delete(src/core/plugin-sdk/types/serverApi.ts:110-117).createPluginRecordinserts unconditionally (server/repositories/plugins.ts:422-437), anddata_jsonfields carry no uniqueness constraint (migrations-sqlite.ts:358-368,migrations-pg.ts:379-389).The consequence for any plugin that keys records by a data field — mine stores SEO metadata under
key = <tableSlug>:<entryId>— is that two concurrent writers can both miss on read and both insert, leaving two records for one logical key.listties oncreated_atare also nondeterministic, so which duplicate wins a later read is not stable.There is no atomic fix available from inside a plugin.
What I do instead, in case it is useful to anyone hitting this: every read resolves newest-wins for a key and the write paths delete the stale duplicates they find (capped per request); delete removes all matches for the key; and the publish-time filter is strictly read-only, so a render never writes and can never create a duplicate under load. It self-heals, but it is a convention every plugin has to reinvent, and it cannot close the window — only narrow it.
Ask: either
collection(id).upsert(data, { uniqueBy: 'field' }), orPluginResourceField, so the database enforces it.Either one turns a race into an error the plugin can handle.