Purpose: Rapid reference guide for LLMs building applications that interact with the vCon database.
Read This If: You need to quickly understand how to create, query, search, and manage vCon data programmatically.
What is this database?
- PostgreSQL database storing IETF-compliant vCon (Virtual Conversation) data
- Normalized relational structure (8 core tables + extensions)
- Built-in full-text search, semantic search, and tag filtering
- Multi-tenant support with Row Level Security (RLS)
- Optional Redis caching for performance
Main Tables:
vcons- Conversation containers (parent)parties- Participantsdialog- Conversation segmentsanalysis- AI/ML resultsattachments- Files and metadatagroups- vCon referencesvcon_embeddings- Semantic search vectors
Access Methods:
- Direct SQL queries (PostgreSQL)
- Supabase client (TypeScript/JavaScript)
- MCP tools (for AI assistants)
- REST API (via Supabase)
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
);
// Step 1: Create vCon
const { data: vconData, error: vconError } = await supabase
.from('vcons')
.insert({
uuid: '550e8400-e29b-41d4-a716-446655440000',
vcon_version: '0.3.0',
subject: 'Customer Support Call',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.select('id, uuid')
.single();
if (vconError) throw vconError;
// Step 2: Add parties
await supabase.from('parties').insert([
{
vcon_id: vconData.id,
party_index: 0,
name: 'Alice Agent',
mailto: 'alice@support.example.com'
},
{
vcon_id: vconData.id,
party_index: 1,
name: 'Bob Customer',
tel: '+1-555-0100'
}
]);
// Step 3: Add dialog
await supabase.from('dialog').insert({
vcon_id: vconData.id,
dialog_index: 0,
type: 'text',
body: 'Hello, how can I help you today?',
encoding: 'none',
parties: [0, 1]
});
// Step 4: Add analysis
await supabase.from('analysis').insert({
vcon_id: vconData.id,
analysis_index: 0,
type: 'summary',
vendor: 'OpenAI', // REQUIRED field
product: 'GPT-4',
schema: 'v1.0', // Note: 'schema' not 'schema_version'
body: 'Positive customer support interaction regarding billing inquiry.',
encoding: 'none'
});
// Step 5: Add tags (as attachment)
await supabase.from('attachments').insert({
vcon_id: vconData.id,
attachment_index: 0,
type: 'tags',
encoding: 'json',
body: JSON.stringify(['status:open', 'priority:normal', 'category:billing'])
});
console.log('vCon created:', vconData.uuid);-- Step 1: Create vCon
INSERT INTO vcons (uuid, vcon_version, subject, created_at, updated_at)
VALUES (
'550e8400-e29b-41d4-a716-446655440000',
'0.3.0',
'Customer Support Call',
NOW(),
NOW()
)
RETURNING id, uuid;
-- Step 2: Add parties (use returned id from above)
INSERT INTO parties (vcon_id, party_index, name, mailto) VALUES
('...vcon-id...', 0, 'Alice Agent', 'alice@support.example.com'),
('...vcon-id...', 1, 'Bob Customer', NULL);
-- Step 3: Add dialog
INSERT INTO dialog (vcon_id, dialog_index, type, body, encoding, parties)
VALUES (
'...vcon-id...',
0,
'text',
'Hello, how can I help you today?',
'none',
ARRAY[0, 1]
);
-- Step 4: Add analysis
INSERT INTO analysis (vcon_id, analysis_index, type, vendor, product, schema, body, encoding)
VALUES (
'...vcon-id...',
0,
'summary',
'OpenAI',
'GPT-4',
'v1.0',
'Positive customer support interaction regarding billing inquiry.',
'none'
);
-- Step 5: Add tags
INSERT INTO attachments (vcon_id, attachment_index, type, encoding, body)
VALUES (
'...vcon-id...',
0,
'tags',
'json',
'["status:open", "priority:normal", "category:billing"]'
);// Get vCon metadata
const { data: vcon } = await supabase
.from('vcons')
.select('*')
.eq('uuid', vconUuid)
.single();
// Get parties
const { data: parties } = await supabase
.from('parties')
.select('*')
.eq('vcon_id', vcon.id)
.order('party_index');
// Get dialog
const { data: dialog } = await supabase
.from('dialog')
.select('*')
.eq('vcon_id', vcon.id)
.order('dialog_index');
// Get analysis
const { data: analysis } = await supabase
.from('analysis')
.select('*')
.eq('vcon_id', vcon.id)
.order('analysis_index');
// Get attachments
const { data: attachments } = await supabase
.from('attachments')
.select('*')
.eq('vcon_id', vcon.id)
.order('attachment_index');
// Assemble complete vCon
const completeVCon = {
vcon: vcon.vcon_version,
uuid: vcon.uuid,
created_at: vcon.created_at,
updated_at: vcon.updated_at,
subject: vcon.subject,
parties,
dialog,
analysis,
attachments
};const { data: vcon } = await supabase
.from('vcons')
.select('uuid, subject, created_at, updated_at')
.eq('uuid', vconUuid)
.single();// Search across subject, parties, dialog, and analysis
const { data, error } = await supabase.rpc('search_vcons_keyword', {
query_text: 'billing refund',
start_date: null,
end_date: null,
tag_filter: {},
max_results: 50
});
// Result format:
// [
// {
// vcon_id: '...',
// doc_type: 'dialog',
// ref_index: 0,
// rank: 0.875,
// snippet: '...billing <b>refund</b> request...'
// }
// ]// First, generate embedding for query (using OpenAI or similar)
const queryEmbedding = await generateEmbedding('frustrated customers');
// Search using vector similarity
const { data, error } = await supabase.rpc('search_vcons_semantic', {
query_embedding: queryEmbedding,
tag_filter: {},
match_threshold: 0.75,
match_count: 20
});
// Result format:
// [
// {
// vcon_id: '...',
// content_type: 'dialog',
// content_reference: '0',
// content_text: 'Original dialog text...',
// similarity: 0.89
// }
// ]const queryEmbedding = await generateEmbedding('billing issues');
const { data, error } = await supabase.rpc('search_vcons_hybrid', {
keyword_query: 'billing refund',
query_embedding: queryEmbedding,
tag_filter: {},
semantic_weight: 0.6, // 60% semantic, 40% keyword
limit_results: 50
});
// Result format:
// [
// {
// vcon_id: '...',
// combined_score: 0.82,
// semantic_score: 0.85,
// keyword_score: 0.78
// }
// ]// Find vCons with specific tags
const { data, error } = await supabase.rpc('search_vcons_by_tags', {
required_tags: {
status: 'open',
priority: 'high',
category: 'billing'
},
max_results: 100
});
// Returns array of vCon UUIDs// By date range
const { data } = await supabase
.from('vcons')
.select('*')
.gte('created_at', '2025-01-01')
.lte('created_at', '2025-12-31')
.order('created_at', { ascending: false })
.limit(100);
// By subject keyword
const { data } = await supabase
.from('vcons')
.select('*')
.ilike('subject', '%billing%')
.limit(50);
// By party email
const { data } = await supabase
.from('vcons')
.select('*, parties(*)')
.eq('parties.mailto', 'user@example.com')
.limit(50);const { data, error } = await supabase
.from('vcons')
.update({
subject: 'Updated Subject',
updated_at: new Date().toISOString()
})
.eq('uuid', vconUuid);// Get current vCon to find next dialog index
const { data: vcon } = await supabase
.from('vcons')
.select('id')
.eq('uuid', vconUuid)
.single();
const { data: existingDialog } = await supabase
.from('dialog')
.select('dialog_index')
.eq('vcon_id', vcon.id)
.order('dialog_index', { ascending: false })
.limit(1);
const nextIndex = existingDialog[0]?.dialog_index + 1 || 0;
// Insert new dialog
await supabase.from('dialog').insert({
vcon_id: vcon.id,
dialog_index: nextIndex,
type: 'text',
body: 'Additional message...',
encoding: 'none',
parties: [0]
});const { data: vcon } = await supabase
.from('vcons')
.select('id')
.eq('uuid', vconUuid)
.single();
const { data: existingAnalysis } = await supabase
.from('analysis')
.select('analysis_index')
.eq('vcon_id', vcon.id)
.order('analysis_index', { ascending: false })
.limit(1);
const nextIndex = existingAnalysis[0]?.analysis_index + 1 || 0;
await supabase.from('analysis').insert({
vcon_id: vcon.id,
analysis_index: nextIndex,
type: 'sentiment',
vendor: 'AWS',
product: 'Comprehend',
schema: 'v2.0',
body: JSON.stringify({ sentiment: 'POSITIVE', score: 0.92 }),
encoding: 'json'
});// Find existing tags attachment
const { data: tagsAttachment } = await supabase
.from('attachments')
.select('id, body')
.eq('vcon_id', vconId)
.eq('type', 'tags')
.single();
if (tagsAttachment) {
// Update existing tags
const currentTags = JSON.parse(tagsAttachment.body);
const newTags = [...currentTags, 'status:closed', 'resolved:yes'];
await supabase
.from('attachments')
.update({ body: JSON.stringify(newTags) })
.eq('id', tagsAttachment.id);
} else {
// Create new tags attachment
const nextIndex = await getNextAttachmentIndex(vconId);
await supabase.from('attachments').insert({
vcon_id: vconId,
attachment_index: nextIndex,
type: 'tags',
encoding: 'json',
body: JSON.stringify(['status:closed', 'resolved:yes'])
});
}// Cascading delete - removes all related data automatically
const { error } = await supabase
.from('vcons')
.delete()
.eq('uuid', vconUuid);
if (!error) {
console.log('vCon and all related data deleted');
}// Add tenant as attachment during vCon creation
await supabase.from('attachments').insert({
vcon_id: vconData.id,
attachment_index: 0,
type: 'tenant',
encoding: 'json',
body: JSON.stringify({
id: 'acme-corp',
name: 'Acme Corporation'
})
});
// Populate tenant_id column (run once after adding tenant attachments)
await supabase.rpc('populate_tenant_ids_batch', {
p_attachment_type: 'tenant',
p_json_path: 'id',
p_batch_size: 1000
});// Set current tenant for session
await supabase.rpc('set_config', {
setting: 'app.current_tenant_id',
value: 'acme-corp',
is_local: true
});
// Now all queries automatically filter by tenant
const { data } = await supabase.from('vcons').select('*');
// Only returns vCons for 'acme-corp'Common Mistakes to Avoid:
| Wrong | Correct | Notes |
|---|---|---|
schema_version |
schema |
Analysis schema field name |
analysis.vendor optional |
analysis.vendor REQUIRED |
Must always provide vendor |
body as JSONB |
body as TEXT |
All body fields are TEXT type |
parties as parties[] |
parties as INTEGER[] |
Dialog parties are integer array |
| Default encoding values | No default encoding | encoding must be explicitly set or NULL |
Required vs Optional Fields:
| Table | Always Required | Often Optional |
|---|---|---|
vcons |
uuid, vcon_version, created_at, updated_at |
subject, extensions, tenant_id |
parties |
vcon_id, party_index |
All contact fields (tel, mailto, name, etc.) |
dialog |
vcon_id, dialog_index, type |
body, parties, start_time |
analysis |
vcon_id, analysis_index, type, vendor |
body, product, schema |
attachments |
vcon_id, attachment_index |
type, body, encoding |
Valid values: 'base64url', 'json', 'none', or NULL
// Text content - use 'none'
body: 'Hello world',
encoding: 'none'
// JSON content - use 'json'
body: JSON.stringify({ sentiment: 'positive' }),
encoding: 'json'
// Binary content - use 'base64url'
body: 'iVBORw0KGgoAAAANSUhEU...',
encoding: 'base64url'Valid values: 'recording', 'text', 'transfer', 'incomplete'
// Text message
{ type: 'text', body: 'Message content', encoding: 'none' }
// Audio recording
{ type: 'recording', url: 'https://...', mediatype: 'audio/wav' }
// Call transfer
{ type: 'transfer', transferor: 0, transferee: 1 }
// Incomplete recording
{ type: 'incomplete', disposition: 'no-answer' }Valid values: 'join', 'drop', 'hold', 'unhold', 'mute', 'unmute'
Valid values: 'no-answer', 'congestion', 'failed', 'busy', 'hung-up', 'voicemail-no-message'
// Good - limits result set
.limit(100)
// Bad - could return millions of rows
// (no limit)// Good - uses index on uuid
.eq('uuid', '...')
// Good - uses index on created_at
.gte('created_at', '2025-01-01')
// Less efficient - full table scan
.like('subject', '%keyword%') // Use search RPC instead// Good - only fetches needed columns
.select('uuid, subject, created_at')
// Less efficient - fetches everything
.select('*')// Good - optimized full-text search
await supabase.rpc('search_vcons_keyword', { ... })
// Bad - slow LIKE query
.like('body', '%keyword%')// Check cache first
const cached = await redis.get(`vcon:${uuid}`);
if (cached) return JSON.parse(cached);
// Fetch from database
const vcon = await fetchFromDatabase(uuid);
// Store in cache (1 hour TTL)
await redis.setex(`vcon:${uuid}`, 3600, JSON.stringify(vcon));// Good - single query for multiple inserts
await supabase.from('parties').insert([
{ vcon_id, party_index: 0, name: 'Alice' },
{ vcon_id, party_index: 1, name: 'Bob' },
{ vcon_id, party_index: 2, name: 'Charlie' }
]);
// Bad - multiple queries
await supabase.from('parties').insert({ vcon_id, party_index: 0, name: 'Alice' });
await supabase.from('parties').insert({ vcon_id, party_index: 1, name: 'Bob' });
await supabase.from('parties').insert({ vcon_id, party_index: 2, name: 'Charlie' });const { data } = await supabase
.from('vcons')
.select('uuid, subject, created_at')
.gte('created_at', startDate)
.lte('created_at', endDate)
.order('created_at', { ascending: false })
.limit(100);const { data } = await supabase
.from('parties')
.select('vcon_id, vcons(uuid, subject, created_at)')
.or(`mailto.eq.${email},tel.eq.${phone}`)
.limit(50);const { data } = await supabase
.from('analysis')
.select('vcon_id, type, body, vcons(uuid, subject)')
.eq('type', 'transcript')
.limit(50);const { data } = await supabase
.from('vcon_tags_mv')
.select('vcon_uuid, tags_object')
.limit(100);const { data } = await supabase
.rpc('count_vcons_by_date', {
start_date: '2025-01-01',
end_date: '2025-12-31',
interval: 'day'
});Error: analysis.vendor cannot be null
// Wrong
analysis: { type: 'summary', body: '...' }
// Right
analysis: { type: 'summary', vendor: 'OpenAI', body: '...' }Error: column "schema_version" does not exist
// Wrong
analysis: { schema_version: 'v1.0' }
// Right
analysis: { schema: 'v1.0' }Error: invalid input value for enum encoding
// Wrong
encoding: 'text'
// Right
encoding: 'none' // or 'json' or 'base64url'Error: value too long for type character varying(10)
// Wrong
vcon_version: '0.3.0-extended'
// Right
vcon_version: '0.3.0'Error: duplicate key value violates unique constraint
// Wrong - reusing party_index
insert({ vcon_id, party_index: 0, ... })
insert({ vcon_id, party_index: 0, ... }) // Error!
// Right - increment party_index
insert({ vcon_id, party_index: 0, ... })
insert({ vcon_id, party_index: 1, ... })import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL!,
process.env.SUPABASE_ANON_KEY!
);
async function testVConOperations() {
console.log('1. Creating vCon...');
const vconUuid = crypto.randomUUID();
const { data: vcon } = await supabase
.from('vcons')
.insert({
uuid: vconUuid,
vcon_version: '0.3.0',
subject: 'Test vCon',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
})
.select('id, uuid')
.single();
console.log('2. Adding party...');
await supabase.from('parties').insert({
vcon_id: vcon!.id,
party_index: 0,
name: 'Test User',
mailto: 'test@example.com'
});
console.log('3. Adding dialog...');
await supabase.from('dialog').insert({
vcon_id: vcon!.id,
dialog_index: 0,
type: 'text',
body: 'Test message',
encoding: 'none'
});
console.log('4. Retrieving vCon...');
const { data: retrieved } = await supabase
.from('vcons')
.select('*, parties(*), dialog(*)')
.eq('uuid', vconUuid)
.single();
console.log('5. Deleting vCon...');
await supabase.from('vcons').delete().eq('uuid', vconUuid);
console.log('✅ All tests passed!');
}
testVConOperations().catch(console.error);- Read the Full Architecture: See
DATABASE_ARCHITECTURE_FOR_LLMS.md - Check Migration Files:
supabase/migrations/for schema details - Review Query Implementation:
src/db/queries.tsfor production patterns - Test Search Features:
scripts/test-search-tools.ts - Setup Multi-Tenancy:
docs/guide/rls-multi-tenant.md - Enable Caching:
docs/guide/redis-supabase-integration.md
Key Takeaways:
- vCon is a standardized conversation container (IETF spec)
- Database is normalized (8 core tables) for efficient querying
- Use search RPCs, don't write your own full-text search
- Always provide
vendorfield in analysis (required) - Field is
schema, notschema_version - All
bodyfields are TEXT type - Use tags for efficient filtering
- Enable RLS for multi-tenant apps
- Cache with Redis for production performance
- Always use limits and indexes
Most Common Operations:
- Create: Insert into
vcons, then child tables - Retrieve: Join
vconswith child tables - Search: Use
search_vcons_keyword()orsearch_vcons_semantic() - Filter: Use tags via
search_vcons_by_tags() - Update: Update
vconsor insert new child records - Delete: Delete from
vcons(cascades to children)
Performance Checklist:
- ✅ Use
LIMITon all queries - ✅ Filter by indexed fields (uuid, created_at, tenant_id)
- ✅ Use search RPCs for full-text search
- ✅ Cache frequently accessed vCons with Redis
- ✅ Batch insert operations when possible
- ✅ Select only needed columns
You're now ready to build applications on the vCon database! 🚀