Skip to content

feat: add image cleanup on deletion#1319

Merged
riderx merged 6 commits into
mainfrom
riderx/cleanup-orphan-images
Dec 29, 2025
Merged

feat: add image cleanup on deletion#1319
riderx merged 6 commits into
mainfrom
riderx/cleanup-orphan-images

Conversation

@riderx
Copy link
Copy Markdown
Member

@riderx riderx commented Dec 28, 2025

Summary

Automatically delete stored images from Supabase Storage when users, apps, or organizations are deleted. This prevents storage from accumulating orphaned images that are no longer referenced in the database.

Test plan

  • User deletion: Verify user avatars at images/{user_id}/* are deleted
  • App deletion: Verify app icons at images/org/{org_id}/{app_id}/icon are deleted both via API and trigger
  • Org deletion: Verify all org images at images/org/{org_id}/* are deleted
  • Weekly CRON: Verify orphaned images (from failed deletions) are cleaned up on Sunday at 03:00

Changes

  • Add image cleanup to user, app, and org deletion triggers

  • Add image cleanup to app and org deletion API endpoints

  • Create cron_clean_orphan_images weekly cleanup job integrated into existing queue system

  • Add database migration with pgmq queue and process_combined_cron_jobs integration

  • My code follows the code style of this project and passes bun run lint:backend && bun run lint.

  • My change requires a change to the documentation.

  • I have updated the documentation accordingly.

  • My change has adequate E2E test coverage.

  • I have tested my code manually, and I have provided steps how to reproduce my tests

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Automatic image cleanup: Images and media files associated with deleted users, organizations, and applications are now automatically removed from storage, improving overall storage efficiency.
    • Scheduled maintenance infrastructure: New system for automating periodic resource cleanup and maintenance operations.
  • Chores

    • Implemented database schema for scheduling and managing automated maintenance tasks.

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

Implement automatic removal of stored images from Supabase Storage when users, apps, or organizations are deleted. This prevents storage from accumulating orphaned images.

Changes:
- Delete user avatar images when users are deleted
- Delete app icons when apps are deleted (both via trigger and API)
- Delete org images when organizations are deleted (both via trigger and API)
- Add cron_clean_orphan_images trigger for weekly cleanup of orphaned images
- Integrate orphan cleanup into existing process_combined_cron_jobs

Fixes #686

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 28, 2025

Warning

Rate limit exceeded

@riderx has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 21 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between bb710e9 and d35d953.

📒 Files selected for processing (1)
  • supabase/migrations/20251228215402_add_orphan_images_cleanup.sql

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

This pull request implements a comprehensive orphan image cleanup system. It adds storage cleanup logic to existing deletion handlers, introduces a new scheduled cron job for periodic orphan image discovery and removal, establishes a database-driven cron task scheduling system, and registers new routes to expose the cleanup functionality.

Changes

Cohort / File(s) Summary
Route registration for cron cleanup
cloudflare_workers/api/index.ts, supabase/functions/triggers/index.ts
Added imports and route registrations for the new /cron_clean_orphan_images endpoint on both public and global trigger routers.
Pre-deletion cleanup in public handlers
supabase/functions/_backend/public/app/delete.ts, supabase/functions/_backend/public/organization/delete.ts
Added storage cleanup steps before app and organization deletion: fetches owner org, lists and removes all related images, logs operations and errors, but does not fail overall deletion if cleanup encounters errors.
Cleanup in deletion triggers
supabase/functions/_backend/triggers/on_app_delete.ts, supabase/functions/_backend/triggers/on_organization_delete.ts, supabase/functions/_backend/triggers/on_user_delete.ts
Integrated storage cleanup into deletion triggers for apps, organizations, and users; lists and deletes associated images under respective storage paths; added error-tolerant logging.
New cron cleanup job
supabase/functions/_backend/triggers/cron_clean_orphan_images.ts
New serverless endpoint implementing two-phase orphan image cleanup: Phase 1 removes avatars for non-existent users; Phase 2 removes images for non-existent orgs and apps. Includes robust error handling, request tracking, and operation logging.
Cron task scheduling system
supabase/migrations/20251228215402_add_orphan_images_cleanup.sql
Database migration creating a new cron task enum type, cron_tasks table with scheduling fields (intervals, exact times, day constraints, batching), and functions cleanup_job_run_details_7days() and process_all_cron_tasks() to evaluate and execute tasks based on configuration. Seeds initial cron tasks for various maintenance jobs.

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as Cron Scheduler
    participant API as cron_clean_orphan_images<br/>Endpoint
    participant DB as Database<br/>(users, orgs, apps)
    participant Storage as Image Storage<br/>(Supabase)

    Scheduler->>API: POST /cron_clean_orphan_images
    activate API

    API->>Storage: Phase 1: List user folders
    Storage-->>API: User folder IDs
    loop For each user folder
        API->>DB: Check if user exists
        alt User not found
            API->>Storage: List images in user folder
            Storage-->>API: Image paths
            API->>Storage: Delete all user images
            API->>API: Log deleted count
        else User exists
            API->>API: Skip (continue)
        end
    end

    API->>Storage: Phase 2: List org folders
    Storage-->>API: Org folder IDs
    loop For each org folder
        API->>DB: Check if org exists
        alt Org not found
            API->>Storage: Delete org + app images
            API->>API: Update counters
        else Org exists
            API->>Storage: List apps in org
            Storage-->>API: App folder IDs
            loop For each app folder
                API->>DB: Check if app exists
                alt App not found
                    API->>Storage: Delete app images
                    API->>API: Update counters
                end
            end
        end
    end

    API->>API: Compile results<br/>(duration, deleted counts, errors)
    API-->>Scheduler: Return JSON summary
    deactivate API
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hop, hop, the orphans must go!
From user folders high to org storage below.
We scrub clean the images that roam without homes,
Now deletion triggers and cronies are friends who groom.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add image cleanup on deletion' accurately summarizes the main change—adding automatic image cleanup when users, apps, and organizations are deleted.
Description check ✅ Passed The PR description includes a summary, detailed test plan, and changes list. However, the checklist items are incomplete (all unchecked) and no manual testing steps are provided beyond the listed test areas.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +7 to +10
-- Update process_combined_cron_jobs to include orphan image cleanup
-- Run weekly on Sunday at 03:00:00
CREATE OR REPLACE FUNCTION "public"."process_combined_cron_jobs"() RETURNS void
LANGUAGE plpgsql
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Schedule cron_clean_orphan_images via process_all_cron_tasks

This migration only defines process_combined_cron_jobs, but the repo’s cron scheduler still invokes process_all_cron_tasks (see supabase/migrations/20251113140646_consolidate_cron_job.sql and later updates). Because this change doesn’t update that scheduled function or the cron.schedule call, the weekly enqueue of cron_clean_orphan_images never runs in production, so the advertised weekly cleanup will not happen.

Useful? React with 👍 / 👎.

Comment on lines +27 to +31
const { data: topLevelFolders, error: listError } = await supabase
.storage
.from('images')
.list('', { limit: 1000 })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Paginate storage listings to avoid missing orphan folders

The cleanup only lists the first 1,000 top‑level folders (and the org-folder listing later in this file is also capped) without pagination. In environments with >1,000 user/org folders, the remaining entries are never scanned, so orphaned images beyond that limit will never be deleted. Consider paging through storage listings until exhausted.

Useful? React with 👍 / 👎.

riderx and others added 3 commits December 28, 2025 22:49
…ation

Fixed the migration to properly extend the function from 20251228033417_webhooks.sql
instead of using an outdated version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Fixed the migration to use the correct function name process_all_cron_tasks
instead of the wrong process_combined_cron_jobs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Replace the hard-coded process_all_cron_tasks function with a table-driven
approach using a new cron_tasks table. This makes adding, modifying, or
disabling cron tasks much easier without code changes.

- Create cron_tasks table with scheduling configuration
- Create cron_task_type enum for different task types
- Migrate all existing cron jobs to table entries
- Refactor process_all_cron_tasks to read from table
- Add cleanup_job_run_details_7days helper function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
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: 3

♻️ Duplicate comments (1)
supabase/functions/_backend/triggers/cron_clean_orphan_images.ts (1)

27-31: Pagination limit may miss orphaned images in large deployments.

The storage listing is capped at 1000 items without pagination. In environments with >1000 user folders, orphaned images beyond that limit will never be scanned. Consider implementing pagination:

🔎 Pagination approach
// Pseudocode for paginated listing
let offset = 0
const limit = 1000
let allFolders: FileObject[] = []

while (true) {
  const { data } = await supabase.storage.from('images').list('', { limit, offset })
  if (!data || data.length === 0) break
  allFolders = allFolders.concat(data)
  if (data.length < limit) break
  offset += limit
}
🧹 Nitpick comments (1)
supabase/functions/_backend/public/organization/delete.ts (1)

33-59: Consider parallelizing folder cleanup for better performance.

The sequential for...of loop with await processes each app folder one at a time. For organizations with many apps, this could be slow. Parallelizing would improve deletion speed.

🔎 Proposed refactor using Promise.all
     if (folders && folders.length > 0) {
-      // For each subfolder (app_id), list and delete files
-      for (const folder of folders) {
-        if (folder.id === null) {
-          // This is a directory (app folder), list its contents
-          const { data: appFiles } = await supabaseAdmin(c)
-            .storage
-            .from('images')
-            .list(`org/${orgId}/${folder.name}`)
-
-          if (appFiles && appFiles.length > 0) {
-            const filePaths = appFiles.map(file => `org/${orgId}/${folder.name}/${file.name}`)
-            await supabaseAdmin(c)
-              .storage
-              .from('images')
-              .remove(filePaths)
-            cloudlog({ requestId: c.get('requestId'), message: 'deleted org app images', count: appFiles.length, folder: folder.name })
+      await Promise.all(folders.map(async (folder) => {
+        if (folder.id === null) {
+          const { data: appFiles } = await supabaseAdmin(c)
+            .storage
+            .from('images')
+            .list(`org/${orgId}/${folder.name}`)
+
+          if (appFiles && appFiles.length > 0) {
+            const filePaths = appFiles.map(file => `org/${orgId}/${folder.name}/${file.name}`)
+            await supabaseAdmin(c)
+              .storage
+              .from('images')
+              .remove(filePaths)
+            cloudlog({ requestId: c.get('requestId'), message: 'deleted org app images', count: appFiles.length, folder: folder.name })
           }
         }
-        else {
-          // This is a file directly in the org folder
+        else {
           await supabaseAdmin(c)
             .storage
             .from('images')
             .remove([`org/${orgId}/${folder.name}`])
         }
-      }
+      }))
       cloudlog({ requestId: c.get('requestId'), message: 'deleted all org images', org_id: orgId })
     }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e72c4da and bb710e9.

📒 Files selected for processing (9)
  • cloudflare_workers/api/index.ts
  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/triggers/cron_clean_orphan_images.ts
  • supabase/functions/_backend/triggers/on_app_delete.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/triggers/index.ts
  • supabase/migrations/20251228215402_add_orphan_images_cleanup.sql
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{ts,tsx,js,jsx,vue}

📄 CodeRabbit inference engine (CLAUDE.md)

Use single quotes and no semicolons per @antfu/eslint-config

Files:

  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/triggers/index.ts
  • supabase/functions/_backend/triggers/cron_clean_orphan_images.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • cloudflare_workers/api/index.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
  • supabase/functions/_backend/triggers/on_app_delete.ts
supabase/functions/_backend/**

📄 CodeRabbit inference engine (CLAUDE.md)

Backend logic should be organized in supabase/functions/_backend/ with subdirectories for plugins, private endpoints, public endpoints, triggers, and utilities

Files:

  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/_backend/triggers/cron_clean_orphan_images.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
  • supabase/functions/_backend/triggers/on_app_delete.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use TypeScript strict mode with path aliases mapping ~/ to src/

Files:

  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/triggers/index.ts
  • supabase/functions/_backend/triggers/cron_clean_orphan_images.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • cloudflare_workers/api/index.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
  • supabase/functions/_backend/triggers/on_app_delete.ts
supabase/functions/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Supabase Edge Functions use Deno runtime

Files:

  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/triggers/index.ts
  • supabase/functions/_backend/triggers/cron_clean_orphan_images.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
  • supabase/functions/_backend/triggers/on_app_delete.ts
supabase/functions/_backend/**/*.{ts,js}

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

