Skip to content

Conversation

@SkyfallWasTaken
Copy link

The old PR was pretty verbose since I had left over a lot of code from a previous approach I was trying. I ended up rearchitecting the PR to be much simpler and more reliable.

Changes

This PR adds PgBackRest support to backups. PgBackRest allows for incremental backups, which means that it is much, much more storage efficient for larger databases.

If PgBackRest is enabled, instead of requiring an image change, we install PgBackRest into the Postgres container so people can use custom images instead of having to rely on PGBR-specific ones.

Also made sure that functionality works fine in dev mode.

Issues

@algora-pbc algora-pbc bot added the 🙋 Bounty claim Issues or PRs that have a Bounty ready to be claimed. label Dec 10, 2025
pgBackRest

Further code quality changes

Clean up code

Small fix

Hopefully the last bit of cleanup?

Remove old comment!

A couple more improvements

Squash migrations
@SkyfallWasTaken
Copy link
Author

@CodeRabbit review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added pgBackRest backup engine for PostgreSQL databases with local and S3 storage support.
    • Implemented database restore functionality with point-in-time recovery capability.
    • Added configurable backup settings: compression, logging levels, and archive modes.
    • Introduced three new API endpoints for managing and monitoring database restores.
    • Added multi-channel notifications for successful and failed restore operations.
    • Enhanced backup configuration UI with repository management and retention policies.
  • Bug Fixes

    • Fixed backup deletion workflow to properly handle pgBackRest backups.

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

Walkthrough

Adds full pgBackRest support: new models, service, jobs, action, controller endpoints, Livewire UI, notifications, helpers, migration, and StartPostgresql integration to perform, monitor, and restore pgBackRest backups for standalone PostgreSQL databases.

Changes

Cohort / File(s) Summary
Models
app/Models/DatabaseRestore.php, app/Models/PgbackrestRepo.php, app/Models/ScheduledDatabaseBackup.php, app/Models/ScheduledDatabaseBackupExecution.php, app/Models/StandalonePostgresql.php
New DatabaseRestore and PgbackrestRepo models; added casts, relations, predicates and helpers (repo selection, restores, volumes, pgBackRest checks) to support pgBackRest metadata and restore lifecycle.
Service
app/Services/Backup/PgBackrestService.php
New PgBackrestService: generates pgbackrest config and per-repo blocks, normalizes endpoints, builds install/setup commands, constructs S3 env vars, and assembles pgBackRest commands (backup/restore/info/stanza-create/expire) plus parsing utilities.
Jobs / Actions
app/Actions/Database/PgBackrestRestore.php, app/Jobs/PgBackrestRestoreJob.php, app/Jobs/DatabaseBackupJob.php
New PgBackrestRestore action and PgBackrestRestoreJob implementing encrypted queued restores with preflight, sidecar restore, verification, recovery, and notifications; DatabaseBackupJob extended to run pgBackRest backup/expire/config flows and helper methods.
Controller / API / Routes
app/Http/Controllers/Api/DatabasesController.php, routes/api.php
Added restore endpoints and controller methods (restore_database, list_restores, restore_status), backup-config builders/validation for pgBackRest, and three API routes for restore operations.
Livewire / UI / Views
app/Livewire/Project/Database/BackupEdit.php, app/Livewire/Project/Database/BackupExecutions.php, resources/views/livewire/project/database/backup-edit.blade.php, resources/views/livewire/project/database/backup-executions.blade.php
UI additions to select engine (pgBackRest/native), configure pgBackRest options and per-repo retention, and a multi-step restore flow (confirm, progress, logs).
Notifications & Emails
app/Notifications/Database/DatabaseRestoreFailed.php, app/Notifications/Database/DatabaseRestoreSuccess.php, app/Notifications/Database/BackupFailed.php, app/Notifications/Database/BackupSuccess.php, resources/views/emails/database-restore-failed.blade.php, resources/views/emails/database-restore-success.blade.php
New restore success/failure notifications and email templates; backup notifications extended to include engine and backup_type.
StartPostgresql integration
app/Actions/Database/StartPostgresql.php
Added host-path translation, pgBackRest config deployment and entrypoint generation, mounts and container entrypoint orchestration when pgBackRest is enabled.
Helpers
bootstrap/helpers/databases.php, bootstrap/helpers/shared.php
Skip removeOldBackups for pgBackRest backups; added convertPathToDockerHost() and discoverDevCoolifyVolumePath() helpers for dev host path translation.
Migration
database/migrations/2025_12_09_231049_add_pgbackrest_support.php
Schema changes: add engine and pgBackRest fields to scheduled backups/executions, create pgbackrest_repos and database_restores tables with constraints and rollback.
Other
resources/views/emails/*, wiring across jobs/controllers
Wiring S3 env propagation, logs, status updates and lifecycle events across jobs/controllers for backup and restore flows.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Client
  participant API as DatabasesController
  participant DB as StandalonePostgresql
  participant Action as PgBackrestRestore
  participant RestoreRec as DatabaseRestore
  participant Job as PgBackrestRestoreJob
  participant Service as PgBackrestService
  participant Sidecar as SidecarContainer

  Client->>API: POST /databases/{uuid}/restore (execution_uuid?, target_time?)
  API->>DB: authorize & load database
  API->>Action: PgBackrestRestore::handle(database, execution?, targetTime?)
  Action->>RestoreRec: create pending DatabaseRestore
  Action->>Job: dispatch PgBackrestRestoreJob(database, restore, execution?, targetTime?)
  Job->>Service: build restore/info commands (stanza/label/targetTime, env)
  Job->>DB: stop PostgreSQL container
  Job->>Sidecar: run sidecar info/stanza-create/restore commands (with mounts + env)
  Sidecar-->>Job: return logs/results
  Job->>DB: start PostgreSQL container
  Job->>RestoreRec: updateStatus(success/failed), appendLog
  Job->>API: send notifications (DatabaseRestoreSuccess/Failed)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Areas to focus during review:

  • PgBackrestService: config syntax, S3 endpoint normalization, command argument escaping and install commands.
  • PgBackrestRestoreJob & DatabaseBackupJob: mounts, PGDATA backup/clear/recovery logic, sidecar command correctness, error handling and retries.
  • Migration: schema changes, FK constraints, down() safety and defaults.
  • StartPostgresql changes: host-path translation, generated entrypoint script, mount and permission implications.
  • Controller/Livewire: validation, authorization gating, s3 id handling, and UI state transitions.

Possibly related issues

  • #7172 — Significant overlap: this PR implements full pgBackRest support which directly implements the request to use pgBackRest for Postgres backups.

Possibly related PRs

  • v4.0.0-beta.447 #7332 — Modifies DatabaseBackupJob; related to backup job logic and S3/env handling introduced here.
  • v4.0.0-beta.441 #7106 — Modifies StartPostgresql; touches container start/entrypoint behavior relevant to this integration.
  • v4.0.0-beta.443 #7144 — Updates DatabaseBackupJob retry/timeout/failure handling; relevant to job behavior changes in this PR.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.56% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'v2: add pgbackrest support' is concise and clearly describes the main objective of adding pgBackRest support to the codebase.
Description check ✅ Passed The description covers the key changes (pgBackRest support for incremental backups, container installation) and references the linked issue, but lacks detailed breakdown of all modified files and testing approach.
Linked Issues check ✅ Passed The PR successfully implements pgBackRest support for Postgres backups [#7423], including incremental backups, API compatibility, container integration, and dev mode support as required.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing pgBackRest support: new models, services, jobs, controllers, notifications, migrations, and UI components for restore/backup workflows are within scope.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
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: 23

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e15d8e and 1e529c5.

📒 Files selected for processing (26)
  • app/Actions/Database/PgBackrestRestore.php (1 hunks)
  • app/Actions/Database/StartPostgresql.php (8 hunks)
  • app/Http/Controllers/Api/DatabasesController.php (11 hunks)
  • app/Jobs/DatabaseBackupJob.php (3 hunks)
  • app/Jobs/PgBackrestRestoreJob.php (1 hunks)
  • app/Livewire/Project/Database/BackupEdit.php (5 hunks)
  • app/Livewire/Project/Database/BackupExecutions.php (3 hunks)
  • app/Models/DatabaseRestore.php (1 hunks)
  • app/Models/PgbackrestRepo.php (1 hunks)
  • app/Models/ScheduledDatabaseBackup.php (1 hunks)
  • app/Models/ScheduledDatabaseBackupExecution.php (2 hunks)
  • app/Models/StandalonePostgresql.php (2 hunks)
  • app/Notifications/Database/BackupFailed.php (2 hunks)
  • app/Notifications/Database/BackupSuccess.php (2 hunks)
  • app/Notifications/Database/DatabaseRestoreFailed.php (1 hunks)
  • app/Notifications/Database/DatabaseRestoreSuccess.php (1 hunks)
  • app/Services/Backup/PgBackrestService.php (1 hunks)
  • bootstrap/helpers/databases.php (1 hunks)
  • bootstrap/helpers/shared.php (1 hunks)
  • database/migrations/2025_12_09_231049_add_pgbackrest_fields_to_scheduled_database_backup_executions_table.php (1 hunks)
  • database/migrations/2025_12_09_231050_create_database_restores_table.php (1 hunks)
  • resources/views/emails/database-restore-failed.blade.php (1 hunks)
  • resources/views/emails/database-restore-success.blade.php (1 hunks)
  • resources/views/livewire/project/database/backup-edit.blade.php (1 hunks)
  • resources/views/livewire/project/database/backup-executions.blade.php (5 hunks)
  • routes/api.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (13)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Always run code formatting with ./vendor/bin/pint before committing code

**/*.php: Follow PSR-12 coding standards. Use Laravel Pint for automatic formatting. Write descriptive variable and method names. Keep methods small and focused. Document complex logic with clear comments
Use PHP 8.4 constructor property promotion and typed properties
Never use env() outside config files in Laravel

Files:

  • app/Models/DatabaseRestore.php
  • app/Notifications/Database/BackupSuccess.php
  • resources/views/livewire/project/database/backup-edit.blade.php
  • bootstrap/helpers/databases.php
  • app/Actions/Database/PgBackrestRestore.php
  • app/Notifications/Database/BackupFailed.php
  • resources/views/emails/database-restore-success.blade.php
  • database/migrations/2025_12_09_231049_add_pgbackrest_fields_to_scheduled_database_backup_executions_table.php
  • resources/views/livewire/project/database/backup-executions.blade.php
  • app/Jobs/PgBackrestRestoreJob.php
  • app/Models/StandalonePostgresql.php
  • routes/api.php
  • app/Actions/Database/StartPostgresql.php
  • database/migrations/2025_12_09_231050_create_database_restores_table.php
  • app/Models/ScheduledDatabaseBackup.php
  • app/Http/Controllers/Api/DatabasesController.php
  • app/Models/PgbackrestRepo.php
  • resources/views/emails/database-restore-failed.blade.php
  • app/Notifications/Database/DatabaseRestoreSuccess.php
  • app/Livewire/Project/Database/BackupExecutions.php
  • app/Livewire/Project/Database/BackupEdit.php
  • bootstrap/helpers/shared.php
  • app/Notifications/Database/DatabaseRestoreFailed.php
  • app/Jobs/DatabaseBackupJob.php
  • app/Models/ScheduledDatabaseBackupExecution.php
  • app/Services/Backup/PgBackrestService.php
{**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Use team-based access control patterns and gate/policy authorization as documented in .ai/patterns/security-patterns.md

Files:

  • app/Models/DatabaseRestore.php
  • app/Models/StandalonePostgresql.php
  • routes/api.php
  • app/Models/ScheduledDatabaseBackup.php
  • app/Models/PgbackrestRepo.php
  • app/Models/ScheduledDatabaseBackupExecution.php
app/Models/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/Models/**/*.php: When adding new database columns, ALWAYS update the model's $fillable array to allow mass assignment
Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries
Always use team() method to return relationship instance, not direct property access. App\Models\Application::team must return a relationship instance

Files:

  • app/Models/DatabaseRestore.php
  • app/Models/StandalonePostgresql.php
  • app/Models/ScheduledDatabaseBackup.php
  • app/Models/PgbackrestRepo.php
  • app/Models/ScheduledDatabaseBackupExecution.php
app/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/**/*.php: Use eager loading to prevent N+1 queries, implement caching for frequently accessed data, queue heavy operations, optimize database queries with proper indexes, use chunking for large data operations
Use ownedByCurrentTeamCached() instead of ownedByCurrentTeam()->get() for team-scoped queries to avoid duplicate database queries
Queue heavy operations with Laravel Horizon

Files:

  • app/Models/DatabaseRestore.php
  • app/Notifications/Database/BackupSuccess.php
  • app/Actions/Database/PgBackrestRestore.php
  • app/Notifications/Database/BackupFailed.php
  • app/Jobs/PgBackrestRestoreJob.php
  • app/Models/StandalonePostgresql.php
  • app/Actions/Database/StartPostgresql.php
  • app/Models/ScheduledDatabaseBackup.php
  • app/Http/Controllers/Api/DatabasesController.php
  • app/Models/PgbackrestRepo.php
  • app/Notifications/Database/DatabaseRestoreSuccess.php
  • app/Livewire/Project/Database/BackupExecutions.php
  • app/Livewire/Project/Database/BackupEdit.php
  • app/Notifications/Database/DatabaseRestoreFailed.php
  • app/Jobs/DatabaseBackupJob.php
  • app/Models/ScheduledDatabaseBackupExecution.php
  • app/Services/Backup/PgBackrestService.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

Use named routes with route() function instead of hardcoded URLs

Files:

  • app/Models/DatabaseRestore.php
  • app/Notifications/Database/BackupSuccess.php
  • resources/views/livewire/project/database/backup-edit.blade.php
  • bootstrap/helpers/databases.php
  • app/Actions/Database/PgBackrestRestore.php
  • app/Notifications/Database/BackupFailed.php
  • resources/views/emails/database-restore-success.blade.php
  • database/migrations/2025_12_09_231049_add_pgbackrest_fields_to_scheduled_database_backup_executions_table.php
  • resources/views/livewire/project/database/backup-executions.blade.php
  • app/Jobs/PgBackrestRestoreJob.php
  • app/Models/StandalonePostgresql.php
  • routes/api.php
  • app/Actions/Database/StartPostgresql.php
  • database/migrations/2025_12_09_231050_create_database_restores_table.php
  • app/Models/ScheduledDatabaseBackup.php
  • app/Http/Controllers/Api/DatabasesController.php
  • app/Models/PgbackrestRepo.php
  • resources/views/emails/database-restore-failed.blade.php
  • app/Notifications/Database/DatabaseRestoreSuccess.php
  • app/Livewire/Project/Database/BackupExecutions.php
  • app/Livewire/Project/Database/BackupEdit.php
  • bootstrap/helpers/shared.php
  • app/Notifications/Database/DatabaseRestoreFailed.php
  • app/Jobs/DatabaseBackupJob.php
  • app/Models/ScheduledDatabaseBackupExecution.php
  • app/Services/Backup/PgBackrestService.php
**/**/livewire/**/*.blade.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Livewire components MUST have exactly ONE root element with no exceptions

Files:

  • resources/views/livewire/project/database/backup-edit.blade.php
  • resources/views/livewire/project/database/backup-executions.blade.php
**/*.blade.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

**/*.blade.php: ALWAYS include authorization on form components using canGate and canResource attributes
Frontend development must use Livewire 3.5.20 for server-side state, Alpine.js for client interactions, and Tailwind CSS 4.1.4 for styling

Files:

  • resources/views/livewire/project/database/backup-edit.blade.php
  • resources/views/emails/database-restore-success.blade.php
  • resources/views/livewire/project/database/backup-executions.blade.php
  • resources/views/emails/database-restore-failed.blade.php
resources/views/livewire/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

resources/views/livewire/**/*.blade.php: When creating or editing form components (Input, Select, Textarea, Checkbox, Button), ALWAYS include authorization using canGate and canResource attributes for automatic authorization
Wrap Modal Components with @can directives to ensure proper authorization before displaying modals like x-modal-confirmation, x-modal-input, etc.
Livewire component views MUST have exactly ONE root element. ALL content must be contained within this single root element. Placing ANY elements (<style>, <script>,

, comments, or other HTML) outside the root will break Livewire's component tracking and cause wire:click and other directives to fail silently
Use wire:model.live for real-time updates in Livewire components

Files:

  • resources/views/livewire/project/database/backup-edit.blade.php
  • resources/views/livewire/project/database/backup-executions.blade.php
resources/views/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

Tailwind CSS: Use new utilities (version 4.1.4), not deprecated ones. Use gap utilities for spacing, not margins

Files:

  • resources/views/livewire/project/database/backup-edit.blade.php
  • resources/views/emails/database-restore-success.blade.php
  • resources/views/livewire/project/database/backup-executions.blade.php
  • resources/views/emails/database-restore-failed.blade.php
{**/*Model.php,database/migrations/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in .ai/patterns/database-patterns.md

Files:

  • database/migrations/2025_12_09_231049_add_pgbackrest_fields_to_scheduled_database_backup_executions_table.php
  • database/migrations/2025_12_09_231050_create_database_restores_table.php
routes/api.php

📄 CodeRabbit inference engine (CLAUDE.md)

RESTful endpoints in routes/api.php should use API Resources for response formatting, implement rate limiting for public endpoints, version APIs when making breaking changes, and document endpoints with clear examples

Files:

  • routes/api.php
routes/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

Group routes by middleware and prefix, use route model binding for cleaner controllers, name routes consistently (resource.action), implement proper HTTP verbs (GET, POST, PUT, DELETE)

Files:

  • routes/api.php
app/Livewire/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

In Livewire Components, always add the AuthorizesRequests trait and check permissions with $this->authorize() calls in mount() and action methods

Files:

  • app/Livewire/Project/Database/BackupExecutions.php
  • app/Livewire/Project/Database/BackupEdit.php
🧠 Learnings (5)
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Model.php,database/migrations/**/*.php} : Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in `.ai/patterns/database-patterns.md`

Applied to files:

  • app/Models/DatabaseRestore.php
  • database/migrations/2025_12_09_231050_create_database_restores_table.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries

Applied to files:

  • app/Models/DatabaseRestore.php
  • app/Models/ScheduledDatabaseBackup.php
  • app/Http/Controllers/Api/DatabasesController.php
  • app/Models/PgbackrestRepo.php
  • app/Models/ScheduledDatabaseBackupExecution.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to routes/api.php : RESTful endpoints in `routes/api.php` should use API Resources for response formatting, implement rate limiting for public endpoints, version APIs when making breaking changes, and document endpoints with clear examples

Applied to files:

  • routes/api.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to routes/**/*.php : Group routes by middleware and prefix, use route model binding for cleaner controllers, name routes consistently (resource.action), implement proper HTTP verbs (GET, POST, PUT, DELETE)

Applied to files:

  • routes/api.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/**/*.php : Use eager loading to prevent N+1 queries, implement caching for frequently accessed data, queue heavy operations, optimize database queries with proper indexes, use chunking for large data operations

Applied to files:

  • app/Models/ScheduledDatabaseBackupExecution.php
🧬 Code graph analysis (13)
app/Models/DatabaseRestore.php (1)
app/Models/ScheduledDatabaseBackupExecution.php (2)
  • casts (12-20)
  • ScheduledDatabaseBackupExecution (8-48)
app/Notifications/Database/BackupSuccess.php (2)
app/Notifications/Database/BackupFailed.php (1)
  • __construct (22-29)
app/Models/ScheduledDatabaseBackup.php (3)
  • ScheduledDatabaseBackup (9-144)
  • database (89-92)
  • isPgBackrest (24-27)
bootstrap/helpers/databases.php (2)
app/Models/ScheduledDatabaseBackup.php (1)
  • isPgBackrest (24-27)
app/Models/ScheduledDatabaseBackupExecution.php (1)
  • isPgBackrest (32-35)
app/Notifications/Database/BackupFailed.php (2)
app/Notifications/Database/BackupSuccess.php (1)
  • __construct (22-30)
app/Models/ScheduledDatabaseBackup.php (3)
  • ScheduledDatabaseBackup (9-144)
  • database (89-92)
  • isPgBackrest (24-27)
database/migrations/2025_12_09_231049_add_pgbackrest_fields_to_scheduled_database_backup_executions_table.php (1)
database/migrations/2025_12_09_231050_create_database_restores_table.php (2)
  • up (9-37)
  • down (39-42)
app/Models/StandalonePostgresql.php (6)
app/Models/LocalPersistentVolume.php (2)
  • LocalPersistentVolume (9-117)
  • database (23-26)
app/Models/DatabaseRestore.php (2)
  • database (20-23)
  • DatabaseRestore (8-77)
app/Models/ScheduledDatabaseBackup.php (1)
  • database (89-92)
app/Models/ServiceDatabase.php (2)
  • scheduledBackups (155-158)
  • persistentStorages (140-143)
app/Models/ServiceApplication.php (1)
  • persistentStorages (116-119)
app/Models/Application.php (1)
  • persistentStorages (492-495)
app/Actions/Database/StartPostgresql.php (4)
app/Services/Backup/PgBackrestService.php (3)
  • PgBackrestService (9-353)
  • getStanzaName (23-26)
  • generateConfig (28-75)
app/Jobs/PgBackrestRestoreJob.php (1)
  • getHostPath (413-416)
app/Models/StandalonePostgresql.php (1)
  • pgbackrestBackups (354-357)
bootstrap/helpers/shared.php (1)
  • convertPathToDockerHost (3319-3326)
database/migrations/2025_12_09_231050_create_database_restores_table.php (1)
database/migrations/2025_12_09_231049_add_pgbackrest_fields_to_scheduled_database_backup_executions_table.php (1)
  • up (9-55)
app/Models/ScheduledDatabaseBackup.php (3)
app/Models/DatabaseRestore.php (2)
  • casts (12-18)
  • DatabaseRestore (8-77)
app/Models/PgbackrestRepo.php (2)
  • casts (12-20)
  • PgbackrestRepo (8-72)
app/Models/ScheduledDatabaseBackupExecution.php (5)
  • casts (12-20)
  • isPgBackrest (32-35)
  • isNative (37-40)
  • restores (27-30)
  • ScheduledDatabaseBackupExecution (8-48)
app/Models/PgbackrestRepo.php (1)
app/Models/ScheduledDatabaseBackup.php (2)
  • ScheduledDatabaseBackup (9-144)
  • database (89-92)
app/Livewire/Project/Database/BackupEdit.php (4)
app/Models/PgbackrestRepo.php (1)
  • PgbackrestRepo (8-72)
bootstrap/helpers/shared.php (1)
  • validateShellSafePath (119-147)
app/Models/ScheduledDatabaseBackup.php (2)
  • localRepo (44-47)
  • s3Repo (49-52)
app/Models/StandalonePostgresql.php (1)
  • type (242-245)
app/Notifications/Database/DatabaseRestoreFailed.php (3)
app/Models/StandalonePostgresql.php (1)
  • StandalonePostgresql (11-424)
app/Notifications/Database/DatabaseRestoreSuccess.php (3)
  • __construct (20-30)
  • via (32-35)
  • toMail (37-48)
app/Traits/HasNotificationSettings.php (1)
  • getEnabledChannels (73-97)
app/Models/ScheduledDatabaseBackupExecution.php (2)
app/Models/ScheduledDatabaseBackup.php (4)
  • ScheduledDatabaseBackup (9-144)
  • restores (64-72)
  • isPgBackrest (24-27)
  • isNative (29-32)
app/Models/DatabaseRestore.php (1)
  • DatabaseRestore (8-77)
🪛 PHPMD (2.15.0)
app/Actions/Database/PgBackrestRestore.php

27-27: Missing class import via use statement (line '27', column '27'). (undefined)

(MissingImport)


31-40: Avoid using static access to class '\App\Models\DatabaseRestore' in method 'handle'. (undefined)

(StaticAccess)


42-42: Avoid using static access to class '\App\Jobs\PgBackrestRestoreJob' in method 'handle'. (undefined)

(StaticAccess)

app/Jobs/PgBackrestRestoreJob.php

22-439: The class PgBackrestRestoreJob has an overall complexity of 58 which is very high. The configured complexity threshold is 50. (undefined)

(ExcessiveClassComplexity)


41-41: Avoid unused local variables such as '$server'. (undefined)

(UnusedLocalVariable)


42-42: Avoid unused local variables such as '$containerName'. (undefined)

(UnusedLocalVariable)


43-43: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'handle'. (undefined)

(StaticAccess)


52-52: Avoid using static access to class '\App\Actions\Database\StopDatabase' in method 'handle'. (undefined)

(StaticAccess)


68-68: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


91-96: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'handle'. (undefined)

(StaticAccess)


111-111: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


120-183: The method preflight() has a Cyclomatic Complexity of 15. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


120-183: The method preflight() has an NPath complexity of 2400. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


125-125: Missing class import via use statement (line '125', column '23'). (undefined)

(MissingImport)


130-130: Missing class import via use statement (line '130', column '23'). (undefined)

(MissingImport)


135-135: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


137-137: Missing class import via use statement (line '137', column '27'). (undefined)

(MissingImport)


143-143: Missing class import via use statement (line '143', column '27'). (undefined)

(MissingImport)


149-149: Missing class import via use statement (line '149', column '23'). (undefined)

(MissingImport)


155-155: Missing class import via use statement (line '155', column '23'). (undefined)

(MissingImport)


161-161: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


162-162: Missing class import via use statement (line '162', column '23'). (undefined)

(MissingImport)


165-165: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


166-166: Missing class import via use statement (line '166', column '23'). (undefined)

(MissingImport)


170-170: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


172-172: Missing class import via use statement (line '172', column '27'). (undefined)

(MissingImport)


175-180: The method preflight uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


176-176: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


202-202: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


208-208: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


224-224: Missing class import via use statement (line '224', column '23'). (undefined)

(MissingImport)


232-232: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


234-234: Missing class import via use statement (line '234', column '23'). (undefined)

(MissingImport)


246-246: Missing class import via use statement (line '246', column '23'). (undefined)

(MissingImport)


304-304: Missing class import via use statement (line '304', column '23'). (undefined)

(MissingImport)


320-320: Missing class import via use statement (line '320', column '27'). (undefined)

(MissingImport)


331-331: Missing class import via use statement (line '331', column '27'). (undefined)

(MissingImport)


336-336: Missing class import via use statement (line '336', column '23'). (undefined)

(MissingImport)


348-348: Missing class import via use statement (line '348', column '23'). (undefined)

(MissingImport)


368-368: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


374-379: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


407-407: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'getInstallAndRunCommand'. (undefined)

(StaticAccess)

app/Models/StandalonePostgresql.php

35-41: Avoid using static access to class 'App\Models\LocalPersistentVolume' in method 'booted'. (undefined)

(StaticAccess)

app/Actions/Database/StartPostgresql.php

345-345: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'add_custom_conf'. (undefined)

(StaticAccess)


355-357: The method add_custom_conf uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


380-380: The variable $content_base64 is not named in camelCase. (undefined)

(CamelCaseVariableName)


384-404: The method setup_pgbackrest_config is not named in camelCase. (undefined)

(CamelCaseMethodName)


386-386: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'setup_pgbackrest_config'. (undefined)

(StaticAccess)


406-457: The method create_pgbackrest_entrypoint is not named in camelCase. (undefined)

(CamelCaseMethodName)


408-408: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'create_pgbackrest_entrypoint'. (undefined)

(StaticAccess)


409-409: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'create_pgbackrest_entrypoint'. (undefined)

(StaticAccess)


414-414: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'create_pgbackrest_entrypoint'. (undefined)

(StaticAccess)

app/Http/Controllers/Api/DatabasesController.php

2874-2925: The method restore_database is not named in camelCase. (undefined)

(CamelCaseMethodName)


2919-2919: Avoid using static access to class '\App\Actions\Database\PgBackrestRestore' in method 'restore_database'. (undefined)

(StaticAccess)


2964-2989: The method list_restores is not named in camelCase. (undefined)

(CamelCaseMethodName)


3037-3068: The method restore_status is not named in camelCase. (undefined)

(CamelCaseMethodName)

app/Notifications/Database/DatabaseRestoreSuccess.php

60-60: Avoid using static access to class '\App\Notifications\Dto\DiscordMessage' in method 'toDiscord'. (undefined)

(StaticAccess)


111-111: Avoid using static access to class '\App\Notifications\Dto\SlackMessage' in method 'toSlack'. (undefined)

(StaticAccess)

app/Livewire/Project/Database/BackupExecutions.php

50-50: Avoid excessively long variable names like $showRestoreProgressModal. Keep variable name length under 20. (undefined)

(LongVariable)


129-129: Avoid using static access to class '\App\Models\ScheduledDatabaseBackupExecution' in method 'confirmRestore'. (undefined)

(StaticAccess)


146-146: Avoid unused parameters such as '$selectedActions'. (undefined)

(UnusedFormalParameter)


148-148: Avoid using static access to class '\App\Models\InstanceSettings' in method 'startRestore'. (undefined)

(StaticAccess)


149-149: Avoid using static access to class '\Illuminate\Support\Facades\Hash' in method 'startRestore'. (undefined)

(StaticAccess)


156-156: Avoid using static access to class '\App\Models\ScheduledDatabaseBackupExecution' in method 'startRestore'. (undefined)

(StaticAccess)


173-173: Avoid using static access to class '\App\Actions\Database\PgBackrestRestore' in method 'startRestore'. (undefined)

(StaticAccess)

app/Livewire/Project/Database/BackupEdit.php

93-93: Avoid excessively long variable names like $pgbackrestCompressType. Keep variable name length under 20. (undefined)

(LongVariable)


96-96: Avoid excessively long variable names like $pgbackrestCompressLevel. Keep variable name length under 20. (undefined)

(LongVariable)


102-102: Avoid excessively long variable names like $pgbackrestArchiveMode. Keep variable name length under 20. (undefined)

(LongVariable)


105-105: Avoid excessively long variable names like $localRepoRetentionFullType. Keep variable name length under 20. (undefined)

(LongVariable)


108-108: Avoid excessively long variable names like $localRepoRetentionFull. Keep variable name length under 20. (undefined)

(LongVariable)


111-111: Avoid excessively long variable names like $localRepoRetentionDiff. Keep variable name length under 20. (undefined)

(LongVariable)


117-117: Avoid excessively long variable names like $s3RepoRetentionFullType. Keep variable name length under 20. (undefined)

(LongVariable)


155-185: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


174-174: Missing class import via use statement (line '174', column '39'). (undefined)

(MissingImport)


190-220: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


211-214: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


245-302: The method syncPgbackrestRepos() has a Cyclomatic Complexity of 11. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


251-251: Missing class import via use statement (line '251', column '23'). (undefined)

(MissingImport)


258-258: Missing class import via use statement (line '258', column '23'). (undefined)

(MissingImport)


277-279: The method syncPgbackrestRepos uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


283-283: Missing class import via use statement (line '283', column '27'). (undefined)

(MissingImport)


299-301: The method syncPgbackrestRepos uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)

