Skip to main content

Overview

Highway uses Supabase as its backend database and real-time data platform. All verification records, call logs, and customer data are stored in Supabase PostgreSQL tables with automatic persistence and querying capabilities.

Database Architecture

Highway’s data model consists of two primary tables with a one-to-many relationship:

Database Schema

Verifications Table

The verifications table stores customer information and the data points that need to be verified.
ColumnTypeConstraintsDescription
idintegerPRIMARY KEY, AUTO INCREMENTUnique verification identifier
nametextNOT NULLCustomer’s full name
phonetextNOT NULL10-digit phone number
typetextNOT NULLBackground/context for verification
datajsonbNOT NULLVerification questions as JSON
created_attimestamp with time zoneDEFAULT now()When verification was created

JSONB Data Field

The data column uses PostgreSQL’s jsonb type for flexible, queryable JSON storage:
  • Flexible Schema: Each verification can have different data fields
  • Queryable: Can search within JSON using PostgreSQL operators
  • Indexed: Supports GIN indexes for fast JSON queries
  • Typed: Validates JSON structure on insert
-- Find verifications with specific DOB
SELECT * FROM verifications
WHERE data->>'date_of_birth' = '1990-01-01';

-- Find verifications containing address field
SELECT * FROM verifications
WHERE data ? 'address';

-- Find verifications with SSN field
SELECT id, name, data->'ssn_last_4' as ssn
FROM verifications
WHERE data ? 'ssn_last_4';
  • Use consistent field naming (snake_case recommended)
  • Keep JSON structure flat when possible
  • Don’t store overly nested objects
  • Use descriptive key names
  • Validate JSON on the frontend before insert

Calls Table

The calls table tracks individual phone verification attempts and their outcomes.
ColumnTypeConstraintsDescription
idintegerPRIMARY KEY, AUTO INCREMENTUnique call identifier
verificationintegerFOREIGN KEY → verifications(id)Associated verification record
statustextNOT NULLCurrent call status
created_attimestamp with time zoneDEFAULT now()When call was initiated

Status Values

The status field uses predefined string values:
type CallStatus = 
  | "in_progress"
  | "successful_call"
  | "unsuccessful_call"
  | "user_hung_up"
  | "system_error";
These status values match the enum in the call_reflection_data function (highway-backend/conversationConfig.js:36-44).

Data Persistence

Highway persists data at several key points in the verification workflow:

1. Creating Verifications

From highway-frontend/src/app/page.tsx:111-129:
const handleSubmit = async (values: typeof form.values) => {
  const supabase = createClient();
  const { data, error } = await supabase.from("verifications").insert({
    name: values.name,
    phone: values.phoneNumber,
    data: JSON.parse(values.userData),
    type: values.type,
  });
  
  if (error) {
    console.error("Error adding verification:", error);
  } else {
    fetchCustomers(); // Refresh list
    closeAddUserModal();
    form.reset();
  }
};
When: User clicks “Add verification” button What’s stored: Customer name, phone, background, and verification data

2. Initiating Calls

From highway-backend/routes.js:35-44:
router.post("/call-customer", async (req, res) => {
  const { to, verification } = req.body;
  
  const { data } = await supabase
    .from("calls")
    .insert([{ verification: verification, status: "in_progress" }])
    .select();
    
  // Call ID is used in Twilio stream URL
  const call = await client.calls.create({
    to: to,
    from: TWILIO_PHONE_NUMBER,
    twiml: `<Stream url="wss://${req.headers.host}/media-stream/${verification}/${data[0].id}" />`
  });
});
When: User clicks “Initiate call” button What’s stored: New call record with in_progress status and verification foreign key

3. Loading Verification Data

From highway-backend/websocket.js:63-76:
openAiWs.on("open", async () => {
  const { data, error } = await supabase
    .from("verifications")
    .select("*")
    .eq("id", streamId);
    
  if (data) {
    bigdata = JSON.stringify(data[0]);
    sendSessionUpdate();
  }
});
When: WebSocket connection to OpenAI is established What’s retrieved: Complete verification record for AI context

4. Updating Call Status

From highway-backend/websocket.js:86-92:
if (response.type === "response.function_call_arguments.done") {
  supabase
    .from("calls")
    .update({ status: response.arguments.status })
    .eq("id", callId);
}
When: AI calls call_reflection_data function What’s updated: Call status changed to final outcome

Supabase Client Setup

Frontend Client

From highway-frontend/src/utils/supabase/client.ts:1-9:
import { createBrowserClient } from "@supabase/ssr";

export const createClient = () =>
  createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  );
Usage:
const supabase = createClient();
const { data } = await supabase.from("verifications").select("*");

Backend Client

From highway-backend/websocket.js:5-9:
const { createClient } = require("@supabase/supabase-js");
const supabase = createClient(
  "https://umbkzjfffeoykaxsghly.supabase.co",
  "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
);
The backend uses hardcoded credentials in the source code. For production deployments, move these to environment variables:
const supabase = createClient(
  process.env.SUPABASE_URL,
  process.env.SUPABASE_ANON_KEY
);

Common Query Patterns

Fetching Verifications

From highway-frontend/src/app/page.tsx:57-69:
const fetchCustomers = async () => {
  const supabase = createClient();
  const { data, error } = await supabase
    .from("verifications")
    .select("*")
    .order("created_at", { ascending: false });
    
  if (error) {
    console.error("Error fetching customers:", error);
  } else {
    setCustomers(data || []);
  }
};

