---
name: skimmerdb
description: Store, search and analyze data in SkimmerDB (skimmerdb.com), a hosted database on object storage with namespaces, exact and regex search, metadata filters, vector and hybrid search, and SQL. Use when the user wants to give an app or agent memory or context, index documents, logs, traces or events, search them, run analytics over them, or import CSV, NDJSON or Parquet into SkimmerDB, or when code calls api.skimmerdb.com, the skimmerdb Python or npm package, or the SkimmerDB MCP server.
---

# SkimmerDB

A hosted database: a **namespace** per user, agent, task or dataset; **documents**
`{"id", "text", "meta", "vector"}`; exact and regex search over `text`, filters over `meta`,
nearest-vector search, and SQL over `meta` as typed columns. Namespaces live in S3 and cost
nothing while idle, so making many is normal.

Full docs: https://docs.skimmerdb.com (the data model: https://docs.skimmerdb.com/guides/data-model;
analytics: https://docs.skimmerdb.com/guides/analytics). All of it as text:
https://skimmerdb.com/llms-full.txt. OpenAPI: https://api.skimmerdb.com/openapi.json.

## Connecting

Pick whichever is available, in this order:

1. **MCP tools** (if a `skimmerdb` MCP server is connected: tools named `search`,
   `write_documents`, `sql_query`, ...). Use them directly.
2. **REST** with a key: base `https://api.skimmerdb.com/v1`, header
   `Authorization: Bearer $SKIMMERDB_KEY`, JSON bodies.
3. **Clients** in the user's code: `pip install skimmerdb` (`SkimmerDB(key).namespace("x")`,
   `.write([...])`, `.search("q", filter={...})`), `npm install skimmerdb` (the same names).
   Both read `SKIMMERDB_KEY` from the environment.

No key? Ask the user for one (or to connect the MCP server:
`claude mcp add --transport http skimmerdb https://api.skimmerdb.com/mcp`). Never print a key
back, write it into a file that's committed, or put it in a URL. Give apps and agents
narrow keys: `POST /v1/keys {"name": "...", "scope": "read" | "write", "namespaces": ["mem-*"]}`.

## Modeling: decide these before writing

1. **Namespaces: one query reads one namespace.** Split by what you'd delete, scope a key to,
   or keep apart (`mem-<user>`, `customer-<id>`, `agent-7-run-42`). Keep together what one
   query must aggregate (all events in `events`, with `user` / `agent` as metadata). Names:
   1-64 of `a-z 0-9 - _ .`. The first write to a name creates it (`"created": true` in the
   answer: check it to catch typos).
2. **Ids: stable natural keys** (`ticket-1042`, `docs/install.md`, `agent-1/run-42/step-3`).
   Writing an id again replaces the document, so retries are safe. Events need unique ids.
   Up to 512 bytes, slashes allowed.
3. **`text`** = what gets searched (`q` matches only `text`, never ids or meta). Up to 2 MB.
4. **`meta`** = what gets filtered, grouped, sorted. Up to 16 KB. Rules:
   - flat top-level keys with scalar values (`"user_plan": "pro"`, not `{"user": {"plan": ...}}`);
   - one type per key, everywhere (never `3` here and `"high"` there);
   - times as **integer seconds since 1970** (`"ts": 1760572800`): filter ranges and
     `DATE_TRUNC` need numbers; ISO strings are only text;
   - tags you filter on as booleans (`"tag_urgent": true`): an array never matches one element;
   - leave a key out instead of writing `0` / `""` placeholders (missing = SQL NULL);
   - `id` and keys starting with `#` are reserved.
5. **`vector`**: one embedding model per namespace (its length is fixed at creation; cosine by
   default). Either send your own, or let SkimmerDB embed `text`: create the namespace with
   `"embed": {"provider": "openai" | "voyage" | "gemini" | "cohere"}` (the tenant's own key,
   stored by the user in the dashboard) or `{"provider": "skimmerdb"}` (hosted, at cost), then
   search with `"semantic": "..."`. A value searched by both text and filter goes in both
   `text` and `meta`.

## Writing

```sh
B=https://api.skimmerdb.com/v1; H="Authorization: Bearer $SKIMMERDB_KEY"
curl -s $B/namespaces/support/docs -H "$H" -d '{"docs": [
  {"id": "t-1", "text": "Refund requested for order ACME-1042", "meta": {"status": "open", "priority": 3, "ts": 1760572800}}
], "deletes": ["t-0"]}'
# -> {"written": 1, "deleted": 1, "version": 3, "stored_bytes": 2817, "created": false}
```

- Batch: up to 10,000 documents / 64 MB a request; a request is atomic.
- `PATCH .../docs` merges `meta` key by key (`null` removes a key); text replaced if given.
- `POST .../delete {"ids": [...]}` deletes; `DELETE /v1/namespaces/{name}` drops everything.
- `"wait": true` only when the next step reads what was just written.
- Files: `POST .../import?format=csv|ndjson|parquet&id=<col>&text=<col,col>` with
  `--data-binary @file` (1M rows / 2 GB a request, all rows visible together).