supabase/functions/_backend/**/*.{ts,js}: Backend code must be placed in supabase/functions/_backend/ as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms
Use createHono from utils/hono.ts for all Hono framework application initialization and routing
All database operations must use getPgClient() or getDrizzleClient() from utils/pg.ts for PostgreSQL access during active migration to Cloudflare D1
All Hono endpoint handlers must accept Context<MiddlewareKeyVariables> and use c.get('requestId'), c.get('apikey'), and c.get('auth') for request context
Use structured logging with cloudlog({ requestId: c.get('requestId'), message: '...' }) for all backend logging
Use middlewareAPISecret for internal API endpoints and middlewareKey for external API keys; validate against owner_org in the apikeys table
Check c.get('auth')?.authType to determine authentication type ('apikey' vs 'jwt') in backend endpoints
Use Drizzle ORM query patterns with schema from postgress_schema.ts for all database operations; use aliasV2() for self-joins or multiple table references

Files:

  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/_backend/triggers/cron_clean_orphan_images.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
  • supabase/functions/_backend/triggers/on_app_delete.ts
supabase/functions/**/*.{ts,js}

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

Backend ESLint must pass before commit; run bun lint:backend for backend files

Files:

  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/triggers/index.ts
  • supabase/functions/_backend/triggers/cron_clean_orphan_images.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/triggers/on_user_delete.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
  • supabase/functions/_backend/triggers/on_app_delete.ts
