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

tencent/hy3 · d2_webhooks · rep 3

pass
model
tencent/hy3
task
d2_webhooks
rep
3
day
2
steps
11
tokens
38,885
est. cost
$0.012
agent time
119s
wave
s4c
started
2026-07-26T03:08:33Z

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 — 11 commands

#1exit 0
env | grep -i supabase; echo "---"; which psql supabase curl jq 2>/dev/null; echo "---"; psql --version 2>/dev/null; supabase --version 2>/dev/null
SUPABASE_DB_PASSWORD=<redacted:SUPABASE_DB_PASSWORD>
SUPABASE_DB_URL=<redacted:SUPABASE_DB_URL>
SUPABASE_URL=https://<ref>.supabase.co
SUPABASE_SERVICE_ROLE_KEY=<redacted:SUPABASE_SERVICE_ROLE_KEY>
SUPABASE_PROJECT_REF=<ref>
SUPABASE_ANON_KEY=<redacted:SUPABASE_ANON_KEY>
---
/usr/bin/psql
/usr/bin/supabase
/usr/bin/curl
/usr/bin/jq
---
psql (PostgreSQL) 18.4 (Debian 18.4-1.pgdg12+1)
2.75.0
#2exit 0
psql "$SUPABASE_DB_URL" -c "\d public.signups" 2>&1 | head -50
                                     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)

#3exit 0
psql "$SUPABASE_DB_URL" -c "\d public.webhook_receipts" 2>&1 | head -40; echo "==== extensions ===="; psql "$SUPABASE_DB_URL" -c "select extname, extversion from pg_extension order by extname;" 2>&1
                                 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)

==== extensions ====
      extname       | extversion 
--------------------+------------
 pg_stat_statements | 1.11
 pgcrypto           | 1.3
 plpgsql            | 1.0
 supabase_vault     | 0.3.1
 uuid-ossp          | 1.1
(5 rows)

#4exit 0
psql "$SUPABASE_DB_URL" -c "select current_user, current_database();" 2>&1
echo "=== try create pg_net ==="
psql "$SUPABASE_DB_URL" -c "create extension if not exists pg_net;" 2>&1
 current_user | current_database 
--------------+------------------
 postgres     | postgres
(1 row)

=== try create pg_net ===
CREATE EXTENSION
#5exit 0
psql "$SUPABASE_DB_URL" -c "\dn" 2>&1
echo "=== vault schema ==="
psql "$SUPABASE_DB_URL" -c "\d vault.*" 2>&1 | head -60
          List of schemas
      Name      |       Owner       
----------------+-------------------
 app            | postgres
 auth           | supabase_admin
 extensions     | postgres
 graphql        | supabase_admin
 graphql_public | supabase_admin
 net            | supabase_admin
 pgbouncer      | pgbouncer
 private        | postgres
 public         | pg_database_owner
 realtime       | supabase_admin
 storage        | supabase_admin
 vault          | supabase_admin
(12 rows)

=== vault schema ===
                        View "vault.decrypted_secrets"
      Column      |           Type           | Collation | Nullable | Default 
------------------+--------------------------+-----------+----------+---------
 id               | uuid                     |           |          | 
 name             | text                     |           |          | 
 description      | text                     |           |          | 
 secret           | text                     |           |          | 
 decrypted_secret | text                     | C         |          | 
 key_id           | uuid                     |           |          | 
 nonce            | bytea                    |           |          | 
 created_at       | timestamp with time zone |           |          | 
 updated_at       | timestamp with time zone |           |          | 

                                       Table "vault.secrets"
   Column    |           Type           | Collation | Nullable |              Default              
-------------+--------------------------+-----------+----------+-----------------------------------
 id          | uuid                     |           | not null | gen_random_uuid()
 name        | text                     |           |          | 
 description | text                     |           | not null | ''::text
 secret      | text                     |           | not null | 
 key_id      | uuid                     |           |          | 
 nonce       | bytea                    |           |          | vault._crypto_aead_det_noncegen()
 created_at  | timestamp with time zone |           | not null | CURRENT_TIMESTAMP
 updated_at  | timestamp with time zone |           | not null | CURRENT_TIMESTAMP
Indexes:
    "secrets_pkey" PRIMARY KEY, btree (id)
    "secrets_name_idx" UNIQUE, btree (name) WHERE name IS NOT NULL

  Index "vault.secrets_name_idx"
 Column | Type | Key? | Definition 
--------+------+------+------------
 name   | text | yes  | name
unique, btree, for table "vault.secrets", predicate (name IS NOT NULL)

    Index "vault.secrets_pkey"
 Column | Type | Key? | Definition 
--------+------+------+------------
 id     | uuid | yes  | id
primary key, btree, for table "vault.secrets"

