Can Agents Run It? · evidence — Issue 1: Supabase

deepseek/deepseek-v4-pro · d2_webhooks · rep 3

pass
model
deepseek/deepseek-v4-pro
task
d2_webhooks
rep
3
day
2
steps
21
tokens
79,227
est. cost
$0.048
agent time
176s
wave
s4
started
2026-07-24T06:47:25Z

The task as the agent received it

You have operator access to a hosted Supabase project. Credentials are in environment variables: SUPABASE_URL, SUPABASE_PROJECT_REF, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, SUPABASE_DB_URL, SUPABASE_DB_PASSWORD. Every INSERT into the existing table public.signups must deliver an HTTP webhook: a POST request with a JSON body containing at least the new row's email and plan values, sent to the receiver endpoint {SUPABASE_URL}/functions/v1/hook-recorder (an edge function that is already deployed on this project; SUPABASE_URL is in your environment). The receiver requires a valid project JWT in the Authorization header and records every payload it accepts. Implement the delivery from the database itself, so it fires for ANY insert into signups no matter which client performs it — no external workers, no cron, no client-side code. Delivery may be asynchronous (a few seconds is fine). Prove it end-to-end with a test insert before finishing: the receiver stores accepted payloads in public.webhook_receipts.

What it ran — 21 commands

#1exit 0
echo "SUPABASE_URL=$SUPABASE_URL" && echo "SUPABASE_PROJECT_REF=$SUPABASE_PROJECT_REF" && echo "SUPABASE_DB_URL=$SUPABASE_DB_URL" | head -c 200
SUPABASE_URL=https://<ref>.supabase.co
SUPABASE_PROJECT_REF=<ref>
SUPABASE_DB_URL=<redacted:SUPABASE_DB_URL>
#2exit 0
psql "$SUPABASE_DB_URL" -c "SELECT table_name FROM information_schema.tables WHERE table_schema='public';"
    table_name    
------------------
 signups
 webhook_receipts
(2 rows)

#3exit 0
psql "$SUPABASE_DB_URL" -c "\d public.signups" && psql "$SUPABASE_DB_URL" -c "\d public.webhook_receipts"
                                     Table "public.signups"
   Column   |           Type           | Collation | Nullable |             Default              
------------+--------------------------+-----------+----------+----------------------------------
 id         | bigint                   |           | not null | generated by default as identity
 email      | text                     |           | not null | 
 plan       | text                     |           | not null | 'free'::text
 created_at | timestamp with time zone |           | not null | now()
Indexes:
    "signups_pkey" PRIMARY KEY, btree (id)

                                 Table "public.webhook_receipts"
   Column    |           Type           | Collation | Nullable |             Default              
-------------+--------------------------+-----------+----------+----------------------------------
 id          | bigint                   |           | not null | generated by default as identity
 received_at | timestamp with time zone |           | not null | now()
 payload     | jsonb                    |           | not null | 
Indexes:
    "webhook_receipts_pkey" PRIMARY KEY, btree (id)

#4exit 0
# Check if pg_net extension is available
psql "$SUPABASE_DB_URL" -c "SELECT * FROM pg_extension WHERE extname='pg_net';"
 oid | extname | extowner | extnamespace | extrelocatable | extversion | extconfig | extcondition 
-----+---------+----------+--------------+----------------+------------+-----------+--------------
(0 rows)

#5exit 0
# Check available extensions for http/networking
psql "$SUPABASE_DB_URL" -c "SELECT * FROM pg_available_extensions WHERE name IN ('pg_net', 'http', 'pg_http', 'pgsql-http', 'supabase');"
  name  | default_version | installed_version |                                  comment                                   
--------+-----------------+-------------------+----------------------------------------------------------------------------
 http   | 1.6             |                   | HTTP client for PostgreSQL, allows web page retrieval inside the database.
 pg_net | 0.20.4          |                   | Async HTTP
(2 rows)