cloudflare_workers/{api,plugin,files}/index.ts

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

Cloudflare Workers are split across three ports: API Worker (8787), Plugin Worker (8788), Files Worker (8789); see routing in cloudflare_workers/{api,plugin,files}/index.ts

Files:

  • cloudflare_workers/api/index.ts
cloudflare_workers/api/index.ts

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

API Worker (port 8787) routes: /bundle, /app, /device, /channel, /private/*, /triggers

Files:

  • cloudflare_workers/api/index.ts
supabase/migrations/**/*.sql

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

Database migrations must be created with supabase migration new <feature_slug> and never modify previously committed migrations

Files:

  • supabase/migrations/20251228215402_add_orphan_images_cleanup.sql
**/{migrations,tests,__tests__}/**/*.{sql,ts,js}

📄 CodeRabbit inference engine (AGENTS.md)

Always cover database changes with Postgres-level tests and complement them with end-to-end tests for affected user flows

Files:

  • supabase/migrations/20251228215402_add_orphan_images_cleanup.sql
supabase/migrations/*.sql

📄 CodeRabbit inference engine (AGENTS.md)

supabase/migrations/*.sql: When creating schema changes, use supabase migration new <feature_slug> to create a single migration file 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 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/20251228215402_add_orphan_images_cleanup.sql
🧠 Learnings (19)
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `middlewareAPISecret` for internal API endpoints and `middlewareKey` for external API keys; validate against `owner_org` in the `apikeys` table

Applied to files:

  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • cloudflare_workers/api/index.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use structured logging with `cloudlog({ requestId: c.get('requestId'), message: '...' })` for all backend logging

Applied to files:

  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/_backend/triggers/cron_clean_orphan_images.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/**/*.{ts,js} : Backend ESLint must pass before commit; run `bun lint:backend` for backend files

