Skip to main content

Connecting to Supabase

📚
Prerequisites

This guide assumes you've completed the Node.js Quickstart. You should understand how to define tools with @DaemoFunction and register services.

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.

1
Daemo Agent

Receives user question (e.g. "Find notes about pricing")

→
2
Your Function

Converts question to embedding & queries Supabase

→
3
Supabase

Returns relevant rows via SQL or Vector Search

Prerequisites​

  1. A Supabase project with the pgvector extension enabled.
  2. 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);

Warning

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)​

src/services/crmFunctions.ts
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.

src/services/crmFunctions.ts (add to CrmFunctions class)
@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.

Supabase Dashboard → SQL Editor → New Query
-- 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:

  1. Generate an embedding for the user's query (using OpenAI/Cohere/etc)
  2. Call the Supabase RPC function

Add this method to your CrmFunctions class:

src/services/crmFunctions.ts (add method)
// ... 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;
}

Info

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.

src/services/crmFunctions.ts (add to CrmFunctions class)
@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).

src/services/daemoService.ts
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:

.env
# Daemo
DAEMO_AGENT_API_KEY=your_agent_api_key_from_app_daemo_ai
DAEMO_GATEWAY_URL=https://engine.daemo.ai: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​

  1. Use Schemas: Always use Zod schemas (inputSchema/outputSchema) to define your data. This helps the AI understand the shape of your database records.
  2. Limit Results: Always add .limit() to your queries to prevent overflowing the AI's context window with thousands of rows.
  3. 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.
  4. Environment Variables: Store SUPABASE_URL and 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.

src/scripts/gen_hash.ts
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)

Tip

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​