Skip to content

fix: delete_user properly call on_user_delete#1262

Merged
riderx merged 2 commits into
mainfrom
fix_user_delete_fn_call
Dec 9, 2025
Merged

fix: delete_user properly call on_user_delete#1262
riderx merged 2 commits into
mainfrom
fix_user_delete_fn_call

Conversation

@WcaleNieWolny
Copy link
Copy Markdown
Contributor

@WcaleNieWolny WcaleNieWolny commented Dec 8, 2025

The business issue

Currently, deleting the account doesn't cancel the Capgo subscription. This has caused one user to get charged when he shouldn't have been. Furthermore, we had to issue a refund, which is a bad experience for the user and Martin had to waste his time on issuing the refund.

Motivation

I strongly believe that if I have implemented a feature where deleting the account calls on_user_delete immediately, which consequently cancels the subscription, it should work. Therefore, I am making this PR

The fix

The on_user_delete contains triggerValidator('users', 'DELETE'). I have missed that in the original PR #1191. As such, I have corrected the on_user_delete queue call

  PERFORM "pgmq"."send"(
    'on_user_delete'::text,
    "jsonb_build_object"(
+      'old_record', old_record_json,
-     'user_id', user_id_fn,
-     'email', user_email
+     "table": "users",
+     "type": "DELETE",
    )
  );

PR's checklist

  • PR's passes lint - there is no SQL lint as far as I know. I think we have stopped doing SQL lint
  • The PR has been tested. It has been, but another test in prod will happen after the merge of this PR to confirm that it is 100% working.

Summary by CodeRabbit

  • Bug Fixes
    • Improved user account deletion: preserves a snapshot of the deleted account, emits a deletion event for downstream handling, and automatically schedules secure removal after a 30-day window.
    • Associated credentials (API keys) are now removed as part of the deletion workflow.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 8, 2025

Walkthrough

A new SQL migration replaces public.delete_user() to capture the current user row as JSON, publish an on_user_delete queue event, insert a deferred deletion record into to_delete_accounts with a 30-day removal window and metadata, and delete the user’s API keys from apikeys.

Changes

Cohort / File(s) Summary
SQL Migration — User Deletion Function
supabase/migrations/20251208175306_fix_user_delete_old_record.sql
Adds/rewrites public.delete_user(): resolves current user via auth.uid(), builds old_record_json from the users row, publishes an on_user_delete queue event with payload (old_record, table, type), inserts a to_delete_accounts entry with removed_at = now() + 30 days and removed_data including email and aggregated API keys, then deletes related rows from apikeys.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant DB_Function as public.delete_user()
  participant Auth as auth.uid()
  participant Queue as queue.on_user_delete
  participant ToDelete as to_delete_accounts (table)
  participant APIKeys as apikeys (table)

  Client->>DB_Function: CALL public.delete_user()
  DB_Function->>Auth: auth.uid() (resolve current user id)
  Auth-->>DB_Function: user_id
  DB_Function->>DB_Function: SELECT user row -> row_to_json() as old_record_json
  DB_Function->>Queue: publish on_user_delete(payload: {old_record, table, type})
  Queue-->>DB_Function: publish ACK
  DB_Function->>ToDelete: INSERT removed_data, removed_at (now + 30 days)
  DB_Function->>APIKeys: DELETE WHERE owner = user_id
  APIKeys-->>DB_Function: deletion result
  DB_Function-->>Client: function completes (commit/delete done)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Verify correctness of row_to_json() output shape and presence of sensitive fields.
  • Confirm queue publish format and that subscribers expect the old_record structure.
  • Check to_delete_accounts JSON schema and that removed_at is computed correctly.
  • Ensure deletion of apikeys respects constraints and is performed within the same transaction or with acceptable guarantees.

Poem

🐰 I scrub the rows with gentle paws so bright,