Applied to files:

  • supabase/functions/_backend/public/app/delete.ts
  • cloudflare_workers/api/index.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All Hono endpoint handlers must accept `Context<MiddlewareKeyVariables>` and use `c.get('requestId')`, `c.get('apikey')`, and `c.get('auth')` for request context

Applied to files:

  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use `createHono` from `utils/hono.ts` for all Hono framework application initialization and routing

Applied to files:

  • supabase/functions/_backend/public/app/delete.ts
  • supabase/functions/triggers/index.ts
  • supabase/functions/_backend/triggers/cron_clean_orphan_images.ts
  • supabase/functions/_backend/public/organization/delete.ts
  • cloudflare_workers/api/index.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
📚 Learning: 2025-12-05T17:34:25.556Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-05T17:34:25.556Z
Learning: Applies to supabase/functions/**/*.ts : Supabase Edge Functions use Deno runtime

Applied to files:

  • supabase/functions/_backend/public/app/delete.ts
  • cloudflare_workers/api/index.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : All database operations must use `getPgClient()` or `getDrizzleClient()` from `utils/pg.ts` for PostgreSQL access during active migration to Cloudflare D1

Applied to files:

  • supabase/functions/_backend/public/app/delete.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Check `c.get('auth')?.authType` to determine authentication type ('apikey' vs 'jwt') in backend endpoints