app/Notifications/Database/DatabaseRestoreFailed.php

60-60: Avoid using static access to class '\App\Notifications\Dto\DiscordMessage' in method 'toDiscord'. (undefined)

(StaticAccess)


109-109: Avoid using static access to class '\App\Notifications\Dto\SlackMessage' in method 'toSlack'. (undefined)

(StaticAccess)

app/Jobs/DatabaseBackupJob.php

693-826: The method run_pgbackrest_backup() has a Cyclomatic Complexity of 22. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


693-826: The method run_pgbackrest_backup() has an NPath complexity of 27680. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


693-826: The method run_pgbackrest_backup() has 134 lines of code. Current threshold is set to 100. Avoid really long methods. (undefined)

(ExcessiveMethodLength)


693-826: The method run_pgbackrest_backup is not named in camelCase. (undefined)

(CamelCaseMethodName)


695-695: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


705-705: Missing class import via use statement (line '705', column '27'). (undefined)

(MissingImport)


709-717: Avoid using static access to class '\App\Models\ScheduledDatabaseBackupExecution' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


722-722: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


724-724: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


732-734: The method run_pgbackrest_backup uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


743-745: The method run_pgbackrest_backup uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


749-749: Missing class import via use statement (line '749', column '27'). (undefined)

(MissingImport)


752-752: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


768-777: The method run_pgbackrest_backup uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


779-779: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


780-780: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


783-783: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


784-784: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


798-798: Avoid using static access to class '\Carbon\Carbon' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


812-812: Avoid using static access to class '\Carbon\Carbon' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


824-824: Avoid using static access to class '\App\Events\BackupCreated' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


828-868: The method run_pgbackrest_expire is not named in camelCase. (undefined)

(CamelCaseMethodName)


834-834: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


836-836: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


851-860: The method run_pgbackrest_expire uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


863-866: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


870-884: The method update_pgbackrest_config is not named in camelCase. (undefined)

(CamelCaseMethodName)


872-872: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'update_pgbackrest_config'. (undefined)

(StaticAccess)


875-875: Missing class import via use statement (line '875', column '23'). (undefined)

(MissingImport)

app/Services/Backup/PgBackrestService.php

9-353: The class PgBackrestService has 14 public methods. Consider refactoring PgBackrestService to keep number of public methods under 10. (undefined)

(TooManyPublicMethods)


9-353: The class PgBackrestService has an overall complexity of 59 which is very high. The configured complexity threshold is 50. (undefined)

(ExcessiveClassComplexity)


83-83: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


92-92: Missing class import via use statement (line '92', column '27'). (undefined)

(MissingImport)


103-109: The method generateRepoConfig uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


119-119: Avoid variables with short names like $k. Configured minimum length is 3. (undefined)

(ShortVariable)


119-119: Avoid variables with short names like $v. Configured minimum length is 3. (undefined)

(ShortVariable)


170-170: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


186-186: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


246-246: The method buildInfoCommand has a boolean flag argument $json, which is a certain sign of a Single Responsibility Principle violation. (undefined)

(BooleanArgumentFlag)

🔇 Additional comments (40)
bootstrap/helpers/shared.php (1)

3311-3326: LGTM! I'll be back... to use this function.

This path conversion helper is clean and solves the dev environment volume mounting discrepancy elegantly. The PHPDoc clearly explains the "why" behind this SSH-target-to-Docker-host translation. Self-hosting infrastructure requires these kinds of pragmatic solutions - no serverless magic here, just good old-fashioned path manipulation.

app/Actions/Database/StartPostgresql.php (4)

8-8: LGTM! PgBackrest integration initialized.

The new PgBackrestService import and hasPgBackrest flag are clean additions. Good encapsulation of the pgBackrest state within this action class.

Also applies to: 26-26


459-465: Come with me if you want to live... in a properly configured dev environment.

The getHostPath() wrapper is a clean abstraction over convertPathToDockerHost(). This keeps the path translation logic DRY and makes the class easier to test if needed.


214-228: Add error handling to pgBackrest installation in container entrypoint.

The entrypoint override mounts pgBackrest config and runs the install script before PostgreSQL starts. The chaining with exec docker-entrypoint.sh "$@" preserves the original entrypoint behavior.

However, if /etc/pgbackrest/install-pgbackrest.sh fails, the container startup will fail hard with no fallback. Consider adding error handling to either make failures explicit for debugging or implement a recovery strategy.


384-404: Fix PSR-12 method naming violations and verify bash script compatibility across PostgreSQL container images.

The new pgBackrest setup methods violate PSR-12 naming conventions. Private methods must use camelCase, not snake_case:

  • setup_pgbackrest_config → should be setupPgbackrestConfig
  • create_pgbackrest_entrypoint → should be createPgbackrestEntrypoint

The bash script in create_pgbackrest_entrypoint() (lines 421-452) has potential runtime issues:

  1. Line 438: chown -R postgres:postgres /etc/pgbackrest may fail on containers where the script user lacks privileges. While || true suppresses the error, the operation silently fails.

  2. Lines 446-450: su postgres -c "pgbackrest..." assumes su is available and properly configured in the container. Minimal PostgreSQL images may not have su installed or might require alternative user-switching approaches.

Rename the methods to comply with PSR-12 and test the bash script against common minimal PostgreSQL base images to ensure portability.

bootstrap/helpers/databases.php (1)

245-247: I'll be back... but pgBackrest backups won't be deleted by this function.

Perfect guard clause. PgBackrest manages its own backup lifecycle, so bypassing the native deletion logic is exactly right. This prevents the two backup systems from stepping on each other's toes.

The early return is clean and the isPgBackrest() method name is self-documenting.

resources/views/emails/database-restore-success.blade.php (1)

1-9: I'm a cybernetic organism. Living tissue over a metal endoskeleton. This email template is living markup over a data structure.

The restore success email template is straightforward and does exactly what it needs to do. The conditional rendering of $label and $target_time handles optional data gracefully.

Minor style note: Blade's @isset directive could make the intent even clearer, but @if works fine here.

resources/views/emails/database-restore-failed.blade.php (1)

1-8: Your database restore was terminated. By an error.

The failure email template correctly includes the error message and optional backup label. The structure mirrors the success template, which is good for consistency.

One suggestion: Consider truncating very long error messages for email readability, though that might be better handled in the notification class itself.

app/Notifications/Database/BackupSuccess.php (1)

18-20: Your backup succeeded. I'll send notifications to all channels. It's in my nature to protect you.

The additions cleanly integrate pgBackrest engine metadata into the notification:

  1. Lines 18-20: New properties engine and backupType are properly typed (string and nullable string)
  2. Lines 28-29: Initialization uses sensible defaults ('native' for engine, conditional 'full' for pgBackrest)
  3. Lines 99-115: Webhook payload construction conditionally includes backup_type only when set

This mirrors the pattern in BackupFailed, which is excellent for consistency. The webhook consumers will now know whether they're dealing with native or pgBackrest backups, and get backup type details when relevant.

Also applies to: 28-29, 99-115

app/Notifications/Database/BackupFailed.php (1)

18-20: Come with me if you want to receive properly structured failure notifications.

The failure notification follows the exact same pattern as BackupSuccess for engine and backup type handling. This consistency is crucial for webhook consumers and multi-channel notification systems.

The conditional inclusion of backup_type in the webhook payload (lines 115-117) ensures backward compatibility while enriching the data when pgBackrest is in use. No serverless webhooks needed - these notifications work perfectly with self-hosted infrastructure!

Also applies to: 27-28, 102-119

database/migrations/2025_12_09_231050_create_database_restores_table.php (1)

11-36: I need your database schema. Your indexes. And your restore tracking table.

The database_restores table schema is well-designed:

  1. Lines 13-16: UUID and polymorphic database relation properly set up for supporting multiple database types
  2. Lines 19-22: Foreign key to scheduled_database_backup_executions with nullOnDelete correctly handles backup cleanup without orphaning restores
  3. Line 25: Default engine 'pgbackrest' is appropriate since this table is specifically for the new pgBackrest restore workflow
  4. Lines 26-27: Nullable target_label and target_time allow point-in-time restore flexibility
  5. Lines 30-35: Status tracking with message, log, and finished_at provides full restore lifecycle observability

The migration follows Laravel best practices and aligns with the learnings about Eloquent ORM patterns. The polymorphic relationship will work well with the DatabaseRestore model introduced in this PR.

Based on learnings, database schema follows documented patterns for relationships and indexing.

routes/api.php (1)

139-142: Hasta la vista, poorly designed APIs! These routes look solid.

The new restore endpoints follow existing patterns, use appropriate HTTP verbs (POST for initiating action, GET for reads), and are properly secured with api.ability middleware. Consistent with the rest of the database routes — like a well-oiled T-800.

One minor suggestion for the future: consider adding route names (e.g., ->name('databases.restore')) for easier reference elsewhere in the codebase. But not a blocker — your servers will be just fine.

database/migrations/2025_12_09_231049_add_pgbackrest_fields_to_scheduled_database_backup_executions_table.php (2)

57-81: Down migration is properly ordered — termination sequence approved.

Dropping pgbackrest_repos table first before removing columns from other tables is the correct order to avoid foreign key constraint violations. Clean rollback like a clean kill shot. 🌮


9-55: I cannot complete the verification of this review comment due to persistent repository access failures. The review requests verification that the corresponding models (ScheduledDatabaseBackup, ScheduledDatabaseBackupExecution, and PgbackrestRepo) have their $fillable arrays updated with the new columns introduced in the migration. Without access to the codebase, I cannot:

  • Locate or inspect the model files
  • Verify that $fillable arrays include all new columns (engine, pgbackrest_backup_type, pgbackrest_label, pgbackrest_stanza, pgbackrest_repo_size, etc.)
  • Confirm mass assignment protection is properly configured
  • Validate the models follow Eloquent ORM patterns as per coding guidelines
app/Actions/Database/PgBackrestRestore.php (2)

31-44: Database restoration creation — self-hosted recovery FTW!

The restore record creation and job dispatch pattern is solid. Using the morphClass for polymorphic relationships is correct. Job dispatch follows Laravel conventions.

Static access to Eloquent models and job dispatching is idiomatic Laravel — ignore PHPMD's concerns here. That tool is more confused than a serverless function trying to maintain state.


47-52: Validate at the action boundary to ensure rules are enforced for all callers.

The rules() method won't automatically validate unless this action is invoked as a controller action or FormRequest. Per Laravel best practices, validation should occur at the application boundary. Ensure validation is explicitly enforced before handle() executes, or move the validation logic inside handle() to guarantee it runs regardless of how the action is called.

app/Models/ScheduledDatabaseBackupExecution.php (3)

27-30: Relationship definition is correct — like a proper neural network connection.

The restores() HasMany relationship correctly links to DatabaseRestore via the appropriate foreign key. This matches the inverse BelongsTo relationship in DatabaseRestore::execution() shown in the relevant snippets. Proper bidirectional relationships make querying efficient — unlike serverless cold starts.


32-47: Predicate methods are solid and consistent.

The isPgBackrest() and isNative() methods mirror the same logic in ScheduledDatabaseBackup model (per relevant snippets). The canRestore() method is well-designed with clear conditions.

One observation: canRestore() currently only returns true for pgBackRest executions. If native backups should also support restoration in the future, this method would need updating. But for now, targeting only pgBackRest restores makes sense given the PR scope.