#6exit 0
psql "$SUPABASE_DB_URL" -c "CREATE EXTENSION IF NOT EXISTS pg_net;"
CREATE EXTENSION
#7exit 0
# Check what SUPABASE_SERVICE_ROLE_KEY looks like (just first/last few chars)
echo "Length: ${#SUPABASE_SERVICE_ROLE_KEY}"
echo "First 30: ${SUPABASE_SERVICE_ROLE_KEY:0:30}"
echo "Last 10: ${SUPABASE_SERVICE_ROLE_KEY: -10}"
Length: 219
First 30: eyJhbGciOiJIUzI1NiIsInR5cCI6Ik
Last 10: Z49JeloS8w
#8exit 0
psql "$SUPABASE_DB_URL" -c "SELECT proname, pronargs, prosrc FROM pg_proc WHERE proname LIKE '%http%' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'net');"
        proname         | pronargs |                                                 prosrc                                                  
------------------------+----------+---------------------------------------------------------------------------------------------------------
 http_get               |        4 |                                                                                                        +
                        |          | declare                                                                                                +
                        |          |     request_id bigint;                                                                                 +
                        |          |     params_array text[];                                                                               +
                        |          | begin                                                                                                  +
                        |          |     select coalesce(array_agg(net._urlencode_string(key) || '=' || net._urlencode_string(value)), '{}')+
                        |          |     into params_array                                                                                  +
                        |          |     from jsonb_each_text(params);                                                                      +
                        |          |                                                                                                        +
                        |          |     -- Add to the request queue                                                                        +
                        |          |     insert into net.http_request_queue(method, url, headers, timeout_milliseconds)                     +
                        |          |     values (                                                                                           +
                        |          |         'GET',                                                                                         +
                        |          |         net._encode_url_with_params_array(url, params_array),                                          +
                        |          |         headers,                                                                                       +
                        |          |         timeout_milliseconds                                                                           +
                        |          |     )                                                                                                  +
                        |          |     returning id                                                                                       +
                        |          |     into request_id;                                                                                   +
                        |          |                                                                                                        +
                        |          |     perform net.wake();                                                                                +
                        |          |                                                                                                        +
                        |          |     return request_id;                                                                                 +
                        |          | end                                                                                                    +
                        |          | 
 http_post              |        5 |                                                                                                        +
                        |          | declare                                                                                    
… 17,074 more bytes — full output in the raw record
#9exit 0
psql "$SUPABASE_DB_URL" -c "SELECT proname, pronargs, prosrc FROM pg_proc WHERE proname LIKE '%http%' AND pronamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'net') ORDER BY proname;"
        proname         | pronargs |                                                 prosrc                                                  
------------------------+----------+---------------------------------------------------------------------------------------------------------
 _http_collect_response |        2 |                                                                                                        +
                        |          | declare                                                                                                +
                        |          |     rec net._http_response;                                                                            +
                        |          |     req_exists boolean;                                                                                +
                        |          | begin                                                                                                  +
                        |          |                                                                                                        +
                        |          |     if not async then                                                                                  +
                        |          |         perform net._await_response(request_id);                                                       +
                        |          |     end if;                                                                                            +
                        |          |                                                                                                        +
                        |          |     select *                                                                                           +
                        |          |     into rec                                                                                           +
                        |          |     from net._http_response                                                                            +
                        |          |     where id = request_id;                                                                             +
                        |          |                                                                                                        +
                        |          |     if rec is null or rec.error_msg is not null then                                                   +
                        |          |         -- The request is either still processing or the request_id provided does not exist            +
                        |          |                                                                                                        +
                        |          |         -- TODO: request in progress is indistinguishable from request that doesn't exist              +
                        |          |                                                                                                        +
                        |          |         -- No request matching request_id found                                                        +
                        |          |         return (                                                                                       +
                        |          |             'ERROR',                                                                                   +
                        |          |             coalesce(rec.error_msg, 'request matching request_id not found'),                          +
                        |          |             null                                                                                       +
                        |          |         )::net.http_response_result;                                                                   +
                        
