Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ COPY --from=frontend-build /frontend/dist /app/ui/build

# Generate Swagger documentation
COPY backend/ ./
RUN go mod tidy
RUN swag init -d . -g cmd/main.go -o swagger

# Compile the backend
Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@
- **SSL support**: Secure connections available
- **Easy restoration**: One-click restore from any backup

### 🧩 **Cluster-based Setup**

- **Discover databases**: Connect to a PostgreSQL cluster and list databases the user can access
- **Bulk import**: Select multiple databases and create them at once
- **Shared setup**: Apply a single backup schedule, storage and notifiers to all selected databases

### 👥 **Suitable for Teams** <a href="https://postgresus.com/access-management">(docs)</a>

- **Workspaces**: Group databases, notifiers and storages for different projects or teams
Expand Down Expand Up @@ -147,13 +153,30 @@ docker compose up -d
## 🚀 Usage

1. **Access the dashboard**: Navigate to `http://localhost:4005`
2. **Add first DB for backup**: Click "New Database" and follow the setup wizard
2. **Add database(s)**: Click "Add database" and choose either:
- **Single database**: classic flow for one DB
- **From cluster**: discover and import multiple DBs from a PostgreSQL cluster
3. **Configure schedule**: Choose from hourly, daily, weekly or monthly intervals
4. **Set database connection**: Enter your PostgreSQL credentials and connection details
5. **Choose storage**: Select where to store your backups (local, S3, Google Drive, etc.)
6. **Add notifications** (optional): Configure email, Telegram, Slack, or webhook notifications
7. **Save and start**: Postgresus will validate settings and begin the backup schedule

### Import multiple databases from a cluster

1. Go to Databases → **Add database** → **From cluster**
2. Enter connection: PostgreSQL version, host, port, username, password, HTTPS if needed
3. Click **Load databases** to list accessible DBs (templates are excluded)
4. Select the databases you want to back up (multi-select)
5. Set a shared backup schedule and storage
6. Select notifiers (optional)
7. Click **Create** — Postgresus creates a database entry per selected DB and applies your shared settings

Notes:
- Requires `CONNECT` privilege to each selected database; templates are excluded automatically
- The specified PostgreSQL version must match the server; choose the actual version of the cluster
- HTTPS toggle uses `sslmode=require`

### 🔑 Resetting Password <a href="https://postgresus.com/password">(docs)</a>

If you need to reset the password, you can use the built-in password reset command:
Expand Down
8 changes: 8 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Keep in mind: you need to use dev-db from docker-compose.yml in this folder
instead of postgresus-db from docker-compose.yml in the root folder.

## Requirements

- Go 1.23+

> Copy .env.example to .env
> Copy docker-compose.yml.example to docker-compose.yml (for development only)
> Go to tools folder and install Postgres versions
Expand Down Expand Up @@ -47,6 +51,10 @@ Swagger URL is:

> http://localhost:4005/api/v1/docs/swagger/index.html#/

## New endpoints

- POST `/api/v1/databases/list-databases-direct` — list accessible databases in a PostgreSQL cluster without saving configuration. See Swagger for request/response schema.

# Project structure

