feat: production email (Resend) + offsite backup (B2)
Resend mail transport:
- composer require resend/resend-laravel (v1.4)
- Laravel 11 ships the 'resend' mailer config in config/mail.php + services
- To switch to production email: set MAIL_MAILER=resend + RESEND_API_KEY,
register the domain at resend.com/domains, and add the TXT + DKIM CNAME
records in Cloudflare. .env.example documents the required steps.
Backblaze B2 offsite backup:
- New filesystems 'b2' disk (S3-compatible, env: B2_KEY/SECRET/BUCKET/REGION/ENDPOINT)
- BackupAllTenantsCommand: after writing each tenant's zip to local disk, it
uploads the same file to the b2 disk under {YYYY-MM-DD}/{slug}.zip — only
when both B2_KEY and B2_BUCKET are set, so unconfigured installs are no-op
- Without offsite, backups live on the same VPS as production: a single
hardware failure loses everything. B2 + Resend together make the install
genuinely production-ready (people get email + offsite backup).
Tests (2 new):
- backup uploads to b2 (fake disk) when configured
- backup skips offsite when env vars not present
Full suite: 140 passed. Force-rebuild deploy required so composer install
picks up resend/resend-php.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,9 @@ class BackupAllTenantsCommand extends Command
|
||||
|
||||
$size = round(filesize($dest) / 1024, 1);
|
||||
$this->info("✓ {$company->slug} → {$size}KB");
|
||||
|
||||
// Offsite copy to B2 (if configured) — disk lazily resolved.
|
||||
$this->uploadOffsite($dest, "{$date}/{$company->slug}.zip");
|
||||
$ok++;
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("✗ {$company->slug}: {$e->getMessage()}");
|
||||
@@ -65,6 +68,20 @@ class BackupAllTenantsCommand extends Command
|
||||
return $failed > 0 ? self::FAILURE : self::SUCCESS;
|
||||
}
|
||||
|
||||
/** Upload one backup zip to the offsite B2 disk if env is configured. */
|
||||
private function uploadOffsite(string $localPath, string $remoteKey): void
|
||||
{
|
||||
if (! env('B2_KEY') || ! env('B2_BUCKET')) return;
|
||||
try {
|
||||
$stream = fopen($localPath, 'rb');
|
||||
\Illuminate\Support\Facades\Storage::disk('b2')->put($remoteKey, $stream);
|
||||
if (is_resource($stream)) fclose($stream);
|
||||
$this->line(" ↑ offsite: {$remoteKey}");
|
||||
} catch (\Throwable $e) {
|
||||
$this->warn(" ✗ offsite upload failed: " . substr($e->getMessage(), 0, 120));
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupOld(int $keep): void
|
||||
{
|
||||
$backupsDir = storage_path('app/backups');
|
||||
|
||||
Reference in New Issue
Block a user