Run 5: BFF Routes — Project Plan
Author: Technical Writer / Team Lead
Date: 2026-04-07
Status: Planning
Phase: 3.5 — BFF Routes
Branch: jmpicnic/image-upload-frontend
Summary
Section titled “Summary”Implement the server-side BFF route modules, security utilities, and the
src/app/api/ thin re-exports in arda-frontend-app. This run produces the
src/server/ directory consumed by Run 6 (SPA integration). All code targets
the Next.js server runtime only — the server-only package enforces the
client/server boundary at build time.
Entry Criteria
Section titled “Entry Criteria”| Criterion | Verification |
|---|---|
Run 1 exit gate passed: @arda-cards/api-proxy published with createImageUploadUrl() on ItemProxy | npm view @arda-cards/api-proxy versions --registry=https://npm.pkg.github.com shows a published version |
Phase 3.0 legacy cleanup committed to arda-frontend-app branch | git -C /Users/jmp/code/arda/projects/image-upload-frontend-worktrees/arda-frontend-app log --oneline -5 — cleanup commit present |
Branch jmpicnic/image-upload-frontend is clean and up to date | git -C /Users/jmp/code/arda/projects/image-upload-frontend-worktrees/arda-frontend-app status — clean |
Task List
Section titled “Task List”| # | Task | Persona | Depends On | Status | Acceptance Criteria |
|---|---|---|---|---|---|
| T-1 | Directory scaffolding and server-only dependency | front-end-engineer | Entry criteria | Pending | npm install server-only succeeds; src/server/ directory created with index.ts, routes/image-upload.ts, routes/storage/check-url.ts, routes/storage/fetch-url.ts, routes/storage/cdn-cookies.ts, lib/ssrf-validator.ts, lib/rate-limiter.ts, lib/cloudfront-signer.ts, __tests__/; src/server/index.ts contains import 'server-only' |
| T-1b | AWS naming utility (src/server/lib/aws-naming.ts) with NEXT_PUBLIC_ env vars | front-end-engineer | T-1 | Pending | Exports AwsNaming interface and resolveAwsNaming(); reads NEXT_PUBLIC_INFRASTRUCTURE and NEXT_PUBLIC_PARTITION with non-prefixed fallback; cookieTtlMs defaults to 30 min; cookieRefreshIntervalMs = 50% of TTL; unit tests pass for all 5 cases in specification |
| T-1c | Generic secrets utility (src/server/lib/secrets.ts) with 8-hour cache TTL | front-end-engineer | T-1 | Pending | Exports getSecret(secretId, cacheTtlMs?) and clearSecretCache(); default TTL is 8 hours; in-memory cache with eviction; unit tests pass for all 8 cases in specification |
| T-2 | SSRF validator (src/server/lib/ssrf-validator.ts) with DNS resolution check | front-end-engineer | T-1b | Pending | Exports async validateUrl(url): Promise<{valid, error?}>; validates HTTPS scheme; checks CDN host pattern; performs DNS resolution via dns.resolve() (not OS resolver); rejects private IP ranges; unit tests pass for all 13 cases in specification |
| T-3 | Rate limiter (src/server/lib/rate-limiter.ts) with machine-readable error codes (FD-12) | front-end-engineer | T-1 | Pending | RateLimiter class with check(tenantId) returning {allowed, retryAfterMs?}; sliding window per tenant; default new RateLimiter(30, 60_000); 429 response body uses { "code": "RATE_LIMITED", "retryAfterMs": <value> }; unit tests pass for all 5 cases |
| T-4 | CloudFront signer (src/server/lib/cloudfront-signer.ts) using Secrets Manager (FD-10) | front-end-engineer | T-1b, T-1c | Pending | Exports sign(tenantId, ttlMs?) returning CloudFrontCookies; fetches private key from Secrets Manager using getSecret(naming.signingKeySecretName); CLOUDFRONT_SIGNING_KEY_SECRET_ARN env var not required (name derived from naming convention); default TTL 30 min; unit tests pass for all 12 cases |
| T-5 | Image upload route (src/server/routes/image-upload.ts) using RequestContext (FD-13, REQ-FE-028) | front-end-engineer | T-4 | Pending | Handler verifies JWT, constructs RequestContext with author, tenantId, userId; calls proxy.createImageUploadUrl(request, { context }); returns backend response unmodified; thin re-export at src/app/api/image-upload/route.ts; unit tests pass for all 4 cases |
| T-6 | Check-URL route (src/server/routes/storage/check-url.ts) with Content-Type validation (REQ-FE-024, REQ-FE-027) | front-end-engineer | T-2, T-3 | Pending | HEAD then GET-with-Range fallback; response body discarded after header inspection; validates Content-Type: image/*; 422 on non-image content type; thin re-export at src/app/api/storage/check-url/route.ts; unit tests pass for all 9 cases |
| T-7 | Fetch-URL route (src/server/routes/storage/fetch-url.ts) with Content-Type validation (REQ-FE-024) | front-end-engineer | T-2, T-3 | Pending | GET with 10s timeout and 10 MB max; validates Content-Type: image/*; 413 on body exceeding 10 MB; streams response; thin re-export at src/app/api/storage/fetch-url/route.ts; unit tests pass for all 7 cases |
| T-8 | CDN cookies route (src/server/routes/storage/cdn-cookies.ts) with Max-Age matching policy TTL (REQ-FE-026) | front-end-engineer | T-4 | Pending | tenantId extracted from session only (never from request body); three Set-Cookie headers with Domain=.arda.cards; Path=/; Max-Age=1800; Secure; HttpOnly; SameSite=Lax; returns 204; thin re-export at src/app/api/storage/cdn-cookies/route.ts; unit tests pass for all 10 cases |
| T-9 | Documentation commit | front-end-engineer | T-1 through T-8 | Pending | make pr-checks passes in documentation worktree; changes committed referencing Phase 3.5 |
Key Decisions in Effect
Section titled “Key Decisions in Effect”| Decision | Summary |
|---|---|
| FD-02 (Hybrid code organization) | All server logic in src/server/; src/app/api/ contains only thin re-exports |
| FD-10 (Secrets Manager for CloudFront key) | Private key fetched at runtime via SecretsManagerClient; cached in memory; no key in env vars |
| FD-12 (Machine-readable error codes) | Rate limit 429 body uses { "code": "RATE_LIMITED", "retryAfterMs": <n> }; SPA formats user message |
| FD-13 (Type re-exports from api-proxy) | RequestContext imported from @arda-cards/api-proxy; used in image-upload route handler |
| REQ-FE-028 (RequestContext) | Route handler constructs RequestContext from JWT claims; passed to proxy |
Exit Criteria
Section titled “Exit Criteria”| Criterion | Verification |
|---|---|
| All routes and utilities implemented with passing unit tests | npx jest --no-coverage --watchAll=false --forceExit — all tests green |
| Lint passes | npm run lint — no errors |
| TypeScript typecheck passes | npx tsc --noEmit — no errors |
Full Next.js build passes (catches server-only violations) | npm run build — succeeds with no errors |
Documentation committed with make pr-checks passing | git -C /Users/jmp/code/arda/projects/image-upload-frontend-worktrees/documentation log --oneline -3 |
Handoff
Section titled “Handoff”Artifacts Consumed
Section titled “Artifacts Consumed”| Artifact | Source | Produced By |
|---|---|---|
@arda-cards/api-proxy published with createImageUploadUrl() on ItemProxy | GitHub Packages | Run 1 |
Phase 3.0 legacy cleanup committed to arda-frontend-app | arda-frontend-app worktree | Run 1 preparation / Phase 3.0 |
Artifacts Produced
Section titled “Artifacts Produced”| Artifact | Location | Consumed By |
|---|---|---|
src/server/ directory with 4 BFF routes (image-upload, check-url, fetch-url, cdn-cookies) and 3 utilities (aws-naming, secrets, ssrf-validator, rate-limiter, cloudfront-signer) | arda-frontend-app worktree | Run 6 (SPA hooks call these routes via fetch()) |
src/app/api/ thin re-exports (4 route files) | arda-frontend-app worktree | Next.js router (maps URL paths to handlers) |
| Documentation commit referencing Phase 3.5 | documentation worktree | Run 7 (documentation PR) |
Agent Prompt Template
Section titled “Agent Prompt Template”Use the following prompt to spawn the front-end-engineer agent for this run.
Paste the full text as the Task tool input.
You are a front-end-engineer working in the arda-frontend-app worktree for theimage-upload-frontend project.
**Primary worktree**: /Users/jmp/code/arda/projects/image-upload-frontend-worktrees/arda-frontend-app**Branch**: jmpicnic/image-upload-frontend**Documentation worktree**: /Users/jmp/code/arda/projects/image-upload-frontend-worktrees/documentation
Load the following skills before starting:- typescript-coding- unit-tests-frontend- path-conventions- document-writing
Read the specification before beginning:/Users/jmp/code/arda/projects/image-upload-frontend-worktrees/documentation/src/content/docs/roadmap/completed/item-image-upload/3-frontend-implementation/3.5-bff-routes/specification.md
Your task is to execute Run 5 of the image-upload-frontend project (Phase 3.5 —BFF Routes). Complete the following tasks in order:
T-1: Install `server-only` via `npm install server-only`. Create `src/server/` directory layout per the specification (§T-1). Create `src/server/index.ts` with `import 'server-only'`.
T-1b: Create `src/server/lib/aws-naming.ts` with `AwsNaming` interface and `resolveAwsNaming()` function. Reads `NEXT_PUBLIC_INFRASTRUCTURE` and `NEXT_PUBLIC_PARTITION` (with non-prefixed fallback). Write unit tests in `src/server/__tests__/aws-naming.test.ts` covering all 5 cases.
T-1c: Create `src/server/lib/secrets.ts` with `getSecret()` and `clearSecretCache()`. Default 8-hour cache TTL. Write unit tests in `src/server/__tests__/secrets.test.ts` covering all 8 cases.
T-2: Create `src/server/lib/ssrf-validator.ts` with async `validateUrl()`. Use `dns.resolve4` and `dns.resolve6` (not OS resolver). Reject private IP ranges, HTTP scheme, CDN host pattern, and DNS failures. Write unit tests covering all 13 cases.
T-3: Create `src/server/lib/rate-limiter.ts` with `RateLimiter` class. Sliding window per tenant. 429 body: `{ "code": "RATE_LIMITED", "retryAfterMs": <value> }` with `Retry-After` header in seconds. Write unit tests covering all 5 cases.
T-4: Create `src/server/lib/cloudfront-signer.ts`. Load private key from Secrets Manager using `getSecret(naming.signingKeySecretName)`. Build CloudFront custom policy JSON, base64-encode, sign with RSA-SHA1. Default TTL 30 min. Write unit tests covering all 12 cases.
T-5: Create `src/server/routes/image-upload.ts`. Verify JWT, construct RequestContext (author, tenantId, userId from JWT claims), call `proxy.createImageUploadUrl(request, { context })`. Create thin re-export at `src/app/api/image-upload/route.ts`. Write unit tests covering all 4 cases.
T-6: Create `src/server/routes/storage/check-url.ts`. HEAD then GET-with-Range fallback; discard body after header inspection; validate Content-Type matches `image/*`. Create thin re-export. Write unit tests covering all 9 cases.
T-7: Create `src/server/routes/storage/fetch-url.ts`. GET with 10s timeout and 10 MB max; validate Content-Type matches `image/*`; stream response. Create thin re-export. Write unit tests covering all 7 cases.
T-8: Create `src/server/routes/storage/cdn-cookies.ts`. Extract tenantId from session only. Set three CloudFront cookies with `Domain=.arda.cards; Path=/; Max-Age=1800; Secure; HttpOnly; SameSite=Lax`. Return 204. Create thin re-export. Write unit tests covering all 10 cases.
T-9: Run the full check suite: npm run lint npx tsc --noEmit npx jest --no-coverage --watchAll=false --forceExit npm run build All must pass. Then in the documentation worktree run `make pr-checks` and commit changes referencing Phase 3.5.
All tool calls must use absolute paths. Never use `cd` — use absolute pathsor `git -C <path>` for git commands.
Report completion with: (a) a summary of all files created, (b) test passcounts, and (c) confirmation that `npm run build` and `make pr-checks` passed.Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved