# 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. - Company: Supabase (https://www.justpush.io/recipes/supabase) - Event: `UPDATE` (Update) - Tags: Backend & auth, Database changes - Install: https://studio.justpush.io/recipes/supabase/row-changed - Web page: https://www.justpush.io/recipes/supabase/row-changed ## Setup 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 (shown in Studio after install) as the URL, adding any options from the setup notes below, and create the webhook. 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 (shown in Studio after install)?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. ## Code Studio calls `handleRequest(request)` with the incoming webhook (`{ method, headers, body, query, raw }`) and sends the message object it returns. Returning `null` sends nothing. ```js // Supabase → Database Webhook UPDATE / DELETE (row changed) // Fires when a row is updated or deleted in a table you picked in Supabase's Database // Webhooks. Updates show which columns changed, old → new. // Docs: https://supabase.com/docs/guides/database/webhooks // // Payload: { type: "UPDATE" | "DELETE", table, schema, record, old_record } // Columns that change all the time and say little on their own. const NOISY = new Set(["updated_at", "modified_at", "last_seen_at"]) // Columns that rarely make a useful summary of a deleted row. const BORING = new Set(["id", "uuid", "created_at", "updated_at", "inserted_at", "deleted_at"]) function has(obj, key) { return obj && typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, key) } // Keep long values readable in a notification. function clip(value, max = 60) { const s = String(value).replace(/\s+/g, " ").trim() return s.length > max ? s.slice(0, max - 1) + "…" : s } // A value as text; null and empty strings show as "empty". function show(value) { if (value === null || value === undefined || value === "") return "empty" return clip(typeof value === "object" ? JSON.stringify(value) : value) } // Replace {column} (and {table}) in a template with the row's values. function fill(template, record, table) { return template .replace(/\{([\w.-]+)\}/g, (_, key) => key === "table" && !has(record, "table") ? table : has(record, key) && record[key] !== null ? show(record[key]) : "") .replace(/\s+/g, " ") .trim() } // Comma-separated list from a query parameter. function list(value) { return typeof value === "string" ? value.split(",").map((s) => s.trim()).filter(Boolean) : [] } // Only link to real http(s) URLs. function link(url, cta) { return typeof url === "string" && /^https?:\/\/\S+$/.test(url) ? [{ cta, url }] : [] } function handleRequest(request) { const body = request.body && typeof request.body === "object" ? request.body : {} const query = request.query && typeof request.query === "object" ? request.query : {} const type = body.type const record = type === "DELETE" ? body.old_record : body.record const old = body.old_record // Guard: only updates and deletes from a Database Webhook. if (!["UPDATE", "DELETE"].includes(type) || !record || typeof record !== "object" || typeof body.table !== "string") { console.log(`Ignoring Supabase "${type ?? "unknown"}" payload`) return null } // ?table=orders,profiles limits which tables push. const tables = list(query.table) if (tables.length && !tables.includes(body.table) && !tables.includes(`${body.schema}.${body.table}`)) { console.log(`Skipping table ${body.schema}.${body.table}`) return null } const table = body.table const fields = list(query.field) // Name the row by ?label=column, else its id. const labelKey = typeof query.label === "string" && has(record, query.label) ? query.label : "id" const label = has(record, labelKey) && record[labelKey] !== null ? show(record[labelKey]) : null const prefix = label ? (labelKey === "id" ? `#${label}` : label) : null let title, message if (type === "UPDATE") { if (!old || typeof old !== "object") { console.log("Update without old_record — skipping") return null } // Columns whose value changed; ?field=… watches only those columns. const changed = Object.keys(record).filter( (k) => !NOISY.has(k) && (!fields.length || fields.includes(k)) && JSON.stringify(record[k]) !== JSON.stringify(old[k]), ) if (!changed.length) { console.log("No watched column changed — skipping") return null } title = `✏️ ${table} updated` message = (prefix ? `${prefix}: ` : "") + changed.slice(0, 3).map((k) => `${k} ${show(old[k])} → ${show(record[k])}`).join(" · ") + (changed.length > 3 ? ` (+${changed.length - 3} more)` : "") } else { // ?deletes=0 turns deletes off if the hook also sends them. if (["0", "false", "no"].includes(String(query.deletes ?? "").toLowerCase())) { console.log("Deletes turned off with ?deletes=0 — skipping") return null } const shown = (fields.length ? fields : Object.keys(record).filter((k) => !BORING.has(k) && ["string", "number", "boolean"].includes(typeof record[k]))) .filter((k) => has(record, k) && record[k] !== null && record[k] !== "") .slice(0, 3) title = `🗑️ Row deleted from ${table}` message = [prefix, shown.map((k) => `${k}: ${show(record[k])}`).join(" · ")].filter(Boolean).join(" — ") || "A row was deleted" } if (typeof query.title === "string" && fill(query.title, record, table)) title = fill(query.title, record, table) // ?priority=-2…2 overrides the default. const wanted = Number(query.priority) const priority = query.priority !== undefined && Number.isInteger(wanted) && wanted >= -2 && wanted <= 2 ? wanted : -1 return { title, message, topic: "Supabase", priority, // Low by default — routine data changes; raise it with ?priority= for tables that matter buttons: typeof query.url === "string" ? link(fill(query.url, record, table).replace(/ /g, "%20"), "Open") : [], } } ``` ## Supabase UPDATE webhook payload (sample) ```json { "type": "UPDATE", "table": "orders", "record": { "id": 42, "status": "paid", "updated_at": "2026-09-29T10:05:12.100Z", "total_cents": 4900, "customer_email": "ada@example.com" }, "schema": "public", "old_record": { "id": 42, "status": "pending", "updated_at": "2026-09-29T09:58:40.300Z", "total_cents": 4900, "customer_email": "ada@example.com" } } ``` ## FAQ ### 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.