This project tests the Cloudflare KV metadata issue where:
- Metadata is supposedly optional but required in TypeScript types
- Passing empty metadata
{}causes values to be stored as{value: XYZ, metadata: {}} - Manual multipart headers may be needed as a workaround
-
Copy
.env.exampleto.envand fill in your Cloudflare credentials:cp .env.example .env
-
Get your credentials:
- API Token: Create one in Cloudflare dashboard with KV permissions
- Account ID: Found in right sidebar of Cloudflare dashboard
- KV Namespace ID: Create a KV namespace and copy its ID
-
Install dependencies:
npm install
npm run test:tsnpm run compile- Writing without metadata - Should work fine
- Writing with empty metadata
{}- The problematic case that wraps values - Writing with actual metadata - Should work but also wraps values
- Writing via REST API with multipart/form-data - Direct API call with metadata
- Reading values back - To see what actually got stored
- metadata is actually optional in the TypeScript types
- The issue is NOT with TypeScript compilation
- You can successfully omit metadata in the value parameter
The problem occurs when using the TypeScript SDK - metadata is not being saved:
-
No metadata: Value stored as plain string ✅
{ value: "hello", account_id: "..." } // Stores: "hello", no metadata
-
Empty metadata: Value stored, but metadata is lost ❌
{ value: "hello", metadata: {}, account_id: "..." } // Stores: "hello", metadata: null/undefined
-
With metadata: Value stored, but metadata is lost ❌
{ value: "hello", metadata: {...}, account_id: "..." } // Stores: "hello", metadata: null/undefined
The TypeScript SDK appears to ignore the metadata parameter entirely, while the REST API properly stores metadata when using multipart/form-data.
Test 4 demonstrates using the Cloudflare KV REST API directly with proper multipart/form-data:
const formData = new FormData();
formData.append('value', 'your-value');
formData.append('metadata', JSON.stringify({
author: 'test-user',
timestamp: new Date().toISOString()
}));
const response = await fetch(
`https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/storage/kv/namespaces/${NAMESPACE_ID}/values/your-key?expiration_ttl=3600`,
{
method: 'PUT',
headers: {
'Authorization': `Bearer ${API_TOKEN}`
},
body: formData
}
);This bypasses the TypeScript SDK's wrapping behavior and stores metadata properly according to the official API specification.
// Good - stores as plain value
await cf.kv.namespaces.values.update(namespaceId, key, {
value: "your-data",
account_id: accountId
// Don't include metadata property at all
});// Good - stores metadata properly without wrapping
const formData = new FormData();
formData.append('value', 'your-data');
formData.append('metadata', JSON.stringify({ author: 'user' }));
await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`, {
method: 'PUT',
headers: { 'Authorization': `Bearer ${apiToken}` },
body: formData
});// Bad - causes wrapping
await cf.kv.namespaces.values.update(namespaceId, key, {
value: "your-data",
account_id: accountId,
metadata: {} // This forces multipart and wraps your value!
});If you must use the TypeScript SDK with metadata, handle the wrapper in your reads:
const result = await cf.kv.namespaces.values.get(namespaceId, key, {account_id});
const actualValue = typeof result === 'object' && result.value ? result.value : result;