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=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

  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