… 17,074 more bytes — full output in the raw record
#10exit 0
psql "$SUPABASE_DB_URL" -c "SELECT pg_get_functiondef('net.http_post'::regproc);" 2>&1 | head -60
                                                                                                          pg_get_functiondef                                                                                                          
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 CREATE OR REPLACE FUNCTION net.http_post(url text, body jsonb DEFAULT '{}'::jsonb, params jsonb DEFAULT '{}'::jsonb, headers jsonb DEFAULT '{"Content-Type": "application/json"}'::jsonb, timeout_milliseconds integer DEFAULT 5000)+
  RETURNS bigint                                                                                                                                                                                                                     +
  LANGUAGE plpgsql                                                                                                                                                                                                                   +
 AS $function$                                                                                                                                                                                                                       +
 declare                                                                                                                                                                                                                             +
     request_id bigint;                                                                                                                                                                                                              +
     params_array text[];                                                                                                                                                                                                            +
     content_type text;                                                                                                                                                                                                              +
 begin                                                                                                                                                                                                                               +
                                                                                                                                                                                                                                     +
     -- Exctract the content_type from headers                                                                                                                                                                                       +
     select                                                                                                                                                                                                                          +
         header_value into content_type                                                                                                                                                                                              +
     from                                                                                                                                                                                                                            +
         jsonb_each_text(coalesce(headers, '{}'::jsonb)) r(header_name, header_value)                                                                                                                                                +
     where                                                               
… 9,178 more bytes — full output in the raw record
#11exit 0
psql "$SUPABASE_DB_URL" -c "SELECT pg_get_functiondef('net.http_post'::regproc);" 2>&1
                                                                                                          pg_get_functiondef                                                                                                          
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 CREATE OR REPLACE FUNCTION net.http_post(url text, body jsonb DEFAULT '{}'::jsonb, params jsonb DEFAULT '{}'::jsonb, headers jsonb DEFAULT '{"Content-Type": "application/json"}'::jsonb, timeout_milliseconds integer DEFAULT 5000)+
  RETURNS bigint                                                                                                                                                                                                                     +
  LANGUAGE plpgsql                                                                                                                                                                                                                   +
 AS $function$                                                                                                                                                                                                                       +
 declare                                                                                                                                                                                                                             +
     request_id bigint;                                                                                                                                                                                                              +
     params_array text[];                                                                                                                                                                                                            +
     content_type text;                                                                                                                                                                                                              +
 begin                                                                                                                                                                                                                               +
                                                                                                                                                                                                                                     +
     -- Exctract the content_type from headers                                                                                                                                                                                       +
     select                                                                                                                                                                                                                          +
         header_value into content_type                                                                                                                                                                                              +
     from                                                                                                                                                                                                                            +
         jsonb_each_text(coalesce(headers, '{}'::jsonb)) r(header_name, header_value)                                                                                                                                                +
     where                                                               
… 9,178 more bytes — full output in the raw record
#12exit 0
# Test the receiver function exists
curl -s -o /dev/null -w "%{http_code}" "${SUPABASE_URL}/functions/v1/hook-recorder" -H "Authorization: Bearer ${SUPABASE_ANON_KEY}" -X POST -H "Content-Type: application/json" -d '{"test": true}'
200
#13exit 0
psql "$SUPABASE_DB_URL" <<'EOF'
-- Create the trigger function
CREATE OR REPLACE FUNCTION public.webhook_signups_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
    url text;
    body jsonb;
    headers jsonb;
    request_id bigint;
BEGIN
    url := 'https://<ref>.supabase.co/functions/v1/hook-recorder';

    body := jsonb_build_object(
        'email', NEW.email,
        'plan', NEW.plan
    );

    headers := jsonb_build_object(
        'Content-Type', 'application/json',
        'Authorization', 'Bearer <jwt:service_role>'
    );

    -- Fire async HTTP POST via pg_net
    request_id := net.http_post(
        url := url,
        body := body,
        headers := headers,
        timeout_milliseconds := 5000
    );

    -- Log the request_id for debugging (optional)
    RAISE LOG 'pg_net request_id % fired for signup id=% email=% plan=%', request_id, NEW.id, NEW.email, NEW.plan;

    RETURN NEW;
END;
$$;

-- Attach trigger to signups table
DROP TRIGGER IF EXISTS trg_webhook_signups_insert ON public.signups;
CREATE TRIGGER trg_webhook_signups_insert
    AFTER INSERT ON public.signups
    FOR EACH ROW
    EXECUTE FUNCTION public.webhook_signups_insert();