Applied to files:

  • supabase/functions/_backend/public/app/delete.ts
📚 Learning: 2025-12-27T03:51:23.575Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-27T03:51:23.575Z
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/functions/triggers/index.ts
  • supabase/functions/_backend/triggers/cron_clean_orphan_images.ts
  • supabase/migrations/20251228215402_add_orphan_images_cleanup.sql
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to cloudflare_workers/api/index.ts : API Worker (port 8787) routes: `/bundle`, `/app`, `/device`, `/channel`, `/private/*`, `/triggers`

Applied to files:

  • cloudflare_workers/api/index.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to cloudflare_workers/plugin/index.ts : Plugin Worker (port 8788) routes: `/updates`, `/channel_self`, `/stats`

Applied to files:

  • cloudflare_workers/api/index.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to cloudflare_workers/{api,plugin,files}/index.ts : Cloudflare Workers are split across three ports: API Worker (8787), Plugin Worker (8788), Files Worker (8789); see routing in `cloudflare_workers/{api,plugin,files}/index.ts`

Applied to files:

  • cloudflare_workers/api/index.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Backend code must be placed in `supabase/functions/_backend/` as shared code deployed to Cloudflare Workers (API/Plugin/Files workers), Supabase Edge Functions, and other platforms

Applied to files:

  • cloudflare_workers/api/index.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Applies to supabase/functions/_backend/**/*.{ts,js} : Use Drizzle ORM query patterns with `schema` from `postgress_schema.ts` for all database operations; use `aliasV2()` for self-joins or multiple table references

Applied to files:

  • cloudflare_workers/api/index.ts
