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:
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Central\Company;
|
||||
use App\Models\Central\Plan;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class OffsiteBackupTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_backup_uploads_to_b2_when_configured(): void
|
||||
{
|
||||
$plan = Plan::create(['name' => 'F', 'slug' => 'free', 'price' => 0, 'features' => []]);
|
||||
Company::create([
|
||||
'plan_id' => $plan->id, 'slug' => 'bkup',
|
||||
'name' => 'Backup Co', 'status' => 'active',
|
||||
]);
|
||||
|
||||
// Pretend B2 is configured + fake the disk so the command's putStream lands here.
|
||||
putenv('B2_KEY=fake');
|
||||
putenv('B2_BUCKET=fake-bucket');
|
||||
$b2 = Storage::fake('b2');
|
||||
|
||||
$this->artisan('backup:tenants --slug=bkup')->assertExitCode(0);
|
||||
|
||||
// The remote key uses today's date + slug.
|
||||
$remoteKey = date('Y-m-d') . '/bkup.zip';
|
||||
$b2->assertExists($remoteKey);
|
||||
|
||||
putenv('B2_KEY');
|
||||
putenv('B2_BUCKET');
|
||||
}
|
||||
|
||||
public function test_backup_skips_offsite_when_not_configured(): void
|
||||
{
|
||||
$plan = Plan::firstOrCreate(['slug' => 'free'], ['name' => 'F', 'price' => 0, 'features' => []]);
|
||||
Company::create([
|
||||
'plan_id' => $plan->id, 'slug' => 'nob2',
|
||||
'name' => 'No B2', 'status' => 'active',
|
||||
]);
|
||||
|
||||
// No B2 env set.
|
||||
$b2 = Storage::fake('b2');
|
||||
|
||||
$this->artisan('backup:tenants --slug=nob2')->assertExitCode(0);
|
||||
|
||||
// Nothing pushed offsite.
|
||||
$this->assertCount(0, $b2->allFiles());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user