EOF
CREATE FUNCTION
DROP TRIGGER
CREATE TRIGGER
NOTICE:  trigger "trg_webhook_signups_insert" for relation "public.signups" does not exist, skipping
#14exit 0
psql "$SUPABASE_DB_URL" <<'EOF'
CREATE OR REPLACE FUNCTION public.webhook_signups_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
    url text;
    body jsonb;
    headers jsonb;
    request_id bigint;
BEGIN
    url := 'https://<ref>.supabase.co/functions/v1/hook-recorder';

    body := jsonb_build_object(
        'email', NEW.email,
        'plan', NEW.plan,
        'id', NEW.id,
        'created_at', NEW.created_at
    );

    headers := jsonb_build_object(
        'Content-Type', 'application/json',
        'Authorization', 'Bearer ' || current_setting('app.settings.service_role_key')
    );

    -- Fire async HTTP POST via pg_net
    request_id := net.http_post(
        url := url,
        body := body,
        headers := headers,
        timeout_milliseconds := 5000
    );

    RAISE LOG 'pg_net request_id % fired for signup id=% email=% plan=%', request_id, NEW.id, NEW.email, NEW.plan;

    RETURN NEW;
END;
$$;
EOF
CREATE FUNCTION
#15exit 0
# Create the function with the real service role key
psql "$SUPABASE_DB_URL" -c "
CREATE OR REPLACE FUNCTION public.webhook_signups_insert()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS \$\$
DECLARE
    url text;
    body jsonb;
    headers jsonb;
    request_id bigint;
BEGIN
    url := 'https://<ref>.supabase.co/functions/v1/hook-recorder';

    body := jsonb_build_object(
        'email', NEW.email,
        'plan', NEW.plan,
        'id', NEW.id,
        'created_at', NEW.created_at
    );

    headers := jsonb_build_object(
        'Content-Type', 'application/json',
        'Authorization', 'Bearer ${SUPABASE_SERVICE_ROLE_KEY}'
    );

    -- Fire async HTTP POST via pg_net
    request_id := net.http_post(
        url := url,
        body := body,
        headers := headers,
        timeout_milliseconds := 5000
    );

    RAISE LOG 'pg_net request_id % fired for signup id=% email=% plan=%', request_id, NEW.id, NEW.email, NEW.plan;

    RETURN NEW;
END;
\$\$;
"
CREATE FUNCTION
#16exit 0
# First check current state of webhook_receipts
psql "$SUPABASE_DB_URL" -c "SELECT * FROM public.webhook_receipts;"
 id |         received_at          |    payload     
----+------------------------------+----------------
  2 | 2026-07-24 06:49:03.76397+00 | {"test": true}
(1 row)

#17exit 0
psql "$SUPABASE_DB_URL" -c "INSERT INTO public.signups (email, plan) VALUES ('e2e-test@example.com', 'pro') RETURNING *;"
 id |        email         | plan |          created_at           
----+----------------------+------+-------------------------------
  1 | e2e-test@example.com | pro  | 2026-07-24 06:49:50.126258+00
(1 row)

INSERT 0 1
#18exit 0
# Give pg_net a moment to deliver, then check receipts
sleep 3
psql "$SUPABASE_DB_URL" -c "SELECT * FROM public.webhook_receipts ORDER BY id DESC LIMIT 5;"
 id |          received_at          |                                                   payload                                                   
----+-------------------------------+-------------------------------------------------------------------------------------------------------------
  3 | 2026-07-24 06:49:50.434306+00 | {"id": 1, "plan": "pro", "email": "e2e-test@example.com", "created_at": "2026-07-24T06:49:50.126258+00:00"}
  2 | 2026-07-24 06:49:03.76397+00  | {"test": true}
(2 rows)

#19exit 0
# Second test - different plan, different email
psql "$SUPABASE_DB_URL" -c "INSERT INTO public.signups (email, plan) VALUES ('another@example.com', 'enterprise') RETURNING *;"
sleep 3
psql "$SUPABASE_DB_URL" -c "SELECT * FROM public.webhook_receipts ORDER BY id DESC LIMIT 5;"
 id |        email        |    plan    |          created_at           
----+---------------------+------------+-------------------------------
  2 | another@example.com | enterprise | 2026-07-24 06:50:05.161823+00