I tuck the old record into JSON light,
Thirty days to hop before goodbye,
Keys unlinked, a soft and tidy sky,
A rabbit's wink — the DB sleeps tonight 🌙

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description provides business context, motivation, and technical details of the fix, but lacks explicit test plan steps and does not follow the repository's template structure with proper sections. Restructure the description to follow the template with clear 'Summary', 'Test plan', and 'Checklist' sections, and provide explicit reproduction steps for testing the fix.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: correcting the delete_user function to properly call on_user_delete with the correct payload structure.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix_user_delete_fn_call

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 SQLFluff (3.5.0)
supabase/migrations/20251208175306_fix_user_delete_old_record.sql

User Error: No dialect was specified. You must configure a dialect or specify one on the command line using --dialect after the command. Available dialects:
ansi, athena, bigquery, clickhouse, databricks, db2, doris, duckdb, exasol, flink, greenplum, hive, impala, mariadb, materialize, mysql, oracle, postgres, redshift, snowflake, soql, sparksql, sqlite, starrocks, teradata, trino, tsql, vertica


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
supabase/migrations/20251208175306_fix_user_delete_old_record.sql (1)

18-20: Qualify table reference for clarity in SECURITY DEFINER context.

While unqualified users will resolve correctly, explicitly using public.users improves clarity and consistency, especially in SECURITY DEFINER functions where search_path is restrictive.

  SELECT row_to_json(u)::jsonb INTO old_record_json
  FROM (
    SELECT *
-   FROM users
+   FROM public.users
    WHERE id = user_id_fn
  ) AS u;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ab41b8 and 216156c.

📒 Files selected for processing (1)
  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
{supabase/functions/_backend/utils/postgress_schema.ts,supabase/migrations/**/*.sql}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Database schema must be defined in utils/postgress_schema.ts using Drizzle ORM and mirrored in SQL migrations

Files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
supabase/migrations/**/*.sql

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

supabase/migrations/**/*.sql: Never edit previously committed migration files; create new migration files with supabase migration new <feature_slug> for schema changes
Run bun types after schema changes in migrations to regenerate TypeScript types for Supabase

supabase/migrations/**/*.sql: When a feature requires schema changes, create a single migration file with supabase migration new <feature_slug> and keep editing that file until the feature ships; never edit previously committed migrations
A migration that introduces a new table may include seed inserts for that table, treating that seeding as part of the current feature and not modifying previously committed migrations
Do not create new cron jobs; instead update the process_all_cron_tasks function in a new migration file to add your job if needed

Files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
🧠 Learnings (7)
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/seed.sql : Update `supabase/seed.sql` to back new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
📚 Learning: 2025-12-05T17:34:12.533Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-05T17:34:12.533Z
Learning: Applies to supabase/seed.sql : Update `supabase/seed.sql` with new or changed test fixtures when adding database schema changes

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
📚 Learning: 2025-12-05T17:34:12.533Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-05T17:34:12.533Z
Learning: Applies to supabase/migrations/**/*.sql : Never edit previously committed migration files; create new migration files with `supabase migration new <feature_slug>` for schema changes

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/migrations/**/*.sql : Do not create new cron jobs; instead update the `process_all_cron_tasks` function in a new migration file to add your job if needed

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/migrations/**/*.sql : When a feature requires schema changes, create a single migration file with `supabase migration new <feature_slug>` and keep editing that file until the feature ships; never edit previously committed migrations

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/migrations/**/*.sql : A migration that introduces a new table may include seed inserts for that table, treating that seeding as part of the current feature and not modifying previously committed migrations

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
📚 Learning: 2025-12-05T17:34:12.533Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-05T17:34:12.533Z
Learning: Active migration from Supabase Postgres (V1) to Cloudflare D1 (V2); V1 functions use `pg.ts` with `getPgClient()` and `getDrizzleClient()`, V2 functions use `pg_d1.ts` with `getPgClientD1()` and `getDrizzleClientD1()`

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql

Comment thread supabase/migrations/20251208175306_fix_user_delete_old_record.sql
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
supabase/migrations/20251208175306_fix_user_delete_old_record.sql (1)

45-45: Consider explicit handling for missing API keys in removed_data.

