BFF Specification
Sub-system specification for the BFF (Backend-for-Frontend) — the Next.js
server application in arda-frontend-app. The BFF acts as an authenticated
proxy between the SPA and the Backend, enriches requests with tenant context,
and provides a CORS fallback for external URL reachability checks. For the
structural overview of all sub-systems and their dependencies, see
design.md.
The BFF does not depend on AWS Storage resources. It never handles file bytes (TD-06), never checks URLs on managed storage (TD-02), and never generates presigned credentials (TD-03 — that is the Backend’s responsibility).
Actor Requirements
Section titled “Actor Requirements”Functional
Section titled “Functional”| ID | Requirement | Scenarios | Source |
|---|---|---|---|
| BFF-FR-001 | Proxy presigned POST credential requests from SPA to Backend. Add Authorization, X-Tenant-Id, X-Author, and X-Request-ID headers. | S1, S2, S5, S6, S7 | FR-020, TD-03 |
| BFF-FR-002 | Proxy entity update requests (PUT) from SPA to Backend. Add authentication and tenant headers. | S1, S2, S3, S5, S6, S7 | FR-022 |
| BFF-FR-003 | Provide a URL reachability check endpoint. Accept a URL from the SPA, perform a HEAD (or GET) request to the external URL, and return reachability status, content type, and content length. | S2 | FR-011, TD-02 |
| BFF-FR-004 | The reachability endpoint shall only check external URLs. Reject requests targeting managed storage URLs (CDN host pattern). | S2 | NFR-008, TD-02 |
| BFF-FR-005 | Provide a URL fetch endpoint (or extend the reachability endpoint) to proxy the full image content when the SPA cannot fetch due to CORS. | S2 | FR-004, TD-01, TD-02 |
| BFF-FR-006 | The reachability/fetch endpoint shall implement request-rate limiting per tenant to prevent abuse. | S2 | NFR-009 |
| BFF-FR-007 | The reachability/fetch endpoint shall validate the target URL to prevent SSRF attacks (reject private IP ranges, localhost, non-HTTPS schemes). | S2 | NFR-009 |
| BFF-FR-008 | Issue CloudFront signed cookies scoped to the active tenant’s key prefix (/<tenantId>/*). Cookies are set on .arda.cards with Secure; HttpOnly; SameSite=Lax. The tenant ID is extracted from the authenticated session context, never accepted as a client parameter. | S4 | FR-036, FR-039, NFR-017, NFR-018, TD-11 |
| BFF-FR-009 | Issue signed cookies on session establishment, on explicit refresh request from the SPA, and support immediate re-issuance on tenant switch. | S4 | FR-037, FR-038, TD-12 |
| BFF-FR-010 | Signed cookies shall have a configurable TTL (default 30 minutes). The BFF shall hold the CloudFront private key (or access it via a secrets manager) to sign cookie policies. | S4 | NFR-019, TD-12 |
Non-Functional
Section titled “Non-Functional”| ID | Requirement | Source |
|---|---|---|
| BFF-NFR-001 | The BFF shall not buffer or store image file bytes. It is not in the data path for uploads. | TD-06 |
| BFF-NFR-002 | The reachability/fetch endpoint shall time out external requests within 10 seconds to prevent hanging connections. | Derived from UX responsiveness |
| BFF-NFR-003 | All BFF endpoints shall require a valid authenticated session (JWT). | Existing architecture |
| BFF-NFR-004 | The CloudFront signing private key shall be stored securely (AWS Secrets Manager or environment variable from sealed secret). It shall not be committed to source control or exposed in logs. | TD-11, operational security |
Interfaces
Section titled “Interfaces”Inbound: SPA → BFF API Routes
Section titled “Inbound: SPA → BFF API Routes”POST /api/image-upload
Section titled “POST /api/image-upload”Proxy for presigned POST credential generation (TD-13).
- Authentication: Session JWT (validated by BFF middleware)
- Request body:
{"contentType": "image/jpeg","contentLength": 245000}
- BFF action: Add
Authorization,X-Tenant-Id,X-Author,X-Request-IDheaders. Forward to BackendPOST /v1/item/image-upload. - Response: Pass through Backend response (url, formFields, objectKey, cdnUrl).
- Errors:
401— session invalid502— Backend unavailable
POST /api/storage/check-url
Section titled “POST /api/storage/check-url”URL reachability check with CORS fallback.
- Authentication: Session JWT
- Request body:
{"url": "https://example.com/product-photo.jpg"}
- BFF action:
- Validate URL is HTTPS and not targeting managed storage host.
- Validate URL is not a private/internal address (SSRF prevention).
- Perform HEAD request to URL (timeout 10s).
- If HEAD fails or returns no content-type, attempt GET with range header.
- Return result.
- Response:
{"reachable": true,"contentType": "image/jpeg","contentLength": 245000}
- Errors:
400— invalid URL, non-HTTPS, or targets managed storage401— session invalid422— URL unreachable, non-image content, or timeout429— rate limit exceeded
POST /api/storage/fetch-url
Section titled “POST /api/storage/fetch-url”Full image fetch proxy for CORS-blocked external URLs.
- Authentication: Session JWT
- Request body:
{"url": "https://example.com/product-photo.jpg"}
- BFF action:
- Same validation as
check-url(HTTPS, not managed storage, not SSRF). - Fetch full image content (GET, timeout 10s, max 10 MB response).
- Stream response back to SPA.
- Same validation as
- Response: Image bytes with
Content-Typeheader. - Errors:
- Same as
check-url 413— response exceeds max size
- Same as
POST /api/storage/cdn-cookies
Section titled “POST /api/storage/cdn-cookies”Issue CloudFront signed cookies for the active tenant.
- Authentication: Session JWT
- Request body: None (tenant derived from session context).
- BFF action:
- Validate session JWT.
- Extract tenant ID from authenticated context (FR-039).
- Build CloudFront custom policy:
Resource:https://assets.*/<tenantId>/*DateLessThan: now + cookie TTL (default 30 minutes)
- Sign policy with CloudFront private key (RSA-SHA1).
- Set three cookies on the response:
CloudFront-Policy(base64-encoded policy)CloudFront-Signature(RSA signature)CloudFront-Key-Pair-Id(public key ID)
- Cookie attributes:
Domain=.arda.cards; Path=/; Max-Age=1800; Secure; HttpOnly; SameSite=Lax(Max-Age matches the policy TTL of 30 minutes)
- Response:
204 No Content(cookies are inSet-Cookieheaders). - Errors:
401— session invalid500— signing key unavailable
Call patterns:
- Session start: SPA calls immediately after login completes.
- Proactive refresh: SPA sets a timer at ~50% of cookie TTL (e.g., every 15 minutes for a 30-minute TTL). Calls in the background.
- Tenant switch: SPA calls immediately when the user switches tenant. Shows loading state on images until the call completes.
- 403 recovery: If an
<img>request receives 403, SPA calls to refresh cookies and retries the image load.
PUT /api/items/<itemEId>
Section titled “PUT /api/items/<itemEId>”Proxy for entity update (existing BFF route pattern).
- Authentication: Session JWT
- BFF action: Add auth/tenant headers. Forward to Backend
PUT /v1/item/<itemEId>. - Response: Pass through Backend response.
Outbound: BFF → Backend
Section titled “Outbound: BFF → Backend”| Target | Method | Path | Purpose |
|---|---|---|---|
| Backend | POST | /v1/item/image-upload | Request presigned POST credentials |
| Backend | PUT | /v1/item/<itemEId> | Persist entity with imageUrl |
Outbound: BFF → External URLs
Section titled “Outbound: BFF → External URLs”| Target | Method | Purpose | Constraint |
|---|---|---|---|
| External HTTPS URL | HEAD / GET | Reachability check and image fetch | Rate-limited, SSRF-protected, 10s timeout |
The BFF is stateless for image operations. It does not cache, buffer, or store images or upload state. All state is either in the SPA (form state, editor state) or the Backend (entity persistence, presigned credential generation).
The only BFF-side state relevant to this feature is the rate limiter for the
reachability/fetch endpoints. The BFF runs as a persistent Next.js server on
AWS Amplify Hosting (not serverless Lambda). This means in-process memory is
available for the lifetime of the server instance. Rate limiting shall use an
in-memory sliding-window counter per instance (e.g., Map<tenantId, {count, windowStart}>). If Amplify scales to multiple instances, each instance
enforces the rate limit independently — the effective per-tenant limit is
configured_limit × instance_count. At current scale (single instance), this
is exact. At higher scale, the per-instance approach provides approximate
enforcement sufficient for abuse prevention. If precise cross-instance limiting
becomes necessary, a DynamoDB-backed counter can replace the in-memory map
without changing the endpoint contract.
Modules
Section titled “Modules”New Modules
Section titled “New Modules”| Module | Location | Purpose | Scenarios |
|---|---|---|---|
image-upload route | src/app/api/image-upload/route.ts | Proxy presigned POST credential requests to Backend. Module-scoped path (TD-13). | S1, S2, S5, S6, S7 |
check-url route | src/app/api/storage/check-url/route.ts | URL reachability check with SSRF protection. | S2 |
fetch-url route | src/app/api/storage/fetch-url/route.ts | Full image fetch proxy for CORS-blocked URLs. | S2 |
cdn-cookies route | src/app/api/storage/cdn-cookies/route.ts | Generate and set CloudFront signed cookies scoped to the active tenant. | S4 (all image display) |
cloudfront-signer | src/server/lib/cloudfront-signer.ts | CloudFront cookie signing utility. Loads private key from Secrets Manager or env var. Generates policy, signature, and key-pair-id cookies. | S4 |
Modified Modules
Section titled “Modified Modules”| Module | Location | Change | Scenarios |
|---|---|---|---|
proxy.ts | src/proxy.ts | Register new /api/storage/* routes in the BFF proxy configuration. | All |
SSRF Protection
Section titled “SSRF Protection”The reachability and fetch endpoints must validate target URLs before making outbound requests:
- Scheme: Only
https:allowed. - Host resolution: Resolve DNS and reject private IP ranges (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.0/8,169.254.0.0/16,::1). - Managed storage: Reject URLs matching the CDN host pattern (these are known-good and should be loaded directly by the SPA).
- Content-Type validation: The BFF shall validate that the response
Content-Typematchesimage/*. Non-image content types shall be rejected with 422. - Rate limiting: Per-tenant, per-minute request cap (configurable; suggested default: 30 requests/minute).
Testing Strategy
Section titled “Testing Strategy”| Test | Target | Infrastructure | Validates |
|---|---|---|---|
CloudFrontSignerTest | cloudfront-signer.ts | Unit (mocked Secrets Manager) | Policy generation, tenant-scoped Resource field, DateLessThan TTL, RSA signature |
CdnCookiesRouteTest | cdn-cookies route | Integration (test session) | Session validation, tenant extraction from context, Set-Cookie headers, cookie attributes (Secure, HttpOnly, SameSite) |
CdnCookiesTenantScopeTest | cdn-cookies route | Integration (multi-tenant) | Cookie policy Resource matches active tenant; tenant switch produces new cookies scoped to new tenant |
CdnCookiesKeyRotationTest | cloudfront-signer.ts | Unit (dual key setup) | Signing with new key while old key is active; verifying both signatures are valid |
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved