Supabase row updated or deleted notifications on your phone.

Get a push notification when a row in a Supabase table is updated or deleted, showing which columns changed from what to what.

How it works

Three steps to your first push

STEP 01

Install the recipe

One click in Studio gives you a personal webhook URL.

STEP 02

Paste your URL into Supabase

Add it as a webhook for Update.

STEP 03

Get a push on your phone

With the text, sound and buttons from the recipe.

Setup

How to set up the Supabase update webhook

  1. Click Install in Studio and sign in. The recipe is added to your account and you get a personal webhook URL.
  2. In the Supabase dashboard, open Integrations > Database Webhooks (enable it if asked) and click Create a new hook.
  3. Pick the table and the Update event, and choose HTTP Request with method POST.
  4. Paste your webhook URL as the URL, adding any options from the setup notes below, and create the webhook.https://••••••••/•••••••• your personal URL, shown after install
  5. Tick **Update** (and **Delete** if you want those too) when you create the hook. Updates show up to three changed columns as `old → new`; `updated_at`-style columns are ignored. Add query parameters to the URL to tune it:
  6. | Parameter | What it does | Example |
  7. | --- | --- | --- |
  8. | `field` | Only push when one of these columns changes, and only show those. | `field=status` |
  9. | `label` | The column that names the row (default `id`, shown as `#42`). | `label=customer_email` |
  10. | `title` | The push title. `{column}` is replaced with the row's (new) value, `{table}` with the table name. | `title=Order {id} is now {status}` |
  11. | `url` | A button link, with `{column}` placeholders. | `url=https://app.example.com/orders/{id}` |
  12. | `priority` | `-2` (lowest) to `2` (highest). Default `-1`. | `priority=0` |
  13. | `deletes` | `0` ignores deletes. | `deletes=0` |
  14. | `table` | Only push for these tables (comma-separated). | `table=orders` |
  15. For example, to hear about order status changes only:
  16. ```
  17. your webhook URL?field=status&label=customer_email&priority=0
  18. ```
  19. Encode spaces as `%20` and `{` `}` as `%7B` `%7D`, or add the parameters under **HTTP Parameters** in the webhook form.
  20. Supabase sends the **whole row, before and after**, to JustPush, so don't hook up tables with secrets or tokens in them. The request isn't signed, so keep your endpoint URL private.
The code

What runs when the webhook arrives

Studio calls handleRequest(request) with the incoming webhook and sends the message it returns. It's yours after install; change anything.