Fetching Calls with Verifications

From highway-frontend/src/app/calls/page.tsx:40-80:
const fetchData = async () => {
  const supabase = createClient();
  
  // 1. Fetch all calls
  const { data: callsData } = await supabase
    .from("calls")
    .select("*")
    .order("created_at", { ascending: false });
    
  // 2. Get unique verification IDs from calls
  const verificationIds = [
    ...new Set(callsData?.map((call) => call.verification)),
  ];
  
  // 3. Fetch all related verifications
  const { data: verificationsData } = await supabase
    .from("verifications")
    .select("*")
    .in("id", verificationIds);
    
  // 4. Create lookup map
  const verificationsMap = verificationsData?.reduce(
    (acc, verification) => {
      acc[verification.id] = verification;
      return acc;
    },
    {}
  );
};
This pattern avoids N+1 queries by fetching all verifications in a single query using .in().

Inserting with Return Data

const { data, error } = await supabase
  .from("verifications")
  .insert({ name: "John Doe", phone: "5551234567", ... })
  .select();
  
// data[0] contains the inserted record with auto-generated id

Updating Records

const { error } = await supabase
  .from("calls")
  .update({ status: "successful_call" })
  .eq("id", callId);

Authentication and Security

Row Level Security (RLS)

The current implementation uses the anon key which provides public access. For production:
  1. Enable Row Level Security on both tables
  2. Create policies to restrict access
  3. Implement authentication
  4. Use authenticated user context in queries
-- Enable RLS
ALTER TABLE verifications ENABLE ROW LEVEL SECURITY;

-- Allow authenticated users to read their own verifications
CREATE POLICY "Users can view own verifications"
ON verifications FOR SELECT
USING (auth.uid() = user_id);

-- Allow authenticated users to create verifications
CREATE POLICY "Users can create verifications"
ON verifications FOR INSERT
WITH CHECK (auth.uid() = user_id);

API Key Security

Current approach:
  • Frontend uses public NEXT_PUBLIC_SUPABASE_ANON_KEY
  • Backend uses same anon key hardcoded
Production recommendations:
  1. Frontend: Keep using anon key with RLS policies
  2. Backend: Use service role key for privileged operations
  3. Environment variables: Never commit keys to version control
.env.local
# Frontend
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...

# Backend
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...  # More privileges

Real-Time Subscriptions

Supabase supports real-time subscriptions for live updates. While not currently implemented in Highway, you could add:
// Subscribe to new calls
const subscription = supabase
  .channel('calls')
  .on(
    'postgres_changes',
    { 
      event: 'INSERT', 
      schema: 'public', 
      table: 'calls' 
    },
    (payload) => {
      console.log('New call:', payload.new);
      // Update UI with new call
    }
  )
  .subscribe();

// Subscribe to call status updates
const statusSub = supabase
  .channel('call-status')
  .on(
    'postgres_changes',
    { 
      event: 'UPDATE', 
      schema: 'public', 
      table: 'calls' 
    },
    (payload) => {
      console.log('Call status updated:', payload.new.status);
      // Update call badge in real-time
    }
  )
  .subscribe();
  • Live call status updates: See status change from “in_progress” to “successful_call” without refreshing
  • New verification notifications: Alert when team members add verifications
  • Dashboard sync: Keep multiple browser tabs in sync
  • Monitoring dashboards: Real-time call volume tracking

Data Backup and Migration

Exporting Data

-- Export verifications to CSV
COPY verifications TO '/tmp/verifications.csv' CSV HEADER;

-- Export calls to CSV
COPY calls TO '/tmp/calls.csv' CSV HEADER;

-- Export with JOIN
COPY (
  SELECT c.*, v.name, v.phone 
  FROM calls c 
  JOIN verifications v ON c.verification = v.id
) TO '/tmp/call_report.csv' CSV HEADER;

Database Migrations

For schema changes, use Supabase migrations:
-- migrations/20241015_add_notes_column.sql
ALTER TABLE verifications
ADD COLUMN notes TEXT;

-- migrations/20241015_add_duration_column.sql
ALTER TABLE calls
ADD COLUMN duration_seconds INTEGER;

Best Practices

  • Use .select() to specify only needed columns
  • Add indexes on frequently queried fields
  • Use .limit() for pagination
  • Avoid N+1 queries with .in() operator
  • Use .explain() to analyze query performance
  • Always validate JSON before inserting into data field
  • Use database constraints (NOT NULL, FOREIGN KEY)
  • Handle errors gracefully in application code
  • Consider adding CHECK constraints for status values
  • Use transactions for multi-step operations
const { data, error } = await supabase
  .from("verifications")
  .insert(newVerification);
  
if (error) {
  console.error("Database error:", error);
  // Show user-friendly error message
  // Log to error tracking service
  // Don't expose database details to users
}
  • Create indexes on foreign keys
  • Use connection pooling
  • Cache frequently accessed data
  • Paginate large result sets
  • Monitor query performance in Supabase dashboard

Next Steps

Verification Management

Learn how to create and manage verification records

Call Monitoring

Query and analyze call data

Configuration

Set up Supabase environment variables

API Reference

Explore the complete API

Build docs developers (and LLMs) love