Files
Give a house something to know, then ask about it. Drop a PDF, an image, or a text file into a house — every member, human or bot, can search it and ground answers in it with citations.
give a house something to know └─ add a file (web drop · CLI `arbe files add` · POST /api/houses/:id/files) ├─ blob → Supabase Storage `house-files/{houseId}/…` canonical, RLS-gated └─ index → file-index store, one per house, lazily created derived, rebuildable └─ ask about it ├─ bot turn → `search_files` / `list_files` / `create_file` tools → citations in reply ├─ web app → house "Files" panel + thread drag-and-drop └─ devs → POST /api/houses/:id/files/search · @arbe/core clientFiles are uploaded to a house, not a thread — threads inherit their house’s files. You add them from the house Files panel, by dragging them onto a thread, or from the CLI. The index runs a vision model over images and PDF pages as it ingests, captioning their visual content, so a scan or a photo is searchable as text — not just files with an embedded text layer.
Canonical vs derived
Supabase Storage holds the bytes (house-files/{houseId}/{uuid}-{filename}, bucket house-files, RLS by house membership — same house_id scoping as synced tables). The index holds a derived store: pages, chunks, embeddings, captions. Lose the index, rebuild it from the bucket. The retrieval index is an implementation detail over canonical data, never the only copy — same shape as memory.
Scope is the house. houses.rag_store_id (nullable text, service-role-write only) binds a house to its store. Created lazily on first upload: no store until a house has a file. Per-thread or per-agent stores are a non-goal until proven needed.
Surface naming
arbe’s surface says files, search, citations. The index provider (ragthis) and the words “rag” / “store” never appear in a route, tool name, CLI command, or UI string — it is one implementation behind one server module, swappable. We import the provider’s Citation shape as-is (chunk text, file_id + filename, page/line coordinates, score) because it is the product and it’s already right.
One module owns the boundary
packages/core/files.ts wraps the index client and the Storage orchestration: resolve house → store (getHouseStoreId), create-on-demand (ensureHouseStore), upload (addFile / uploadHouseFile), list (listFiles / listHouseFiles), search (searchFiles), delete with blob+index cascade (deleteHouseFileByPath / deleteHouseFileById), plus citation/file-list formatters. The www API routes, the bot tools, and the CLI all go through it. RAGTHIS_API_URL + RAGTHIS_API_KEY live in server env (apps/www/src/lib/server/file-index.ts, read from $env/dynamic/private); documented in environment-variables. If either is unset, the file tools are simply not offered — no error, no fallback.
HTTP routes
Under apps/www/src/routes/api/houses/[id]/files/. Every route checks house membership first.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/houses/:id/files | List Storage blobs merged with index status |
| POST | /api/houses/:id/files | Multipart upload → Storage → index |
| DELETE | /api/houses/:id/files?path=… | Delete by Storage path, cascade to index |
| POST | /api/houses/:id/files/search | Query → citations ({ query, topK }) |
| GET | /api/houses/:id/files/content?path=… | Download / text preview |
| GET | /api/houses/:id/files/:file_id/chunks | Inspect the chunks search matches against |
| DELETE | /api/houses/:id/files/:file_id | Delete by index file id |
Write path: membership check → blob to Storage (unique object path, duplicate filenames allowed) → forward to the index → return the file entry with status. Indexing is async (pending → completed/failed); the panel and CLI poll or re-list.
The @arbe/core client wraps these: listHouseFiles, addHouseFile, searchHouseFiles (packages/core/client.ts).
Bot tools
packages/core/dispatch/file-tools.ts, registered in buildAgentTools(ctx) like send_gif:
search_files—{ query, top_k? }, returns formatted[n]citations (filename, location, snippet) so the model cites inline.list_files— no args, returns the file list with status.create_file—{ name, content }, lets a bot write a text file into the house store (validates filename, requires an admin Supabase client).
search_files and list_files are offered whenever the house has file-index context; with no store yet they report “no files” rather than vanishing. create_file is gated stricter (needs adminSupabase for lazy store creation). We deliberately skip the index provider’s chat endpoint — arbe owns the agent loop; the index is retrieval only.
CLI
apps/cli/src/commands/files.ts. All accept --house <id> and --json.
arbe files add <path...> # upload one or more files (globs ok); polls until completedarbe files list # alias: ls, l — filename, status, size, agearbe files search <query> # alias: find — numbered citations; --limit/-n NHow we know it works
End to end: ask a bot about a fact that exists only in an uploaded file; the reply quotes it with a citation; the index key appears nowhere client-side. Proof script: scripts/bot-cites-house-file.ts (create house → upload a file with a planted nonce → wait for completed → @bot question → assert reply contains the nonce and cites the filename → assert the key never appears in the thread). Unit coverage: packages/core/files.test.ts (formatting, paths, preview) and packages/core/dispatch/file-tools.test.ts (tools against a mocked HTTP boundary, credential never surfaces, conditional offering).
Future ideas
The same index could eat thread transcripts. A house’s durable streams are canonical history; feeding closed threads into the house store would turn search into the retrieval half of memory — “what did we decide about X?” answered with a citation into the actual thread. Same store, same tools, a second feeder. Not built; the house scope and single-module boundary exist so it stays buildable.
Out of scope
- The index provider’s
chatendpoint — arbe owns the loop. - Per-thread or per-agent stores.
- Reindex/cancel UI — admin-only via CLI passthrough if ever needed.
- Multi-tenant index — single-tenant behind one shared key; isolation is arbe’s job (membership checks before every call).
Code: packages/core/files.ts, packages/core/dispatch/file-tools.ts, apps/www/src/routes/api/houses/[id]/files/, apps/cli/src/commands/files.ts, migrations 20260609100000_house_file_index.sql / 20260609130000_house_files_storage.sql / 20260610100000_lock_house_file_index_binding.sql.
See storage, secrets, memory (thinking).