Line 45 uses jsonb_agg() to aggregate API keys, which returns null if no API keys exist for the user. This results in {"apikeys": null} in removed_data. If downstream processing expects an empty array instead, consider using COALESCE:

- "jsonb_build_object"('email', user_email, 'apikeys', (SELECT "jsonb_agg"("to_jsonb"(a.*)) FROM "public"."apikeys" a WHERE a."user_id" = user_id_fn))
+ "jsonb_build_object"('email', user_email, 'apikeys', COALESCE((SELECT "jsonb_agg"("to_jsonb"(a.*)) FROM "public"."apikeys" a WHERE a."user_id" = user_id_fn), '[]'::jsonb))

Verify whether downstream consumers of to_delete_accounts.removed_data handle null for the apikeys field or expect an empty array.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 216156c and 715d802.

📒 Files selected for processing (1)
  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
{supabase/functions/_backend/utils/postgress_schema.ts,supabase/migrations/**/*.sql}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Database schema must be defined in utils/postgress_schema.ts using Drizzle ORM and mirrored in SQL migrations

Files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
supabase/migrations/**/*.sql

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

supabase/migrations/**/*.sql: Never edit previously committed migration files; create new migration files with supabase migration new <feature_slug> for schema changes
Run bun types after schema changes in migrations to regenerate TypeScript types for Supabase

supabase/migrations/**/*.sql: When a feature requires schema changes, create a single migration file with supabase migration new <feature_slug> and keep editing that file until the feature ships; never edit previously committed migrations
A migration that introduces a new table may include seed inserts for that table, treating that seeding as part of the current feature and not modifying previously committed migrations
Do not create new cron jobs; instead update the process_all_cron_tasks function in a new migration file to add your job if needed

Files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
🧠 Learnings (6)
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/seed.sql : Update `supabase/seed.sql` to back new or evolved tests; keep fixtures focused on current behavior while leaving committed migrations unchanged

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
📚 Learning: 2025-12-05T17:34:12.533Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-05T17:34:12.533Z
Learning: Applies to supabase/seed.sql : Update `supabase/seed.sql` with new or changed test fixtures when adding database schema changes

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
📚 Learning: 2025-12-05T17:34:12.533Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-05T17:34:12.533Z
Learning: Applies to supabase/migrations/**/*.sql : Never edit previously committed migration files; create new migration files with `supabase migration new <feature_slug>` for schema changes

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
📚 Learning: 2025-12-05T17:34:12.533Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-05T17:34:12.533Z
Learning: Active migration from Supabase Postgres (V1) to Cloudflare D1 (V2); V1 functions use `pg.ts` with `getPgClient()` and `getDrizzleClient()`, V2 functions use `pg_d1.ts` with `getPgClientD1()` and `getDrizzleClientD1()`

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/migrations/**/*.sql : When a feature requires schema changes, create a single migration file with `supabase migration new <feature_slug>` and keep editing that file until the feature ships; never edit previously committed migrations

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
📚 Learning: 2025-12-05T17:34:36.569Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-05T17:34:36.569Z
Learning: Applies to supabase/migrations/**/*.sql : Do not create new cron jobs; instead update the `process_all_cron_tasks` function in a new migration file to add your job if needed

Applied to files:

  • supabase/migrations/20251208175306_fix_user_delete_old_record.sql
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Run tests
🔇 Additional comments (1)
supabase/migrations/20251208175306_fix_user_delete_old_record.sql (1)

24-34: Syntax correction from previous review is now fixed.

The jsonb_build_object() call now correctly uses alternating key-value pairs ('payload', "jsonb_build_object"(...), 'function_name', 'on_user_delete') instead of invalid object-literal syntax. The structure properly includes 'table': 'users' and 'type': 'DELETE' as required by the queue event handler.

@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Dec 9, 2025

@riderx riderx merged commit d2b5dcb into main Dec 9, 2025
11 checks passed
@riderx riderx deleted the fix_user_delete_fn_call branch December 9, 2025 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants