Database Schema

Every table below is accessible via the Supabase client. All queries respect row-level security โ€” you only see data from organizations where youโ€™re a team member.

organizations

Top-level billing entity. Every user belongs to at least one.

ColumnTypeDefaultDescription
iduuidautoPrimary key
nametextrequiredOrganization name
slugtextunique, requiredURL-safe identifier
plantext'free'free, growth, or pro
monthly_message_limitinteger50AI replies allowed per month
messages_used_this_monthinteger0Current usage count
stripe_customer_idtextnullStripe billing link
created_attimestamptznow()
const { data: org } = await sendhub
  .from('organizations')
  .select('id, name, plan, messages_used_this_month, monthly_message_limit')
  .single()

businesses

Workspaces within an organization. Each has its own AI config, channels, and contacts.

ColumnTypeDefaultDescription
iduuidautoPrimary key
org_iduuidrequiredParent organization
nametextrequiredWorkspace name
slugtextrequiredUnique within org
ai_enabledbooleantrueAI auto-reply on/off
ai_system_prompttextnullDefault AI prompt for this workspace
ai_modeltext'gemini-2.5-flash'AI model to use
ai_temperaturereal0.7AI creativity (0โ€“1)
created_attimestamptznow()
const { data: workspaces } = await sendhub
  .from('businesses')
  .select('id, name, ai_enabled, ai_system_prompt')
  .eq('org_id', orgId)

channels

Connected WhatsApp numbers โ€” business API or personal bridge.

ColumnTypeDefaultDescription
iduuidautoPrimary key
business_iduuidrequiredParent workspace
org_iduuidrequiredParent organization
platformtext'whatsapp'Always whatsapp
channel_typetext'business' or 'personal'
phone_numbertextnullE.164 phone number
phone_number_idtextuniqueMeta API phone number ID
waba_idtextnullWhatsApp Business Account ID
access_tokentextnullMeta API access token
labeltext'Main'Display name
activebooleantrueChannel enabled
ai_prompt_overridetextnullPer-channel AI prompt
ai_delay_secondsinteger10Seconds to wait before AI replies
ai_confidence_thresholdinteger80Below this โ†’ draft, above โ†’ auto-send
ai_auto_sendbooleanfalseWhen false, all AI replies are saved as drafts for human review
matrix_user_iduuidnullDevice link user (personal channels)
created_attimestamptznow()
const { data: channels } = await sendhub
  .from('channels')
  .select('id, label, phone_number, channel_type, active, ai_delay_seconds, ai_confidence_threshold, ai_auto_send')
  .eq('business_id', workspaceId)
  .eq('active', true)

contacts

WhatsApp users who have messaged your channels. Created automatically on first message.

ColumnTypeDefaultDescription
iduuidautoPrimary key
business_iduuidrequiredParent workspace
org_iduuidrequiredParent organization
platformtext'whatsapp'
platform_idtextrequiredWhatsApp phone number
nametextnullDisplay name
phonetextnullFormatted phone number
avatar_urltextnullProfile picture URL
tagstext[]'{}'Array of tag labels
notestextnullFree-text notes
created_attimestamptznow()
// Search contacts by name or phone
const { data: contacts } = await sendhub
  .from('contacts')
  .select('id, name, phone, tags, avatar_url')
  .eq('business_id', workspaceId)
  .or(`name.ilike.%${query}%,phone.ilike.%${query}%`)
  .order('name')
  .limit(50)

// Update tags
await sendhub
  .from('contacts')
  .update({ tags: ['vip', 'returning'] })
  .eq('id', contactId)

conversations

A thread between a contact and your team/AI on a specific channel.

ColumnTypeDefaultDescription
iduuidautoPrimary key
business_iduuidrequiredParent workspace
org_iduuidrequiredParent organization
channel_iduuidrequiredWhatsApp channel
contact_iduuidrequiredThe contact
external_idtextnullBridge room ID (personal channels)
statustext'ai_handling'ai_handling, assigned, resolved
assigned_touuidnullTeam member ID
last_message_attimestamptznow()
created_attimestamptznow()
// List conversations with related data
const { data } = await sendhub
  .from('conversations')
  .select(`
    id, status, assigned_to, last_message_at,
    contacts(id, name, phone, tags, avatar_url),
    channels(label, phone_number, channel_type)
  `)
  .eq('business_id', workspaceId)
  .eq('status', 'ai_handling')
  .order('last_message_at', { ascending: false })

