Connecting to Supabase
Learn how to build an agent that connects to your Supabase PostgreSQL database, including how to implement semantic search using pgvector.
Overview
Since Daemo functions are just standard code, connecting to Supabase is as simple as using the standard @supabase/supabase-js client.
Receives user question (e.g. "Find notes about pricing")
Converts question to embedding & queries Supabase
Returns relevant rows via SQL or Vector Search
Prerequisites
- A Supabase project with the
pgvectorextension enabled. - The necessary libraries:
npm install @supabase/supabase-js daemo-engine zod axios
## Project Structure
Here's where each file goes in a typical Daemo + Supabase project:
your-project/ ├── src/ │ ├── app.ts # Express server + Daemo init │ ├── services/ │ │ ├── daemoService.ts # DaemoBuilder registration │ │ └── crmFunctions.ts # Your @DaemoFunction methods │ └── utils/ │ └── supabaseClient.ts # Supabase connection singleton ├── .env # API keys (never commit!) ├── package.json └── tsconfig.json
<Tip>
Each code block below shows its file path in the header. Copy each file to the correct location.
</Tip>
## 1. Setup the Client
Create a singleton client instance to reuse across your functions.
```typescript title="src/utils/supabaseClient.ts"
import { createClient } from '@supabase/supabase-js';
import * as dotenv from 'dotenv';
dotenv.config();
const supabaseUrl = process.env.SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_KEY!; // Service role key for backend access
export const supabase = createClient(supabaseUrl, supabaseKey);
Security Note: Since your agent runs on the backend, use the service_role key if you need to bypass Row Level Security (RLS) or act as an admin. If acting on behalf of users, handle authentication tokens carefully.
2. Basic CRUD Operations
Here is a pattern for exposing standard database operations to the AI.
Reading Data (GET)
import { DaemoFunction } from 'daemo-engine';
import { supabase } from '../utils/supabaseClient';
import { z } from 'zod';
export class CrmFunctions {
@DaemoFunction({
description: "Get all contacts from the CRM system.",
inputSchema: z.object({
owner_id: z.string().uuid().optional().describe("Filter by owner ID"),
limit: z.number().default(10).describe("Max records to return")
}),
outputSchema: z.array(z.object({
id: z.string(),
email: z.string(),
first_name: z.string()
}))
})
async getAllContacts(input: { owner_id?: string; limit: number }) {
let query = supabase.from("contacts").select("*").limit(input.limit);
if (input.owner_id) {
query = query.eq("owner_id", input.owner_id);
}
const { data, error } = await query;
if (error) throw new Error(`Database error: ${error.message}`);
return data;
}
}
Search (Filter)
Allowing the AI to search with ILIKE is powerful for fuzzy matching names or emails.
@DaemoFunction({
description: "Search contacts by email (partial match supported).",
inputSchema: z.object({
email: z.string().describe("Partial email to search for")
})
})
async searchContacts(input: { email: string }) {
const { data, error } = await supabase
.from("contacts")
.select("*")
.ilike("email", `%${input.email}%`);
if (error) throw new Error(error.message);
return data;
}
3. Advanced: Semantic Search (Vector Embeddings)
One of the most powerful features you can add to an agent is semantic search — allowing users to find records by meaning ("Find notes about pricing") rather than exact keywords.
Step A: Database Setup (SQL)
Run this in your Supabase SQL Editor to enable pgvector and create a search function.
-- 1. Enable extension
create extension if not exists vector;
-- 2. Create table with vector column (1536 dim for OpenAI Ada-002)
create table notes (
id uuid primary key default uuid_generate_v4(),
owner_id uuid, -- For multi-tenancy
content text,
embedding vector(1536),
created_at timestamp with time zone default now()
);
-- 3. Create an index for performance (Recommended for production)
create index on notes using ivfflat (embedding vector_cosine_ops)
with (lists = 100);
-- 4. Create a search function
create or replace function search_notes(
query_embedding vector(1536),
match_threshold float,
match_count int,
p_owner_id uuid default null
)
returns table (
id uuid,
content text,
similarity float
)
language plpgsql
as $$
begin
return query
select
n.id,
n.content,
1 - (n.embedding <=> query_embedding) as similarity
from notes n
where (p_owner_id is null or n.owner_id = p_owner_id)
and 1 - (n.embedding <=> query_embedding) > match_threshold
order by n.embedding <=> query_embedding
limit match_count;
end;
$$;
Step B: The Function
Your agent needs to:
- Generate an embedding for the user's query (using OpenAI/Cohere/etc)
- Call the Supabase RPC function
Add this method to your CrmFunctions class:
// ... imports
import axios from 'axios';
// Helper: Get embedding from OpenAI
async function getEmbedding(text: string) {
const resp = await axios.post(
"[https://api.openai.com/v1/embeddings](https://api.openai.com/v1/embeddings)",
{ input: text, model: "text-embedding-ada-002" },
{ headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` } }
);
return resp.data.data[0].embedding;
}
// Add inside CrmFunctions class
@DaemoFunction({
description: "Search notes by meaning (semantic search). Use this when the user asks vague questions like 'What did we discuss about pricing?'",
inputSchema: z.object({
query: z.string().describe("The natural language search query"),
owner_id: z.string().uuid().optional().describe("Filter by owner ID")
})
})
async searchNotes(input: { query: string; owner_id?: string }) {
// 1. Vectorize the query
const embedding = await getEmbedding(input.query);
// 2. Prepare RPC call parameters
const rpcParams: any = {
query_embedding: embedding,
match_threshold: 0.7, // Only relevant matches
match_count: 5
};
if (input.owner_id) {
rpcParams.p_owner_id = input.owner_id;
}
// 3. Call Supabase RPC
const { data, error } = await supabase.rpc("search_notes", rpcParams);
if (error) throw new Error(error.message);
return data;
}
Why this matters: This pattern turns your database into a "Long Term Memory" for your agent. The AI can now recall specific details from thousands of documents instantly.
Relationships (Joins)
Supabase handles joins efficiently. You can fetch related data in a single tool call to give the AI more context.
@DaemoFunction({
description: "Get a deal and its associated contacts.",
inputSchema: z.object({
deal_id: z.string().uuid().describe("The deal's unique ID")
})
})
async getDealWithContacts(input: { deal_id: string }) {
const { data, error } = await supabase
.from("deals")
.select(`
*,
contacts:deal_contacts(contact_id) -- Join through junction table
`)
.eq("id", input.deal_id)
.single();
if (error) throw new Error(`Deal not found: ${error.message}`);
return data;
}
Registering Your Service
Don't forget to register your functions with Daemo. We recommend passing your service instances into the initialization function (Dependency Injection).
import { DaemoBuilder, SessionData } from "daemo-engine";
import { CrmFunctions } from "./crmFunctions";
const systemPrompt = `You are a helpful CRM assistant.
You can help users:
- Search and manage contacts
- Track deals through the pipeline
- Search notes by meaning (semantic search)
Always validate IDs before operations and provide clear error messages.`;
export function initializeDaemoService(crmFunctions: CrmFunctions): SessionData {
const builder = new DaemoBuilder()
.withServiceName("crm_service")
.withSystemPrompt(systemPrompt);
// Register your function class instance
builder.registerService(crmFunctions);
return builder.build();
}
Environment Variables
Create a .env file:
# Daemo
DAEMO_AGENT_API_KEY=your_agent_api_key_from_app_daemo_ai
DAEMO_GATEWAY_URL=localhost:50052
# Supabase
SUPABASE_URL=[https://your-project.supabase.co](https://your-project.supabase.co)
SUPABASE_KEY=your_service_role_key
# OpenAI (for semantic search embeddings)
OPENAI_API_KEY=sk-...
Best Practices
- Use Schemas: Always use Zod schemas (
inputSchema/outputSchema) to define your data. This helps the AI understand the shape of your database records. - Limit Results: Always add
.limit()to your queries to prevent overflowing the AI's context window with thousands of rows. - Handle Errors: Throw descriptive errors. If a record isn't found, throw
new Error("Contact not found"). The AI can read this error and tell the user. - Environment Variables: Store
SUPABASE_URLand keys in.env, never in your code.
Useful Utility Scripts
The reference project includes utility scripts in src/scripts that help with development tasks. These are optional but can speed up your workflow.
1. Password Hashing (gen_hash.ts)
When seeding your database with initial users, you'll need to store hashed passwords, not plain text. This script generates a bcrypt hash for a given password.
import bcrypt from "bcryptjs";
(async () => {
const password = "password"; // Replace with your desired password
const salt = await bcrypt.genSalt(10);
const password_hash = await bcrypt.hash(password, salt);
console.log(password_hash);
})();
Usage:
npx ts-node src/scripts/gen_hash.ts
# Output: $2a$10$abcdef... (copy this into your SQL insert statement)
In a real production app, you would have a registration endpoint (/auth/register) handling this. This script is strictly for manually creating admin/test users via SQL.
Next Steps
- Test Your Agent — CLI scripts, Playground, and debugging guides
- SF 311 Example — Complete API project structure
- Defining Tools — More on Zod schemas and decorator options
- External APIs — Connect to REST/GraphQL APIs