#6exit 0
psql "$SUPABASE_DB_URL" -c "select proname, pg_get_function_arguments(oid) from pg_proc where proname in ('create_secret','decrypted_secrets') order by proname;" 2>&1
echo "=== store secret ==="
psql "$SUPABASE_DB_URL" <<'SQL' 2>&1
select vault.create_secret(
  secret => current_setting('app.settings.svc_key', true),
  name   => 'supabase_service_jwt',
  description => 'Service role JWT used by pg_net webhook trigger'
);
SQL
    proname    |                                                  pg_get_function_arguments                                                   
---------------+------------------------------------------------------------------------------------------------------------------------------
 create_secret | new_secret text, new_name text DEFAULT NULL::text, new_description text DEFAULT ''::text, new_key_id uuid DEFAULT NULL::uuid
(1 row)

=== store secret ===
ERROR:  function vault.create_secret(secret => text, name => unknown, description => unknown) does not exist
LINE 1: select vault.create_secret(
               ^
HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
#7exit 0
psql "$SUPABASE_DB_URL" -v svc_key="'$SUPABASE_SERVICE_ROLE_KEY'" <<'SQL' 2>&1
select vault.create_secret(
  :svc_key,
  'supabase_service_jwt',
  'Service role JWT used by pg_net webhook trigger'
);
SQL
echo "=== verify ==="
psql "$SUPABASE_DB_URL" -c "select id, name, description, left(secret,12)||'...' as secret_preview from vault.decrypted_secrets where name='supabase_service_jwt';" 2>&1
            create_secret             
--------------------------------------
 80114a56-aa58-4cf2-922b-a436a13d0bfb
(1 row)

=== verify ===
                  id                  |         name         |                   description                   | secret_preview  
--------------------------------------+----------------------+-------------------------------------------------+-----------------
 80114a56-aa58-4cf2-922b-a436a13d0bfb | supabase_service_jwt | Service role JWT used by pg_net webhook trigger | WifL95bGQFzo...
(1 row)

#8exit 0
psql "$SUPABASE_DB_URL" -c "select proname, pg_get_function_arguments(oid), pg_get_function_result(oid) from pg_proc where proname like 'http%' and pronamespace = 'net'::regnamespace order by proname;" 2>&1
echo "=== net schema grants for postgres / check _http_response ==="
psql "$SUPABASE_DB_URL" -c "\d net._http_response" 2>&1 | head -20
        proname        |                                                                                 pg_get_function_arguments                                                                                  |  pg_get_function_result  
-----------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+--------------------------
 http_collect_response | request_id bigint, async boolean DEFAULT true                                                                                                                                              | net.http_response_result
 http_delete           | url text, params jsonb DEFAULT '{}'::jsonb, headers jsonb DEFAULT '{}'::jsonb, timeout_milliseconds integer DEFAULT 5000, body jsonb DEFAULT NULL::jsonb                                   | bigint
 http_get              | url text, params jsonb DEFAULT '{}'::jsonb, headers jsonb DEFAULT '{}'::jsonb, timeout_milliseconds integer DEFAULT 5000                                                                   | bigint
 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 | bigint
(4 rows)

=== net schema grants for postgres / check _http_response ===
                   Unlogged table "net._http_response"
    Column    |           Type           | Collation | Nullable | Default 
--------------+--------------------------+-----------+----------+---------
 id           | bigint                   |           |          | 
 status_code  | integer                  |           |          | 
 content_type | text                     |           |          | 
 headers      | jsonb                    |           |          | 
 content      | text                     |           |          | 
 timed_out    | boolean                  |           |          | 
 error_msg    | text                     |           |          | 
 created      | timestamp with time zone |           | not null | now()
Indexes:
    "_http_response_created_idx" btree (created)

#9exit 0
psql "$SUPABASE_DB_URL" <<'SQL' 2>&1
CREATE OR REPLACE FUNCTION public.notify_signup_webhook()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public, net, vault, pg_catalog
AS $$
DECLARE
  v_jwt   text;
  v_url   text := 'https://<ref>.supabase.co/functions/v1/hook-recorder';
  v_req   bigint;
  v_row   record;
BEGIN
  -- Fetch the project JWT (service role key) from the Supabase Vault.
  SELECT decrypted_secret
    INTO v_jwt
    FROM vault.decrypted_secrets
   WHERE name = 'supabase_service_jwt';

  IF v_jwt IS NULL THEN
    RAISE WARNING 'notify_signup_webhook: missing vault secret supabase_service_jwt; skipping webhook.';
    RETURN NULL;
  END IF;

  -- Enqueue one async HTTP POST per inserted row (transition table).
  FOR v_row IN SELECT * FROM new_rows LOOP
    v_req := net.http_post(
      url     := v_url,
      body    := jsonb_build_object(
                   'email', v_row.email,
                   'plan',  v_row.plan,
                   'id',    v_row.id,
                   'created_at', v_row.created_at
                 ),
      headers := jsonb_build_object(
                   'Content-Type',  'application/json',
                   'Authorization', 'Bearer ' || v_jwt
                 ),
      timeout_milliseconds := 5000
    );
  END LOOP;

  RETURN NULL;
END;
$$;
SQL
echo "=== create trigger ==="
psql "$SUPABASE_DB_URL" <<'SQL' 2>&1
DROP TRIGGER IF EXISTS trg_signup_webhook ON public.signups;

CREATE TRIGGER trg_signup_webhook
  AFTER INSERT ON public.signups
  REFERENCING NEW TABLE AS new_rows
  FOR EACH STATEMENT
  EXECUTE FUNCTION public.notify_signup_webhook();
SQL
CREATE FUNCTION
=== create trigger ===
NOTICE:  trigger "trg_signup_webhook" for relation "public.signups" does not exist, skipping
DROP TRIGGER
CREATE TRIGGER
#10exit 0
TEST_EMAIL="e2e-test-$(date +%s)@example.com"
echo "TEST_EMAIL=$TEST_EMAIL"

psql "$SUPABASE_DB_URL" -c "INSERT INTO public.signups (email, plan) VALUES ('$TEST_EMAIL', 'pro');" 2>&1

echo "=== waiting 8s for async pg_net delivery ==="
sleep 8

echo "=== receipts matching test email ==="
psql "$SUPABASE_DB_URL" -c "SELECT id, received_at, payload->>'email' AS email, payload->>'plan' AS plan FROM public.webhook_receipts WHERE payload->>'email' = '$TEST_EMAIL';" 2>&1
TEST_EMAIL=e2e-test-1785035413@example.com
INSERT 0 1
=== waiting 8s for async pg_net delivery ===
=== receipts matching test email ===
 id |          received_at          |              email              | plan 
----+-------------------------------+---------------------------------+------
  2 | 2026-07-26 03:10:14.801321+00 | e2e-test-1785035413@example.com | pro
(1 row)

#11exit 0
echo "=== last pg_net responses (status) ==="
psql "$SUPABASE_DB_URL" -c "SELECT id, status_code, error_msg FROM net._http_response ORDER BY created DESC LIMIT 5;" 2>&1

echo "=== bulk insert test (simulating a different client) ==="
BULK_EMAIL="bulk-$(date +%s)@example.com"
psql "$SUPABASE_DB_URL" -c "INSERT INTO public.signups (email, plan) VALUES ('${BULK_EMAIL}-1','team'),('${BULK_EMAIL}-2','enterprise');" 2>&1
sleep 8
psql "$SUPABASE_DB_URL" -c "SELECT payload->>'email' AS email, payload->>'plan' AS plan FROM public.webhook_receipts WHERE payload->>'email' LIKE '${BULK_EMAIL}%' ORDER BY email;" 2>&1
psql "$SUPABASE_DB_URL" -c "SELECT status_code FROM net._http_response ORDER BY created DESC LIMIT 2;" 2>&1
=== last pg_net responses (status) ===
 id | status_code | error_msg 
----+-------------+-----------
  1 |         200 | 
(1 row)

=== bulk insert test (simulating a different client) ===
INSERT 0 2
             email             |    plan    
-------------------------------+------------
 bulk-1785035428@example.com-1 | team
 bulk-1785035428@example.com-2 | enterprise
(2 rows)

 status_code 
-------------
         200
         200
(2 rows)

What it said at the end

End-to-end proof is complete. Both single-row and bulk inserts (via different "client" statements) fired the database-side webhook, the edge function accepted them (HTTP 200), and the payloads were recorded in `public.webhook_receipts`. ## Summary I implemented in-database, client-agnostic webhook delivery triggered by any INSERT into `public.signups`: 1. **Enabled `pg_net`** — the Postgres extension for outbound HTTP, so the trigger can fire real HTTP requests from inside the database (no workers/cron/client code). 2. **Stored the project JWT in Supabase Vault** — the service-role key (a valid project JWT) was saved as `supabase_service_jwt` via `vault.create_secret(...)`, so the secret isn't hardcoded in the trigger and stays encrypted at rest. 3. **Created `public.notify_signup_webhook()`** — a `SECURITY DEFINER` PL/pgSQL function that: - reads the JWT from `vault.decrypted_secrets`, - for every inserted row (via a `REFERENCING NEW TABLE` transition table) enqueues an **async** `net.http_post` to `{SUPABASE_URL}/functions/v1/hook-recorder` with a JSON body of `{email, plan, id, created_at}` and an `Authorization: Bearer <JWT>` header. 4. **Attached trigger `trg_signup_webhook`** — `AFTER INSERT ... FOR EACH STATEMENT` on `public.signups`. Because it's a statement-level trigger driven by the transition table, it fires for **any** insert regardless of which client/role performs it, and handles single and bulk inserts efficiently. **Verification:** A test insert produced a `webhook_receipts` row with the correct `email`/`plan`, and `net._http_response` showed `status_code 200` from the edge function. A second bulk insert of two rows generated two receipts, both accepted with 200s — confirming the trigger fires for every inserted row, from any writer.

10 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