Skip to the page

Supabase

Keep a Supabase table filled with live web data. SpecStream sends the records each request finds to an Edge Function, which checks the signature and upserts them into Postgres with the source page behind every value. Saved requests send only what's new or changed, and Realtime pushes each record to your app.

What you need

  • A SpecStream account and an API key from Settings > API keys. Starting is free.
  • A Supabase project and the Supabase CLI, linked to it.
  1. 1. Create the table

    One row per record. fields holds the values by their label; sources holds the page each value came from and when it was read. Run this in the SQL editor.

    SQL
    create table public.web_records (
    entity_id text primary key,
    find_id text not null,
    fields jsonb not null,
    sources jsonb not null,
    updated_at timestamptz not null default now()
    );
    alter table public.web_records enable row level security;
    alter publication supabase_realtime add table public.web_records;

    Row level security is on with no policies, so only the Edge Function's service role writes. Add a select policy for the people or apps that should read the table.

  2. 2. Deploy the Edge Function

    The function rejects any body whose signature doesn't match, then upserts each record by its entity_id, so a record sent twice updates the same row.

    typescript
    import { createClient } from 'npm:@supabase/supabase-js@2'
    const secret = Deno.env.get('SPECSTREAM_WEBHOOK_SECRET')!
    const supabase = createClient(Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!)
    const encoder = new TextEncoder()
    // Specstream-Signature is "t=<unix seconds>,v1=<hex HMAC-SHA256 of 't.body'>".
    async function signed(header: string | null, body: string): Promise<boolean> {
    const parts = Object.fromEntries((header ?? '').split(',').map((part) => part.split('=')))
    if (!parts.t || !parts.v1 || Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false
    const key = await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify'])
    const mac = new Uint8Array(parts.v1.match(/../g)!.map((byte: string) => parseInt(byte, 16)))
    return crypto.subtle.verify('HMAC', key, mac, encoder.encode(`${parts.t}.${body}`))
    }
    Deno.serve(async (req) => {
    const body = await req.text()
    if (!(await signed(req.headers.get('Specstream-Signature'), body))) {
    return new Response('signature does not match', { status: 401 })
    }
    const event = JSON.parse(body)
    const rows = (event.records ?? []).map((record: any) => ({
    entity_id: record.entity_id,
    find_id: event.find_id,
    fields: Object.fromEntries(record.cells.map((cell: any) => [cell.label, cell.value])),
    sources: Object.fromEntries(
    record.cells
    .filter((cell: any) => cell.provenance?.source_url)
    .map((cell: any) => [cell.label, { url: cell.provenance.source_url, read_at: cell.provenance.observed_at }])
    ),
    updated_at: new Date().toISOString(),
    }))
    if (rows.length) {
    const { error } = await supabase.from('web_records').upsert(rows, { onConflict: 'entity_id' })
    if (error) return new Response(error.message, { status: 500 })
    }
    return new Response('ok')
    })

    SpecStream signs each delivery itself, so deploy without Supabase's JWT check:

    Terminal
    supabase functions new specstream
    # paste the function above into supabase/functions/specstream/index.ts
    supabase functions deploy specstream --no-verify-jwt
  3. 3. Send records to it

    Register the function's URL as a webhook. find.completed sends every result of a finished request; monitor.found sends the new and changed records of each scheduled run.

    Terminal
    curl https://api.specstream.ai/v1/account/webhooks \
    -H "Authorization: Bearer $SPECSTREAM_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"url":"https://<project-ref>.supabase.co/functions/v1/specstream","events":["find.completed","monitor.found"]}'

    The response includes the signing secret once. Store it where the function reads it:

    Terminal
    supabase secrets set SPECSTREAM_WEBHOOK_SECRET=<the secret from the response>

Keep it current

Ask for the data in the workspace, the API or your agent over MCP, like "dental clinics in Austin rated 4.5 or better, with their phone" or "companies that filed with the SEC to raise $5M or more this week". Then save it with a schedule, from hourly to yearly. Each run sends only records that are new or changed since the last one, so the table follows the live web without a scraper or a cron job of your own. See schedules.

Deliveries

Each delivery carries a Specstream-Event-Id header. A delivery that doesn't get a 2xx answer is tried again with the same id and the same records.

Query the records

Every value sits next to its source, so a screen or a report can link straight to the page behind it.

SQL
select fields->>'Name' as name,
fields->>'Phone' as phone,
sources->'Phone'->>'url' as phone_source,
updated_at
from public.web_records
order by updated_at desc;

Cost

Webhooks cost nothing. Requests are paid from your SpecStream balance, only for the results that fit, and each one shows the most it can cost before it starts. Scheduled rechecks cost half. See balance, quotes and billing. For help, email [email protected].