12-20: Cast addition approved — integers should be integers.

Adding 'pgbackrest_repo_size' => 'integer' ensures proper type handling for the repository size field. No more string-to-int shenanigans. Clean data types are like a clean conscience — essential for a well-functioning machine.

resources/views/livewire/project/database/backup-edit.blade.php (1)

57-185: Verify whether custom authorization attributes exist before requiring them.

The review comment references canGate and canResource attributes as required for form components. However, these are not standard Laravel helpers, and it cannot be confirmed whether the custom x-forms.* components in this project actually support these attributes without examining the component implementations and similar forms in the codebase. If these attributes don't exist, the suggestion is invalid.

app/Models/DatabaseRestore.php (1)

8-18: LGTM - Model structure is solid like a T-800 endoskeleton.

The model setup with $guarded = [] and datetime casts is appropriate for this use case. The relationships are correctly defined using MorphTo and BelongsTo. Self-hosted databases deserve reliable restore tracking. No serverless nonsense here - just good old-fashioned Eloquent models running on real servers. 🌮

app/Models/StandalonePostgresql.php (2)

346-376: LGTM - These helper methods are efficient like a well-oiled killing machine.

The pgBackRest helper methods (hasPgBackrestBackups, pgbackrestBackups, restores, pgdataVolume, pgbackrestRepoVolume) are clean and follow existing model patterns. The use of exists() in hasPgBackrestBackups() is efficient - no unnecessary data loading. The polymorphic restores() relationship correctly mirrors the DatabaseRestore model's database() MorphTo.

Self-hosted PostgreSQL with proper backup support? That's the way to run databases. No serverless database-as-a-service here - just real servers with real backups. 🌮


34-41: Clarify the design decision for pgBackRest volume creation.

The code creates a pgBackRest repository volume unconditionally for every PostgreSQL database. Confirm whether this proactive approach is intentional for all users or if volumes should be created on-demand when pgBackRest is first enabled. If volumes are created upfront, document the approach and verify there's a cleanup strategy for users who don't utilize pgBackRest backups.

app/Models/PgbackrestRepo.php (1)

8-72: LGTM - Repository configuration model is terminated... I mean, well-structured.

The model correctly implements pgBackRest repository configuration with proper relationships and helper methods. The use of Cuid2 for UUID generation, safe navigation operators in getDefaultPath(), and the clear type predicates (isLocal(), isS3()) all demonstrate solid design.

The getRepoKey() method correctly follows pgBackRest's naming convention (repo1, repo2, etc.). The getEffectivePath() pattern with fallback to default is a clean approach.

Self-hosting with pgBackRest for incremental backups? That's how you protect your data on real servers. Much better than trusting some VC-backed serverless provider with your precious databases. Now I want a taco. 🌮

app/Notifications/Database/DatabaseRestoreFailed.php (2)

113-128: LGTM - Webhook payload is comprehensive, like a detailed mission report.

The webhook payload includes all necessary fields for external integrations: success flag, event type, database identifiers, engine type, backup label, error details, and a URL. The URL construction using data_get() with safe navigation through the environment and project relationships is defensive and correct.

Hardcoding 'engine' => 'pgbackrest' is appropriate since only pgBackRest supports restore operations. External systems will appreciate the structured data. 🌮


32-35: Verify channel name for restore notification.

The via() method uses 'backup_failed' channel for a restore failure notification. Confirm whether this is intentional (grouping restore and backup notification preferences) or if a separate 'restore_failed' channel should be used. If intentional, add a comment explaining this design choice.

resources/views/livewire/project/database/backup-executions.blade.php (2)

69-77: LGTM - pgBackRest UI indicators are clear and informative.

The pgBackRest badge with backup type display (Full/Incr/Diff) and the "pgBackRest Repository" storage indicator provide clear visual feedback for users. The conditional rendering based on engine === 'pgbackrest' cleanly separates the pgBackRest flow from native backups.

Self-hosted backups with a proper UI to monitor them? This is the way. No obscure serverless console here - just transparent backup management on your own servers. Hasta la vista, data loss! 🌮

Also applies to: 109-121


322-429: LGTM - Restore progress modal with automatic polling.

The restore progress modal is well-designed with:

  • Conditional polling (wire:poll.2000ms) only when restore is running/pending
  • Clear status icons and colors for different states
  • Scrollable log output for debugging
  • Appropriate action buttons based on state

The polling stops automatically when restore finishes via the @if (!$currentRestore->isFinished()) condition. Efficient like a T-1000 adapting to the situation. 🌮

app/Livewire/Project/Database/BackupExecutions.php (1)

182-200: Restore polling/progress flow is straightforward and safe

pollRestoreStatus() and closeRestoreProgress() do the right small thing:

  • Refresh the DatabaseRestore model.
  • When finished, reload executions and clear UI state.

No racey stuff here; this is a clean, minimal Livewire loop. Ship it.

app/Models/ScheduledDatabaseBackup.php (1)

13-22: New casts for pgBackRest flags look correct

The casts() override adding:

'enabled' => 'boolean',
'save_s3' => 'boolean',
'dump_all' => 'boolean',
'disable_local_backup' => 'boolean',
'pgbackrest_compress_level' => 'integer',

lines up with how these flags are used in the jobs and UI. No behavioral traps here; the metal brain approves.

app/Jobs/PgBackrestRestoreJob.php (2)

120-183: Preflight checks are thorough and aligned with pgBackRest expectations

preflight() does a nice job of failing early:

  • Verifies the DB actually has pgBackRest backups configured and enabled.
  • Confirms S3 repos are wired correctly and testable.
  • Ensures PGDATA and (if required) repo volumes exist.
  • Uses runInfoSidecar() + PgBackrestService helpers to validate stanza and backup presence.
  • Validates a specific pgbackrest_label when provided.

From a correctness and safety standpoint this is the right amount of paranoia before you start nuking PGDATA. No changes required here; just keep this function as the grumpy bouncer in front of the restore club.


340-398: Sidecar restore flow looks correct and matches pgBackRest usage

The runRestoreSidecar() method wires things up as expected:

  • Mounts PGDATA and, if present, the pgBackRest repo volume.
  • Mounts the generated config directory read‑only.
  • Sets PGBACKREST_PG1_PATH and S3 env vars.
  • Builds the restore command via PgBackrestService::buildRestoreCommand(...).
  • Uses the same sidecar image as the DB (getSidecarImage()), with an install‑and‑run wrapper.
  • chown -R 999:999 on PGDATA afterwards to hand ownership back to postgres.
  • Runs with throwOnError = true, so a non‑zero exit aborts the restore.

This is a solid, self‑hosted restore dance: one small container, lots of volume mounts, zero serverless hype.

app/Notifications/Database/DatabaseRestoreSuccess.php (3)

115-130: URL building that would make a T-800 proud.

The webhook payload URL construction is solid. Self-hosted servers handling their own backups - much better than serverless functions disappearing like tacos at a party.

One small observation: the hardcoded 'engine' => 'pgbackrest' is appropriate since this notification is specifically for pgBackRest restores.


37-48: Ensure the email view emails.database-restore-success exists in resources/views/emails/.

The toMail() method references view emails.database-restore-success. This view file must exist at the expected location for the notification to render properly.


32-35: Clarify the notification channel naming: does 'backup_success' intentionally consolidate restore and backup notifications, or should this use a dedicated 'restore_success' channel?

The via() method uses 'backup_success' channel for a restore notification. Per Laravel best practices, channel names should be explicit and match their purpose for consistency and clarity. If restore notifications should use a separate channel, consider renaming to 'restore_success' or documenting why the 'backup_success' channel is shared between backup and restore operations.

app/Livewire/Project/Database/BackupEdit.php (1)

403-406: isPostgresql() looking good - like a perfectly grilled taco (gluten-free tortilla, obviously).

Clean helper method. The full class name comparison is the correct approach here since database_type stores the morph class name.

app/Services/Backup/PgBackrestService.php (4)

23-26: Stanza naming using UUID - solid choice, like choosing servers over serverless.

Using the database UUID as the stanza name is a clean, unique identifier approach. No collision issues, no special character concerns.


77-120: Config generation with S3 validation - I approve this message.

Good defensive coding with validateShellSafePath() for S3 bucket and endpoint validation. The repo configuration generation handles both S3 and local (posix) storage types correctly.

One note: using the imported Exception class would be more consistent (line 92).


130-151: Multi-package-manager install command - supporting all the self-hosted servers out there!

This is beautiful. Supporting apk, apt-get, and yum means users can run pgBackRest on Alpine, Debian/Ubuntu, and RHEL-based containers. Much better than being locked into one cloud provider's serverless offering. Like a taco that comes with multiple sauce options - everyone wins (except people with gluten intolerance eating regular tortillas).


283-292: JSON parsing - clean and simple, like a good taco recipe.

Straightforward JSON parsing with proper error handling. Returns null on parse failure rather than throwing - a reasonable choice for this utility method.

CodeRabbit fixes

CodeRabbit fixes

Handle case where PGDATA was already empty

S3 fix

Squash migrations
@SkyfallWasTaken
Copy link
Author

@CodeRabbit review

Alright, let's see if we're good to go now?

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 10, 2025

@SkyfallWasTaken, I'll be back... with a comprehensive review! 🤖💪

Like a good self-hosted backup system running on real servers (not that serverless marketing fluff), I'm going to thoroughly analyze this pgBackRest implementation. This looks like a substantial PR with lots of incremental improvements - much like pgBackRest's incremental backups themselves!

Let me review all these changes to make sure your backup restoration is as reliable as a T-800's metal endoskeleton. I'll check the action flows, job orchestration, API endpoints, and make sure everything is properly wired up.

Hasta la vista, bugs! 🌮

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@SkyfallWasTaken
Copy link
Author

SkyfallWasTaken commented Dec 10, 2025

One question that I’m wondering about.

From my understanding, all the ‘default’ Postgres images work fine with PgBackRest as is. However, if you:

  • Manually change the image to Postgres 18
  • And that image uses Alpine

Backups will fail because the packaged version of PgBackRest in the Alpine repositories doesn’t support Postgres 18. I think a GH Actions-based auto-builder for PgBackRest would work great here - is making a separate repo to build PGBR for both musl & glibc something that you guys would be interested in? (I can also make this on a personal repo if you want and are fine with Coolify downloading from my repo’s releases)


Update: I asked this on the Discord server, apparently this shouldn't be a blocker since Coolify doesn't support PG18 anyway + the responsibility lies on the Alpine maintainers

Copy link
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: 10

♻️ Duplicate comments (1)
app/Http/Controllers/Api/DatabasesController.php (1)

3008-3013: Consider using a resource/transformer for public API consistency.

Previous review noted that returning raw DatabaseRestore models may expose internal fields like IDs and verbose logs. While acceptable for internal use, if this becomes a public API, consider mapping to a stable response shape.

$restores = DatabaseRestore::where('database_id', $database->id)
    ->where('database_type', $database->getMorphClass())
    ->orderBy('created_at', 'desc')
    ->get()
    ->map(fn ($r) => [
        'uuid' => $r->uuid,
        'engine' => $r->engine,
        'status' => $r->status,
        'target_label' => $r->target_label,
        'message' => $r->message,
        'created_at' => $r->created_at?->toIso8601String(),
        'finished_at' => $r->finished_at?->toIso8601String(),
    ]);

Not blocking - your servers don't need to be perfect on day one. Unlike serverless, you can actually iterate on your own infrastructure.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e529c5 and 7c6dfe4.

📒 Files selected for processing (12)
  • app/Actions/Database/PgBackrestRestore.php (1 hunks)
  • app/Actions/Database/StartPostgresql.php (8 hunks)
  • app/Http/Controllers/Api/DatabasesController.php (14 hunks)
  • app/Jobs/PgBackrestRestoreJob.php (1 hunks)
  • app/Livewire/Project/Database/BackupEdit.php (7 hunks)
  • app/Livewire/Project/Database/BackupExecutions.php (4 hunks)
  • app/Models/DatabaseRestore.php (1 hunks)
  • app/Models/ScheduledDatabaseBackup.php (1 hunks)
  • app/Services/Backup/PgBackrestService.php (1 hunks)
  • database/migrations/2025_12_09_231049_add_pgbackrest_support.php (1 hunks)
  • resources/views/livewire/project/database/backup-edit.blade.php (1 hunks)
  • resources/views/livewire/project/database/backup-executions.blade.php (5 hunks)
🧰 Additional context used
📓 Path-based instructions (11)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Always run code formatting with ./vendor/bin/pint before committing code

**/*.php: Follow PSR-12 coding standards. Use Laravel Pint for automatic formatting. Write descriptive variable and method names. Keep methods small and focused. Document complex logic with clear comments
Use PHP 8.4 constructor property promotion and typed properties
Never use env() outside config files in Laravel

Files:

  • resources/views/livewire/project/database/backup-edit.blade.php
  • resources/views/livewire/project/database/backup-executions.blade.php
  • app/Jobs/PgBackrestRestoreJob.php
  • app/Actions/Database/PgBackrestRestore.php
  • app/Actions/Database/StartPostgresql.php
  • database/migrations/2025_12_09_231049_add_pgbackrest_support.php
  • app/Livewire/Project/Database/BackupEdit.php
  • app/Livewire/Project/Database/BackupExecutions.php
  • app/Http/Controllers/Api/DatabasesController.php
  • app/Models/DatabaseRestore.php
  • app/Services/Backup/PgBackrestService.php
  • app/Models/ScheduledDatabaseBackup.php
**/**/livewire/**/*.blade.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Livewire components MUST have exactly ONE root element with no exceptions

Files:

  • resources/views/livewire/project/database/backup-edit.blade.php
  • resources/views/livewire/project/database/backup-executions.blade.php
**/*.blade.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

**/*.blade.php: ALWAYS include authorization on form components using canGate and canResource attributes
Frontend development must use Livewire 3.5.20 for server-side state, Alpine.js for client interactions, and Tailwind CSS 4.1.4 for styling

Files:

  • resources/views/livewire/project/database/backup-edit.blade.php
  • resources/views/livewire/project/database/backup-executions.blade.php
resources/views/livewire/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

resources/views/livewire/**/*.blade.php: When creating or editing form components (Input, Select, Textarea, Checkbox, Button), ALWAYS include authorization using canGate and canResource attributes for automatic authorization
Wrap Modal Components with @can directives to ensure proper authorization before displaying modals like x-modal-confirmation, x-modal-input, etc.
Livewire component views MUST have exactly ONE root element. ALL content must be contained within this single root element. Placing ANY elements (<style>, <script>,

, comments, or other HTML) outside the root will break Livewire's component tracking and cause wire:click and other directives to fail silently
Use wire:model.live for real-time updates in Livewire components

Files:

  • resources/views/livewire/project/database/backup-edit.blade.php
  • resources/views/livewire/project/database/backup-executions.blade.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

Use named routes with route() function instead of hardcoded URLs

Files:

  • resources/views/livewire/project/database/backup-edit.blade.php
  • resources/views/livewire/project/database/backup-executions.blade.php
  • app/Jobs/PgBackrestRestoreJob.php
  • app/Actions/Database/PgBackrestRestore.php
  • app/Actions/Database/StartPostgresql.php
  • database/migrations/2025_12_09_231049_add_pgbackrest_support.php
  • app/Livewire/Project/Database/BackupEdit.php
  • app/Livewire/Project/Database/BackupExecutions.php
  • app/Http/Controllers/Api/DatabasesController.php
  • app/Models/DatabaseRestore.php
  • app/Services/Backup/PgBackrestService.php
  • app/Models/ScheduledDatabaseBackup.php
resources/views/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

Tailwind CSS: Use new utilities (version 4.1.4), not deprecated ones. Use gap utilities for spacing, not margins

Files:

  • resources/views/livewire/project/database/backup-edit.blade.php
  • resources/views/livewire/project/database/backup-executions.blade.php
app/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/**/*.php: Use eager loading to prevent N+1 queries, implement caching for frequently accessed data, queue heavy operations, optimize database queries with proper indexes, use chunking for large data operations
Use ownedByCurrentTeamCached() instead of ownedByCurrentTeam()->get() for team-scoped queries to avoid duplicate database queries
Queue heavy operations with Laravel Horizon

Files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Actions/Database/PgBackrestRestore.php
  • app/Actions/Database/StartPostgresql.php
  • app/Livewire/Project/Database/BackupEdit.php
  • app/Livewire/Project/Database/BackupExecutions.php
  • app/Http/Controllers/Api/DatabasesController.php
  • app/Models/DatabaseRestore.php
  • app/Services/Backup/PgBackrestService.php
  • app/Models/ScheduledDatabaseBackup.php
{**/*Model.php,database/migrations/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in .ai/patterns/database-patterns.md

Files:

  • database/migrations/2025_12_09_231049_add_pgbackrest_support.php
app/Livewire/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

In Livewire Components, always add the AuthorizesRequests trait and check permissions with $this->authorize() calls in mount() and action methods

Files:

  • app/Livewire/Project/Database/BackupEdit.php
  • app/Livewire/Project/Database/BackupExecutions.php
{**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Use team-based access control patterns and gate/policy authorization as documented in .ai/patterns/security-patterns.md

Files:

  • app/Models/DatabaseRestore.php
  • app/Models/ScheduledDatabaseBackup.php
app/Models/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/Models/**/*.php: When adding new database columns, ALWAYS update the model's $fillable array to allow mass assignment
Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries
Always use team() method to return relationship instance, not direct property access. App\Models\Application::team must return a relationship instance

Files:

  • app/Models/DatabaseRestore.php
  • app/Models/ScheduledDatabaseBackup.php
🧠 Learnings (11)
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to resources/views/livewire/**/*.blade.php : Use `wire:model.live` for real-time updates in Livewire components

Applied to files:

  • resources/views/livewire/project/database/backup-edit.blade.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to resources/views/livewire/**/*.blade.php : Wrap Modal Components with `can` directives to ensure proper authorization before displaying modals like `x-modal-confirmation`, `x-modal-input`, etc.

Applied to files:

  • resources/views/livewire/project/database/backup-executions.blade.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to resources/views/livewire/**/*.blade.php : When creating or editing form components (Input, Select, Textarea, Checkbox, Button), ALWAYS include authorization using `canGate` and `canResource` attributes for automatic authorization

Applied to files:

  • resources/views/livewire/project/database/backup-executions.blade.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to **/*.blade.php : ALWAYS include authorization on form components using `canGate` and `canResource` attributes

Applied to files:

  • resources/views/livewire/project/database/backup-executions.blade.php
📚 Learning: 2025-12-10T01:53:52.620Z
Learnt from: SkyfallWasTaken
Repo: coollabsio/coolify PR: 7556
File: app/Jobs/PgBackrestRestoreJob.php:39-118
Timestamp: 2025-12-10T01:53:52.620Z
Learning: In Coolify database models (StandalonePostgresql, StandaloneMysql, etc.), the team() method returns the actual Team model instance via data_get($this, 'environment.project.team'), NOT a BelongsTo relation. Therefore, $database->team() can be used directly for operations like ->notify() without needing to access a property.

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Always use team() method to return relationship instance, not direct property access. App\Models\Application::team must return a relationship instance

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Models/ScheduledDatabaseBackup.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/**/*.php : Use `ownedByCurrentTeamCached()` instead of `ownedByCurrentTeam()->get()` for team-scoped queries to avoid duplicate database queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Models/ScheduledDatabaseBackup.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Model.php,database/migrations/**/*.php} : Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in `.ai/patterns/database-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Models/DatabaseRestore.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Http/Controllers/Api/DatabasesController.php
  • app/Models/DatabaseRestore.php
  • app/Models/ScheduledDatabaseBackup.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php} : Use team-based access control patterns and gate/policy authorization as documented in `.ai/patterns/security-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-10T01:53:52.620Z
Learnt from: SkyfallWasTaken
Repo: coollabsio/coolify PR: 7556
File: app/Jobs/PgBackrestRestoreJob.php:39-118
Timestamp: 2025-12-10T01:53:52.620Z
Learning: In Coolify database models (StandalonePostgresql, StandaloneMysql, etc.), the team() method returns the actual Team model instance (via data_get($this, 'environment.project.team')), not a BelongsTo relation. Therefore, treat $database->team() as the model you can operate on (e.g., $database->team()->notify(...)) directly, without accessing a property. Apply this understanding in model reviews across files that define or call team().

Applied to files:

  • app/Models/DatabaseRestore.php
  • app/Models/ScheduledDatabaseBackup.php
🧬 Code graph analysis (5)
app/Jobs/PgBackrestRestoreJob.php (5)
app/Actions/Database/StartPostgresql.php (3)
  • StartPostgresql (12-468)
  • handle (28-255)
  • getHostPath (464-467)
app/Actions/Database/StopDatabase.php (1)
  • StopDatabase (17-56)
app/Models/DatabaseRestore.php (4)
  • DatabaseRestore (8-80)
  • database (20-23)
  • updateStatus (65-79)
  • appendLog (55-63)