transform.js
1// Supabase → Database Webhook UPDATE / DELETE (row changed)
2// Fires when a row is updated or deleted in a table you picked in Supabase's Database
3// Webhooks. Updates show which columns changed, old → new.
4// Docs: https://supabase.com/docs/guides/database/webhooks
5//
6// Payload: { type: "UPDATE" | "DELETE", table, schema, record, old_record }
7
8// Columns that change all the time and say little on their own.
9const NOISY = new Set(["updated_at", "modified_at", "last_seen_at"])
10// Columns that rarely make a useful summary of a deleted row.
11const BORING = new Set(["id", "uuid", "created_at", "updated_at", "inserted_at", "deleted_at"])
12
13function has(obj, key) {
14 return obj && typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, key)
15}
16
17// Keep long values readable in a notification.
18function clip(value, max = 60) {
19 const s = String(value).replace(/\s+/g, " ").trim()
20 return s.length > max ? s.slice(0, max - 1) + "…" : s
21}
22
23// A value as text; null and empty strings show as "empty".
24function show(value) {
25 if (value === null || value === undefined || value === "") return "empty"
26 return clip(typeof value === "object" ? JSON.stringify(value) : value)
27}
28
29// Replace {column} (and {table}) in a template with the row's values.
30function fill(template, record, table) {
31 return template
32 .replace(/\{([\w.-]+)\}/g, (_, key) =>
33 key === "table" && !has(record, "table") ? table : has(record, key) && record[key] !== null ? show(record[key]) : "")
34 .replace(/\s+/g, " ")
35 .trim()
36}
37
38// Comma-separated list from a query parameter.
39function list(value) {
40 return typeof value === "string" ? value.split(",").map((s) => s.trim()).filter(Boolean) : []
41}
42
43// Only link to real http(s) URLs.
44function link(url, cta) {
45 return typeof url === "string" && /^https?:\/\/\S+$/.test(url) ? [{ cta, url }] : []
46}
47
48function handleRequest(request) {
49 const body = request.body && typeof request.body === "object" ? request.body : {}
50 const query = request.query && typeof request.query === "object" ? request.query : {}
51 const type = body.type
52 const record = type === "DELETE" ? body.old_record : body.record
53 const old = body.old_record
54
55 // Guard: only updates and deletes from a Database Webhook.
56 if (!["UPDATE", "DELETE"].includes(type) || !record || typeof record !== "object" || typeof body.table !== "string") {
57 console.log(`Ignoring Supabase "${type ?? "unknown"}" payload`)
58 return null
59 }
60
61 // ?table=orders,profiles limits which tables push.
62 const tables = list(query.table)
63 if (tables.length && !tables.includes(body.table) && !tables.includes(`${body.schema}.${body.table}`)) {
64 console.log(`Skipping table ${body.schema}.${body.table}`)
65 return null
66 }
67
68 const table = body.table
69 const fields = list(query.field)
70 // Name the row by ?label=column, else its id.
71 const labelKey = typeof query.label === "string" && has(record, query.label) ? query.label : "id"
72 const label = has(record, labelKey) && record[labelKey] !== null ? show(record[labelKey]) : null
73 const prefix = label ? (labelKey === "id" ? `#${label}` : label) : null
74
75 let title, message
76 if (type === "UPDATE") {
77 if (!old || typeof old !== "object") {
78 console.log("Update without old_record — skipping")
79 return null
80 }
81 // Columns whose value changed; ?field=… watches only those columns.
82 const changed = Object.keys(record).filter(
83 (k) => !NOISY.has(k) && (!fields.length || fields.includes(k)) && JSON.stringify(record[k]) !== JSON.stringify(old[k]),
84 )
85 if (!changed.length) {
86 console.log("No watched column changed — skipping")
87 return null
88 }
89 title = `✏️ ${table} updated`
90 message =
91 (prefix ? `${prefix}: ` : "") +
92 changed.slice(0, 3).map((k) => `${k} ${show(old[k])} → ${show(record[k])}`).join(" · ") +
93 (changed.length > 3 ? ` (+${changed.length - 3} more)` : "")
94 } else {
95 // ?deletes=0 turns deletes off if the hook also sends them.
96 if (["0", "false", "no"].includes(String(query.deletes ?? "").toLowerCase())) {
97 console.log("Deletes turned off with ?deletes=0 — skipping")
98 return null
99 }
100 const shown = (fields.length ? fields : Object.keys(record).filter((k) => !BORING.has(k) && ["string", "number", "boolean"].includes(typeof record[k])))
101 .filter((k) => has(record, k) && record[k] !== null && record[k] !== "")
102 .slice(0, 3)
103 title = `🗑️ Row deleted from ${table}`
104 message = [prefix, shown.map((k) => `${k}: ${show(record[k])}`).join(" · ")].filter(Boolean).join(" — ") || "A row was deleted"
105 }
106
107 if (typeof query.title === "string" && fill(query.title, record, table)) title = fill(query.title, record, table)
108
109 // ?priority=-2…2 overrides the default.
110 const wanted = Number(query.priority)
111 const priority = query.priority !== undefined && Number.isInteger(wanted) && wanted >= -2 && wanted <= 2 ? wanted : -1
112
113 return {
114 title,
115 message,
116 topic: "Supabase",
117 priority, // Low by default — routine data changes; raise it with ?priority= for tables that matter
118 buttons: typeof query.url === "string" ? link(fill(query.url, record, table).replace(/ /g, "%20"), "Open") : [],
119 }
120}
Payload

The Supabase UPDATE webhook

This is what Supabase sends to your URL for update. It is a sample, trimmed to the fields recipes use.

UPDATE · sample.json
1{
2 "type": "UPDATE",
3 "table": "orders",
4 "record": {
5 "id": 42,
6 "status": "paid",
7 "updated_at": "2026-09-29T10:05:12.100Z",
8 "total_cents": 4900,
9 "customer_email": "[email protected]"
10 },
11 "schema": "public",
12 "old_record": {
13 "id": 42,
14 "status": "pending",
15 "updated_at": "2026-09-29T09:58:40.300Z",
16 "total_cents": 4900,
17 "customer_email": "[email protected]"
18 }
19}
FAQ

Supabase row updated or deleted notifications: questions

Does this work on iPhone and Android?

Yes. Install the JustPush app from the App Store or Google Play and sign in. Every phone signed in to your account gets the notification.

Do I need to write code?

No. Install the recipe in Studio, paste your webhook URL into Supabase and you are done. The code is there if you want to change the text, the sound or the buttons.

Can I change what the notification says?

Yes. After install the recipe's code is yours. Edit it in Studio and test it against the sample payload before you save.

What does it cost?

JustPush is free for 30 days. After that it's $19.99 a year, or $39.99 once. There is no extra charge for recipes.

Ready when you are

Supabase on your phone in two minutes.

Install the recipe, paste one URL, done. Free for 30 days, no credit card required.