📚 Learning: 2025-12-05T17:34:25.556Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-05T17:34:25.556Z
Learning: Applies to supabase/functions/_backend/** : Backend logic should be organized in `supabase/functions/_backend/` with subdirectories for plugins, private endpoints, public endpoints, triggers, and utilities

Applied to files:

  • cloudflare_workers/api/index.ts
  • supabase/functions/_backend/triggers/on_organization_delete.ts
📚 Learning: 2025-12-23T02:53:12.055Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-23T02:53:12.055Z
Learning: Use shared backend code from `supabase/functions/_backend/` across all deployment platforms; never create platform-specific implementations outside this directory

Applied to files:

  • cloudflare_workers/api/index.ts
📚 Learning: 2025-12-27T03:51:23.575Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-27T03:51:23.575Z
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/20251228215402_add_orphan_images_cleanup.sql
📚 Learning: 2025-12-27T03:51:23.575Z
Learnt from: CR
Repo: Cap-go/capgo PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-27T03:51:23.575Z
Learning: Applies to supabase/migrations/*.sql : A migration that introduces a new table may include seed inserts for that table, treating seeding as part of the current feature and not modifying previously committed migrations

Applied to files:

  • supabase/migrations/20251228215402_add_orphan_images_cleanup.sql
📚 Learning: 2025-12-25T11:22:13.039Z
Learnt from: WcaleNieWolny
Repo: Cap-go/capgo PR: 1300
File: supabase/migrations/20251224103713_2fa_enforcement.sql:85-96
Timestamp: 2025-12-25T11:22:13.039Z
Learning: In SQL migrations under the repository (e.g., supabase/migrations), enforce that when an org has enforcing_2fa=true, all users (including super_admins) must have 2FA enabled before accessing any org functions, including check_org_members_2fa_enabled. Do not grant admin exceptions to 2FA requirements. This ensures consistent security enforcement across all org-related operations; implement this rule within relevant migrations and associated stored procedures/tests.

Applied to files:

  • supabase/migrations/20251228215402_add_orphan_images_cleanup.sql
🧬 Code graph analysis (5)
supabase/functions/_backend/public/app/delete.ts (1)
supabase/functions/_backend/utils/logging.ts (1)
  • cloudlog (3-15)
supabase/functions/_backend/triggers/cron_clean_orphan_images.ts (5)
supabase/functions/_backend/triggers/on_app_delete.ts (1)
  • app (8-8)
supabase/functions/_backend/triggers/on_organization_delete.ts (1)
  • app (9-9)
supabase/functions/_backend/triggers/on_user_delete.ts (1)
  • app (11-11)
supabase/functions/_backend/utils/hono.ts (3)
  • MiddlewareKeyVariables (27-41)
  • middlewareAPISecret (118-133)
  • BRES (135-135)
supabase/functions/_backend/utils/logging.ts (2)
  • cloudlog (3-15)
  • cloudlogErr (29-41)
supabase/functions/_backend/public/organization/delete.ts (1)
supabase/functions/_backend/utils/logging.ts (1)
  • cloudlog (3-15)
supabase/functions/_backend/triggers/on_user_delete.ts (1)
supabase/functions/_backend/utils/logging.ts (1)
  • cloudlog (3-15)
supabase/functions/_backend/triggers/on_app_delete.ts (1)
supabase/functions/_backend/utils/logging.ts (1)
  • cloudlog (3-15)
⏰ 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). (2)
  • GitHub Check: Run tests
  • GitHub Check: Analyze (java-kotlin)
🔇 Additional comments (12)
supabase/functions/_backend/public/organization/delete.ts (1)

4-5: LGTM!

The imports correctly add cloudlog for structured logging and supabaseAdmin for privileged storage access, both of which are needed for the new cleanup logic. Follows coding guidelines (single quotes, no semicolons).

supabase/functions/_backend/triggers/on_app_delete.ts (1)

30-51: LGTM! App icon cleanup implementation is correct.

The storage cleanup logic properly:

  • Guards on owner_org existence before attempting cleanup
  • Lists files then removes them in a non-blocking try/catch
  • Logs success/failure appropriately with requestId

This aligns with the similar cleanup patterns in the organization and user deletion triggers.

supabase/functions/_backend/triggers/on_user_delete.ts (1)

80-104: LGTM! User avatar cleanup properly integrated into deletion workflow.

The async function pattern cleanly integrates storage cleanup into the existing Promise.all workflow, ensuring avatar deletion runs concurrently with other cleanup operations while maintaining isolated error handling.

supabase/functions/_backend/public/app/delete.ts (1)

19-47: Storage cleanup duplicated between API and trigger—intentional defense-in-depth.

This endpoint performs the same icon cleanup as on_app_delete.ts trigger. This is acceptable as defense-in-depth: the API cleans up pre-deletion, and the trigger serves as a fallback if the API path isn't used. Both operations are idempotent.

However, note that the extra DB query on lines 20-24 adds latency. If performance becomes a concern, consider passing owner_org from the caller or caching it.

supabase/functions/_backend/triggers/on_organization_delete.ts (1)

27-68: LGTM! Org image cleanup handles nested folder structure correctly.

The logic properly:

  • Distinguishes directories (folder.id === null) from files
  • Recursively processes app subfolders within the org folder
  • Handles both nested files and direct org-level files
  • Wraps everything in try/catch for resilience
supabase/functions/_backend/triggers/cron_clean_orphan_images.ts (2)

40-78: N+1 query pattern for user existence checks—acceptable for weekly cron.

Each user folder triggers a separate DB query. For a weekly maintenance job, this is acceptable. If run frequency increases, consider batching user IDs and using a single IN query.


14-14: Verify middlewareAPISecret is sufficient protection for this endpoint.

This cron endpoint only uses middlewareAPISecret for authentication. Ensure the cron scheduler provides the correct apisecret header when invoking this endpoint via the queue system.

supabase/migrations/20251228215402_add_orphan_images_cleanup.sql (3)

133-136: Verify weekly schedule configuration is correct.

The orphan_images_cleanup task is configured with run_at_hour=3, run_at_minute=0, run_at_second=0, run_on_dow=0 (Sunday at 03:00:00 UTC). Confirm this matches the intended schedule from the PR objectives.


159-169: LGTM! Helper function for job cleanup is well-defined.

The cleanup_job_run_details_7days function cleanly encapsulates the 7-day retention policy with proper ownership and search_path settings.


50-157: Table-driven cron task system is a good refactor.

Migrating hardcoded cron logic to a cron_tasks table improves maintainability and allows runtime modifications. The seed data correctly captures all existing jobs.

Minor note: Ensure there's a mechanism to add tasks at runtime if needed (the table structure supports it).

cloudflare_workers/api/index.ts (1)

26-26: LGTM! Route registration follows established patterns.

The import and route registration for cron_clean_orphan_images correctly follows the existing trigger routing conventions in the Cloudflare Workers API.

Also applies to: 86-86

supabase/functions/triggers/index.ts (1)

2-2: LGTM! Trigger route properly registered in Supabase Edge Functions.

The cron_clean_orphan_images trigger is correctly exposed alongside existing triggers, ensuring consistency between Cloudflare Workers and Supabase Edge Functions deployments.

Also applies to: 50-50

Comment on lines +63 to +65
catch (error) {
cloudlog({ requestId: c.get('requestId'), message: 'error deleting org images', error, org_id: orgId })
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Error object may not serialize properly in logs.

Passing the raw error object to cloudlog may result in {} or [object Object] in logs, making debugging difficult. Consider extracting the error message.

🔎 Proposed fix for error serialization
   catch (error) {
-    cloudlog({ requestId: c.get('requestId'), message: 'error deleting org images', error, org_id: orgId })
+    cloudlog({ requestId: c.get('requestId'), message: 'error deleting org images', error: error instanceof Error ? error.message : String(error), org_id: orgId })
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
catch (error) {
cloudlog({ requestId: c.get('requestId'), message: 'error deleting org images', error, org_id: orgId })
}
catch (error) {
cloudlog({ requestId: c.get('requestId'), message: 'error deleting org images', error: error instanceof Error ? error.message : String(error), org_id: orgId })
}
🤖 Prompt for AI Agents
In supabase/functions/_backend/public/organization/delete.ts around lines 63 to
65, the catch block passes the raw error object to cloudlog which may serialize
to {} or [object Object]; change it to extract serializable fields (e.g.,
error_message and error_stack) by checking if error is an Error and using
error.message and error.stack, otherwise use String(error) and/or JSON.stringify
with a safe replacer, then pass those scalar fields to cloudlog (e.g.,
error_message: ..., error_stack: ...) instead of the raw error object.

Comment on lines +22 to +25
// Cancel subscription if customer_id exists
if (record.customer_id) {
cancelSubscription(c, record.customer_id)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

cancelSubscription is not awaited—potential silent failure.

The subscription cancellation call is fire-and-forget. If it fails, the error won't be caught or logged. Consider awaiting it or wrapping in a try/catch with logging:

🔎 Proposed fix
   // Cancel subscription if customer_id exists
   if (record.customer_id) {
-    cancelSubscription(c, record.customer_id)
+    try {
+      await cancelSubscription(c, record.customer_id)
+    }
+    catch (error) {
+      cloudlog({ requestId: c.get('requestId'), message: 'error canceling subscription', error, org_id: record.id })
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Cancel subscription if customer_id exists
if (record.customer_id) {
cancelSubscription(c, record.customer_id)
}
// Cancel subscription if customer_id exists
if (record.customer_id) {
try {
await cancelSubscription(c, record.customer_id)
}
catch (error) {
cloudlog({ requestId: c.get('requestId'), message: 'error canceling subscription', error, org_id: record.id })
}
}
🤖 Prompt for AI Agents
In supabase/functions/_backend/triggers/on_organization_delete.ts around lines
22 to 25, the call to cancelSubscription(record.customer_id) is fire-and-forget
and may fail silently; change it to await the async call and wrap it in a
try/catch that logs any error (and optionally rethrows or marks failure) so
cancellation errors are surfaced and handled instead of being ignored.

Comment on lines +230 to +232
CASE task.task_type
WHEN 'function' THEN
EXECUTE 'SELECT ' || task.target;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

SQL injection risk in dynamic EXECUTE statement.

The task.target value is concatenated directly into the SQL string. While the cron_tasks table is presumably admin-controlled, this pattern is risky. Use format() with proper escaping:

🔎 Proposed fix
          WHEN 'function' THEN
-            EXECUTE 'SELECT ' || task.target;
+            EXECUTE format('SELECT %s', task.target);

Or better yet, if the function names are known and validated, use a whitelist approach or regprocedure casting for validation.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In supabase/migrations/20251228215402_add_orphan_images_cleanup.sql around lines
230-232, the dynamic EXECUTE concatenates task.target directly into SQL causing
SQL injection risk; replace the string concatenation with EXECUTE format(...)
using identifier-escaping (e.g. format('SELECT %I', task.target)) or, better,
validate against a whitelist of allowed function names or cast to regprocedure
before execution; implement the chosen approach and add a guard that raises/logs
and skips the task when the target is not valid.

riderx and others added 2 commits December 28, 2025 23:40
Add security rules to prevent anon/authenticated users from accessing
the cron_tasks table. Uses REVOKE, GRANT, and RLS as defense in depth.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Simplify table security: REVOKE FROM PUBLIC covers all roles
- Add REVOKE/GRANT for cleanup_job_run_details_7days function
- Add REVOKE/GRANT for process_all_cron_tasks function
- Remove redundant anon/authenticated revokes and OWNER statements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

@riderx riderx merged commit f356d38 into main Dec 29, 2025
11 checks passed
@riderx riderx deleted the riderx/cleanup-orphan-images branch December 29, 2025 01:06
@coderabbitai coderabbitai Bot mentioned this pull request Jan 8, 2026
Dalanir pushed a commit that referenced this pull request Jan 12, 2026
* feat: add image cleanup on user, app, and org deletion

Implement automatic removal of stored images from Supabase Storage when users, apps, or organizations are deleted. This prevents storage from accumulating orphaned images.

Changes:
- Delete user avatar images when users are deleted
- Delete app icons when apps are deleted (both via trigger and API)
- Delete org images when organizations are deleted (both via trigger and API)
- Add cron_clean_orphan_images trigger for weekly cleanup of orphaned images
- Integrate orphan cleanup into existing process_combined_cron_jobs

Fixes #686

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix: use correct process_combined_cron_jobs function from latest migration

Fixed the migration to properly extend the function from 20251228033417_webhooks.sql
instead of using an outdated version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix: use correct function name process_all_cron_tasks

Fixed the migration to use the correct function name process_all_cron_tasks
instead of the wrong process_combined_cron_jobs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* feat: introduce table-driven cron tasks for better maintainability

Replace the hard-coded process_all_cron_tasks function with a table-driven
approach using a new cron_tasks table. This makes adding, modifying, or
disabling cron tasks much easier without code changes.

- Create cron_tasks table with scheduling configuration
- Create cron_task_type enum for different task types
- Migrate all existing cron jobs to table entries
- Refactor process_all_cron_tasks to read from table
- Add cleanup_job_run_details_7days helper function

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* security: restrict cron_tasks table access to service_role only

Add security rules to prevent anon/authenticated users from accessing
the cron_tasks table. Uses REVOKE, GRANT, and RLS as defense in depth.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: simplify security rules and secure functions

- Simplify table security: REVOKE FROM PUBLIC covers all roles
- Add REVOKE/GRANT for cleanup_job_run_details_7days function
- Add REVOKE/GRANT for process_all_cron_tasks function
- Remove redundant anon/authenticated revokes and OWNER statements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
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.

1 participant