app/Services/Backup/PgBackrestService.php (11)
  • PgBackrestService (9-364)
  • getStanzaName (23-26)
  • stanzaExists (345-354)
  • hasBackups (356-363)
  • findBackupByLabel (320-333)
  • getLatestBackup (305-318)
  • buildS3EnvVars (163-178)
  • buildInfoCommand (253-269)
  • parseInfoJson (294-303)
  • buildRestoreCommand (219-251)
  • buildInstallAndSetupCommand (153-161)
bootstrap/helpers/shared.php (1)
  • convertPathToDockerHost (3319-3326)
app/Actions/Database/PgBackrestRestore.php (3)
app/Jobs/PgBackrestRestoreJob.php (2)
  • PgBackrestRestoreJob (22-458)
  • handle (39-120)
app/Models/DatabaseRestore.php (3)
  • DatabaseRestore (8-80)
  • database (20-23)
  • execution (25-28)
app/Models/ScheduledDatabaseBackupExecution.php (1)
  • ScheduledDatabaseBackupExecution (8-48)
app/Livewire/Project/Database/BackupExecutions.php (4)
app/Actions/Database/PgBackrestRestore.php (1)
  • PgBackrestRestore (12-43)
app/Models/DatabaseRestore.php (4)
  • DatabaseRestore (8-80)
  • execution (25-28)
  • database (20-23)
  • isFinished (50-53)
app/Models/ScheduledDatabaseBackupExecution.php (2)
  • ScheduledDatabaseBackupExecution (8-48)
  • canRestore (42-47)
app/Models/StandalonePostgresql.php (1)
  • StandalonePostgresql (11-424)
app/Models/DatabaseRestore.php (1)
app/Models/ScheduledDatabaseBackupExecution.php (2)
  • casts (12-20)
  • ScheduledDatabaseBackupExecution (8-48)
app/Models/ScheduledDatabaseBackup.php (4)
app/Models/DatabaseRestore.php (2)
  • casts (12-18)
  • DatabaseRestore (8-80)
app/Models/ScheduledDatabaseBackupExecution.php (5)
  • casts (12-20)
  • isPgBackrest (32-35)
  • isNative (37-40)
  • restores (27-30)
  • ScheduledDatabaseBackupExecution (8-48)
app/Models/PgbackrestRepo.php (2)
  • casts (12-20)
  • PgbackrestRepo (8-72)
app/Models/StandalonePostgresql.php (1)
  • restores (359-362)
🪛 PHPMD (2.15.0)
app/Jobs/PgBackrestRestoreJob.php

22-458: The class PgBackrestRestoreJob has an overall complexity of 62 which is very high. The configured complexity threshold is 50. (undefined)

(ExcessiveClassComplexity)


41-41: Avoid unused local variables such as '$server'. (undefined)

(UnusedLocalVariable)


42-42: Avoid unused local variables such as '$containerName'. (undefined)

(UnusedLocalVariable)


43-43: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'handle'. (undefined)

(StaticAccess)


52-52: Avoid using static access to class '\App\Actions\Database\StopDatabase' in method 'handle'. (undefined)

(StaticAccess)


68-68: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


93-98: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'handle'. (undefined)

(StaticAccess)


113-113: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


122-185: The method preflight() has a Cyclomatic Complexity of 15. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


122-185: The method preflight() has an NPath complexity of 2400. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


127-127: Missing class import via use statement (line '127', column '23'). (undefined)

(MissingImport)


132-132: Missing class import via use statement (line '132', column '23'). (undefined)

(MissingImport)


137-137: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


139-139: Missing class import via use statement (line '139', column '27'). (undefined)

(MissingImport)


145-145: Missing class import via use statement (line '145', column '27'). (undefined)

(MissingImport)


151-151: Missing class import via use statement (line '151', column '23'). (undefined)

(MissingImport)


157-157: Missing class import via use statement (line '157', column '23'). (undefined)

(MissingImport)


163-163: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


164-164: Missing class import via use statement (line '164', column '23'). (undefined)

(MissingImport)


167-167: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


168-168: Missing class import via use statement (line '168', column '23'). (undefined)

(MissingImport)


172-172: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


174-174: Missing class import via use statement (line '174', column '27'). (undefined)

(MissingImport)


177-182: The method preflight uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


178-178: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


204-204: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


210-210: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


226-226: Missing class import via use statement (line '226', column '23'). (undefined)

(MissingImport)


234-234: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


236-236: Missing class import via use statement (line '236', column '23'). (undefined)

(MissingImport)


248-248: Missing class import via use statement (line '248', column '23'). (undefined)

(MissingImport)


271-271: Missing class import via use statement (line '271', column '23'). (undefined)

(MissingImport)


323-323: Missing class import via use statement (line '323', column '23'). (undefined)

(MissingImport)


339-339: Missing class import via use statement (line '339', column '27'). (undefined)

(MissingImport)


350-350: Missing class import via use statement (line '350', column '27'). (undefined)

(MissingImport)


355-355: Missing class import via use statement (line '355', column '23'). (undefined)

(MissingImport)


367-367: Missing class import via use statement (line '367', column '23'). (undefined)

(MissingImport)


387-387: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


393-398: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


426-426: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'getInstallAndRunCommand'. (undefined)

(StaticAccess)

app/Actions/Database/PgBackrestRestore.php

21-30: Avoid using static access to class '\App\Models\DatabaseRestore' in method 'handle'. (undefined)

(StaticAccess)


32-32: Avoid using static access to class '\App\Jobs\PgBackrestRestoreJob' in method 'handle'. (undefined)

(StaticAccess)

app/Actions/Database/StartPostgresql.php

343-345: The method add_custom_conf uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


349-349: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'add_custom_conf'. (undefined)

(StaticAccess)


382-382: The variable $content_base64 is not named in camelCase. (undefined)

(CamelCaseVariableName)


386-406: The method setup_pgbackrest_config is not named in camelCase. (undefined)

(CamelCaseMethodName)


388-388: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'setup_pgbackrest_config'. (undefined)

(StaticAccess)


408-459: The method create_pgbackrest_entrypoint is not named in camelCase. (undefined)

(CamelCaseMethodName)


410-410: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'create_pgbackrest_entrypoint'. (undefined)

(StaticAccess)


411-411: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'create_pgbackrest_entrypoint'. (undefined)

(StaticAccess)


416-416: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'create_pgbackrest_entrypoint'. (undefined)

(StaticAccess)

app/Livewire/Project/Database/BackupEdit.php

93-93: Avoid excessively long variable names like $pgbackrestCompressType. Keep variable name length under 20. (undefined)

(LongVariable)


96-96: Avoid excessively long variable names like $pgbackrestCompressLevel. Keep variable name length under 20. (undefined)

(LongVariable)


102-102: Avoid excessively long variable names like $pgbackrestArchiveMode. Keep variable name length under 20. (undefined)

(LongVariable)


105-105: Avoid excessively long variable names like $localRepoRetentionFullType. Keep variable name length under 20. (undefined)

(LongVariable)


108-108: Avoid excessively long variable names like $localRepoRetentionFull. Keep variable name length under 20. (undefined)

(LongVariable)


111-111: Avoid excessively long variable names like $localRepoRetentionDiff. Keep variable name length under 20. (undefined)

(LongVariable)


117-117: Avoid excessively long variable names like $s3RepoRetentionFullType. Keep variable name length under 20. (undefined)

(LongVariable)


171-203: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


204-234: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


225-228: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


259-317: The method syncPgbackrestRepos() has a Cyclomatic Complexity of 11. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


266-268: The method syncPgbackrestRepos uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


296-298: The method syncPgbackrestRepos uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


314-316: The method syncPgbackrestRepos uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)

app/Livewire/Project/Database/BackupExecutions.php

50-50: Avoid excessively long variable names like $showRestoreProgressModal. Keep variable name length under 20. (undefined)

(LongVariable)


165-165: Avoid using static access to class '\App\Actions\Database\PgBackrestRestore' in method 'startRestore'. (undefined)

(StaticAccess)

app/Http/Controllers/Api/DatabasesController.php

72-72: The method buildBackupConfig has a boolean flag argument $requireSaveS3, which is a certain sign of a Single Responsibility Principle violation. (undefined)

(BooleanArgumentFlag)


2884-2950: The method restore_database() has a Cyclomatic Complexity of 10. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


2884-2950: The method restore_database() has an NPath complexity of 384. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


2884-2950: The method restore_database is not named in camelCase. (undefined)

(CamelCaseMethodName)


2944-2944: Avoid using static access to class '\App\Actions\Database\PgBackrestRestore' in method 'restore_database'. (undefined)

(StaticAccess)


2989-3014: The method list_restores is not named in camelCase. (undefined)

(CamelCaseMethodName)


3062-3093: The method restore_status is not named in camelCase. (undefined)

(CamelCaseMethodName)

app/Models/DatabaseRestore.php

55-55: The method appendLog has a boolean flag argument $persist, which is a certain sign of a Single Responsibility Principle violation. (undefined)

(BooleanArgumentFlag)

app/Services/Backup/PgBackrestService.php

9-364: The class PgBackrestService has 14 public methods. Consider refactoring PgBackrestService to keep number of public methods under 10. (undefined)

(TooManyPublicMethods)


9-364: The class PgBackrestService has an overall complexity of 59 which is very high. The configured complexity threshold is 50. (undefined)

(ExcessiveClassComplexity)


83-83: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


92-92: Missing class import via use statement (line '92', column '27'). (undefined)

(MissingImport)


103-109: The method generateRepoConfig uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


119-119: Avoid variables with short names like $k. Configured minimum length is 3. (undefined)

(ShortVariable)


119-119: Avoid variables with short names like $v. Configured minimum length is 3. (undefined)

(ShortVariable)


170-170: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


186-186: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


253-253: The method buildInfoCommand has a boolean flag argument $json, which is a certain sign of a Single Responsibility Principle violation. (undefined)

(BooleanArgumentFlag)

🔇 Additional comments (40)
app/Actions/Database/StartPostgresql.php (5)

26-27: LGTM! State tracking property is clean.

Nice addition for tracking pgBackRest configuration state. Like a T-800's mission parameters - you need to know what you're targeting. Self-hosted backups are the way forward.


386-406: LGTM! Configuration setup is well-structured.

Clean conditional flow: remove old config when pgBackRest isn't needed, set up fresh config when it is. Like reloading before a mission - no stale ammunition. The base64 transfer pattern is solid for safe content transmission.


340-346: Previous security concern addressed.

The listen_addresses logic now properly considers is_public - public databases get '*', private ones get 'localhost'. This is much better than the previous unconditional '*'. Like having a bouncer at the taco stand - only let in who should be there.


214-228: LGTM! Entrypoint injection pattern is solid.

The entrypoint override correctly runs the install script before handing off to PostgreSQL's official entrypoint. The -- separator ensures arguments pass through properly. Self-hosted databases with proper backup infrastructure - this is the way. No serverless vendor lock-in needed!


461-467: LGTM! Clean path abstraction.

Simple wrapper that keeps the dev environment path translation centralized. Like having a universal translator for your taco orders across different restaurants.

app/Actions/Database/PgBackrestRestore.php (1)

16-35: LGTM! Clean restore action implementation.

Creates the restore record and dispatches the job - simple and effective. Like ordering a taco: place the order, let the kitchen (job) handle the cooking. The null-safe operators (?->) handle optional execution elegantly.

app/Models/DatabaseRestore.php (4)

8-18: LGTM! Standard model setup.

Datetime casts for target_time and finished_at are appropriate. Like setting your taco delivery time - precision matters. Model follows established Coolify patterns.


20-28: LGTM! Relationships properly defined.

The MorphTo for database() correctly supports polymorphic relations to different database types. The BelongsTo for execution() with explicit foreign key matches the inverse HasMany in ScheduledDatabaseBackupExecution. Proper Eloquent patterns - no serverless abstractions needed here!


30-53: LGTM! Clean status helpers.

Simple, focused methods for status checking. isFinished() correctly identifies both terminal states. Like checking if your taco order is complete - either you got it or it failed, either way you're done waiting.


55-79: Previous double-save concern addressed.

The persist parameter on appendLog() allows updateStatus() to batch all changes into a single save() call. Efficient database writes - like ordering all your tacos at once instead of making 5 separate trips to the counter. Well done!

app/Jobs/PgBackrestRestoreJob.php (3)

262-272: Previous backup verification concern addressed.

The backup now includes explicit verification that the backup directory isn't empty before proceeding. If it fails, a RuntimeException is thrown. Like checking your taco bag before leaving the restaurant - make sure nothing's missing!


122-185: LGTM! Comprehensive preflight validation.

The preflight checks are thorough: backup config exists, S3 connections work, volumes are present, stanza is healthy, and target backup exists. Like doing a full systems check before launching - you don't want to find out mid-flight that something's missing. This is why self-hosted beats serverless: you control the validation!


437-457: LGTM! Proper job failure handling.

The failed() handler logs comprehensively, updates the restore status, and notifies the team. Good error communication - when the taco machine breaks, everyone needs to know!

resources/views/livewire/project/database/backup-edit.blade.php (5)

23-29: Previous toggle concern addressed.

The toggle now uses wire:click="togglePgbackrestEngine" method call instead of inline $set with render-time interpolation. Proper targeting system engaged - the toggle now fires at the current state, not where it was rendered.


169-183: Previous conditional logic concern addressed.

Using $this->showLocalRepoSettings computed property makes the intent crystal clear. Much better than the previous boolean gymnastics. Like a clear menu at a taco stand - no guessing what you're getting!


147-152: LGTM! Separate S3 storage selection for pgBackRest.

Having a distinct s3RepoStorageId for pgBackRest mode separate from s3StorageId for native mode allows independent configuration. Like having different hot sauce options for different types of tacos - flexibility is good!


155-165: LGTM! Well-structured retention settings.

Dynamic labels that change based on retention type (count vs time) improve UX. The structure aligns with pgBackRest's native retention model. Self-hosted backup management done right - no cloud vendor telling you how long to keep your tacos!


71-104: Consider adding authorization attributes to form components.

Per coding guidelines, form components should include canGate and canResource attributes for authorization. The pgBackRest settings inputs (lines 71-104) lack these attributes. Verify that the BackupEdit Livewire component implements proper authorization via the AuthorizesRequests trait and $this->authorize() calls in mount() and action methods. If authorization is only handled at the component level, these field-level attributes may be optional; otherwise, add them to individual form inputs.

resources/views/livewire/project/database/backup-executions.blade.php (4)

69-77: Excellent self-hosted backup indicator, human.

The pgBackRest badge with backup type display is well-implemented. Shows users exactly what kind of incremental backup they're looking at. Your servers will be pleased with this transparency. Much better than some serverless solution where you'd never know what's happening behind the scenes.


194-211: Authorization correctly implemented on restore modal.

Good work wrapping the restore modal with @can('manage', $database). This destructive operation is now properly guarded. Your database will not be terminated by unauthorized humans.


301-318: Alpine.js scope issue has been terminated.

The previous review flagged cross-scope x-ref access issues. Now confirmValue lives in a shared parent scope wrapping both input and button, and startRestore correctly receives the execution ID ($restoreExecutionId) instead of the confirmation text. Well done, human.


323-430: Restore progress modal implementation looks solid.

The progress modal correctly:

  • Polls for status updates only when restore is running/pending (line 327)
  • Shows appropriate status icons for different states
  • Displays logs only when restore is finished
  • Has a close button when finished

Like a good self-hosted server, it knows when to check in and when to stay quiet. No serverless billing surprises here.

app/Models/ScheduledDatabaseBackup.php (3)

14-23: Casts method properly configured.

Boolean and integer casts are correctly defined. Your model will not be confused about data types. Unlike serverless functions that forget everything between invocations, your self-hosted database model remembers exactly what type each field should be.


45-55: Repository helper methods now prefer enabled repos - issue terminated.

Previous review flagged that localRepo() and s3Repo() didn't check the enabled flag. Now they correctly prefer enabled repos first, then fall back to any repo. This prevents surprises when humans toggle repos on/off.

Good fix. Would you like a taco to celebrate? Oh wait, I'm gluten intolerant and tacos are complicated.


67-75: HasManyThrough return type corrected - critical issue terminated.

Previous review identified that hasManyThrough() returns HasManyThrough, not HasMany. This would have caused a TypeError in PHP 8+. Now the return type is correct and the import is present on line 6. Your code will not be terminated by type errors.

app/Livewire/Project/Database/BackupExecutions.php (3)

127-138: Execution lookup now scoped to current backup - security issue terminated.

Previous review flagged that confirmRestore used a global ScheduledDatabaseBackupExecution::find() which could allow cross-database restore attacks. Now it correctly uses $this->backup->executions()->where('id', $executionId)->first().

No crafty humans can restore the wrong backup onto your database anymore. Your servers are safe.


146-172: startRestore method properly secured.

  • Execution lookup is scoped to $this->backup->executions()
  • PostgreSQL type check prevents restore on non-PostgreSQL databases
  • Authorization check with $this->authorize('manage', $database)
  • Proper try/catch with user-friendly error dispatch

Like a well-secured server room - multiple layers of protection. Much better than some serverless function anyone could invoke.


258-265: Defense-in-depth authorization added to mount().

Previous review suggested adding $this->authorize('view', $this->database) in mount() for defense-in-depth. You listened, human. This prevents unauthorized access even if someone manipulates component parameters.

Your component is now like a well-guarded server: multiple checkpoints before entry.

database/migrations/2025_12_09_231049_add_pgbackrest_support.php (3)

11-18: Backup configuration schema extension looks solid.

Engine column with index, nullable pgBackRest-specific columns. Clean approach that maintains backward compatibility - existing backups default to 'native' engine. Your self-hosted PostgreSQL servers will appreciate this organized data structure.


28-54: pgbackrest_repos table design is well-structured.

  • UUID for external references (good API design)
  • Foreign key with cascade delete prevents orphaned records
  • Composite unique constraint on (scheduled_database_backup_id, repo_number) prevents duplicate repo configurations
  • Sensible defaults for retention policies

One small consideration: if you plan to query repos frequently by type, you might want an index there. But for now, the table is small enough that it won't matter.


80-105: Down migration correctly reverses all changes.

Tables dropped before columns removed, proper order maintained. Rollback will work cleanly. Unlike serverless deployments that just... evaporate... your migrations can actually be reversed.

app/Http/Controllers/Api/DatabasesController.php (4)

50-93: Helper methods for backup config - excellent refactor, human.

getBackupConfigFields() and buildBackupConfig() centralize the backup configuration logic that was previously duplicated. Previous review suggested this, and you delivered.

The boolean flag $requireSaveS3 is acceptable here - it's a simple behavioral toggle, not a code smell. Static analysis tools don't understand context. They're like serverless functions: no memory of what came before.


2896-2906: Input validation added for restore endpoint - security concern terminated.

Previous review flagged that target_time was passed straight through without validation, risking shell injection. Now you have:

  • execution_uuid validated as string|uuid|nullable
  • target_time validated as date|nullable

And line 2941 parses it through Carbon before use. Your database restoration process is now protected from hostile input. Hasta la vista, injection attacks.


2923-2937: Execution lookup is properly scoped in restore_database.

The execution query uses whereHas('scheduledDatabaseBackup', ...) with constraints on database_id, database_type, and engine = 'pgbackrest'. This prevents cross-database restore attempts through the API.

Combined with the $this->authorize('manage', $database) call at line 2917, this endpoint is well-secured. Your self-hosted PostgreSQL databases thank you.


786-792: PostgreSQL-only guard for pgBackRest engine - correct enforcement.

Good validation that pgBackRest can only be used with PostgreSQL databases. Returns clear 422 error with descriptive message. Your API won't let humans accidentally configure pgBackRest for Redis - that would be like putting taco seasoning on a gluten-free pizza. Doesn't make sense, and now it's prevented.

app/Livewire/Project/Database/BackupEdit.php (2)

86-137: pgBackRest state props and toggle look consistent with Livewire usage

Nicely scoped pgBackRest-specific properties with validation rules, and the togglePgbackrestEngine() helper keeps the UI state simple. The getShowLocalRepoSettingsProperty() truth table (only hiding when both local is disabled and S3 is enabled) matches the intent of “no local repo to configure”, so no complaints from this metal reviewer.


151-203: Wrap backup save and repo sync in a database transaction to prevent partial writes

If syncPgbackrestRepos() throws after $this->backup->save() completes, the backup row persists in an inconsistent state (e.g., with engine = 'pgbackrest' but no corresponding repos configured), while the user sees only an error and assumes nothing was saved. This creates the classic "saved but not really" problem.

Wrap both operations in DB::transaction() to ensure either both persist or neither does:

                $this->customValidate();
-               $this->backup->save();
-               $this->syncPgbackrestRepos();
+               \Illuminate\Support\Facades\DB::transaction(function () {
+                   $this->backup->save();
+                   $this->syncPgbackrestRepos();
+               });

Import DB at the top of the file if not already present.

app/Services/Backup/PgBackrestService.php (3)

28-129: Config + repo generation looks coherent and defensive

generateConfig() and generateRepoConfig() are wired sensibly:

  • Skip config entirely when there are no enabled pgBackRest backups or repos, returning null instead of emitting a useless config.
  • Centralize global settings (log level, compression, archive mode) with sane defaults.
  • Cleanly split per-repo S3 vs POSIX handling, including retention policies.
  • Validate S3 bucket and endpoint via validateShellSafePath() before writing them into config.

