docs: sync OpenSpec archives and propose foundation parity
Archive completed public-site and production-provider changes into main specs, remove duplicate active changes, and add complete-foundation-parity so Phase 0 staging and remaining foundation gaps block Phase 2 cleanly. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-07-29
|
||||
@@ -0,0 +1,80 @@
|
||||
## Context
|
||||
|
||||
Fase 0–1 entregaram CMS com uploads de mídia via `PublicImageUploadRules`, que hoje seleciona disco `public` localmente e `s3` quando `FILESYSTEM_DISK=s3`. Laravel 13 já inclui mailer `resend` em [`config/mail.php`](../../config/mail.php) e chave em [`config/services.php`](../../config/services.php), mas faltam dependências (`resend/resend-php`, `league/flysystem-aws-s3-v3`) e configuração explícita para Cloudflare R2.
|
||||
|
||||
Produção usará **Resend** para e-mail transacional e **Cloudflare R2** para mídia pública via domínio customizado. Local e CI continuam com `MAIL_MAILER=log`/`array` e disco `public`.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Resend como transporte nativo Laravel em produção (`MAIL_MAILER=resend`, `RESEND_API_KEY`).
|
||||
- Disco dedicado `r2` com variáveis explícitas (`R2_*`) e URL pública via `R2_URL` (domínio customizado).
|
||||
- Seleção de disco de mídia alinhada a `FILESYSTEM_DISK=r2`.
|
||||
- Documentação de env vars e testes automatizados de config/seleção (sem chamadas live).
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Templates de e-mail, filas de notificação de leads, ou fluxos CRM.
|
||||
- Buckets privados, signed URLs, ou automação de DNS/CDN.
|
||||
- Mudança de queue driver ou provisionamento Cloudflare via código.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Resend via transporte nativo Laravel
|
||||
|
||||
**Decisão:** usar mailer `resend` já presente em `config/mail.php` + pacote `resend/resend-php`. Credencial em `config/services.php` → `RESEND_API_KEY`.
|
||||
|
||||
**Alternativa rejeitada:** SMTP genérico — menos idiomático; perde integração nativa Laravel 13.
|
||||
|
||||
### 2. Disco dedicado `r2` (não reutilizar nome `s3`)
|
||||
|
||||
**Decisão:** adicionar disco `r2` em `config/filesystems.php` com driver `s3`, endpoint R2 (`https://<account_id>.r2.cloudflarestorage.com`), `use_path_style_endpoint=true`, bucket e credenciais via `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`, `R2_ENDPOINT`, `R2_URL` (domínio público customizado).
|
||||
|
||||
**Alternativa rejeitada:** reutilizar disco `s3` com vars `AWS_*` — funciona tecnicamente, mas obscurece provedor real e conflita com futuro uso de AWS S3.
|
||||
|
||||
### 3. `FILESYSTEM_DISK=r2` em produção
|
||||
|
||||
**Decisão:** default disk de produção = `r2`. Local/test = `local` ou `public`. `PublicImageUploadRules::disk()` retorna `r2` quando default é `r2`, `s3` quando default é `s3`, senão `public`.
|
||||
|
||||
**Alternativa rejeitada:** mapear `r2` para disco `s3` internamente — confunde operadores e quebra intenção explícita do env.
|
||||
|
||||
### 4. URLs públicas via domínio customizado
|
||||
|
||||
**Decisão:** `R2_URL` aponta para domínio customizado (ex.: `https://media.example.com`) mapeado ao bucket via Cloudflare. Disco `r2` define `url => env('R2_URL')` e `visibility => public`.
|
||||
|
||||
**Alternativa rejeitada:** URL `r2.dev` gerenciada — menos controle de marca e SEO; usuário escolheu custom domain.
|
||||
|
||||
### 5. Testes sem integração live
|
||||
|
||||
**Decisão:** testes usam `Mail::fake`, `Storage::fake('r2')`, e asserts de config (`config('mail.default')`, `config('filesystems.disks.r2')`, `PublicImageUploadRules::disk()`). Nenhuma chamada HTTP a Resend/R2 no CI.
|
||||
|
||||
### 6. Defaults seguros por ambiente
|
||||
|
||||
**Decisão:**
|
||||
|
||||
| Ambiente | `MAIL_MAILER` | `FILESYSTEM_DISK` | Disco de upload CMS |
|
||||
|---|---|---|---|
|
||||
| Local dev | `log` | `local` | `public` |
|
||||
| Testes (phpunit) | `array` | (unset → local) | `public` |
|
||||
| Produção | `resend` | `r2` | `r2` |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[R2 credentials missing in prod]** → app boot ok, upload fails at runtime; document required vars in `.env.example` and README; config test asserts disk definition exists.
|
||||
- **[Custom domain not configured]** → broken public image URLs; `R2_URL` documented as required for production media.
|
||||
- **[Resend API key missing]** → mail send fails; lead creation must not depend on mail (future phases per SPEC).
|
||||
- **[Package version drift]** → pin compatible versions in `composer.json`; run `composer security-audit` in CI.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Merge change; run `composer install` in production image build.
|
||||
2. Create R2 bucket, API token, and custom domain in Cloudflare dashboard (manual).
|
||||
3. Create Resend API key and verify sending domain.
|
||||
4. Set production env: `MAIL_MAILER=resend`, `RESEND_API_KEY`, `FILESYSTEM_DISK=r2`, `R2_*`, `R2_URL`, `MAIL_FROM_*`.
|
||||
5. Deploy; upload test image via Filament; verify public URL resolves.
|
||||
6. **Rollback:** revert env to `MAIL_MAILER=log`, `FILESYSTEM_DISK=public`; existing DB paths remain valid for local disk until re-upload.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- _(none — custom domain confirmed; Resend as sole production mail provider for this change)_
|
||||
@@ -0,0 +1,43 @@
|
||||
## Why
|
||||
|
||||
The application already supports CMS media uploads and will soon send transactional email (lead notifications, password reset), but production still lacks concrete provider wiring. SPEC §9.1 and §15.4 require S3-compatible object storage in production and transactional email when configured. Resend and Cloudflare R2 are the chosen providers; this change adds the configuration, dependencies, and selection rules so production can send mail and persist public media outside the container filesystem.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Add **Resend** as the production mail transport via Laravel's native `resend` mailer and `RESEND_API_KEY`.
|
||||
- Add a dedicated **`r2` filesystem disk** for Cloudflare R2 (S3-compatible API) with explicit R2 env vars and public URLs served through a **custom domain** (`R2_URL`).
|
||||
- Install required dependencies: `resend/resend-php` and `league/flysystem-aws-s3-v3`.
|
||||
- Update `.env.example`, README, and deployment guidance with production vs local/test defaults.
|
||||
- Fix media disk selection in `PublicImageUploadRules` so `FILESYSTEM_DISK=r2` uses the R2 disk instead of falling back to local `public`.
|
||||
- Add automated config and media-disk selection tests; no live calls to Resend or R2 in CI.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
Conforme [SPEC.md §4.2](../../SPEC.md), **não** fazem parte desta change:
|
||||
|
||||
- Lead notification emails, briefing confirmation flows, or CRM email templates (future phases).
|
||||
- Private/signed URL access to media; production media is public via custom domain.
|
||||
- Cloudflare Workers, CDN provisioning, or DNS automation (manual infra setup).
|
||||
- Redis, queue driver changes, or FrankenPHP worker mode.
|
||||
- Replacing local `public` disk behavior for development and tests.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `transactional-email`: Resend-backed transactional mail transport with safe local/test defaults and production configuration via environment variables (SPEC §9.1, §15.4, §12.1–12.2).
|
||||
- `object-storage`: Cloudflare R2 object storage via dedicated `r2` disk, custom-domain public URLs, and environment-driven disk selection (SPEC §6.4, §9.1, §15.4).
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `content-media`: extend disk selection so production `FILESYSTEM_DISK=r2` stores and serves public content images from R2 instead of local `public`.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Dependencies**: `resend/resend-php`, `league/flysystem-aws-s3-v3` in `composer.json`.
|
||||
- **Config**: `config/mail.php`, `config/services.php`, `config/filesystems.php`, `.env.example`.
|
||||
- **Application**: `app/Support/PublicImageUploadRules.php` disk selection logic.
|
||||
- **Tests**: new Pest feature/unit tests for mail and filesystem config; media disk selection regression test.
|
||||
- **Docs**: `README.md` production provider section.
|
||||
- **Infra (manual)**: Resend API key, R2 bucket, R2 API token, and custom domain mapped to the bucket.
|
||||
- **No breaking change** for local development: defaults remain `MAIL_MAILER=log` and `FILESYSTEM_DISK=local`/`public`.
|
||||
@@ -0,0 +1,20 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: CMS public image uploads select production object storage disk
|
||||
|
||||
Public content image uploads (Filament FileUpload via `PublicImageUploadRules`) MUST store files on the disk matching the configured default filesystem (SPEC §6.4). When `FILESYSTEM_DISK` is `r2`, uploads MUST use the `r2` disk. When `FILESYSTEM_DISK` is `s3`, uploads MUST use the `s3` disk. Otherwise uploads MUST use the local `public` disk.
|
||||
|
||||
#### Scenario: Production R2 disk is used for CMS uploads
|
||||
|
||||
- **WHEN** `FILESYSTEM_DISK=r2`
|
||||
- **THEN** `PublicImageUploadRules::disk()` MUST return `r2`
|
||||
|
||||
#### Scenario: Legacy S3 default still supported
|
||||
|
||||
- **WHEN** `FILESYSTEM_DISK=s3`
|
||||
- **THEN** `PublicImageUploadRules::disk()` MUST return `s3`
|
||||
|
||||
#### Scenario: Local development uses public disk
|
||||
|
||||
- **WHEN** `FILESYSTEM_DISK` is `local`, unset, or any value other than `r2`/`s3`
|
||||
- **THEN** `PublicImageUploadRules::disk()` MUST return `public`
|
||||
@@ -0,0 +1,44 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Cloudflare R2 is available as dedicated filesystem disk
|
||||
|
||||
The system SHALL provide an `r2` filesystem disk using Laravel's S3-compatible driver pointed at Cloudflare R2 (SPEC §6.4, §9.1, §15.4). Configuration MUST use explicit `R2_*` environment variables: `R2_ACCESS_KEY_ID`, `R2_SECRET_ACCESS_KEY`, `R2_BUCKET`, `R2_ENDPOINT`, and `R2_URL`.
|
||||
|
||||
#### Scenario: R2 disk is defined in filesystem config
|
||||
|
||||
- **WHEN** the application boots
|
||||
- **THEN** `config('filesystems.disks.r2')` MUST exist
|
||||
- **AND** the disk driver MUST be `s3`
|
||||
- **AND** credentials MUST resolve from `R2_ACCESS_KEY_ID` and `R2_SECRET_ACCESS_KEY`
|
||||
|
||||
#### Scenario: R2 endpoint uses path-style addressing
|
||||
|
||||
- **WHEN** the `r2` disk is configured
|
||||
- **THEN** `use_path_style_endpoint` MUST be `true`
|
||||
- **AND** `endpoint` MUST resolve from `R2_ENDPOINT`
|
||||
|
||||
#### Scenario: Public URLs use custom domain
|
||||
|
||||
- **WHEN** a file is stored on the `r2` disk with public visibility
|
||||
- **THEN** generated URLs MUST use `R2_URL` as base
|
||||
- **AND** MUST NOT require signed URLs for browser-facing media
|
||||
|
||||
### Requirement: Production default filesystem disk is R2
|
||||
|
||||
Production deployments MUST set `FILESYSTEM_DISK=r2` so application code using the default disk stores files on R2.
|
||||
|
||||
#### Scenario: Default disk resolves to R2 in production
|
||||
|
||||
- **WHEN** `FILESYSTEM_DISK=r2` is set
|
||||
- **THEN** `config('filesystems.default')` MUST be `r2`
|
||||
|
||||
#### Scenario: Local development keeps local disk
|
||||
|
||||
- **WHEN** `FILESYSTEM_DISK` is unset or set to `local`/`public` in development
|
||||
- **THEN** the default disk MUST NOT require R2 credentials to boot
|
||||
|
||||
#### Scenario: Tests do not call R2
|
||||
|
||||
- **WHEN** the test suite runs
|
||||
- **THEN** tests MUST use `Storage::fake('r2')` or local fakes
|
||||
- **AND** no live HTTP request to Cloudflare R2 MUST occur during CI
|
||||
@@ -0,0 +1,32 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Production transactional email uses Resend
|
||||
|
||||
The system SHALL support Resend as the production mail transport via Laravel's native `resend` mailer (SPEC §9.1, §15.4). The Resend API key MUST be read from `RESEND_API_KEY` through `config/services.php`. Production deployments MUST set `MAIL_MAILER=resend` when transactional email is enabled.
|
||||
|
||||
#### Scenario: Resend mailer is configured
|
||||
|
||||
- **WHEN** the application boots with valid configuration
|
||||
- **THEN** a `resend` mailer MUST exist in `config/mail.php`
|
||||
- **AND** `config('services.resend.key')` MUST resolve from `RESEND_API_KEY`
|
||||
|
||||
#### Scenario: Local development uses safe mail default
|
||||
|
||||
- **WHEN** `APP_ENV` is `local` and `MAIL_MAILER` is unset
|
||||
- **THEN** the default mailer MUST be `log`
|
||||
- **AND** no outbound email MUST be sent to external providers
|
||||
|
||||
#### Scenario: Tests do not call Resend
|
||||
|
||||
- **WHEN** the test suite runs
|
||||
- **THEN** `MAIL_MAILER` MUST be `array` (or tests MUST use `Mail::fake`)
|
||||
- **AND** no live HTTP request to Resend MUST occur during CI
|
||||
|
||||
### Requirement: Global from address is configurable
|
||||
|
||||
The system SHALL read the global sender address and name from `MAIL_FROM_ADDRESS` and `MAIL_FROM_NAME` (SPEC §15.4).
|
||||
|
||||
#### Scenario: From address applied to outbound mail
|
||||
|
||||
- **WHEN** the application sends mail through the configured mailer
|
||||
- **THEN** the message MUST use the configured from address and name
|
||||
@@ -0,0 +1,33 @@
|
||||
## 1. Dependencies
|
||||
|
||||
- [x] 1.1 Add `resend/resend-php` to `composer.json` require
|
||||
- [x] 1.2 Add `league/flysystem-aws-s3-v3` to `composer.json` require
|
||||
- [x] 1.3 Run `composer update resend/resend-php league/flysystem-aws-s3-v3 --with-all-dependencies` and commit lockfile
|
||||
|
||||
## 2. Transactional email (transactional-email)
|
||||
|
||||
- [x] 2.1 Confirm `config/mail.php` resend mailer and `config/services.php` `RESEND_API_KEY` binding
|
||||
- [x] 2.2 Add `RESEND_API_KEY` and production mail vars to `.env.example` with safe local defaults (`MAIL_MAILER=log`)
|
||||
- [x] 2.3 Write feature test asserting resend mailer config and services key resolution
|
||||
|
||||
## 3. Object storage (object-storage)
|
||||
|
||||
- [x] 3.1 Add dedicated `r2` disk to `config/filesystems.php` with `R2_*` env vars, path-style endpoint, and `R2_URL`
|
||||
- [x] 3.2 Add R2 env vars to `.env.example` with comments for custom domain setup
|
||||
- [x] 3.3 Write feature test asserting `r2` disk config (driver, endpoint, path-style, url from `R2_URL`)
|
||||
|
||||
## 4. CMS media disk selection (content-media)
|
||||
|
||||
- [x] 4.1 Update `PublicImageUploadRules::disk()` to return `r2` when `FILESYSTEM_DISK=r2`, keep `s3` and `public` fallbacks
|
||||
- [x] 4.2 Write unit/feature test for disk selection matrix: `r2`, `s3`, local/unset → `public`
|
||||
|
||||
## 5. Documentation
|
||||
|
||||
- [x] 5.1 Update `README.md` with production provider section (Resend + R2 + custom domain)
|
||||
- [x] 5.2 Document production env checklist: `MAIL_MAILER=resend`, `FILESYSTEM_DISK=r2`, required secrets
|
||||
|
||||
## 6. Verification
|
||||
|
||||
- [x] 6.1 Run `composer pint:check` and `composer phpstan`
|
||||
- [x] 6.2 Run `composer test:unit` and `composer test:feature` for new provider tests
|
||||
- [x] 6.3 Run `composer quality` and fix any failures
|
||||
Reference in New Issue
Block a user