Default endpoint structure is:
Expand Down
7 changes: 7 additions & 0 deletions backend/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"postgresus-backend/internal/features/audit_logs"
"postgresus-backend/internal/features/backups/backups"
backups_config "postgresus-backend/internal/features/backups/config"
"postgresus-backend/internal/features/clusters"
"postgresus-backend/internal/features/databases"
"postgresus-backend/internal/features/disk"
healthcheck_attempt "postgresus-backend/internal/features/healthcheck/attempt"
Expand Down Expand Up @@ -192,6 +193,7 @@ func setUpRoutes(r *gin.Engine) {
notifiers.GetNotifierController().RegisterRoutes(protected)
storages.GetStorageController().RegisterRoutes(protected)
databases.GetDatabaseController().RegisterRoutes(protected)
clusters.GetClusterController().RegisterRoutes(protected)
backups.GetBackupController().RegisterRoutes(protected)
restores.GetRestoreController().RegisterRoutes(protected)
healthcheck_config.GetHealthcheckConfigController().RegisterRoutes(protected)
Expand All @@ -210,6 +212,7 @@ func setUpDependencies() {
audit_logs.SetupDependencies()
notifiers.SetupDependencies()
storages.SetupDependencies()
clusters.SetupDependencies()
}

func runBackgroundTasks(log *slog.Logger) {
Expand All @@ -224,6 +227,10 @@ func runBackgroundTasks(log *slog.Logger) {
backups.GetBackupBackgroundService().Run()
})

go runWithPanicLogging(log, "cluster background service", func() {
clusters.GetClusterBackgroundService().Run()
})

go runWithPanicLogging(log, "restore background service", func() {
restores.GetRestoreBackgroundService().Run()
})
Expand Down
4 changes: 4 additions & 0 deletions backend/internal/features/backups/backups/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,10 @@ func (s *BackupService) deleteBackup(backup *Backup) error {
return s.backupRepository.DeleteByID(backup.ID)
}

func (s *BackupService) DeleteBackupsForDatabase(databaseID uuid.UUID) error {
return s.deleteDbBackups(databaseID)
}

func (s *BackupService) deleteDbBackups(databaseID uuid.UUID) error {
dbBackupsInProgress, err := s.backupRepository.FindByDatabaseIdAndStatus(
databaseID,
Expand Down
4 changes: 4 additions & 0 deletions backend/internal/features/backups/config/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ type BackupConfig struct {
Storage *storages.Storage `json:"storage" gorm:"foreignKey:StorageID"`
StorageID *uuid.UUID `json:"storageId" gorm:"column:storage_id;type:uuid;"`

// Cluster management
ClusterID *uuid.UUID `json:"clusterId" gorm:"column:cluster_id;type:uuid"`
ManagedByCluster bool `json:"managedByCluster" gorm:"column:managed_by_cluster;type:boolean;not null"`

SendNotificationsOn []BackupNotificationType `json:"sendNotificationsOn" gorm:"-"`
SendNotificationsOnString string `json:"-" gorm:"column:send_notifications_on;type:text;not null"`

Expand Down
2 changes: 1 addition & 1 deletion backend/internal/features/backups/config/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (r *BackupConfigRepository) GetWithEnabledBackups() ([]*BackupConfig, error
GetDb().
Preload("BackupInterval").
Preload("Storage").
Where("is_backups_enabled = ?", true).
Where("is_backups_enabled = ? AND (managed_by_cluster = FALSE OR cluster_id IS NULL)", true).
Find(&backupConfigs).Error; err != nil {
return nil, err
}
Expand Down
6 changes: 6 additions & 0 deletions backend/internal/features/backups/config/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,12 @@ func (s *BackupConfigService) GetBackupConfigByDbId(
return config, nil
}

func (s *BackupConfigService) FindBackupConfigByDbIdNoInit(
databaseID uuid.UUID,
) (*BackupConfig, error) {
return s.backupConfigRepository.FindByDatabaseID(databaseID)
}

func (s *BackupConfigService) IsStorageUsing(
user *users_models.User,
storageID uuid.UUID,
Expand Down
59 changes: 59 additions & 0 deletions backend/internal/features/clusters/background_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package clusters

import (
"log/slog"
"time"

"github.com/google/uuid"
)

// ClusterBackgroundService periodically checks clusters and triggers RunBackup on their intervals.
type ClusterBackgroundService struct {
service *ClusterService
repo *ClusterRepository
logger *slog.Logger
}

func (s *ClusterBackgroundService) Run() {
for {
if err := s.tick(); err != nil {
s.logger.Error("Cluster scheduler tick failed", "error", err)
}
time.Sleep(1 * time.Minute)
}
}

func (s *ClusterBackgroundService) tick() error {
clusters, err := s.repo.FindAll()
if err != nil {
return err
}

now := time.Now().UTC()
for _, c := range clusters {
// require interval and backups enabled
if !c.IsBackupsEnabled || c.BackupInterval == nil {
continue
}

var last *time.Time
if c.LastRunAt != nil && !c.LastRunAt.IsZero() {
last = c.LastRunAt
}

if c.BackupInterval.ShouldTriggerBackup(now, last) {
if err := s.service.RunBackupScheduled(c.ID); err != nil {
s.logger.Error("Failed to run cluster backup", "clusterId", c.ID, "error", err)
}
// Update last run regardless of outcome to avoid tight loop
_ = s.repo.UpdateLastRunAt(c.ID, now)
}
}

return nil
}

// For tests or manual invocations
func (s *ClusterBackgroundService) RunOnceFor(clusterID uuid.UUID) error {
return s.service.RunBackupScheduled(clusterID)
}
Loading