(1 row)

INSERT 0 1
 id |          received_at          |                                                      payload                                                      
----+-------------------------------+-------------------------------------------------------------------------------------------------------------------
  4 | 2026-07-24 06:50:05.313528+00 | {"id": 2, "plan": "enterprise", "email": "another@example.com", "created_at": "2026-07-24T06:50:05.161823+00:00"}
  3 | 2026-07-24 06:49:50.434306+00 | {"id": 1, "plan": "pro", "email": "e2e-test@example.com", "created_at": "2026-07-24T06:49:50.126258+00:00"}
  2 | 2026-07-24 06:49:03.76397+00  | {"test": true}
(3 rows)

#20exit 0
curl -s "${SUPABASE_URL}/rest/v1/signups" \
  -H "apikey: ${SUPABASE_ANON_KEY}" \
  -H "Authorization: Bearer ${SUPABASE_ANON_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"email": "api-user@example.com", "plan": "free"}'
#21exit 0
sleep 3
psql "$SUPABASE_DB_URL" -c "SELECT * FROM public.signups ORDER BY id DESC LIMIT 3;"
echo "---"
psql "$SUPABASE_DB_URL" -c "SELECT * FROM public.webhook_receipts ORDER BY id DESC LIMIT 5;"
 id |        email         |    plan    |          created_at           
----+----------------------+------------+-------------------------------
  3 | api-user@example.com | free       | 2026-07-24 06:50:13.722081+00
  2 | another@example.com  | enterprise | 2026-07-24 06:50:05.161823+00
  1 | e2e-test@example.com | pro        | 2026-07-24 06:49:50.126258+00
(3 rows)

---
 id |          received_at          |                                                      payload                                                      
----+-------------------------------+-------------------------------------------------------------------------------------------------------------------
  5 | 2026-07-24 06:50:13.870153+00 | {"id": 3, "plan": "free", "email": "api-user@example.com", "created_at": "2026-07-24T06:50:13.722081+00:00"}
  4 | 2026-07-24 06:50:05.313528+00 | {"id": 2, "plan": "enterprise", "email": "another@example.com", "created_at": "2026-07-24T06:50:05.161823+00:00"}
  3 | 2026-07-24 06:49:50.434306+00 | {"id": 1, "plan": "pro", "email": "e2e-test@example.com", "created_at": "2026-07-24T06:49:50.126258+00:00"}
  2 | 2026-07-24 06:49:03.76397+00  | {"test": true}
(4 rows)

What it said at the end

All three tests passed flawlessly. Here's the summary: ## What I Did **Implemented a PostgreSQL trigger** on `public.signups` that fires an async HTTP POST webhook via the `pg_net` extension for every row inserted — regardless of how the insert happens (SQL, REST API, or any Supabase client). ### Key pieces: 1. **Enabled the `pg_net` extension** — Supabase's async HTTP client that runs inside the database itself, no external workers needed. 2. **Created `public.webhook_signups_insert()` trigger function** — a `SECURITY DEFINER` function that: - Builds a JSON body with `email`, `plan`, `id`, and `created_at` from `NEW` - Sets the `Authorization` header to the project's `service_role` JWT (required by the receiver) - Calls `net.http_post()` to POST asynchronously to `{SUPABASE_URL}/functions/v1/hook-recorder` 3. **Attached `AFTER INSERT` trigger** on `public.signups` for each row. ### Proof (3/3 inserts delivered): | Insert method | Email | Plan | Receipt ID | |---|---|---|---| | Direct SQL | `e2e-test@example.com` | `pro` | #3 ✅ | | Direct SQL | `another@example.com` | `enterprise` | #4 ✅ | | REST API (anon key) | `api-user@example.com` | `free` | #5 ✅ | Every receipt in `public.webhook_receipts` contains the full payload with `email`, `plan`, `id`, and `created_at` — confirming the webhook fires for **any** insert into `signups`, from **any** client.

7 earlier narration turn(s) are in the raw transcript.

How it was graded

A shell script executed against the live project and asserted on the state the agent left behind. No model judged another model.

exit 0 OK

Check it yourself

Raw transcript · Task definition and verifier · Every attempt