// Assign to a team member
await sendhub
  .from('conversations')
  .update({ assigned_to: memberId, status: 'assigned' })
  .eq('id', convoId)

// Resolve
await sendhub
  .from('conversations')
  .update({ status: 'resolved' })
  .eq('id', convoId)

messages

Individual messages within a conversation.

ColumnTypeDefaultDescription
iduuidautoPrimary key
conversation_iduuidrequiredParent conversation
business_iduuidrequiredParent workspace
org_iduuidrequiredParent organization
directiontextrequired'inbound' or 'outbound'
sender_typetextrequired'contact', 'ai', or 'agent'
sender_iduuidnullTeam member ID (for agent messages)
contenttextrequiredMessage text
media_urltextnullAttachment URL
media_typetextnullMIME type of attachment
wa_message_idtextnullWhatsApp message ID (deduplication)
delivery_statustextnullqueued โ†’ sending โ†’ sent โ†’ delivered โ†’ read / failed
is_draftbooleanfalseAI draft awaiting human review
confidenceintegernullAI confidence score (0โ€“100)
readbooleanfalseRead by agent
created_attimestamptznow()
// List messages in a conversation
const { data: messages } = await sendhub
  .from('messages')
  .select('id, direction, sender_type, content, delivery_status, confidence, is_draft, created_at')
  .eq('conversation_id', convoId)
  .order('created_at', { ascending: true })

// Send a message
const { data } = await sendhub.from('messages').insert({
  conversation_id: convoId,
  business_id: workspaceId,
  org_id: orgId,
  direction: 'outbound',
  sender_type: 'agent',
  content: 'Your order has shipped!'
}).select('id').single()

// Search messages
const { data: results } = await sendhub
  .from('messages')
  .select('id, conversation_id, content, sender_type, created_at')
  .ilike('content', `%${searchQuery}%`)
  .order('created_at', { ascending: false })
  .limit(50)

team_members

Users with access to an organization.

ColumnTypeDefaultDescription
iduuidautoPrimary key
org_iduuidrequiredParent organization
user_iduuidrequiredSupabase auth user
nametextrequiredDisplay name
emailtextrequiredEmail address
org_roletext'agent'owner, admin, or agent
activebooleantrue
deleted_attimestamptznullSoft delete
created_attimestamptznow()
const { data: team } = await sendhub
  .from('team_members')
  .select('id, name, email, org_role, active')
  .eq('org_id', orgId)
  .is('deleted_at', null)

team_assignments

Maps team members to specific workspaces with a role.

ColumnTypeDefaultDescription
iduuidautoPrimary key
team_member_iduuidrequired
business_iduuidrequired
roletext'agent'Role within this workspace
created_attimestamptznow()

api_keys

Programmatic access keys scoped to an organization.

ColumnTypeDefaultDescription
iduuidautoPrimary key
org_iduuidrequired
business_iduuidnullOptional workspace scope
key_hashtextuniqueHashed API secret key
key_prefixtextrequiredFirst 8 chars for identification
labeltext'Default'
scopestext[]'{read,write}'Permission scopes
last_used_attimestamptznull
created_attimestamptznow()

Real-time Subscriptions

Subscribe to changes on any table:

// New messages across all conversations
sendhub
  .channel('all-messages')
  .on('postgres_changes', {
    event: 'INSERT',
    schema: 'public',
    table: 'messages',
    filter: `business_id=eq.${workspaceId}`,
  }, (payload) => {
    console.log('New message:', payload.new)
  })
  .subscribe()

// Conversation status changes
sendhub
  .channel('convo-updates')
  .on('postgres_changes', {
    event: 'UPDATE',
    schema: 'public',
    table: 'conversations',
  }, (payload) => {
    console.log('Status changed:', payload.new.status)
  })
  .subscribe()

// Delivery status tracking
sendhub
  .channel('delivery')
  .on('postgres_changes', {
    event: 'UPDATE',
    schema: 'public',
    table: 'messages',
    filter: `conversation_id=eq.${convoId}`,
  }, (payload) => {
    console.log('Delivery:', payload.new.delivery_status)
  })
  .subscribe()