Skip to content

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).

IDRequirementScenariosSource
BFF-FR-001Proxy presigned POST credential requests from SPA to Backend. Add Authorization, X-Tenant-Id, X-Author, and X-Request-ID headers.S1, S2, S5, S6, S7FR-020, TD-03
BFF-FR-002Proxy entity update requests (PUT) from SPA to Backend. Add authentication and tenant headers.S1, S2, S3, S5, S6, S7FR-022
BFF-FR-003Provide 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.S2FR-011, TD-02
BFF-FR-004The reachability endpoint shall only check external URLs. Reject requests targeting managed storage URLs (CDN host pattern).S2NFR-008, TD-02
BFF-FR-005Provide a URL fetch endpoint (or extend the reachability endpoint) to proxy the full image content when the SPA cannot fetch due to CORS.S2FR-004, TD-01, TD-02
BFF-FR-006The reachability/fetch endpoint shall implement request-rate limiting per tenant to prevent abuse.S2NFR-009
BFF-FR-007The reachability/fetch endpoint shall validate the target URL to prevent SSRF attacks (reject private IP ranges, localhost, non-HTTPS schemes).S2NFR-009
BFF-FR-008Issue 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.S4FR-036, FR-039, NFR-017, NFR-018, TD-11
BFF-FR-009Issue signed cookies on session establishment, on explicit refresh request from the SPA, and support immediate re-issuance on tenant switch.S4FR-037, FR-038, TD-12
BFF-FR-010Signed 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.S4NFR-019, TD-12
IDRequirementSource
BFF-NFR-001The BFF shall not buffer or store image file bytes. It is not in the data path for uploads.TD-06
BFF-NFR-002The reachability/fetch endpoint shall time out external requests within 10 seconds to prevent hanging connections.Derived from UX responsiveness
BFF-NFR-003All BFF endpoints shall require a valid authenticated session (JWT).Existing architecture
BFF-NFR-004The 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

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-ID headers. Forward to Backend POST /v1/item/image-upload.
  • Response: Pass through Backend response (url, formFields, objectKey, cdnUrl).
  • Errors:
    • 401 — session invalid
    • 502 — Backend unavailable

URL reachability check with CORS fallback.

  • Authentication: Session JWT
  • Request body:
    {
    "url": "https://example.com/product-photo.jpg"
    }
  • BFF action:
    1. Validate URL is HTTPS and not targeting managed storage host.
    2. Validate URL is not a private/internal address (SSRF prevention).
    3. Perform HEAD request to URL (timeout 10s).
    4. If HEAD fails or returns no content-type, attempt GET with range header.
    5. Return result.
  • Response:
    {
    "reachable": true,
    "contentType": "image/jpeg",
    "contentLength": 245000
    }
  • Errors:
    • 400 — invalid URL, non-HTTPS, or targets managed storage
    • 401 — session invalid
    • 422 — URL unreachable, non-image content, or timeout
    • 429 — rate limit exceeded

Full image fetch proxy for CORS-blocked external URLs.

  • Authentication: Session JWT
  • Request body:
    {
    "url": "https://example.com/product-photo.jpg"
    }
  • BFF action:
    1. Same validation as check-url (HTTPS, not managed storage, not SSRF).
    2. Fetch full image content (GET, timeout 10s, max 10 MB response).
    3. Stream response back to SPA.
  • Response: Image bytes with Content-Type header.
  • Errors:
    • Same as check-url
    • 413 — response exceeds max size

Issue CloudFront signed cookies for the active tenant.

  • Authentication: Session JWT
  • Request body: None (tenant derived from session context).
  • BFF action:
    1. Validate session JWT.
    2. Extract tenant ID from authenticated context (FR-039).
    3. Build CloudFront custom policy:
      • Resource: https://assets.*/<tenantId>/*
      • DateLessThan: now + cookie TTL (default 30 minutes)
    4. Sign policy with CloudFront private key (RSA-SHA1).
    5. Set three cookies on the response:
      • CloudFront-Policy (base64-encoded policy)
      • CloudFront-Signature (RSA signature)
      • CloudFront-Key-Pair-Id (public key ID)
    6. 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 in Set-Cookie headers).
  • Errors:
    • 401 — session invalid
    • 500 — 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.

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.
TargetMethodPathPurpose
BackendPOST/v1/item/image-uploadRequest presigned POST credentials
BackendPUT/v1/item/<itemEId>Persist entity with imageUrl
TargetMethodPurposeConstraint
External HTTPS URLHEAD / GETReachability check and image fetchRate-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.


ModuleLocationPurposeScenarios
image-upload routesrc/app/api/image-upload/route.tsProxy presigned POST credential requests to Backend. Module-scoped path (TD-13).S1, S2, S5, S6, S7
check-url routesrc/app/api/storage/check-url/route.tsURL reachability check with SSRF protection.S2
fetch-url routesrc/app/api/storage/fetch-url/route.tsFull image fetch proxy for CORS-blocked URLs.S2
cdn-cookies routesrc/app/api/storage/cdn-cookies/route.tsGenerate and set CloudFront signed cookies scoped to the active tenant.S4 (all image display)
cloudfront-signersrc/server/lib/cloudfront-signer.tsCloudFront cookie signing utility. Loads private key from Secrets Manager or env var. Generates policy, signature, and key-pair-id cookies.S4
ModuleLocationChangeScenarios
proxy.tssrc/proxy.tsRegister new /api/storage/* routes in the BFF proxy configuration.All

The reachability and fetch endpoints must validate target URLs before making outbound requests:

  1. Scheme: Only https: allowed.
  2. 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).
  3. Managed storage: Reject URLs matching the CDN host pattern (these are known-good and should be loaded directly by the SPA).
  4. Content-Type validation: The BFF shall validate that the response Content-Type matches image/*. Non-image content types shall be rejected with 422.
  5. Rate limiting: Per-tenant, per-minute request cap (configurable; suggested default: 30 requests/minute).
TestTargetInfrastructureValidates
CloudFrontSignerTestcloudfront-signer.tsUnit (mocked Secrets Manager)Policy generation, tenant-scoped Resource field, DateLessThan TTL, RSA signature
CdnCookiesRouteTestcdn-cookies routeIntegration (test session)Session validation, tenant extraction from context, Set-Cookie headers, cookie attributes (Secure, HttpOnly, SameSite)
CdnCookiesTenantScopeTestcdn-cookies routeIntegration (multi-tenant)Cookie policy Resource matches active tenant; tenant switch produces new cookies scoped to new tenant
CdnCookiesKeyRotationTestcloudfront-signer.tsUnit (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