@cachely-io/sdk

Universal JavaScript SDK for Cachely. Routes CMS API and asset requests through your project's edge domain — adding edge caching, image optimization, and server-side token injection without changing your application logic.

Works in any JavaScript environment: Node.js 18+, browsers, Cloudflare Workers, Deno.

Built-in providers: Contentful, Prismic — plus any CMS via createGenericProvider


Install

The SDK ships a deterministic CLI that handles detection and boilerplate generation.

Generic setup — detects framework + CMS + package manager automatically:

npx @cachely-io/sdk setup

Generates a typed helper file (lib/cachely.ts for Next.js, composables/useCachely.ts for Nuxt, cachely.ts for Vite/generic), adds CACHELY_PROXY_URL to .env.example, and installs the SDK.

# Specify tenant explicitly (generic helper flow)
npx @cachely-io/sdk setup --tenant my-project
# Preview without writing
npx @cachely-io/sdk setup --dry-run
# Non-interactive CI mode (generic helper for a framework you pick)
npx @cachely-io/sdk setup --yes --tenant my-project --framework next

Since 0.12.0, passing --provider prismic / --provider contentful (and sanity / storyblok) routes to the provider's onboarder, not the generic helper flow. See the CLI reference and provider-specific onboarding below.

Provider-specific onboarding — deeper, CMS-aware setup. Available for Prismic (full integration) and Contentful (client-file patching):

npx @cachely-io/sdk setup --provider prismic --tenant my-project
npx @cachely-io/sdk setup prismic --tenant my-project
npx @cachely-io/sdk init prismic --tenant my-project

All three forms are equivalent (since 0.12.0).

Prismic onboarding generates composables/useCachelyPrismicClient.ts as the primary integration surface. On greenfield or trivially-patchable projects, it also writes app/prismic/client.js and patches config/nuxt/prismic.{js,ts}. Complex existing clients are not modified — manual instructions are printed. nuxt.config is never auto-patched.

See the CLI reference for the full flag set, integration-mode matrix, and what each step writes.

Manual install

npm install @cachely-io/sdk
# or: pnpm add @cachely-io/sdk
# or: yarn add @cachely-io/sdk
# or: bun add @cachely-io/sdk

Then write your own helper using createCachelyFetch — see Quick start below.


How it works

Instead of calling Contentful or Prismic directly, requests go through your project's edge domain:

Your app
  → Cachely edge (https://my-project.cachely.io)
    → Contentful / Prismic / ...

@cachely-io/sdk handles the URL rewriting at the transport layer. You configure it once and drop it into your CMS SDK — no other code changes needed.


Quick start

Contentful

import { createCachelyFetch } from '@cachely-io/sdk'
import { createClient } from 'contentful'

const cachelyFetch = createCachelyFetch({
  tenant: 'my-project',       // your Cachely project slug
  provider: 'contentful',
  providerConfig: {
    spaceId: 'abc123xyz',     // your Contentful space ID
  },
})

const client = createClient({
  space: 'abc123xyz',
  accessToken: '',            // leave empty — injected server-side by the proxy
  adapter: cachelyFetch,
})

// All API calls go through the proxy automatically:
// https://cdn.contentful.com/spaces/abc123xyz/entries
// → https://my-project.cachely.io/~api/spaces/abc123xyz/entries

Prismic

import { createCachelyFetch } from '@cachely-io/sdk'
import * as prismic from '@prismicio/client'

const cachelyFetch = createCachelyFetch({
  tenant: 'my-project',
  provider: 'prismic',
})

const client = prismic.createClient('my-repo', {
  fetch: cachelyFetch,
})

// All API calls go through the proxy automatically:
// https://my-repo.cdn.prismic.io/api/v2/documents/search?ref=...
// → https://my-project.cachely.io/~api/api/v2/documents/search?ref=...

Any CMS via the generic provider

import { createGenericProvider, createCachelyFetch } from '@cachely-io/sdk'

const storyblok = createGenericProvider({
  id: 'storyblok',
  apiHosts: ['api.storyblok.com'],
  assetHosts: ['a.storyblok.com'],
})

const cachelyFetch = createCachelyFetch({
  tenant: 'my-project',
  provider: storyblok,   // pass ProviderDefinition directly — no registry needed
})

// https://api.storyblok.com/v2/stories → https://my-project.cachely.io/~api/v2/stories
// https://a.storyblok.com/f/123/hero.jpg → https://my-project.cachely.io/f/123/hero.jpg

URL rewriting without fetch

Use createCachelyUrlRewriter when you need to transform URLs without intercepting fetch — useful for server-side response body processing, logging, or building custom adapters.

import { createCachelyUrlRewriter } from '@cachely-io/sdk'

const rewrite = createCachelyUrlRewriter({
  tenant: 'my-project',
  provider: 'contentful',
  providerConfig: { spaceId: 'abc123xyz' },
})

// API URL
rewrite('https://cdn.contentful.com/spaces/abc123xyz/entries?locale=en')
// → { url: 'https://my-project.cachely.io/~api/spaces/abc123xyz/entries?locale=en', kind: 'api', rewritten: true }

// Asset URL — image optimization params are preserved
rewrite('https://images.ctfassets.net/abc123xyz/assetId/token/hero.jpg?w=1200&fm=webp')
// → { url: 'https://my-project.cachely.io/assetId/token/hero.jpg?w=1200&fm=webp', kind: 'asset', rewritten: true }

// Non-CMS URL — passes through unchanged
rewrite('https://fonts.googleapis.com/css2?family=Inter')
// → { url: 'https://fonts.googleapis.com/css2?family=Inter', kind: 'unknown', rewritten: false }

Response transformer

When you fetch a CMS response on the server and want to rewrite the asset URLs inside the JSON body (rather than intercepting fetch calls), use the response transformer. It's pure JavaScript with zero deps and ships in the same @cachely-io/sdk package.

import { transformAssetUrls } from '@cachely-io/sdk'

// process.env.CACHELY_PROXY_URL = 'https://my-project.cachely.io'

const data = await fetch('https://cdn.contentful.com/spaces/abc/entries').then(r => r.json())

const transformed = transformAssetUrls(data, {
  cms: 'contentful',
  spaceId: 'abc',
  // Reads from process.env.CACHELY_PROXY_URL automatically.
  // Pass `proxyUrl: 'https://...'` to override.
})

Per-CMS exports

import {
  transformPrismicAssetUrls,
  transformContentfulAssetUrls,
  transformSanityAssetUrls,
  transformShopifyAssetUrls,
  transformCloudinaryAssetUrls,
  transformImgixAssetUrls,
  transformGenericAssetUrls,
} from '@cachely-io/sdk'

// Prismic
const out = transformPrismicAssetUrls(data, {
  repository: 'my-repo',
  proxyUrl: 'https://my-project.cachely.io',
})

// Contentful
const out2 = transformContentfulAssetUrls(data, {
  spaceId: 'abc123xyz',
  proxyUrl: 'https://my-project.cachely.io',
})

getCachelyProxyBase(options)

Resolve the proxy base URL the same way the transformer does:

  1. options.proxyUrl (explicit override)
  2. process.env.CACHELY_PROXY_URL
  3. process.env.CMS_ASSETS_URL — temporary launch-window fallback for projects migrating from the legacy env var
import { getCachelyProxyBase } from '@cachely-io/sdk'

const base = getCachelyProxyBase()
// 'https://my-project.cachely.io/'  (trailing slash; query/hash stripped)

Required options per CMS

cmsRequired option
prismicrepository: string
contentfulspaceId: string
sanityprojectId: string (dataset?: string, default 'production')
shopifystoreDomain: string
cloudinarycloudName: string
imgiximgixDomain: string
genericoriginUrl: string

API reference

createCachelyFetch(options)

Returns a fetch-compatible function that rewrites CMS URLs through the proxy. Safe to use as a global fetch replacement — non-CMS requests pass through unchanged.

const cachelyFetch = createCachelyFetch({
  tenant: 'my-project',       // required — Cachely project slug
  provider: 'contentful',     // required — provider id string OR a ProviderDefinition object
  providerConfig: {           // optional — provider-specific config
    spaceId: 'abc123xyz',     //   Contentful: spaceId
  },
  enableApiProxy: true,       // optional — rewrite API requests (default: true)
  enableAssetProxy: true,     // optional — rewrite asset requests (default: true)
  registry: myRegistry,       // optional — custom provider registry (used when provider is a string)
})

Selective proxying:

// Asset proxy only — API calls go directly to Contentful
const cachelyFetch = createCachelyFetch({
  tenant: 'my-project',
  provider: 'contentful',
  enableApiProxy: false,
})

// API proxy only — images are not proxied
const cachelyFetch = createCachelyFetch({
  tenant: 'my-project',
  provider: 'prismic',
  enableAssetProxy: false,
})

createCachelyUrlRewriter(options)

Returns a bound (url: string | URL) => RewriteResult function. Same options as createCachelyFetch.

import { createCachelyUrlRewriter } from '@cachely-io/sdk'

const rewrite = createCachelyUrlRewriter({
  tenant: 'my-project',
  provider: 'prismic',
})

const { url, kind, rewritten } = rewrite('https://images.prismic.io/my-repo/hero.jpg?w=800&fm=webp')
// url:       'https://my-project.cachely.io/my-repo/hero.jpg?w=800&fm=webp'
// kind:      'asset'
// rewritten: true

classifyUrl(url, provider)

Classify a URL as 'api', 'asset', or 'unknown' based on its hostname.

import { classifyUrl, contentful, prismic } from '@cachely-io/sdk'

classifyUrl('https://cdn.contentful.com/spaces/abc/entries', contentful)  // 'api'
classifyUrl('https://images.ctfassets.net/abc/assetId/token/img.jpg', contentful)  // 'asset'
classifyUrl('https://my-repo.cdn.prismic.io/api/v2/documents/search', prismic)  // 'api'
classifyUrl('https://images.prismic.io/my-repo/photo.jpg', prismic)  // 'asset'
classifyUrl('https://example.com/logo.svg', contentful)  // 'unknown'

rewriteUrl(url, kind, provider, tenant, config?)

Low-level rewrite — takes a pre-classified URL and returns a RewriteResult.

import { classifyUrl, rewriteUrl, contentful } from '@cachely-io/sdk'

const url = new URL('https://cdn.contentful.com/spaces/abc/entries?locale=en')
const kind = classifyUrl(url, contentful)
const result = rewriteUrl(url, kind, contentful, 'my-project', { spaceId: 'abc' })

// result.url       → 'https://my-project.cachely.io/~api/spaces/abc/entries?locale=en'
// result.kind      → 'api'
// result.rewritten → true

ProviderRegistry

Register custom or extended provider definitions.

import { ProviderRegistry, contentful, prismic } from '@cachely-io/sdk'

const registry = new ProviderRegistry()
  .register(contentful)
  .register(prismic)

const cachelyFetch = createCachelyFetch({
  tenant: 'my-project',
  provider: 'contentful',
  registry,
})

The defaultRegistry is a shared singleton pre-populated with contentful and prismic. You only need a custom registry if you want to override a built-in provider or register your own.


createGenericProvider(config)

Create a ProviderDefinition from a config object — use any CMS or API origin without writing a custom provider from scratch.

import { createGenericProvider } from '@cachely-io/sdk'

const provider = createGenericProvider({
  id: 'storyblok',                        // unique provider id
  apiHosts: ['api.storyblok.com'],        // API hostnames (exact or wildcard)
  assetHosts: ['a.storyblok.com'],        // asset hostnames

  // Optional — stored for future Worker-side support, not yet processed by proxy
  auth: {
    header: 'Authorization',
    format: 'Bearer {token}',
  },
  previewBypass: {
    queryParams: ['preview', 'draft'],
  },
})

The returned ProviderDefinition plugs directly into createCachelyFetch, createCachelyUrlRewriter, classifyUrl, rewriteUrl, and ProviderRegistry.

URL rewriting follows standard Cachely conventions:

API:   https://api.storyblok.com/v2/stories → https://{tenant}.cachely.io/~api/v2/stories
Asset: https://a.storyblok.com/f/123/img.jpg → https://{tenant}.cachely.io/f/123/img.jpg

API_PREFIX

The /~api path prefix used by the Cachely API proxy.

import { API_PREFIX } from '@cachely-io/sdk'

console.log(API_PREFIX) // '/~api'

Providers

Contentful

KindHosts
APIcdn.contentful.com, preview.contentful.com
Assetsimages.ctfassets.net, videos.ctfassets.net, assets.ctfassets.net, downloads.ctfassets.net

providerConfig.spaceId — Required for correct asset path stripping. When provided, the space ID segment is removed from the proxy URL path.

// Without spaceId:
// https://images.ctfassets.net/abc/assetId/token/img.jpg
// → https://my-project.cachely.io/abc/assetId/token/img.jpg

// With spaceId: 'abc':
// https://images.ctfassets.net/abc/assetId/token/img.jpg
// → https://my-project.cachely.io/assetId/token/img.jpg  ← spaceId stripped

Prismic

KindHosts
API*.cdn.prismic.io (wildcard — matches any repo subdomain)
Assetsimages.prismic.io, prismic-io.imgix.net

Prismic image optimization params (w, h, q, fm, auto, fit) are preserved end-to-end.

// Image optimization is preserved:
// https://images.prismic.io/my-repo/hero.jpg?auto=format,compress&w=920&fm=webp&q=80
// → https://my-project.cachely.io/my-repo/hero.jpg?auto=format,compress&w=920&fm=webp&q=80

Prismic's ref parameter changes on every content publish, making each published version a distinct cache key — no manual cache invalidation needed.


TypeScript

All types are exported from the package root:

import type {
  // Fetch / rewrite
  RequestKind,             // 'api' | 'asset' | 'unknown'
  ProviderDefinition,      // shape of a provider
  ProviderConfig,          // Record<string, string | undefined>
  RewriteResult,           // { url, kind, rewritten }
  CachelyFetchOptions,     // options for createCachelyFetch
  UrlRewriterOptions,      // options for createCachelyUrlRewriter
  GenericProviderConfig,   // config shape for createGenericProvider

  // Response transformer
  TransformOptions,         // shared base options
  PrismicTransformOptions,
  ContentfulTransformOptions,
  SanityTransformOptions,
  ShopifyTransformOptions,
  CloudinaryTransformOptions,
  ImgixTransformOptions,
  GenericTransformOptions,
  CmsDispatchOptions,       // discriminated union for transformAssetUrls
  SupportedCms,             // 'prismic' | 'contentful' | … | 'generic'
  StringTransformer,        // (jsonStr, ctx) => jsonStr
  TransformerContext,       // { base, nodeEnv }
} from '@cachely-io/sdk'

Custom providers

The easiest way to add support for any CMS:

import { createGenericProvider, createCachelyFetch } from '@cachely-io/sdk'

const sanity = createGenericProvider({
  id: 'sanity',
  apiHosts: ['*.api.sanity.io'],
  assetHosts: ['cdn.sanity.io'],
  auth: { header: 'Authorization', format: 'Bearer {token}' },
})

// Pass directly — no registry needed
const cachelyFetch = createCachelyFetch({ tenant: 'my-project', provider: sanity })

// Or register and use by id string
import { defaultRegistry } from '@cachely-io/sdk'
defaultRegistry.register(sanity)
const cachelyFetch2 = createCachelyFetch({ tenant: 'my-project', provider: 'sanity' })

Using ProviderDefinition directly (advanced)

For full control over rewrite logic, implement the interface manually:

import { ProviderDefinition, defaultRegistry } from '@cachely-io/sdk'

const sanity: ProviderDefinition = {
  id: 'sanity',
  apiHosts: ['*.api.sanity.io'],
  assetHosts: ['cdn.sanity.io'],
  rewriteApiUrl(url, tenant) {
    return `https://${tenant}.cachely.io/~api${url.pathname}${url.search}`
  },
  rewriteAssetUrl(url, tenant) {
    return `https://${tenant}.cachely.io${url.pathname}${url.search}`
  },
}

defaultRegistry.register(sanity)

Requirements

  • Node.js 18+ (or any runtime with the Fetch API)
  • No dependencies
Need help understanding this?Ask Cachely Copilot about features, setup, or integrations.
Ask Copilot →