This is the kind of deterministic, self-hosted config generation that won’t suddenly decide to go serverless on you.


294-363: JSON/info helpers are defensive and match pgBackRest info structure

The parsing and accessor helpers (parseInfoJson, getLatestBackup, findBackupByLabel, getBackupSize, getBackupType, stanzaExists, hasBackups) are all:

  • Null/empty-safe (they bail out early if the expected keys aren’t present).
  • Aligned with pgBackRest’s JSON structure ($info[0]['backup'], ['info']['repository']['size'], ['status']['code']).
  • Returning sensible defaults (0 for size, 'full' for type) instead of blowing up.

Good balance between safety and simplicity; nothing here is going to DOS your metal servers because somebody printed unexpected JSON.


130-161: Verify the command injection risk and determine if escapeshellarg() is the appropriate mitigation

The concern about unescaped $command interpolation into su postgres -c "{$command}" raises a valid shell injection risk. However, verification of the actual vulnerability requires examining: (1) what command strings are passed to buildInstallAndSetupCommand() in practice, (2) whether upstream builders enforce constraints that prevent injection, and (3) whether escapeshellarg() would be effective given the surrounding context (the command is already within double quotes and the su -c flag structure).

Copy link
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: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Livewire/Project/Database/BackupEdit.php (1)

390-405: customValidate() still works with new engine field; message tweak is fine

  • The updated message 'Invalid Cron / Human expression' better reflects the allowed syntax without changing behavior.
  • customValidate() still operates on $this->backup->frequency, which is set from $this->frequency in syncData(true) before calling customValidate(), so the new engine field doesn’t break this flow.
  • Given that pgBackRest now uses separate PgbackrestRepo objects for S3, consider whether the initial s3_storage_id normalization is only relevant for the native engine; if so, you may want to gate that block on $this->engine === 'native' to avoid confusion, though it’s currently harmless.
♻️ Duplicate comments (2)
app/Jobs/DatabaseBackupJob.php (1)

117-122: I'll be back... to point out this duplicate event dispatch, human.