- `429 write_busy`: wait `Retry-After` seconds, resend the same batch. `503 write_unknown`:
  resend it (writes are by id).

## Reading

```sh
# exact text (literal, case-insensitive), filtered; newest first
curl -s $B/namespaces/support/search -H "$H" -d '{"q": "acme-1042", "filter": {"status": "open"}}'
# regex (RE2; case-sensitive unless (?i)) and a numeric range
curl -s $B/namespaces/support/search -H "$H" -d '{"q": "E[0-9]{3}", "regex": true, "filter": {"priority": {"gte": 2}}, "limit": 50}'
# list with a filter (empty q); page with "next" -> "cursor"
curl -s $B/namespaces/support/search -H "$H" -d '{"q": "", "filter": {"status": ["open", "pending"]}, "limit": 100}'
# by meaning, in a namespace with an embedding model (embed); or with your own vector
curl -s $B/namespaces/kb/search -H "$H" -d '{"semantic": "how do I get reimbursed", "filter": {"topic": "billing"}, "limit": 5}'
# nearest vectors, optionally hybrid with q and filter
curl -s $B/namespaces/notes/search -H "$H" -d '{"vector": [0.1, ...], "q": "timeout", "filter": {"team": "api"}, "limit": 10}'
# exact count; one document
curl -s $B/namespaces/support/count -H "$H" -d '{"q": "refund", "filter": {"status": "open"}}'
curl -s $B/namespaces/support/docs/t-1 -H "$H"
```

- Filter operators: equal (`"k": v`), one of (`[...]` or `{"in": [...]}`), `ne`, ranges
  `gt gte lt lte` (numbers only). Every key must hold.
- Hits carry `id`, `match`, `line` (the matched line), `meta`, `written_at`, not the whole
  text: `GET .../docs/{id}` for that.
- Give a regex a literal of 3+ characters or a class like `[0-9]`; a pattern the index can't
  narrow (`.*x$`) is refused on big namespaces (422 `scan_refused`) unless `"scan": true`.
- `partial: true` means some hits may be missing (a storage read failed or the time limit
  passed): say so rather than treating the answer as complete.
- Before a burst of queries on a namespace: `POST .../warm {"queries": ["..."]}`.

## SQL (analytics)

`POST .../sql {"sql": "..."}` (MCP: `sql_query`). Columns are `id` plus every top-level meta
key; run `SHOW COLUMNS` first when you don't know the schema.

```sql
SELECT tool, COUNT(*) AS calls, SUM(CASE WHEN ok = 0 THEN 1 ELSE 0 END) AS errors, AVG(latency_ms) AS avg_ms
FROM docs WHERE ts >= 1760572800 GROUP BY tool ORDER BY calls DESC

SELECT DATE_TRUNC('hour', ts) AS hour, COUNT(*) AS n FROM docs GROUP BY hour ORDER BY hour

SELECT agent, COUNT(*) AS n FROM docs WHERE grep(text, 'E5[0-9]{2}') GROUP BY agent ORDER BY n DESC
```

- One `SELECT ... FROM docs`: `WHERE GROUP BY HAVING ORDER BY LIMIT OFFSET`, `COUNT`,
  `COUNT(DISTINCT)`, `SUM AVG MIN MAX`, `CASE WHEN`, `COALESCE`, `LIKE`, `IN`, `IS NULL`,
  `DATE_TRUNC('minute'|'hour'|'day', x)`, `EXTRACT(MINUTE FROM x)`, `REGEXP_REPLACE`, `LENGTH`
  (bytes). `contains(text, '...')` and `grep(text, 'regex')` in `WHERE` use the text index.
- **Not supported yet:** joins, subqueries, `UNION`, CTEs, window functions, `*` `/` `%`,
  `BETWEEN`, `SELECT DISTINCT` (use `GROUP BY`), `STDDEV`/`MEDIAN`. Compute ratios in code from
  counts; for joins, query each side and combine in code.
- Booleans are `1` / `0`; text compared with a number is an error. Answers stop at 10,000 rows
  (`truncated: true`). `text` itself isn't a column.

## Recipes

- **Agent memory:** namespace `mem-<user>`; id `<user>/<topic or message id>`; text the fact or
  message; meta `{"kind": "preference" | "fact" | "episode", "ts": ..., "source": ...}`; recall
  with `q` (exact names, ids, error codes) and, with embeddings, `vector` + `q` hybrid.
- **Agent analytics / logs:** one `events` namespace; id `<agent>/<run>/<step>`; text the call
  and its error; meta `ts, agent, tool, ok, latency_ms, tokens`; SQL as above.
- **Knowledge base / RAG:** one namespace per corpus; id the source path plus chunk number
  (`docs/install.md#3`); text the chunk; meta `{"source", "section", "updated"}`; vectors for
  meaning, `q` for exact terms, both together for hybrid.

## Errors

Every error is `{"error": "a sentence", "code": "a_word"}`. 401 bad key; 403 the key's scope or
namespaces don't allow it; 404 no such namespace or document (or one the key can't see);
402 the free plan's usage or namespace count is used up; 422 invalid (read `error`, and
`fields`); 429 rate limits (`Retry-After`); 503 retry; 507 storage quota full.
