{"id":16188257,"url":"https://github.com/konsumer/cross-cf","last_synced_at":"2025-07-22T12:35:07.944Z","repository":{"id":57685650,"uuid":"474469274","full_name":"konsumer/cross-cf","owner":"konsumer","description":"Cross-environment DO and KV access","archived":false,"fork":false,"pushed_at":"2022-09-21T20:50:09.000Z","size":157,"stargazers_count":3,"open_issues_count":2,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-09T15:55:32.445Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/konsumer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-03-26T21:22:25.000Z","updated_at":"2025-04-25T19:41:21.000Z","dependencies_parsed_at":"2022-09-18T23:11:52.538Z","dependency_job_id":null,"html_url":"https://github.com/konsumer/cross-cf","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"purl":"pkg:github/konsumer/cross-cf","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/konsumer%2Fcross-cf","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/konsumer%2Fcross-cf/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/konsumer%2Fcross-cf/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/konsumer%2Fcross-cf/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/konsumer","download_url":"https://codeload.github.com/konsumer/cross-cf/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/konsumer%2Fcross-cf/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":264755085,"owners_count":23659192,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-10T07:25:38.055Z","updated_at":"2025-07-22T12:35:07.915Z","avatar_url":"https://github.com/konsumer.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cross-cf\n\n\nCross-environment Cloudflare [DO](https://developers.cloudflare.com/workers/runtime-apis/durable-objects/) and [KV](https://developers.cloudflare.com/workers/runtime-apis/kv) access.\n\nI kept needing to inject things into my local (miniflare) or remote KV, or interact with my DO, in scripts or unit-tests that run in node. This lets you mess with them directly, and it keeps the same API in all environments.\n\nYou can chack out the [example](https://github.com/konsumer/cross-cf/blob/main/example) for an idea of how to use it in your own thing. Basically, I have a graphql endpoint worker that I want to be able to migrate data to/from and run unit-tests on it locally.\n\n\n## installation\n\n```\nnpm i -D cross-cf\n```\n\n## usage\n\n### CLI\n\nIncluded is a CLI you can use to migrate KV, since it's such a useful task. Get help like this:\n\n```sh\nnpx cross-cf\n```\n\nFor remote-operations (remote KV, DO, etc) you will need a global `fetch`. New versions of node have it built-in. You can add to older versions of node with code like this:\n\n```js\nimport fetch from 'cross-fetch'\n\nglobalThis.fetch = fetch\n```\n\nYou can also exploit this to mock `fetch` in unit-tests, with things like [msw](https://mswjs.io/).\n\n### KV\n\nKV can be used remotely (if you have an API key and the ID of the KV) or locally (using miniflare filesystem, directly.)\n\n#### basic setup\n\n```js\nimport { CrossKV } from 'cross-cf'\n\nconst MYDB = new CrossKV('MYDB')\n\nawait MYDB.put('coolA', JSON.stringify({ cool: true }))\nawait MYDB.put('coolB', JSON.stringify({ cool: true }))\nawait MYDB.put('coolB', JSON.stringify({ cool: true }))\n\nconsole.log(await MYDB.list())\n\n```\n\n#### local\n\nThis is the default `target`. It uses local miniflare perisitant file-system-based KV, so you can set things up for unit-tests, or mock-scripts, or whatever. You could also just use it as a general purpose, local JSON-based KV.\n\nYou can also set the path where your database is:\n\n```js\nconst MYDB = new CrossKV('MYDB', {filepath: './some/other/place/where/I/keep/my/db'})\n```\n\n#### remote\n\nThis lets you use a KV id and API creds to interact with a remote KV.\n\nSet these environment variables to get auth working automatically:\n\n```sh\nCF_TOKEN      # your auth-token\nCF_ACCOUNTID  # your worker's accountID\n```\n\n```js\nconst MYDB = new CrossKV('MYDB', { target: 'remote', kvID: 'XXX' })\n```\n\nYou can also setup auth, directly:\n\n```js\nconst MYDB = new CrossKV('MYDB', {\n  target: 'remote',\n  kvID: 'XXX',\n  accountToken: 'XXX',\n  accountID: 'XXX'\n})\n```\n\n#### memory\n\nThis can be handy for unit-tests, where you don't want to persist anything, but you want the same API. Personally, I use this for my unit-tests, so any existing data in the persistant `local` setup doesn't effect my tests.\n\n```js\nconst MYDB = new CrossKV('MYDB', { target: 'memory' })\n```\n\n#### cf\n\nThis `target` means that you want to use KV from actual cloudflare (or inside miniflare.) I'm not totally sure what the usecase is, but this will let you keep your code more similar in different environments, which is the overall goal of this library.\n\n```js\n// this will use the global KV MYDB\nconst MYDB = new CrossKV('MYDB', { target: 'cf' })\n\n// this will use MYDB in env from esm worker request-handler\nconst MYDB2 = new CrossKV('MYDB', { target: 'cf', env})\n```\n\n#### bulk\n\n\u003e **WARNING** These are not part of the original KV API\n\n2 common needs when hitting remote data are bulk `put` and `delete`, so I added that to the API for all `target`s, too. If you use it on non-remote, it will just call the corresponding single function on each record. Your `key` and `value` must both be strings.\n\n```js\nawait MYDB.bulkput([\n  { key: '0', value: 'A' },\n  { key: '1', value: 'B' },\n  { key: '2', value: 'C' },\n  { key: '3', value: 'D' },\n  { key: '4', value: 'E' },\n  { key: '5', value: 'F' },\n  { key: '6', value: 'G' },\n  { key: '7', value: 'H' },\n  { key: '8', value: 'I' },\n  { key: '9', value: 'J' },\n  { key: '10', value: 'J' },\n  { key: '11', value: 'K' },\n  { key: '12', value: 'L' },\n  { key: '13', value: 'M' },\n  { key: '14', value: 'N' },\n  { key: '15', value: 'O' },\n  { key: '16', value: 'P' },\n  { key: '17', value: 'Q' },\n  { key: '18', value: 'R' },\n  { key: '19', value: 'S' },\n  { key: '20', value: 'T' },\n  { key: '21', value: 'U' },\n  { key: '22', value: 'V' },\n  { key: '23', value: 'W' },\n  { key: '24', value: 'X' },\n  { key: '25', value: 'Y' },\n  { key: '26', value: 'Z' }\n])\n\nawait MYDB.bulkdelete(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23', '24', '25', '26'])\n```\n\n\n#### examples\n\nHere is an example migration script. I'm not paging in this example, so this will be limited to 1000 records, but you can page records too, if you want.\n\n\n```js\nimport { CrossKV } from 'cross-cf'\n// or const { CrossKV } = require('cross-cf')\n\n// I also setup CF_TOKEN, CF_ACCOUNTID for auth creds\nconst { CF_MYDB_ID } = process.env\n\nconst db = {\n  local: new CrossKV('MYDB'),\n  remote: new CrossKV('MYDB', { kvID: CF_MYDB_ID, target: 'remote' })\n}\n\nasync function main() {\n  const { keys } = await db.remote.list()\n  for (const r of await keys) {\n    db.local.put(r.name, await db.remote.get(r.name))\n  }\n}\nmain()\n```\n\nTo go the other way, `bulkput` is probly a better choice for performance. Since it works the same locally, but is better remote, we could write a more general migrator that works in both directions, with any KVs, like this:\n\n```js\nimport { CrossKV } from 'cross-cf'\n\nconst [, progname, name, from, to] = process.argv\nif (!name || !from || !to) {\n  console.error(`Usage: ${progname} NAME FROM:LOCATION TO:LOCATION`)\n  console.error('')\n  console.error('\\tWhere targets look like \"local:dir\" or \"remote:kvID\"')\n  console.error('')\n  console.error('\\tExamples:')\n  console.error(`\\t\\t${progname} MYDB 'local:.mf/kv' 'remote:XXX'   # copy local miniflare KV \"MYDB\" to remote kvID XXX`)\n  process.exit(1)\n}\n\nconst [fromTarget, fromPath] = from.split(':')\nconst [toTarget, toPath] = to.split(':')\n\nconst db = {\n  local: toTarget === 'local' ? new CrossKV(name, { filepath: toPath }) : new CrossKV(name, { target: 'remote', kvID: toPath }),\n  remote: fromTarget === 'local' ? new CrossKV(name, { filepath: fromPath }) : new CrossKV(name, { target: 'remote', kvID: fromPath })\n}\n\nasync function main () {\n  const { keys } = await db.remote.list()\n  const records = await Promise.all(keys.map(async ({ name }) =\u003e {\n    const value = await db.remote.get(name)\n    return { key: name, value }\n  }))\n  await db.local.bulkput(records)\n}\nmain()\n```\n\nThis is very similar to the CLI I include.\n\n### DO\n\n#### remote\n\nDurable Objects are not exposed in any way to external access by default, so you will need to mount the fetch of your DO onto a worker, to make it work. Here is example client code:\n\n```js\nimport { CrossDO } from 'cross-cf'\n// or const { CrossDO } = require('cross-cf')\n\nconst POKEMON = new CrossDO('https://mine.workers.dev')\n\nconst DEMO_QUERY = `\n{\n  pokemon {\n    id\n    name\n  }\n}\n`\n\nasync function main() {\n  // works just like CF DOs\n  const pokemon = POKEMON.get(POKEMON.idFromName('test'))\n  \n  // on real DO, first arg should be user-request or new Request(new URL('YOUR_URL')) if you are in a cron-job or whatever. It will be ignored here, but I use same format as I would use in a cron-job.\n  const pokemon = await pokemon.fetch(new Request(new URL('https://pokemon.name.workers.dev')), { headers: { 'content-type': 'application/json', body: JSON.stringify({ query: DEMO_QUERY }) } })\n}\nmain()\n```\n\nYour worker DO proxy will look something like this:\n\n```js\nexport default {\n  async fetch (request, env) {\n    // only service application/json requests, like graphql\n    if (request.headers.get('content-type') === 'application/json') {\n      // get an instance of the DO for this region, and run the user's graphql query on it\n      if (!env?.POKEMON) {\n        console.error('DurableObject bindings have broken.')\n      }\n      const pokemon = env.POKEMON.get(env.POKEMON.idFromName(request.cf.colo))\n      return pokemon.fetch(request)\n    }\n\n    return new Response('\u003ch1\u003eNothing to see here.\u003c/h1\u003e', {\n      headers: {\n        'content-type': 'text/html',\n      }\n    })\n  }\n}\n```\n\nThis is a simple API example that will pass user-requests on to the DO, if the content-type is JSON. You will of course need to bind `POKEMON` to your DO in your worker, [in the regular way](https://developers.cloudflare.com/workers/learning/using-durable-objects/). You can do anything else you want in your worker, like check for auth-tokens in headers, or throttle requests, etc.\n\n### why?\n\nYou may ask yourself \"Why should I do this instead of just using a regular `fetch` or a nice graphql client directly with my worker?\" That is a fair question. That would totally work, and this library is designed around workers that are setup to accomodate that. Main thing I like about this style is I can swap out the DO and it works in both environments seemlessly, which is good for library-code (like if you wanted to wrap your DO/remote in a client-library that works in browser, node, other CF-workers, DOs, cron-jobs, etc.)\n\n\n### testing\n\nOne handy side-effect of this stuff is you will get an interface that works the same as the real thing, but can do remote requests, that you can mock (like with [jest](https://jestjs.io/docs/mock-functions)) and then make your code do stuff. You can mock `cross-fetch` module or global fetch for remote requests (and make them local calls you can look at), or just mock the interface directly. For my stuff, I like unit-tests that could run on the real DO if I set it up (for instant integration tests.) It's also handy if you are copy/pasting text from some worker-code. See [test.js](https://github.com/konsumer/cross-cf/blob/main/test.js) for some examples. You can look at [example/](https://github.com/konsumer/cross-cf/blob/main/example) for an example worker project.\n\n\n## todo\n\n- read wrangler.toml for named KV, and use less config (ids, etc)\n- allow lookup (from remote) of KV by name (not ID)\n- put throttling limits and fail-recovery into API stuff\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkonsumer%2Fcross-cf","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkonsumer%2Fcross-cf","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkonsumer%2Fcross-cf/lists"}