---
title: Stores and connectors
description: Stores save conversations so they survive refreshes and restarts. Connectors give an agent safe access to your data.
---

Loom keeps two jobs separate:

- A **store** saves threads and messages.
- A **connector** exposes an external system to the agent as tools.

Both are optional imports. Anything you do not import is never bundled.

## Stores

A store saves every thread when a reply finishes. `useAgent` loads it again on the next page load, so a refresh or a server restart picks up the conversation where it left off.

### SQLite

`sqliteStore()` saves threads in a SQLite file. It uses Node's built-in `node:sqlite`, so there is nothing to install.

```ts agents/support/agent.ts
import { defineAgent } from "useloom";
import { sqliteStore } from "useloom/sqlite";

export default defineAgent({
  model,
  store: sqliteStore(), // .loom/loom.db
});
```

| Option | Default | |
| --- | --- | --- |
| `path` | `.loom/loom.db` | Database file, created if missing. `sqliteStore("data/chat.db")` is shorthand. |
| `table` | `loom_threads` | Table for threads, created if missing. |
| `database` | | An open database to use instead of `path`: `node:sqlite`'s `DatabaseSync` or a better-sqlite3 instance. |

Requirements and limits:

- **Node 22.13 or newer**, for `node:sqlite`. On older Node, pass a better-sqlite3 database as `database`.
- **Local disk.** The file lives on the machine running your app. That suits development, a VPS, or a container with a volume. On serverless hosts the filesystem is temporary, so threads would not survive between requests there.
- **Shut down cleanly** with `await store.close()` if your server handles shutdown signals.

### In memory

`memoryStore()` keeps threads in memory: fine for tests, but threads are lost on restart.

```ts
import { defineAgent, memoryStore } from "useloom";

defineAgent({ model, store: memoryStore() });
```

### Loading threads

With a store, `GET /api/agents/<name>?threadId=<id>` returns a saved thread, and `useAgent` calls it for you. When the agent has an `auth` hook, threads are scoped to the signed-in user. Without `auth`, the thread id is what keeps a conversation private, so Loom generates 128-bit random ids.

## Supabase connector

`supabaseTools()` gives an agent read access to the tables you list, and optionally write access.

```ts agents/support/agent.ts
import { createServerClient } from "@supabase/ssr";
import { defineAgent } from "useloom";
import { supabaseTools } from "useloom/supabase";

export default defineAgent({
  model,
  connectors: [
    supabaseTools({
      client: ({ request }) => createServerClient(url, anonKey, { cookies: cookiesFrom(request) }),
      tables: {
        orders: { description: "Customer orders", columns: ["id", "status", "total", "created_at"] },
        products: { columns: ["id", "name", "price"] },
        tickets: { write: true },
      },
    }),
  ],
});
```

The agent gets these tools:

| Tool | Available for | Does |
| --- | --- | --- |
| `supabaseQuery` | every listed table | Select columns, filter, order, and limit rows |
| `supabaseInsert` | tables with `write: true` | Insert up to 20 rows, after the user approves |
| `supabaseUpdate` | tables with `write: true` | Update rows matching at least one filter, after the user approves |

The model writes these queries, so the connector is conservative:

- **Your client, your user.** `client` runs per request. Create it with the signed-in user's session so your row level security policies apply. Loom never creates a client itself; a service-role client bypasses RLS and is only used if you pass one.
- **Allowlisted tables.** The agent sees only the tables you name.
- **Plain columns only.** Column names must be simple identifiers, and must be in `columns` when you set it. Relation embeds, JSON paths, and casts are refused, so a query cannot reach a table you did not list.
- **Writes need approval.** Each insert and update waits for the user to [approve](/docs/agents#approvals) it. Pass `approveWrites: false` to turn that off.
- **Bounded.** Queries return at most `maxRows` rows (default 50). Updates without a filter are refused.
- **Errors go to the model.** A database or RLS error comes back as `{ error }` so the agent can explain it, instead of failing the request.

Install `@supabase/supabase-js` (or `@supabase/ssr`) yourself; Loom does not bundle it.

## Your own connectors

A connector is a named set of tools. Use `connector()` to package your own:

```ts
import { connector, tool } from "useloom";

export const billing = connector("billing", {
  getInvoice: tool({ description: "...", input, run }),
});
```

Tool names must be unique across an agent's `tools/`, `agent.ts`, and connectors. Loom throws at startup when two collide.