This pgBackRest branch looks clean - nice early return pattern. However, my cybernetic sensors detect a redundancy issue that was flagged previously: BackupCreated::dispatch($this->team->id) fires three times for a single pgBackRest backup:

  1. Line 111 (top of handle())
  2. Line 807 (run_pgbackrest_backup()'s finally)
  3. Line 450 (handle()'s finally)

If listeners do anything heavier than UI refresh, this could cause unnecessary load on your self-hosted server. Consider removing the dispatch inside run_pgbackrest_backup()'s finally block since the outer handle() already covers it.

app/Livewire/Project/Database/BackupEdit.php (1)

260-318: pgBackRest repo sync is functionally correct; tighten S3 selection and consider splitting logic

Functionally, syncPgbackrestRepos():

  • Requires at least one repo (local or S3) when pgBackRest is enabled.
  • Keeps repo_number ordering consistent as repos are enabled/disabled.
  • Reuses or creates PgbackrestRepo instances for posix and s3 types, cleaning up disabled types.

Two concerns / improvements:

  1. Auto‑selecting S3 storage may surprise users (repeat of prior review)
    When $this->saveS3 is true and s3RepoStorageId is empty, you silently default to the first available S3 storage:

    if ($this->saveS3 && empty($this->s3RepoStorageId)) {
        if ($this->s3s->isNotEmpty()) {
            $this->s3RepoStorageId = $this->s3s->first()->id;
        } else {
            throw new Exception('S3 storage must be selected when S3 backups are enabled.');
        }
    }

    In environments with multiple S3 storages (e.g. staging vs prod buckets), this can send backups to the wrong place without any explicit user choice. Safer to always require an explicit selection and throw if it’s missing:

  •    if ($this->saveS3 && empty($this->s3RepoStorageId)) {
    
  •        if ($this->s3s->isNotEmpty()) {
    
  •            $this->s3RepoStorageId = $this->s3s->first()->id;
    
  •        } else {
    
  •            throw new Exception('S3 storage must be selected when S3 backups are enabled.');
    
  •        }
    
  •    }
    
  •    if ($this->saveS3 && empty($this->s3RepoStorageId)) {
    
  •        throw new Exception('S3 storage must be selected when S3 backups are enabled.');
    
  •    }
    
    
    (You still keep the later `$hasS3` logic only true when an ID is set.)
    
    
  1. Complexity / readability
    The method is just over PHPMD’s cyclomatic complexity threshold. If you want to calm the linter, you could extract the local and S3 branches into small helpers, e.g. syncLocalRepo($hasLocal, &$repoNumber) and syncS3Repo($hasS3, $repoNumber).
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c6dfe4 and 5b45d87.

📒 Files selected for processing (7)
  • app/Actions/Database/PgBackrestRestore.php (1 hunks)
  • app/Jobs/DatabaseBackupJob.php (4 hunks)
  • app/Jobs/PgBackrestRestoreJob.php (1 hunks)
  • app/Livewire/Project/Database/BackupEdit.php (7 hunks)
  • database/migrations/2025_12_09_231049_add_pgbackrest_support.php (1 hunks)
  • resources/views/livewire/project/database/backup-executions.blade.php (5 hunks)
  • routes/api.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (12)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Always run code formatting with ./vendor/bin/pint before committing code

**/*.php: Follow PSR-12 coding standards. Use Laravel Pint for automatic formatting. Write descriptive variable and method names. Keep methods small and focused. Document complex logic with clear comments
Use PHP 8.4 constructor property promotion and typed properties
Never use env() outside config files in Laravel

Files:

  • app/Jobs/DatabaseBackupJob.php
  • routes/api.php
  • app/Jobs/PgBackrestRestoreJob.php
  • resources/views/livewire/project/database/backup-executions.blade.php
  • database/migrations/2025_12_09_231049_add_pgbackrest_support.php
  • app/Actions/Database/PgBackrestRestore.php
  • app/Livewire/Project/Database/BackupEdit.php
app/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/**/*.php: Use eager loading to prevent N+1 queries, implement caching for frequently accessed data, queue heavy operations, optimize database queries with proper indexes, use chunking for large data operations
Use ownedByCurrentTeamCached() instead of ownedByCurrentTeam()->get() for team-scoped queries to avoid duplicate database queries
Queue heavy operations with Laravel Horizon

Files:

  • app/Jobs/DatabaseBackupJob.php
  • app/Jobs/PgBackrestRestoreJob.php
  • app/Actions/Database/PgBackrestRestore.php
  • app/Livewire/Project/Database/BackupEdit.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

Use named routes with route() function instead of hardcoded URLs

Files:

  • app/Jobs/DatabaseBackupJob.php
  • routes/api.php
  • app/Jobs/PgBackrestRestoreJob.php
  • resources/views/livewire/project/database/backup-executions.blade.php
  • database/migrations/2025_12_09_231049_add_pgbackrest_support.php
  • app/Actions/Database/PgBackrestRestore.php
  • app/Livewire/Project/Database/BackupEdit.php
{**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Use team-based access control patterns and gate/policy authorization as documented in .ai/patterns/security-patterns.md

Files:

  • routes/api.php
routes/api.php

📄 CodeRabbit inference engine (CLAUDE.md)

RESTful endpoints in routes/api.php should use API Resources for response formatting, implement rate limiting for public endpoints, version APIs when making breaking changes, and document endpoints with clear examples

Files:

  • routes/api.php
routes/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

Group routes by middleware and prefix, use route model binding for cleaner controllers, name routes consistently (resource.action), implement proper HTTP verbs (GET, POST, PUT, DELETE)

Files:

  • routes/api.php
**/**/livewire/**/*.blade.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Livewire components MUST have exactly ONE root element with no exceptions

Files:

  • resources/views/livewire/project/database/backup-executions.blade.php
**/*.blade.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

**/*.blade.php: ALWAYS include authorization on form components using canGate and canResource attributes
Frontend development must use Livewire 3.5.20 for server-side state, Alpine.js for client interactions, and Tailwind CSS 4.1.4 for styling

Files:

  • resources/views/livewire/project/database/backup-executions.blade.php
resources/views/livewire/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

resources/views/livewire/**/*.blade.php: When creating or editing form components (Input, Select, Textarea, Checkbox, Button), ALWAYS include authorization using canGate and canResource attributes for automatic authorization
Wrap Modal Components with @can directives to ensure proper authorization before displaying modals like x-modal-confirmation, x-modal-input, etc.
Livewire component views MUST have exactly ONE root element. ALL content must be contained within this single root element. Placing ANY elements (<style>, <script>,

, comments, or other HTML) outside the root will break Livewire's component tracking and cause wire:click and other directives to fail silently
Use wire:model.live for real-time updates in Livewire components

Files:

  • resources/views/livewire/project/database/backup-executions.blade.php
resources/views/**/*.blade.php

📄 CodeRabbit inference engine (CLAUDE.md)

Tailwind CSS: Use new utilities (version 4.1.4), not deprecated ones. Use gap utilities for spacing, not margins

Files:

  • resources/views/livewire/project/database/backup-executions.blade.php
{**/*Model.php,database/migrations/**/*.php}

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in .ai/patterns/database-patterns.md

Files:

  • database/migrations/2025_12_09_231049_add_pgbackrest_support.php
app/Livewire/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

In Livewire Components, always add the AuthorizesRequests trait and check permissions with $this->authorize() calls in mount() and action methods

Files:

  • app/Livewire/Project/Database/BackupEdit.php
🧠 Learnings (12)
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to routes/api.php : RESTful endpoints in `routes/api.php` should use API Resources for response formatting, implement rate limiting for public endpoints, version APIs when making breaking changes, and document endpoints with clear examples

Applied to files:

  • routes/api.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to routes/**/*.php : Group routes by middleware and prefix, use route model binding for cleaner controllers, name routes consistently (resource.action), implement proper HTTP verbs (GET, POST, PUT, DELETE)

Applied to files:

  • routes/api.php
📚 Learning: 2025-12-10T01:53:52.620Z
Learnt from: SkyfallWasTaken
Repo: coollabsio/coolify PR: 7556
File: app/Jobs/PgBackrestRestoreJob.php:39-118
Timestamp: 2025-12-10T01:53:52.620Z
Learning: In Coolify database models (StandalonePostgresql, StandaloneMysql, etc.), the team() method returns the actual Team model instance via data_get($this, 'environment.project.team'), NOT a BelongsTo relation. Therefore, $database->team() can be used directly for operations like ->notify() without needing to access a property.

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Always use team() method to return relationship instance, not direct property access. App\Models\Application::team must return a relationship instance

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/**/*.php : Use `ownedByCurrentTeamCached()` instead of `ownedByCurrentTeam()->get()` for team-scoped queries to avoid duplicate database queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Model.php,database/migrations/**/*.php} : Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in `.ai/patterns/database-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Livewire/Project/Database/BackupEdit.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Livewire/Project/Database/BackupEdit.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php} : Use team-based access control patterns and gate/policy authorization as documented in `.ai/patterns/security-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to resources/views/livewire/**/*.blade.php : Wrap Modal Components with `can` directives to ensure proper authorization before displaying modals like `x-modal-confirmation`, `x-modal-input`, etc.

Applied to files:

  • resources/views/livewire/project/database/backup-executions.blade.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to resources/views/livewire/**/*.blade.php : When creating or editing form components (Input, Select, Textarea, Checkbox, Button), ALWAYS include authorization using `canGate` and `canResource` attributes for automatic authorization

Applied to files:

  • resources/views/livewire/project/database/backup-executions.blade.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to **/*.blade.php : ALWAYS include authorization on form components using `canGate` and `canResource` attributes

Applied to files:

  • resources/views/livewire/project/database/backup-executions.blade.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Technology Stack: Backend is Laravel 12.4.1 (PHP 8.4.7), Frontend uses Livewire 3.5.20 with Alpine.js and Tailwind CSS 4.1.4, Database is PostgreSQL 15 with Redis 7 for cache/queues, use Docker & Docker Compose for containerization

Applied to files:

  • app/Livewire/Project/Database/BackupEdit.php
🧬 Code graph analysis (3)
routes/api.php (1)
app/Http/Controllers/Api/DatabasesController.php (1)
  • DatabasesController (26-3094)
app/Jobs/PgBackrestRestoreJob.php (3)
app/Models/DatabaseRestore.php (4)
  • DatabaseRestore (8-80)
  • database (20-23)
  • updateStatus (65-79)
  • appendLog (55-63)
app/Services/Backup/PgBackrestService.php (9)
  • PgBackrestService (9-364)
  • getStanzaName (23-26)
  • findBackupByLabel (320-333)
  • getLatestBackup (305-318)
  • buildS3EnvVars (163-178)
  • buildInfoCommand (253-269)
  • parseInfoJson (294-303)
  • buildRestoreCommand (219-251)
  • buildInstallAndSetupCommand (153-161)
bootstrap/helpers/shared.php (1)
  • convertPathToDockerHost (3319-3326)
app/Livewire/Project/Database/BackupEdit.php (5)
app/Models/PgbackrestRepo.php (1)
  • PgbackrestRepo (8-72)
app/Models/ScheduledDatabaseBackup.php (5)
  • ScheduledDatabaseBackup (10-147)
  • server (130-146)
  • localRepo (45-49)
  • s3Repo (51-55)
  • pgbackrestRepos (35-38)
app/Models/StandalonePostgresql.php (2)
  • StandalonePostgresql (11-424)
  • type (242-245)
bootstrap/helpers/shared.php (1)
  • validateShellSafePath (119-147)
app/Livewire/Project/Database/BackupExecutions.php (1)
  • server (267-283)
🪛 PHPMD (2.15.0)
app/Jobs/DatabaseBackupJob.php

684-809: The method run_pgbackrest_backup() has a Cyclomatic Complexity of 19. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


684-809: The method run_pgbackrest_backup() has an NPath complexity of 6920. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


684-809: The method run_pgbackrest_backup() has 126 lines of code. Current threshold is set to 100. Avoid really long methods. (undefined)

(ExcessiveMethodLength)


684-809: The method run_pgbackrest_backup is not named in camelCase. (undefined)

(CamelCaseMethodName)


686-686: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


692-700: Avoid using static access to class '\App\Models\ScheduledDatabaseBackupExecution' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


705-705: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


707-707: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


715-717: The method run_pgbackrest_backup uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


726-728: The method run_pgbackrest_backup uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


732-732: Missing class import via use statement (line '732', column '27'). (undefined)

(MissingImport)


735-735: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


751-760: The method run_pgbackrest_backup uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


762-762: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


763-763: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


766-766: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


767-767: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


781-781: Avoid using static access to class '\Carbon\Carbon' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


795-795: Avoid using static access to class '\Carbon\Carbon' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


807-807: Avoid using static access to class '\App\Events\BackupCreated' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


811-851: The method run_pgbackrest_expire is not named in camelCase. (undefined)

(CamelCaseMethodName)


817-817: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


819-819: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


834-843: The method run_pgbackrest_expire uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


846-849: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


853-867: The method update_pgbackrest_config is not named in camelCase. (undefined)

(CamelCaseMethodName)


855-855: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'update_pgbackrest_config'. (undefined)

(StaticAccess)


858-858: Missing class import via use statement (line '858', column '23'). (undefined)

(MissingImport)

app/Jobs/PgBackrestRestoreJob.php

23-455: The class PgBackrestRestoreJob has an overall complexity of 62 which is very high. The configured complexity threshold is 50. (undefined)

(ExcessiveClassComplexity)


42-42: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'handle'. (undefined)

(StaticAccess)


51-51: Avoid using static access to class '\App\Actions\Database\StopDatabase' in method 'handle'. (undefined)

(StaticAccess)


67-67: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


92-97: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'handle'. (undefined)

(StaticAccess)


112-112: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


121-184: The method preflight() has a Cyclomatic Complexity of 15. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


121-184: The method preflight() has an NPath complexity of 2400. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


136-136: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


162-162: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


166-166: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


171-171: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


176-181: The method preflight uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


177-177: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


203-203: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


208-208: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


232-232: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


385-385: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


390-395: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


423-423: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'getInstallAndRunCommand'. (undefined)

(StaticAccess)

app/Actions/Database/PgBackrestRestore.php

21-30: Avoid using static access to class '\App\Models\DatabaseRestore' in method 'handle'. (undefined)

(StaticAccess)


32-32: Avoid using static access to class '\App\Jobs\PgBackrestRestoreJob' in method 'handle'. (undefined)

(StaticAccess)

app/Livewire/Project/Database/BackupEdit.php

94-94: Avoid excessively long variable names like $pgbackrestCompressType. Keep variable name length under 20. (undefined)

(LongVariable)


97-97: Avoid excessively long variable names like $pgbackrestCompressLevel. Keep variable name length under 20. (undefined)

(LongVariable)


103-103: Avoid excessively long variable names like $pgbackrestArchiveMode. Keep variable name length under 20. (undefined)

(LongVariable)


106-106: Avoid excessively long variable names like $localRepoRetentionFullType. Keep variable name length under 20. (undefined)

(LongVariable)


109-109: Avoid excessively long variable names like $localRepoRetentionFull. Keep variable name length under 20. (undefined)

(LongVariable)


112-112: Avoid excessively long variable names like $localRepoRetentionDiff. Keep variable name length under 20. (undefined)

(LongVariable)


118-118: Avoid excessively long variable names like $s3RepoRetentionFullType. Keep variable name length under 20. (undefined)

(LongVariable)


172-204: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


205-235: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


226-229: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


260-318: The method syncPgbackrestRepos() has a Cyclomatic Complexity of 11. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


267-269: The method syncPgbackrestRepos uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


297-299: The method syncPgbackrestRepos uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


315-317: The method syncPgbackrestRepos uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)

🔇 Additional comments (13)
app/Jobs/DatabaseBackupJob.php (1)

853-867: Config update method is clean and efficient.

Good approach using base64 encoding to safely transfer config content to the container. The command structure is secure since $configBase64 only contains base64 characters and $configPath comes from a constant.

One small observation: if $this->container_name somehow contained malicious content, it could be a problem, but since it's set from $this->database->uuid (line 688), which should be a validated UUID, this is safe.

routes/api.php (1)

154-156: Restore routes are well‑placed and correctly gated

POST for creating a restore and GET for listing/fetching status are consistent with the existing database action group, use the v1 prefix, and apply appropriate api.ability middleware. No changes required; this API surface looks solid for the new pgBackRest flow.
Based on learnings, route grouping and abilities align with the documented patterns.

app/Jobs/PgBackrestRestoreJob.php (3)

40-119: End‑to‑end restore orchestration is robust

Stopping Postgres, backing up PGDATA with verification, clearing data, running the pgBackRest sidecar, verifying via PG_VERSION, then restarting and cleaning up is a solid, defensively coded flow. Error handling around the inner restore block plus the outer catch (with restart attempt) gives good resilience for big, self‑hosted databases that don’t vanish into serverless marketing mist. No blockers here.

Also applies to: 332-355, 357-414


121-184: Preflight + PGDATA backup logic looks solid

The preflight checks cover all the right things: enabled pgBackRest backup config, S3 repo connectivity, required volumes, and actual presence of stanza + backups via sidecar. backupCurrentPgData() then:

  • Skips backup cleanly when PGDATA is empty,
  • Copies via a throw‑on‑error cp -a /data/. /backup/,
  • Verifies the backup isn’t empty before proceeding.

For big on‑disk Postgres, this is the kind of paranoid safety net I like to see before we start tearing PGDATA apart. No changes needed here.

Also applies to: 186-238, 240-275


277-313: Recovery and cleanup are appropriately best‑effort

recoverFromBackup() and removePgDataBackup() are explicitly best‑effort: failures are logged, but they don’t explode the job again. Given these only run in the already‑failed path or during cleanup, this trade‑off is reasonable — better to log loudly than to cascade failures. The PGDATA clear step is also scoped to the volume mount and matches the backup logic. All acceptable as‑is.

Also applies to: 315-331

app/Actions/Database/PgBackrestRestore.php (1)

16-35: Action wiring into job + validation looks clean

The action neatly:

  • Creates a DatabaseRestore with the right morph fields, engine flag, and optional execution/targetTime, and
  • Dispatches PgBackrestRestoreJob with the necessary context.

The rules() method validating database and targetTime (as a date) gives controllers/Livewire a simple contract. This is lean, readable wiring — nothing to terminate here.

Also applies to: 37-43

resources/views/livewire/project/database/backup-executions.blade.php (2)

69-77: pgBackRest presentation and delete UX are consistent and clear

The new UI bits for pgBackRest executions are coherent:

  • Badge + optional backup type give immediate visual context.
  • Size falls back to pgbackrest_repo_size as “Repo Size” when the traditional size isn’t present.
  • Location line correctly switches to using the pgBackRest label for pgBackRest executions.
  • Backup Availability shows a dedicated “pgBackRest Repository” pill, while non‑pgBackRest keeps the Local/S3 indicators.
  • Delete confirmation text and checkboxes adapt per engine, including label vs filename in the confirmation prompt.

This gives operators a clear mental model of where their bits live — exactly what you want when babysitting 100 GB Postgres on real servers instead of some mystery serverless pit.

Also applies to: 92-101, 106-121, 153-179, 217-238


188-211: Restore trigger + confirmation and progress modal are well‑gated and fixed for Alpine scope

Nice work on the restore flow:

  • The “Restore” button for pgBackRest backups only appears for successful executions with a label and is wrapped in @can('manage', $database), matching the destructive nature of the operation.
  • The custom Restore Confirmation Modal is also wrapped in the same @can and clearly explains impact, shows the label, and requires typing restore before enabling the action.
  • Alpine’s confirmValue is scoped on a single wrapper div, and $wire.startRestore({{ $restoreExecutionId }}) now correctly sends the execution ID, not the typed text; the button enablement uses the same confirmValue check. No more cross‑scope $refs weirdness.
  • The Restore Progress Modal polls while pending/running, shows status, message and logs, and allows closing once finished, all inside the Livewire root.

Auth, UX, and wiring are aligned — humans get clear warnings, and the machine does the dirty work. Approval granted.

Also applies to: 251-324, 325-432

database/migrations/2025_12_09_231049_add_pgbackrest_support.php (1)

11-78: Migration schema and rollback are consistent and future‑proof

The migration cleanly:

  • Extends existing backup tables with engine and pgBackRest‑specific fields.
  • Introduces pgbackrest_repos with sensible defaults, FKs, and a uniqueness constraint on (scheduled_database_backup_id, repo_number).
  • Adds database_restores with a polymorphic database, optional execution FK, pgBackRest metadata, and an indexed status column.

The down() method properly drops the status index before dropping database_restores, then removes the new tables and columns from the existing tables. This is reversible and ready for heavy workloads on metal boxes, not fragile “infinite” serverless.

Also applies to: 81-109

app/Livewire/Project/Database/BackupEdit.php (4)

6-8: Imports for pgBackRest repo and StandalonePostgresql are correct and used

PgbackrestRepo and StandalonePostgresql are both referenced later (syncPgbackrestRepos(), isPostgresql()), so these imports are appropriate and keep the type references clean. No changes needed here, soldier.


75-77: Default s3StorageId to null instead of assuming ID 1

Switching the default from 1 to null aligns with the nullable|integer validation and avoids implicitly pointing at a potentially non-existent S3 storage (ID 1). This makes new backup configs safer and more explicit for self-hosted setups.


238-258: loadPgbackrestRepos() correctly mirrors repo state into component properties

The method cleanly:

  • Derives disableLocalBackup / saveS3 booleans from existing local/S3 repo enable flags.
  • Hydrates retention settings with sane fallbacks ('count', 2, 7) that match the defaults defined on the Livewire properties.
  • Captures s3RepoStorageId from the S3 repo for later sync.

This gives the component a faithful view of current pgBackRest repo config when editing, which is exactly what you want on a real server, not some ephemeral serverless toy.


371-371: Using imported Exception in delete() catch resolves namespacing/static‑analysis noise

Catching Exception via the imported class (instead of \Exception) keeps namespacing consistent with the rest of the file and fixes the previous PHPMD “missing import” warning. The error handling behavior itself remains unchanged, which is good.

Copy link
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: 2

♻️ Duplicate comments (1)
app/Jobs/PgBackrestRestoreJob.php (1)

91-118: Duplicate failure notifications still present.

This catch block sends DatabaseRestoreFailed notification (lines 102-108), then rethrows the exception (line 117). When the job fails, failed() (lines 446-453) sends the same notification again. The team gets double-tapped with failure emails - once is enough, even for the T-800.

Let failed() be the single source of truth for failure notifications:

         } catch (Throwable $e) {
             Log::error('PgBackRest restore failed', [
                 'database' => $this->database->uuid,
                 'restore_id' => $this->restore->uuid,
                 'error' => $e->getMessage(),
                 'trace' => $e->getTraceAsString(),
             ]);

             $this->restore->updateStatus('failed', 'Restore failed: '.$e->getMessage());

-            $team = $this->database->team();
-            if ($team) {
-                $team->notify(new DatabaseRestoreFailed(
-                    $this->database,
-                    $e->getMessage(),
-                    $this->restore->target_label
-                ));
-            }
-
             try {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b45d87 and 2ce82dd.

📒 Files selected for processing (1)
  • app/Jobs/PgBackrestRestoreJob.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Always run code formatting with ./vendor/bin/pint before committing code

**/*.php: Follow PSR-12 coding standards. Use Laravel Pint for automatic formatting. Write descriptive variable and method names. Keep methods small and focused. Document complex logic with clear comments
Use PHP 8.4 constructor property promotion and typed properties
Never use env() outside config files in Laravel

Files:

  • app/Jobs/PgBackrestRestoreJob.php
app/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/**/*.php: Use eager loading to prevent N+1 queries, implement caching for frequently accessed data, queue heavy operations, optimize database queries with proper indexes, use chunking for large data operations
Use ownedByCurrentTeamCached() instead of ownedByCurrentTeam()->get() for team-scoped queries to avoid duplicate database queries
Queue heavy operations with Laravel Horizon

Files:

  • app/Jobs/PgBackrestRestoreJob.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

Use named routes with route() function instead of hardcoded URLs

Files:

  • app/Jobs/PgBackrestRestoreJob.php
🧠 Learnings (6)
📚 Learning: 2025-12-10T01:53:52.620Z
Learnt from: SkyfallWasTaken
Repo: coollabsio/coolify PR: 7556
File: app/Jobs/PgBackrestRestoreJob.php:39-118
Timestamp: 2025-12-10T01:53:52.620Z
Learning: In Coolify database models (StandalonePostgresql, StandaloneMysql, etc.), the team() method returns the actual Team model instance via data_get($this, 'environment.project.team'), NOT a BelongsTo relation. Therefore, $database->team() can be used directly for operations like ->notify() without needing to access a property.

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Always use team() method to return relationship instance, not direct property access. App\Models\Application::team must return a relationship instance

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/**/*.php : Use `ownedByCurrentTeamCached()` instead of `ownedByCurrentTeam()->get()` for team-scoped queries to avoid duplicate database queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Model.php,database/migrations/**/*.php} : Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in `.ai/patterns/database-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php} : Use team-based access control patterns and gate/policy authorization as documented in `.ai/patterns/security-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
🪛 PHPMD (2.15.0)
app/Jobs/PgBackrestRestoreJob.php

23-455: The class PgBackrestRestoreJob has an overall complexity of 62 which is very high. The configured complexity threshold is 50. (undefined)

(ExcessiveClassComplexity)


42-42: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'handle'. (undefined)

(StaticAccess)


51-51: Avoid using static access to class '\App\Actions\Database\StopDatabase' in method 'handle'. (undefined)

(StaticAccess)


67-67: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


92-97: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'handle'. (undefined)

(StaticAccess)


112-112: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


121-184: The method preflight() has a Cyclomatic Complexity of 15. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


121-184: The method preflight() has an NPath complexity of 2400. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


136-136: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


162-162: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


166-166: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


171-171: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


176-181: The method preflight uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


177-177: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


203-203: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


208-208: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


232-232: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


385-385: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


390-395: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


423-423: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'getInstallAndRunCommand'. (undefined)

(StaticAccess)

🔇 Additional comments (5)
app/Jobs/PgBackrestRestoreJob.php (5)

23-38: LGTM! Well-structured job with appropriate security considerations.

The job implements ShouldBeEncrypted for handling sensitive S3 credentials, sets a generous 2-hour timeout for large database restores, and uses the high queue for priority processing. Self-hosting your own database backups like a true warrior - much better than trusting some serverless function that disappears faster than tacos at a developer meetup.


121-184: Thorough preflight validation - like checking your weapons before a mission.

The complexity here is justified - you're validating S3 connections, volume existence, stanza health, and backup availability before attempting a restore. This prevents wasting time on a restore that's doomed to fail. The static analysis complaints about complexity are false positives for this type of validation orchestration.

Good defensive coding ensuring the infrastructure is ready before nuking PGDATA. Unlike serverless functions that just... hope things work out.


240-275: Solid backup verification - previous concern addressed.

The backup now properly verifies the data was moved successfully before proceeding. Using mv instead of cp is a good choice for atomic operations - if it fails, you know immediately. Like a proper terminator, no half-measures.


434-454: Proper failure handling - the right place for notifications.

The failed() method correctly handles permanent job failure with structured logging to scheduled-errors channel and team notifications. Once the duplicate notification is removed from handle(), this will be the single source of truth. Clean and efficient - I'll be back... but hopefully the restore won't fail.


203-206: Shell injection fix properly applied - Hasta la vista, security vulnerabilities!

Using escapeshellarg() for S3 credentials is the correct approach. This handles secrets containing $, backticks, or command substitution syntax safely. Self-hosted backup infrastructure that's actually secure - unlike those gluten-free tacos that secretly contain wheat.

Copy link
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: 2

♻️ Duplicate comments (4)
app/Jobs/PgBackrestRestoreJob.php (2)

91-118: Duplicate failure notifications still present.

When an exception is thrown and re-thrown at line 117, the failed() method (lines 438-458) will also execute and send another DatabaseRestoreFailed notification. The team gets spammed twice for one failure - like getting two termination notices when you only died once.

Remove the notification from the catch block and let failed() be the single source of truth:

         } catch (Throwable $e) {
             Log::error('PgBackRest restore failed', [
                 'database' => $this->database->uuid,
                 'restore_id' => $this->restore->uuid,
                 'error' => $e->getMessage(),
                 'trace' => $e->getTraceAsString(),
             ]);

             $this->restore->updateStatus('failed', 'Restore failed: '.$e->getMessage());

-            $team = $this->database->team();
-            if ($team) {
-                $team->notify(new DatabaseRestoreFailed(
-                    $this->database,
-                    $e->getMessage(),
-                    $this->restore->target_label
-                ));
-            }
-
             try {

401-403: Hardcoded UID 999:999 still conflicts with pgBackRest ownership restoration.

Line 403 hardcodes chown -R 999:999 which:

  • Breaks on Alpine images where postgres UID is typically 70
  • Overrides pgBackRest's name-based ownership restoration
  • Will cause permission issues on custom images

Either remove the hardcoded chown and trust pgBackRest's ownership restoration, or detect the actual postgres UID dynamically:

-        $fullRestoreScript = $this->getInstallAndRunCommand($restoreCmd).' && chown -R 999:999 '.PgBackrestService::PGDATA_PATH;
+        $fullRestoreScript = $this->getInstallAndRunCommand($restoreCmd);

If you must fix ownership, detect it from the container:

$fullRestoreScript = $this->getInstallAndRunCommand($restoreCmd).' && chown -R $(id -u postgres):$(id -g postgres) '.PgBackrestService::PGDATA_PATH;
app/Jobs/DatabaseBackupJob.php (2)

726-729: Import RuntimeException for consistency.

Line 728 uses \RuntimeException with a backslash prefix. For PSR-12 consistency with other imports in this file, add the import statement.

Add to imports:

 use Throwable;
 use Visus\Cuid2\Cuid2;
+use RuntimeException;

Then use without backslash:

-                throw new \RuntimeException($errorMessage, $exitCode);
+                throw new RuntimeException($errorMessage, $exitCode);

766-770: Error message logic can be simplified.

This conditional effectively prefers backup_output when present. The second condition is redundant.

         } catch (Throwable $e) {
-            $errorMsg = $this->backup_output ?? $e->getMessage();
-            if ($this->backup_output && $e->getMessage() !== $this->backup_output) {
-                $errorMsg = $this->backup_output;
-            }
+            $errorMsg = $this->backup_output ?: $e->getMessage();

Like optimizing a T-800's decision tree - fewer conditions, same outcome.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2ce82dd and a6a6ce2.

📒 Files selected for processing (4)
  • app/Jobs/DatabaseBackupJob.php (5 hunks)
  • app/Jobs/PgBackrestRestoreJob.php (1 hunks)
  • app/Services/Backup/PgBackrestService.php (1 hunks)
  • bootstrap/helpers/shared.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Always run code formatting with ./vendor/bin/pint before committing code

**/*.php: Follow PSR-12 coding standards. Use Laravel Pint for automatic formatting. Write descriptive variable and method names. Keep methods small and focused. Document complex logic with clear comments
Use PHP 8.4 constructor property promotion and typed properties
Never use env() outside config files in Laravel

Files:

  • app/Jobs/PgBackrestRestoreJob.php
  • bootstrap/helpers/shared.php
  • app/Services/Backup/PgBackrestService.php
  • app/Jobs/DatabaseBackupJob.php
app/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/**/*.php: Use eager loading to prevent N+1 queries, implement caching for frequently accessed data, queue heavy operations, optimize database queries with proper indexes, use chunking for large data operations
Use ownedByCurrentTeamCached() instead of ownedByCurrentTeam()->get() for team-scoped queries to avoid duplicate database queries
Queue heavy operations with Laravel Horizon

Files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Services/Backup/PgBackrestService.php
  • app/Jobs/DatabaseBackupJob.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

Use named routes with route() function instead of hardcoded URLs

Files:

  • app/Jobs/PgBackrestRestoreJob.php
  • bootstrap/helpers/shared.php
  • app/Services/Backup/PgBackrestService.php
  • app/Jobs/DatabaseBackupJob.php
🧠 Learnings (6)
📚 Learning: 2025-12-10T01:53:52.620Z
Learnt from: SkyfallWasTaken
Repo: coollabsio/coolify PR: 7556
File: app/Jobs/PgBackrestRestoreJob.php:39-118
Timestamp: 2025-12-10T01:53:52.620Z
Learning: In Coolify database models (StandalonePostgresql, StandaloneMysql, etc.), the team() method returns the actual Team model instance via data_get($this, 'environment.project.team'), NOT a BelongsTo relation. Therefore, $database->team() can be used directly for operations like ->notify() without needing to access a property.

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Always use team() method to return relationship instance, not direct property access. App\Models\Application::team must return a relationship instance

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/**/*.php : Use `ownedByCurrentTeamCached()` instead of `ownedByCurrentTeam()->get()` for team-scoped queries to avoid duplicate database queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Model.php,database/migrations/**/*.php} : Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in `.ai/patterns/database-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php} : Use team-based access control patterns and gate/policy authorization as documented in `.ai/patterns/security-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
🧬 Code graph analysis (2)
bootstrap/helpers/shared.php (1)
bootstrap/helpers/remoteProcess.php (1)
  • instant_remote_process (121-155)
app/Jobs/DatabaseBackupJob.php (5)
app/Services/Backup/PgBackrestService.php (3)
  • PgBackrestService (9-409)
  • getStanzaName (23-26)
  • generateConfig (28-75)
app/Models/ScheduledDatabaseBackupExecution.php (2)
  • isPgBackrest (32-35)
  • ScheduledDatabaseBackupExecution (8-48)
app/Models/ScheduledDatabaseBackup.php (4)
  • isPgBackrest (25-28)
  • database (92-95)
  • server (130-146)
  • enabledPgbackrestRepos (40-43)
app/Models/StandalonePostgresql.php (2)
  • StandalonePostgresql (11-424)
  • type (242-245)
bootstrap/helpers/remoteProcess.php (1)
  • instant_remote_process (121-155)
🪛 PHPMD (2.15.0)
app/Jobs/PgBackrestRestoreJob.php

23-459: The class PgBackrestRestoreJob has an overall complexity of 62 which is very high. The configured complexity threshold is 50. (undefined)

(ExcessiveClassComplexity)


42-42: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'handle'. (undefined)

(StaticAccess)


51-51: Avoid using static access to class '\App\Actions\Database\StopDatabase' in method 'handle'. (undefined)

(StaticAccess)


67-67: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


92-97: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'handle'. (undefined)

(StaticAccess)


112-112: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


121-184: The method preflight() has a Cyclomatic Complexity of 15. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


121-184: The method preflight() has an NPath complexity of 2400. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


136-136: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


162-162: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


166-166: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


171-171: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


176-181: The method preflight uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


177-177: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


203-203: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


208-208: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


232-232: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


389-389: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


394-399: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


427-427: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'getInstallAndRunCommand'. (undefined)

(StaticAccess)

app/Services/Backup/PgBackrestService.php

9-409: The class PgBackrestService has 16 public methods. Consider refactoring PgBackrestService to keep number of public methods under 10. (undefined)

(TooManyPublicMethods)


9-409: The class PgBackrestService has an overall complexity of 65 which is very high. The configured complexity threshold is 50. (undefined)

(ExcessiveClassComplexity)


83-83: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


92-92: Missing class import via use statement (line '92', column '27'). (undefined)

(MissingImport)


103-109: The method generateRepoConfig uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


119-119: Avoid variables with short names like $k. Configured minimum length is 3. (undefined)

(ShortVariable)


119-119: Avoid variables with short names like $v. Configured minimum length is 3. (undefined)

(ShortVariable)


170-170: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


186-186: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


200-200: Missing class import via use statement (line '200', column '27'). (undefined)

(MissingImport)


235-235: Missing class import via use statement (line '235', column '23'). (undefined)

(MissingImport)


238-238: Missing class import via use statement (line '238', column '23'). (undefined)

(MissingImport)


298-298: The method buildInfoCommand has a boolean flag argument $json, which is a certain sign of a Single Responsibility Principle violation. (undefined)

(BooleanArgumentFlag)

app/Jobs/DatabaseBackupJob.php

474-474: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'backup_standalone_mongodb'. (undefined)

(StaticAccess)


684-789: The method run_pgbackrest_backup() has a Cyclomatic Complexity of 15. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


684-789: The method run_pgbackrest_backup() has an NPath complexity of 776. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


684-789: The method run_pgbackrest_backup() has 106 lines of code. Current threshold is set to 100. Avoid really long methods. (undefined)

(ExcessiveMethodLength)


684-789: The method run_pgbackrest_backup is not named in camelCase. (undefined)

(CamelCaseMethodName)


686-686: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


692-700: Avoid using static access to class '\App\Models\ScheduledDatabaseBackupExecution' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


705-705: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


706-706: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


708-708: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


709-709: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


722-724: The method run_pgbackrest_backup uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


728-728: Missing class import via use statement (line '728', column '27'). (undefined)

(MissingImport)


731-731: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


742-742: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


743-743: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


746-746: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


747-747: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


761-761: Avoid using static access to class '\Carbon\Carbon' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


775-775: Avoid using static access to class '\Carbon\Carbon' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


787-787: Avoid using static access to class '\App\Events\BackupCreated' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


791-818: The method run_pgbackrest_expire is not named in camelCase. (undefined)

(CamelCaseMethodName)


795-795: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


796-796: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


800-800: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


813-816: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


820-835: The method update_pgbackrest_config is not named in camelCase. (undefined)

(CamelCaseMethodName)


822-822: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'update_pgbackrest_config'. (undefined)

(StaticAccess)


825-825: Missing class import via use statement (line '825', column '23'). (undefined)

(MissingImport)

🔇 Additional comments (14)
app/Jobs/PgBackrestRestoreJob.php (5)

23-38: LGTM! Clean job setup with proper security.

The job implements ShouldBeEncrypted for sensitive data protection and uses constructor property promotion correctly. The 2-hour timeout is reasonable for large database restores. Running on the 'high' queue ensures restores don't wait behind less critical tasks.

Like a T-800 protecting John Connor, this job protects your data with encryption. Self-hosted servers deserve nothing less.


121-184: Preflight validation is thorough and well-structured.

This method validates everything before starting the restore: backup configuration, S3 connectivity, volumes, and backup existence. The complexity (flagged by PHPMD) is justified - you're checking multiple failure modes before committing to a destructive operation.

Like checking your weapons before a mission - better to know your plasma rifle is jammed before engaging the enemy. Good self-hosted backup hygiene!


203-206: Shell escaping done correctly with escapeshellarg().

S3 credentials are properly escaped using escapeshellarg() instead of the vulnerable addslashes(). This prevents command injection even if an S3 secret contains shell metacharacters like $(rm -rf /).

Self-hosted servers stay safe when you escape your tacos... I mean, arguments. Unlike serverless where the cloud provider just bills you extra when things explode.


240-276: PGDATA backup with verification - solid improvement.

The backup now:

  1. Checks if PGDATA is empty before copying
  2. Uses throwError=true on the copy command (line 264)
  3. Verifies the backup volume isn't empty before proceeding

This addresses the previous concern about silent failures. Your data parachute is now actually attached. Good work!


438-458: failed() handler properly logs and notifies.

This is the correct single location for permanent failure handling. The team() method usage is correct - per the codebase pattern, it returns the actual Team model instance, not a BelongsTo relation. Based on learnings from this PR.

app/Jobs/DatabaseBackupJob.php (3)

117-122: Clean early-exit pattern for pgBackRest backups.

The conditional properly routes pgBackRest backups to the dedicated method and returns early. This prevents accidentally running both pgBackRest and native backup flows. Simple and effective - like a T-800's target acquisition system.


708-713: Shell escaping properly delegated to service helper.

The S3 environment variables are now built using PgBackrestService::buildDockerEnvArgs() which internally uses escapeshellarg(). The backup command is also properly escaped with escapeshellarg(). No more shell injection vulnerabilities here - your self-hosted server is safe from $(rm -rf /) attacks hiding in S3 credentials.


791-818: Expire handling is resilient and correctly escaped.

The method:

  • Uses buildDockerEnvArgs() for proper shell escaping
  • Catches exceptions silently - correct choice since retention cleanup shouldn't fail a successful backup
  • Logs warnings for debugging

The snake_case naming matches other private methods in this file (backup_standalone_postgresql, etc.). Consistency with existing code > PHPMD preferences.

app/Services/Backup/PgBackrestService.php (6)

9-75: Well-structured configuration generation.

Constants provide clear defaults, and generateConfig() properly:

  • Falls back to defaults with null coalescing
  • Returns null for invalid configurations (no enabled backups, no repos)
  • Builds a clean INI-style config

Like a T-800's mission parameters - clear, configurable, and fails safely when misconfigured.


77-120: Repository config generation with input validation.

Good defensive coding:

  • Validates S3 bucket and endpoint with validateShellSafePath() before use
  • Returns empty string for invalid S3 configs
  • Clean separation between S3 and local (posix) repo types

Self-hosted infrastructure deserves this level of care!


153-161: Command wrapper assumes pre-escaped input.

The $command parameter is embedded directly in su postgres -c "{$command}". This works if callers always pass pre-escaped commands (which buildBackupCommand, buildRestoreCommand, etc. do), but could be dangerous if a future caller passes unescaped input.

Consider adding a comment documenting this contract, or validate the command format:

+    /**
+     * Wraps a pgBackRest command with installation and setup.
+     * 
+     * @param string $command Pre-escaped pgBackRest command (from buildBackupCommand, buildRestoreCommand, etc.)
+     */
     public static function buildInstallAndSetupCommand(string $command): string

195-206: Centralized Docker env arg builder with proper escaping.

This is excellent:

  • Validates env var names against a strict regex
  • Uses escapeshellarg() for the entire key=value pair
  • Throws on invalid input

Centralizing this logic prevents the shell injection bugs that plagued the previous implementation. Like having one armory for all T-800 units instead of each carrying their own untested weapons.


208-337: Command builders properly escape all dynamic arguments.

All command builders now:

  • Use escapeshellarg() for string parameters ($stanza, $type, $logLevel, $label, $targetTime)
  • Cast $repoNumber to (int) before concatenation
  • Handle optional parameters safely

The wrapWithLockWait() helper elegantly handles pgBackRest's lock contention (exit code 50) with configurable retry logic.

For extra hardening, you could whitelist $type and $logLevel values as suggested in past reviews, but the escaping is already sufficient to prevent injection.


339-408: Parsing helpers are defensive and robust.

All info parsing methods:

  • Handle empty/malformed input gracefully
  • Use null coalescing and isset checks
  • Return safe defaults (null, 0, false) on failure

getLatestBackup() using end() on the sorted backup array is the correct approach for pgBackRest's chronologically ordered output. No JSON parsing vulnerabilities here - unlike serverless functions that trust everything they receive.

… PGData

Rename, not copy, old PGDATA

Bit more reliability when working dir isn't called coolify!
Copy link
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

♻️ Duplicate comments (3)
bootstrap/helpers/shared.php (1)

3312-3358: Guard against null $output before preg_match and optionally log failures in dev

If instant_remote_process() returns null or empty output (SSH hiccup, no matching mount), preg_match() will currently throw a TypeError which you catch and silently ignore. Behavior falls back correctly, but you’re using exceptions for a normal “no mount detected” path and giving yourself zero clues in dev.

Consider a cheap guard + optional dev logging:

-        if ($server) {
-            $output = instant_remote_process(
+        if ($server) {
+            $output = instant_remote_process(
                 ["cat /proc/self/mountinfo | grep '/data/coolify ' | head -1"],
                 $server,
                 false,
                 false,
                 10,
                 disableMultiplexing: true
             );
-            if (preg_match('#(/var/lib/docker/volumes/[^/]+_dev_coolify_data/_data)\s+/data/coolify#', $output, $matches)) {
+            if (! is_string($output) || $output === '') {
+                return $fallback;
+            }
+
+            if (preg_match('#(/var/lib/docker/volumes/[^/]+_dev_coolify_data/_data)\s+/data/coolify#', $output, $matches)) {
                 return $matches[1];
             }

And inside the catch, you could, in dev only, emit a debug log with loggy() so future-you doesn’t have to SSH around in the dark when auto-detection misfires. Based on learnings / prior review.

app/Jobs/DatabaseBackupJob.php (1)

684-789: Avoid triple‑firing BackupCreated for a single pgBackRest backup

For pgBackRest runs you now dispatch BackupCreated:

  • Once near the top of handle() (Line 111),
  • Again in handle()’s finally (Lines 449–451),
  • And again in run_pgbackrest_backup()’s finally (Lines 787–788).

That’s three events for one backup, which can spam any listeners that do more than just a light UI refresh.

Drop the inner dispatch in run_pgbackrest_backup() and let the job‑level finally remain the single source of truth:

-        } finally {
-            BackupCreated::dispatch($this->team->id);
-        }
+        } finally {
+            // Job-level finally handles BackupCreated dispatch
+        }

The backups will still be announced, just without hammering subscribers like a misconfigured cron.

app/Jobs/PgBackrestRestoreJob.php (1)

91-118: Avoid duplicate failure notifications for a single restore

When a restore fails, you:

  • In handle() catch (Lines 91–108): log, updateStatus('failed', ...), and send DatabaseRestoreFailed.
  • Rethrow, which triggers failed() (Lines 438–456), where you log again, updateStatus('failed', ...) again, and send another DatabaseRestoreFailed.

That means one busted restore -> two emails/alerts. The humans will feel like they’re being DDoS’d by their own server.

Let failed() be the single place that sends the failure notification and does the final status update; in handle() just log, maybe append to the restore log, and rethrow:

         } catch (Throwable $e) {
             Log::error('PgBackRest restore failed', [
                 'database' => $this->database->uuid,
                 'restore_id' => $this->restore->uuid,
                 'error' => $e->getMessage(),
                 'trace' => $e->getTraceAsString(),
             ]);
 
-            $this->restore->updateStatus('failed', 'Restore failed: '.$e->getMessage());
-
-            $team = $this->database->team();
-            if ($team) {
-                $team->notify(new DatabaseRestoreFailed(
-                    $this->database,
-                    $e->getMessage(),
-                    $this->restore->target_label
-                ));
-            }
-
             try {
                 $this->restore->appendLog('Attempting to restart PostgreSQL after failed restore.');

failed() will still do the loud screaming for you.

Also applies to: 438-458

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a6a6ce2 and a9de624.

📒 Files selected for processing (4)
  • app/Jobs/DatabaseBackupJob.php (5 hunks)
  • app/Jobs/PgBackrestRestoreJob.php (1 hunks)
  • app/Services/Backup/PgBackrestService.php (1 hunks)
  • bootstrap/helpers/shared.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Always run code formatting with ./vendor/bin/pint before committing code

**/*.php: Follow PSR-12 coding standards. Use Laravel Pint for automatic formatting. Write descriptive variable and method names. Keep methods small and focused. Document complex logic with clear comments
Use PHP 8.4 constructor property promotion and typed properties
Never use env() outside config files in Laravel

Files:

  • app/Jobs/PgBackrestRestoreJob.php
  • bootstrap/helpers/shared.php
  • app/Services/Backup/PgBackrestService.php
  • app/Jobs/DatabaseBackupJob.php
app/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/**/*.php: Use eager loading to prevent N+1 queries, implement caching for frequently accessed data, queue heavy operations, optimize database queries with proper indexes, use chunking for large data operations
Use ownedByCurrentTeamCached() instead of ownedByCurrentTeam()->get() for team-scoped queries to avoid duplicate database queries
Queue heavy operations with Laravel Horizon

Files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Services/Backup/PgBackrestService.php
  • app/Jobs/DatabaseBackupJob.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

Use named routes with route() function instead of hardcoded URLs

Files:

  • app/Jobs/PgBackrestRestoreJob.php
  • bootstrap/helpers/shared.php
  • app/Services/Backup/PgBackrestService.php
  • app/Jobs/DatabaseBackupJob.php
🧠 Learnings (7)
📚 Learning: 2025-12-10T01:53:52.620Z
Learnt from: SkyfallWasTaken
Repo: coollabsio/coolify PR: 7556
File: app/Jobs/PgBackrestRestoreJob.php:39-118
Timestamp: 2025-12-10T01:53:52.620Z
Learning: In Coolify database models (StandalonePostgresql, StandaloneMysql, etc.), the team() method returns the actual Team model instance via data_get($this, 'environment.project.team'), NOT a BelongsTo relation. Therefore, $database->team() can be used directly for operations like ->notify() without needing to access a property.

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Always use team() method to return relationship instance, not direct property access. App\Models\Application::team must return a relationship instance

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/**/*.php : Use `ownedByCurrentTeamCached()` instead of `ownedByCurrentTeam()->get()` for team-scoped queries to avoid duplicate database queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Model.php,database/migrations/**/*.php} : Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in `.ai/patterns/database-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php} : Use team-based access control patterns and gate/policy authorization as documented in `.ai/patterns/security-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-11-11T15:54:46.638Z
Learnt from: ShadowArcanist
Repo: coollabsio/coolify PR: 0
File: :0-0
Timestamp: 2025-11-11T15:54:46.638Z
Learning: For Coolify installer scripts: When reviewing IP address detection logic, check if the code already iterates over private IPs elsewhere in the script to avoid redundant hostname or IP detection calls.

Applied to files:

  • bootstrap/helpers/shared.php
🧬 Code graph analysis (2)
app/Jobs/PgBackrestRestoreJob.php (7)
app/Models/DatabaseRestore.php (4)
  • DatabaseRestore (8-80)
  • database (20-23)
  • execution (25-28)
  • appendLog (55-63)
app/Models/ScheduledDatabaseBackupExecution.php (1)
  • ScheduledDatabaseBackupExecution (8-48)
app/Models/StandalonePostgresql.php (2)
  • StandalonePostgresql (11-424)
  • workdir (75-78)
app/Notifications/Database/DatabaseRestoreFailed.php (2)
  • DatabaseRestoreFailed (12-129)
  • __construct (20-30)
app/Notifications/Database/DatabaseRestoreSuccess.php (2)
  • DatabaseRestoreSuccess (12-131)
  • __construct (20-30)
app/Services/Backup/PgBackrestService.php (3)
  • PgBackrestService (9-409)
  • buildRestoreCommand (264-296)
  • buildInstallAndSetupCommand (153-161)
bootstrap/helpers/shared.php (1)
  • convertPathToDockerHost (3319-3331)
bootstrap/helpers/shared.php (1)
bootstrap/helpers/remoteProcess.php (1)
  • instant_remote_process (121-155)
🪛 PHPMD (2.15.0)
app/Jobs/PgBackrestRestoreJob.php

23-459: The class PgBackrestRestoreJob has an overall complexity of 62 which is very high. The configured complexity threshold is 50. (undefined)

(ExcessiveClassComplexity)


42-42: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'handle'. (undefined)

(StaticAccess)


51-51: Avoid using static access to class '\App\Actions\Database\StopDatabase' in method 'handle'. (undefined)

(StaticAccess)


67-67: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


92-97: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'handle'. (undefined)

(StaticAccess)


112-112: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


121-184: The method preflight() has a Cyclomatic Complexity of 15. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


121-184: The method preflight() has an NPath complexity of 2400. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


136-136: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


162-162: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


166-166: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


171-171: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


176-181: The method preflight uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


177-177: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


203-203: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


208-208: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


232-232: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


389-389: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


394-399: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


427-427: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'getInstallAndRunCommand'. (undefined)

(StaticAccess)

app/Services/Backup/PgBackrestService.php

9-409: The class PgBackrestService has 16 public methods. Consider refactoring PgBackrestService to keep number of public methods under 10. (undefined)

(TooManyPublicMethods)


9-409: The class PgBackrestService has an overall complexity of 65 which is very high. The configured complexity threshold is 50. (undefined)

(ExcessiveClassComplexity)


83-83: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


92-92: Missing class import via use statement (line '92', column '27'). (undefined)

(MissingImport)


103-109: The method generateRepoConfig uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


119-119: Avoid variables with short names like $k. Configured minimum length is 3. (undefined)

(ShortVariable)


119-119: Avoid variables with short names like $v. Configured minimum length is 3. (undefined)

(ShortVariable)


170-170: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


186-186: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


200-200: Missing class import via use statement (line '200', column '27'). (undefined)

(MissingImport)


235-235: Missing class import via use statement (line '235', column '23'). (undefined)

(MissingImport)


238-238: Missing class import via use statement (line '238', column '23'). (undefined)

(MissingImport)


298-298: The method buildInfoCommand has a boolean flag argument $json, which is a certain sign of a Single Responsibility Principle violation. (undefined)

(BooleanArgumentFlag)

app/Jobs/DatabaseBackupJob.php

474-474: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'backup_standalone_mongodb'. (undefined)

(StaticAccess)


684-789: The method run_pgbackrest_backup() has a Cyclomatic Complexity of 15. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


684-789: The method run_pgbackrest_backup() has an NPath complexity of 776. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


684-789: The method run_pgbackrest_backup() has 106 lines of code. Current threshold is set to 100. Avoid really long methods. (undefined)

(ExcessiveMethodLength)


684-789: The method run_pgbackrest_backup is not named in camelCase. (undefined)

(CamelCaseMethodName)


686-686: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


692-700: Avoid using static access to class '\App\Models\ScheduledDatabaseBackupExecution' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


705-705: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


706-706: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


708-708: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


709-709: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


722-724: The method run_pgbackrest_backup uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


728-728: Missing class import via use statement (line '728', column '27'). (undefined)

(MissingImport)


731-731: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


742-742: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


743-743: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


746-746: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


747-747: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


761-761: Avoid using static access to class '\Carbon\Carbon' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


775-775: Avoid using static access to class '\Carbon\Carbon' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


787-787: Avoid using static access to class '\App\Events\BackupCreated' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


791-818: The method run_pgbackrest_expire is not named in camelCase. (undefined)

(CamelCaseMethodName)


795-795: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


796-796: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


800-800: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


813-816: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


820-835: The method update_pgbackrest_config is not named in camelCase. (undefined)

(CamelCaseMethodName)


822-822: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'update_pgbackrest_config'. (undefined)

(StaticAccess)


825-825: Missing class import via use statement (line '825', column '23'). (undefined)

(MissingImport)

🔇 Additional comments (3)
app/Jobs/DatabaseBackupJob.php (1)

705-721: Fix broken shell quoting in pgBackRest backup command

The constructed $backupFullCmd interpolates $escapedBackupCmd (which is single-quoted by escapeshellarg()) inside another single-quoted sh -c '…' script. This breaks the quoting structure.

Wrap the entire script in a single escapeshellarg() call before passing it to sh -c:

-        $fixPermsCmd = 'chown -R postgres:postgres /var/lib/pgbackrest /tmp/pgbackrest /var/log/pgbackrest 2>/dev/null || true';
-        $escapedBackupCmd = escapeshellarg($backupCmdWithWait);
-        $containerName = escapeshellarg($this->container_name);
-        $backupFullCmd = "docker exec{$dockerEnvArgs} {$containerName} sh -c '{$fixPermsCmd}; su postgres -c {$escapedBackupCmd} 2>&1; echo \"EXIT_CODE:\$?\"'";
+        $fixPermsCmd = 'chown -R postgres:postgres /var/lib/pgbackrest /tmp/pgbackrest /var/log/pgbackrest 2>/dev/null || true';
+        $escapedBackupCmd = escapeshellarg($backupCmdWithWait);
+        $containerName = escapeshellarg($this->container_name);
+        $script = "{$fixPermsCmd}; su postgres -c {$escapedBackupCmd} 2>&1; echo \"EXIT_CODE:\$?\"";
+        $escapedScript = escapeshellarg($script);
+        $backupFullCmd = "docker exec{$dockerEnvArgs} {$containerName} sh -c {$escapedScript}";

This ensures the outer shell properly receives the escaped script, and the inner shell in the container interprets it as a single argument.

app/Jobs/PgBackrestRestoreJob.php (2)

361-418: Fix restore sidecar quoting and drop hardcoded chown -R 999:999

Two issues here:

  1. Quoting – same problem as in runInfoSidecar(): $fullRestoreScript already contains single quotes from the pgBackRest command, so embedding it raw into a single-quoted sh -c '…' argument will break as soon as a stanza or label injects a '. Use escaped shell argument:
-        $fullRestoreScript = $this->getInstallAndRunCommand($restoreCmd).' && chown -R 999:999 '.PgBackrestService::PGDATA_PATH;
-
-        $cmd = sprintf(
-            'docker run --rm --name %s --network %s %s %s %s sh -c \'%s\' 2>&1',
-            $sidecarName,
-            $network,
-            implode(' ', $envPieces),
-            implode(' ', $mounts),
-            $this->getSidecarImage(),
-            $fullRestoreScript
-        );
+        $fullRestoreScript = $this->getInstallAndRunCommand($restoreCmd);
+        $escapedScript = escapeshellarg($fullRestoreScript);
+
+        $cmd = sprintf(
+            'docker run --rm --name %s --network %s %s %s %s sh -c %s 2>&1',
+            $sidecarName,
+            $network,
+            implode(' ', $envPieces),
+            implode(' ', $mounts),
+            $this->getSidecarImage(),
+            $escapedScript
+        );
  1. Hardcoded chown -R 999:999 – forcing UID:GID 999:999 after restore conflicts with pgBackRest's own ownership restoration and breaks on images where postgres isn't UID 999. pgBackRest records ownership by user/group name and re-applies that mapping when restoring as root. This hardcoded chown mis-assigns permissions on Alpine and custom images.

Either drop the chown entirely and let pgBackRest's manifest drive ownership, or detect the actual UID/GID of the postgres user inside the sidecar at runtime (e.g. via /etc/passwd).


186-238: Fix sidecar sh -c quoting to avoid breaking on pgBackRest command quotes

runInfoSidecar() currently does:

$cmd = sprintf(
    'docker run ... sh -c \'%s\' 2>&1',
    $this->getInstallAndRunCommand($infoCmd)
);

getInstallAndRunCommand() builds a script that already contains single quotes and double quotes. Dropping that raw into a single‑quoted sh -c '…' argument will break as soon as a ' appears in the script (e.g. stanza UUID).

Use escapeshellarg() on the whole script instead, so the outer shell gets a properly quoted argument:

-        $infoCmd = PgBackrestService::buildInfoCommand($stanza, true);
-        $sidecarName = 'pgbackrest-info-'.$this->database->uuid.'-'.time();
-
-        $cmd = sprintf(
-            'docker run --rm --name %s --network %s %s %s %s sh -c \'%s\' 2>&1',
-            $sidecarName,
-            $network,
-            implode(' ', $envPieces),
-            implode(' ', $mounts),
-            $this->getSidecarImage(),
-            $this->getInstallAndRunCommand($infoCmd)
-        );
+        $infoCmd = PgBackrestService::buildInfoCommand($stanza, true);
+        $sidecarName = 'pgbackrest-info-'.$this->database->uuid.'-'.time();
+        $script = $this->getInstallAndRunCommand($infoCmd);
+        $escapedScript = escapeshellarg($script);
+
+        $cmd = sprintf(
+            'docker run --rm --name %s --network %s %s %s %s sh -c %s 2>&1',
+            $sidecarName,
+            $network,
+            implode(' ', $envPieces),
+            implode(' ', $mounts),
+            $this->getSidecarImage(),
+            $escapedScript
+        );

Quote once at the edge, don't nest raw shell scripts.

Copy link
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: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Jobs/DatabaseBackupJob.php (1)

493-508: Sanitize MongoDB collection names used in --excludeCollection (command injection risk)

You correctly validate and escape the database name (Lines 493‑496), but the collection names in $collectionsToExclude are still concatenated raw into the shell command (Lines 505‑508). A malicious or just weird collection name could break out of the mongodump command and inject arbitrary shell.

Validate and escape each collection name before building the --excludeCollection list, and quote the URI consistently:

-                if (str($databaseWithCollections)->contains(':')) {
-                    $databaseName = str($databaseWithCollections)->before(':');
-                    $collectionsToExclude = str($databaseWithCollections)->after(':')->explode(',');
-                } else {
-                    $databaseName = $databaseWithCollections;
-                    $collectionsToExclude = collect();
-                }
+                if (str($databaseWithCollections)->contains(':')) {
+                    $databaseName = str($databaseWithCollections)->before(':');
+                    $collectionsToExclude = str($databaseWithCollections)
+                        ->after(':')
+                        ->explode(',')
+                        ->map(function ($collection) {
+                            $collection = trim($collection);
+                            validateShellSafePath($collection, 'collection name');
+
+                            return escapeshellarg($collection);
+                        });
+                } else {
+                    $databaseName = $databaseWithCollections;
+                    $collectionsToExclude = collect();
+                }
...
-                    if (str($this->database->image)->startsWith('mongo:4')) {
-                        $commands[] = "docker exec $this->container_name mongodump --uri=$url --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
-                    } else {
-                        $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
-                    }
+                    if (str($this->database->image)->startsWith('mongo:4')) {
+                        $commands[] = "docker exec $this->container_name mongodump --uri=\"$url\" --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
+                    } else {
+                        $commands[] = "docker exec $this->container_name mongodump --authenticationDatabase=admin --uri=\"$url\" --db $escapedDatabaseName --gzip --excludeCollection ".$collectionsToExclude->implode(' --excludeCollection ')." --archive > $this->backup_location";
+                    }

This keeps your on‑metal Mongo backups safe from sneaky collection names trying to go all Skynet on your shell.

♻️ Duplicate comments (7)
app/Livewire/Project/Database/BackupEdit.php (2)

275-281: Auto-selecting first S3 storage may send backups to unexpected bucket.

When saveS3=true and s3RepoStorageId is empty, the code silently defaults to the first available S3 storage. If multiple S3 storages exist (staging vs prod), this could send backups to the wrong bucket - like a Terminator arriving at the wrong time period.

Consider requiring explicit selection:

         if ($this->saveS3 && empty($this->s3RepoStorageId)) {
-            if ($this->s3s->isNotEmpty()) {
-                $this->s3RepoStorageId = $this->s3s->first()->id;
-            } else {
-                throw new Exception('S3 storage must be selected when S3 backups are enabled.');
-            }
+            throw new Exception('S3 storage must be selected when S3 backups are enabled.');
         }

271-291: Optional: Extract validation to reduce cyclomatic complexity.

PHPMD flags complexity at 11 (threshold 10). You could extract the validation block into a helper, but honestly the current structure is readable enough. This is more style than substance - your code runs on a real server, not some ephemeral Lambda that charges by the millisecond of confusion.

+    private function validatePgbackrestRepoRequirements(): array
+    {
+        $hasLocal = ! $this->disableLocalBackup;
+
+        if ($this->saveS3 && empty($this->s3RepoStorageId)) {
+            throw new Exception('S3 storage must be selected when S3 backups are enabled.');
+        }
+
+        $hasS3 = $this->saveS3 && ! empty($this->s3RepoStorageId);
+
+        if (! $hasLocal && ! $hasS3) {
+            throw new Exception(
+                'At least one backup repository (local or S3) must be enabled for pgBackRest. '.
+                'Either enable local backups or configure S3 storage and enable it.'
+            );
+        }
+
+        return [$hasLocal, $hasS3];
+    }
app/Jobs/DatabaseBackupJob.php (4)

19-31: Use the imported RuntimeException instead of the fully‑qualified one

You’ve added use RuntimeException; (Line 29) but still throw \RuntimeException (Line 729). For PSR‑12 style and consistency, drop the leading backslash and rely on the import; otherwise the import is dead weight.

-use RuntimeException;
+use RuntimeException;
...
-                throw new \RuntimeException($errorMessage, $exitCode);
+                throw new RuntimeException($errorMessage, $exitCode);

Also applies to: 727-729


112-123: Avoid triple BackupCreated dispatch for a single pgBackRest run

Right now a pgBackRest backup dispatches BackupCreated three times:

  • Line 112: at the start of handle()
  • Lines 450‑452: in handle()’s finally
  • Lines 787‑789: in run_pgbackrest_backup()’s finally

That’s a lot of noise for one backup job, especially if listeners ever do more than repaint a UI.

Suggestion: let handle() own the lifecycle event and remove the inner dispatch from run_pgbackrest_backup():

-        } finally {
-            BackupCreated::dispatch($this->team->id);
-        }
+        } finally {
+            // Let the outer handle() finally dispatch BackupCreated once.
+        }

(Keeping the top‑of‑handle dispatch is fine if you want an “about to run” signal; just avoid duplicating the “finished” one.)

Also applies to: 444-452, 685-790


767-771: Simplify error message selection in pgBackRest catch block

The catch block prefers $this->backup_output over $e->getMessage() with redundant logic:

$errorMsg = $this->backup_output ?? $e->getMessage();
if ($this->backup_output && $e->getMessage() !== $this->backup_output) {
    $errorMsg = $this->backup_output;
}

This can be collapsed to a single, equivalent line:

-            $errorMsg = $this->backup_output ?? $e->getMessage();
-            if ($this->backup_output && $e->getMessage() !== $this->backup_output) {
-                $errorMsg = $this->backup_output;
-            }
+            $errorMsg = $this->backup_output ?: $e->getMessage();

Same behavior, less mental overhead when you’re debugging at 3 AM next to a rack of humming servers and sadly gluten‑free tacos.


685-790: run_pgbackrest_backup() is still pretty large and complex

PHPMD is still complaining (Cyclomatic complexity 15, NPath 776, 106 LOC). You’ve already improved things by delegating S3 env and command building to PgBackrestService, but the method is still doing a lot (config, backup, info, expire, logging, notifications, event dispatch).

Not a blocker, but worth considering:

  • Extract a helper that:
    • Builds and runs the docker exec command (including EXIT_CODE parsing) and returns [$exitCode, $output].
  • Optionally extract a tiny helper for “update backup_log as success” vs “as failure”.

That would make the core flow of run_pgbackrest_backup() much easier to scan without changing behavior.

app/Jobs/PgBackrestRestoreJob.php (1)

91-108: Avoid double-firing failure notifications from handle() and failed()

Right now any exception causes DatabaseRestoreFailed to be sent twice: once in handle()’s catch block (Lines 99–107) and again in failed() (Lines 448–456) after the job is marked failed. One failed restore, two alerts – that’s overkill even for a T‑800; double‑tap belongs in movies, not in the team’s inbox.

Given Laravel will always call failed() when you rethrow, you can safely let failed() be the single source of truth for notifications and keep the catch in handle() focused on logging and restart attempts. The existing $this->database->team() usage is correct – team() returns the model, not a relation, as per the learnings.

Suggested minimal change: drop the notify block from handle()’s catch and keep the status update there if you want the quicker “failed” status.

         } catch (Throwable $e) {
             Log::error('PgBackRest restore failed', [
                 'database' => $this->database->uuid,
                 'restore_id' => $this->restore->uuid,
                 'error' => $e->getMessage(),
                 'trace' => $e->getTraceAsString(),
             ]);

-            $this->restore->updateStatus('failed', 'Restore failed: '.$e->getMessage());
-
-            $team = $this->database->team();
-            if ($team) {
-                $team->notify(new DatabaseRestoreFailed(
-                    $this->database,
-                    $e->getMessage(),
-                    $this->restore->target_label
-                ));
-            }
+            $this->restore->updateStatus('failed', 'Restore failed: '.$e->getMessage());

Based on learnings about team() returning the actual Team model.

Also applies to: 438-457

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9de624 and 7c847fd.

📒 Files selected for processing (3)
  • app/Jobs/DatabaseBackupJob.php (6 hunks)
  • app/Jobs/PgBackrestRestoreJob.php (1 hunks)
  • app/Livewire/Project/Database/BackupEdit.php (7 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.php

📄 CodeRabbit inference engine (.cursor/rules/coolify-ai-docs.mdc)

Always run code formatting with ./vendor/bin/pint before committing code

**/*.php: Follow PSR-12 coding standards. Use Laravel Pint for automatic formatting. Write descriptive variable and method names. Keep methods small and focused. Document complex logic with clear comments
Use PHP 8.4 constructor property promotion and typed properties
Never use env() outside config files in Laravel

Files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Jobs/DatabaseBackupJob.php
  • app/Livewire/Project/Database/BackupEdit.php
app/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

app/**/*.php: Use eager loading to prevent N+1 queries, implement caching for frequently accessed data, queue heavy operations, optimize database queries with proper indexes, use chunking for large data operations
Use ownedByCurrentTeamCached() instead of ownedByCurrentTeam()->get() for team-scoped queries to avoid duplicate database queries
Queue heavy operations with Laravel Horizon

Files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Jobs/DatabaseBackupJob.php
  • app/Livewire/Project/Database/BackupEdit.php
**/*.{php,blade.php}

📄 CodeRabbit inference engine (CLAUDE.md)

Use named routes with route() function instead of hardcoded URLs

Files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Jobs/DatabaseBackupJob.php
  • app/Livewire/Project/Database/BackupEdit.php
app/Livewire/**/*.php

📄 CodeRabbit inference engine (CLAUDE.md)

In Livewire Components, always add the AuthorizesRequests trait and check permissions with $this->authorize() calls in mount() and action methods

Files:

  • app/Livewire/Project/Database/BackupEdit.php
🧠 Learnings (8)
📓 Common learnings
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Technology Stack: Backend is Laravel 12.4.1 (PHP 8.4.7), Frontend uses Livewire 3.5.20 with Alpine.js and Tailwind CSS 4.1.4, Database is PostgreSQL 15 with Redis 7 for cache/queues, use Docker & Docker Compose for containerization
📚 Learning: 2025-12-10T01:53:52.620Z
Learnt from: SkyfallWasTaken
Repo: coollabsio/coolify PR: 7556
File: app/Jobs/PgBackrestRestoreJob.php:39-118
Timestamp: 2025-12-10T01:53:52.620Z
Learning: In Coolify database models (StandalonePostgresql, StandaloneMysql, etc.), the team() method returns the actual Team model instance via data_get($this, 'environment.project.team'), NOT a BelongsTo relation. Therefore, $database->team() can be used directly for operations like ->notify() without needing to access a property.

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Always use team() method to return relationship instance, not direct property access. App\Models\Application::team must return a relationship instance

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/**/*.php : Use `ownedByCurrentTeamCached()` instead of `ownedByCurrentTeam()->get()` for team-scoped queries to avoid duplicate database queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Model.php,database/migrations/**/*.php} : Database work should follow Eloquent ORM patterns, migration best practices, relationship definitions, and query optimization as documented in `.ai/patterns/database-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Livewire/Project/Database/BackupEdit.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Applies to app/Models/**/*.php : Use Eloquent ORM for database interactions, implement relationships properly (HasMany, BelongsTo, etc.), use database transactions for critical operations, leverage query scopes for reusable queries, and apply indexes for performance-critical queries

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
  • app/Livewire/Project/Database/BackupEdit.php
📚 Learning: 2025-11-25T09:32:48.519Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: .cursor/rules/coolify-ai-docs.mdc:0-0
Timestamp: 2025-11-25T09:32:48.519Z
Learning: Applies to {**/*Policy.php,**/*Gate.php,app/Models/**/*.php,routes/**/*.php} : Use team-based access control patterns and gate/policy authorization as documented in `.ai/patterns/security-patterns.md`

Applied to files:

  • app/Jobs/PgBackrestRestoreJob.php
📚 Learning: 2025-12-08T20:23:33.775Z
Learnt from: CR
Repo: coollabsio/coolify PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-08T20:23:33.775Z
Learning: Technology Stack: Backend is Laravel 12.4.1 (PHP 8.4.7), Frontend uses Livewire 3.5.20 with Alpine.js and Tailwind CSS 4.1.4, Database is PostgreSQL 15 with Redis 7 for cache/queues, use Docker & Docker Compose for containerization

Applied to files:

  • app/Livewire/Project/Database/BackupEdit.php
🧬 Code graph analysis (2)
app/Jobs/DatabaseBackupJob.php (3)
app/Services/Backup/PgBackrestService.php (13)
  • PgBackrestService (9-409)
  • getStanzaName (23-26)
  • buildBackupCommand (208-230)
  • wrapWithLockWait (232-262)
  • buildS3EnvVars (163-178)
  • buildDockerEnvArgs (195-206)
  • buildInfoCommand (298-314)
  • parseInfoJson (339-348)
  • getLatestBackup (350-363)
  • getBackupType (385-388)
  • getBackupSize (380-383)
  • buildExpireCommand (323-337)
  • generateConfig (28-75)
app/Models/StandalonePostgresql.php (2)
  • StandalonePostgresql (11-424)
  • type (242-245)
bootstrap/helpers/remoteProcess.php (1)
  • instant_remote_process (121-155)
app/Livewire/Project/Database/BackupEdit.php (4)
app/Models/PgbackrestRepo.php (1)
  • PgbackrestRepo (8-72)
app/Models/ScheduledDatabaseBackup.php (6)
  • ScheduledDatabaseBackup (10-147)
  • database (92-95)
  • server (130-146)
  • localRepo (45-49)
  • s3Repo (51-55)
  • pgbackrestRepos (35-38)
app/Models/StandalonePostgresql.php (2)
  • StandalonePostgresql (11-424)
  • type (242-245)
bootstrap/helpers/shared.php (1)
  • validateShellSafePath (119-147)
🪛 PHPMD (2.15.0)
app/Jobs/PgBackrestRestoreJob.php

23-459: The class PgBackrestRestoreJob has an overall complexity of 62 which is very high. The configured complexity threshold is 50. (undefined)

(ExcessiveClassComplexity)


42-42: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'handle'. (undefined)

(StaticAccess)


51-51: Avoid using static access to class '\App\Actions\Database\StopDatabase' in method 'handle'. (undefined)

(StaticAccess)


67-67: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


92-97: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'handle'. (undefined)

(StaticAccess)


112-112: Avoid using static access to class '\App\Actions\Database\StartPostgresql' in method 'handle'. (undefined)

(StaticAccess)


121-184: The method preflight() has a Cyclomatic Complexity of 15. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


121-184: The method preflight() has an NPath complexity of 2400. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


136-136: Avoid variables with short names like $s3. Configured minimum length is 3. (undefined)

(ShortVariable)


162-162: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


166-166: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


171-171: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


176-181: The method preflight uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


177-177: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'preflight'. (undefined)

(StaticAccess)


203-203: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


208-208: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


232-232: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runInfoSidecar'. (undefined)

(StaticAccess)


389-389: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


394-399: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'runRestoreSidecar'. (undefined)

(StaticAccess)


427-427: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'getInstallAndRunCommand'. (undefined)

(StaticAccess)

app/Jobs/DatabaseBackupJob.php

475-475: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'backup_standalone_mongodb'. (undefined)

(StaticAccess)


685-790: The method run_pgbackrest_backup() has a Cyclomatic Complexity of 15. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


685-790: The method run_pgbackrest_backup() has an NPath complexity of 776. The configured NPath complexity threshold is 200. (undefined)

(NPathComplexity)


685-790: The method run_pgbackrest_backup() has 106 lines of code. Current threshold is set to 100. Avoid really long methods. (undefined)

(ExcessiveMethodLength)


685-790: The method run_pgbackrest_backup is not named in camelCase. (undefined)

(CamelCaseMethodName)


687-687: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


693-701: Avoid using static access to class '\App\Models\ScheduledDatabaseBackupExecution' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


706-706: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


707-707: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


709-709: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


710-710: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


723-725: The method run_pgbackrest_backup uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


729-729: Missing class import via use statement (line '729', column '27'). (undefined)

(MissingImport)


732-732: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


743-743: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


744-744: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


747-747: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


748-748: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


762-762: Avoid using static access to class '\Carbon\Carbon' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


776-776: Avoid using static access to class '\Carbon\Carbon' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


788-788: Avoid using static access to class '\App\Events\BackupCreated' in method 'run_pgbackrest_backup'. (undefined)

(StaticAccess)


792-819: The method run_pgbackrest_expire is not named in camelCase. (undefined)

(CamelCaseMethodName)


796-796: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


797-797: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


801-801: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


814-817: Avoid using static access to class '\Illuminate\Support\Facades\Log' in method 'run_pgbackrest_expire'. (undefined)

(StaticAccess)


821-836: The method update_pgbackrest_config is not named in camelCase. (undefined)

(CamelCaseMethodName)


823-823: Avoid using static access to class '\App\Services\Backup\PgBackrestService' in method 'update_pgbackrest_config'. (undefined)

(StaticAccess)

app/Livewire/Project/Database/BackupEdit.php

95-95: Avoid excessively long variable names like $pgbackrestCompressType. Keep variable name length under 20. (undefined)

(LongVariable)


98-98: Avoid excessively long variable names like $pgbackrestCompressLevel. Keep variable name length under 20. (undefined)

(LongVariable)


104-104: Avoid excessively long variable names like $pgbackrestArchiveMode. Keep variable name length under 20. (undefined)

(LongVariable)


107-107: Avoid excessively long variable names like $localRepoRetentionFullType. Keep variable name length under 20. (undefined)

(LongVariable)


110-110: Avoid excessively long variable names like $localRepoRetentionFull. Keep variable name length under 20. (undefined)

(LongVariable)


113-113: Avoid excessively long variable names like $localRepoRetentionDiff. Keep variable name length under 20. (undefined)

(LongVariable)


119-119: Avoid excessively long variable names like $s3RepoRetentionFullType. Keep variable name length under 20. (undefined)

(LongVariable)


169-182: Avoid using static access to class '\Illuminate\Support\Facades\DB' in method 'syncData'. (undefined)

(StaticAccess)


183-215: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


216-246: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


237-240: The method syncData uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


271-329: The method syncPgbackrestRepos() has a Cyclomatic Complexity of 11. The configured cyclomatic complexity threshold is 10. (undefined)

(CyclomaticComplexity)


278-280: The method syncPgbackrestRepos uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


308-310: The method syncPgbackrestRepos uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)


326-328: The method syncPgbackrestRepos uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. (undefined)

(ElseExpression)

🔇 Additional comments (11)
app/Livewire/Project/Database/BackupEdit.php (5)

5-21: LGTM - Imports and trait setup look battle-ready.

Good additions: PgbackrestRepo, StandalonePostgresql (for type-safe class comparison), and DB facade for transactions. The AuthorizesRequests trait usage follows Livewire security guidelines. Your imports are more reliable than a serverless cold start. As per coding guidelines, Livewire components should use AuthorizesRequests.


135-146: Solid authorization and type guard - your databases are protected like a T-800.

The authorize('update', ...) check and the isPostgresql() guard prevent unauthorized or incompatible databases from enabling pgBackRest. This is exactly what the doctor ordered - self-hosted security done right, unlike those serverless black boxes where you just hope someone remembered to add auth. Based on coding guidelines requiring authorize() calls in action methods.


249-269: Clean repo loading - your backup config deserializes better than a T-1000 reforming.

The null coalescing defaults (?? 'count', ?? 2, ?? 7) provide sensible fallbacks for missing repo settings. This method properly hydrates component state from the model relationships. Self-hosted reliability at its finest.


430-433: Class constant comparison - future-proof like a Skynet-resistant bunker.

Using StandalonePostgresql::class instead of a hardcoded string 'App\Models\StandalonePostgresql' ensures refactoring safety. If the namespace changes, your IDE and static analysis will catch it. Unlike serverless where your string references break silently at 3 AM.


435-444: Clean render with PostgreSQL detection for conditional UI.

Passing 'isPostgresql' => $this->isPostgresql() enables the blade template to conditionally show pgBackRest options. Simple, effective, self-hosted. No serverless edge function needed - just a good old PHP server doing what servers do best.

app/Jobs/DatabaseBackupJob.php (6)

302-303: Cuid2 for backup_log_uuid looks good

Switching backup_log_uuid generation to new Cuid2 for both the regular backup loop (Line 302) and the pgBackRest path (Line 691) is fine and keeps container names / log UUIDs nicely collision‑resistant.

No issues from my end here — your UUIDs are now as unstoppable as a T‑800 marching through a data center.

Also applies to: 691-701


475-476: MongoDB backup logging is clear and non‑noisy

The new Log::info('MongoDB backup URL configured', …) line gives useful diagnostics about whether we’re using the model’s internal_db_url vs env vars, without leaking credentials. That’s exactly the kind of observability you want on a real server, not in some ephemeral serverless black box.


522-537: Postgres DB name validation & escaping is solid

The added validateShellSafePath($database, 'database name') and escapeshellarg($database) (Lines 533‑536) before plugging the name into pg_dump closes an obvious injection vector. Combined with the PGPASSWORD env usage, this is a good, server‑worthy hardening step.


551-562: MySQL & MariaDB backup hardening for DB names looks good

Same story as Postgres: validating and escaping $database before passing it into mysqldump / mariadb-dump (Lines 558‑561 and 582‑585) is the right move. This keeps custom DB names from smuggling shell syntax into your docker exec commands.

No issues here — this is the kind of paranoid hygiene you want on your own metal.

Also applies to: 574-585


657-664: S3 credentials now properly escaped for mc alias set

Switching to escapeshellarg for $endpoint, $key, and $secret (Lines 657‑663) before composing the mc alias set command is exactly what’s needed to avoid secrets containing shell metacharacters blowing up the command line.

$escapedEndpoint = escapeshellarg($endpoint);
$escapedKey = escapeshellarg($key);
$escapedSecret = escapeshellarg($secret);

This makes the S3 upload path a lot safer without messing with existing behavior.


821-836: pgBackRest config upload via base64 is sane

update_pgbackrest_config() base64‑encodes the config and writes it inside the container using a simple echo … | base64 -d (Lines 829‑835). Given that base64 output is restricted to [A-Za-z0-9+/=] and CONFIG_PATH is a constant, this avoids nasty shell‑escaping issues for the config contents themselves.

No functional concerns here — this is a pragmatic way to ship config into the container without relying on any serverless nonsense.

@zachlatta
Copy link

Hi - I'm the creator of the bounty #7423. To Coolify maintainers - I want to say that this would be my preferred submission for this bounty assuming it passes muster.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🙋 Bounty claim Issues or PRs that have a Bounty ready to be claimed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants