---
title: "Key-value source"
description: "Wrap any Key-Value driver as a source, powered by unstorage."
canonical_url: "https://content.comark.dev/sources/unstorage"
---
# Key-value source

> Wrap any Key-Value driver as a source, powered by unstorage.

The [`unstorage`](https://unstorage.unjs.io) source is a generic adapter that supports 20+ popular drivers such as Redis, S3, and more. Use it directly when you need a backend the built-ins don't cover.

## `unstorageSource(options)`{lang="ts"}

Wraps any [unstorage driver](https://unstorage.unjs.io/drivers) as a source.

```ts [content.ts]
import { comarkContent } from 'comark-content'
import unstorageSource from 'comark-content/sources/unstorage'
import s3Driver from 'unstorage/drivers/s3'

const content = comarkContent({
  source: unstorageSource({
    driver: s3Driver({
      accessKeyId: process.env.AWS_ACCESS_KEY!,
      secretAccessKey: process.env.AWS_SECRET_KEY!,
      bucket: 'acme-content',
      region: 'us-east-1',
    }),
  }),
})
```

**Parameters:**

- `options` - Configuration object.

**Returns:** [`Source`](https://content.comark.dev/reference/types/content#source). Pass it to `comarkContent({ source })`.

## Options

The `options` argument accepts:

| Option                        | Type         | Default     | Description                                                  |
| ----------------------------- | ------------ | ----------- | ------------------------------------------------------------ |
| [`driver`](#options-driver)   | `Driver`     | -           | Any unstorage driver. Required.                              |
| [`prefix`](#options-prefix)   | `string`     | `undefined` | Prepended to each entry's `path` (for example `/blog`).      |
| [`exclude`](#options-exclude) | `string[]`   | `undefined` | Picomatch globs of keys to drop before parsing.              |
| [`schema`](#options-schema)   | `JsonSchema` | `undefined` | Explicit document `data` schema for types and query columns. |

### `driver`

Any [unstorage driver](https://unstorage.unjs.io/drivers). The source delegates key listing, raw reads, and watching to the driver. Text reads prefer `getItemRaw()` and decode its bytes so formats such as JSON reach their Content parser without unstorage deserializing them first; a missing key remains `null`. See [Compatible drivers](#compatible-drivers) for the common ones.

### `prefix`

Prepended to every entry's public `path`. The prefix does **not** affect [`meta.key`](https://content.comark.dev/guide/files-and-paths#internal-identifiers) (which stays `<instance name>/<file key>`), only the public `path` returned by [`content.get()`](https://content.comark.dev/reference/content/get) and [`content.list()`](https://content.comark.dev/reference/content/list).

### `exclude`

Picomatch globs matched against each entry's key. Excluded files are dropped **before** parsing, so they never enter the manifest:

```ts [drafts.ts]
unstorageSource({
  driver: s3Driver({ /* ... */ }),
  exclude: ['drafts/**', '**/*.draft.md'],
})
```

### `schema`

An explicit JSON Schema for the source's `data`. When set, the Content instance reports Markdown, JSON, and YAML frontmatter validation issues during source load and on direct updates, and uses the schema to generate types and query columns.

## Compatible drivers

Every [unstorage driver](https://unstorage.unjs.io/drivers) works. The most common ones to use with Comark Content:

::card-group{cols="3"}
  :::card{icon="i-simple-icons-amazons3" title="S3 / R2"}
  Object storage with prefix-based listing.
  :::

  :::card{icon="i-simple-icons-cloudflare" title="Cloudflare KV"}
  Durable KV for edge workers.
  :::

  :::card{icon="i-lucide-globe" title="HTTP"}
  Read-only over any HTTP endpoint that lists keys.
  :::

  :::card{icon="i-simple-icons-redis" title="Redis"}
  Drop-in for ephemeral or self-hosted Redis.
  :::

  :::card{icon="i-lucide-database" title="In-memory"}
  Ideal for tests and seed fixtures.
  :::

  :::card{icon="i-simple-icons-vercel" title="Vercel Runtime Cache"}
  Managed key-value on Vercel.
  :::
::

::callout{to="https://unstorage.unjs.io/drivers"}
Check out all the supported drivers for `unstorage`.
::

## Refresh

Unlike the filesystem driver, most remote unstorage drivers don't emit change events. The manifest is built when the Content instance initializes and won't pick up new or changed files on its own, so refresh it on your own schedule rather than relying on a watcher.

If you keep a [cache](https://content.comark.dev/advanced/caching), give it a `ttl`. With stale-while-revalidate on (the default) this is lazy, not a timer: the next read of an entry older than `ttl` serves the cached (stale) copy immediately and re-reads the **source** in the background, so fresh content lands on the following read. Nothing refreshes until the content is read again after it ages out.

```ts [ttl.ts]
const content = comarkContent({
  source: unstorageSource({ driver }),
  cache: { ttl: 60000 },  // after 60s, the next read serves stale and refreshes in the background
})
```

Or refresh explicitly from a webhook or admin action. You can re-read the whole source, or drop a single entry so the next read re-parses it:

```ts [refresh.ts]
import { content } from './content'

await content.refresh() // re-read every entry, add/remove index entries
await content.cache.invalidate('default:posts/hello.md') // drop one entry (key is `<name>:<path-in-source>`)
```

See the [caching docs](https://content.comark.dev/advanced/caching) for TTL and stale-while-revalidate, and the [snapshot source](https://content.comark.dev/sources/snapshot) for shipping stored content.

::note{to="https://content.comark.dev/reference/content/watch"}
If a driver does implement `watch` (the filesystem driver does, and custom drivers can), [`content.watch()`](https://content.comark.dev/reference/content/watch) picks it up automatically. See watch API reference for the full event model.
::


## Sitemap

See the full [sitemap](https://content.comark.dev/sitemap.md) for all pages.
