Skip to content

Implement Newsletter API integration (subscribe, confirm, unsubscribe, preferences) #447

Description

@0xdevcollins

Integrate the Boundless Newsletter API into the frontend: double opt-in subscribe, confirmation and unsubscribe by token, legacy unsubscribe by email, and update preferences. The backend exposes the endpoints below; this issue covers Next.js proxy routes, client API layer, and UI so the app uses the new newsletter flow end-to-end.

Background / current state

  • Subscribe: components/overview/Newsletter.tsx uses newsletterSubscribe() from lib/api/waitlist.ts. The Next.js route app/api/newsletter/subscribe/route.ts currently proxies to waitlist (${backendUrl}/api/waitlist/subscribe), not the newsletter API.
  • Types: NewsletterSubscribeRequest in lib/api/waitlist.ts only has email and name; the new API adds source and tags.
  • No flows yet: Confirm (double opt-in), one-click unsubscribe by token, unsubscribe by email, or update preferences. No frontend pages for confirmation/unsubscribe success.

Backend API contract (reference)

Base URL: NEXT_PUBLIC_API_URL (e.g. http://localhost:8000 or https://staging-api.boundlessfi.xyz). All paths below are relative to the backend.

Method Endpoint Purpose
POST /api/newsletter/subscribe Subscribe email (double opt-in); rate limited 5 req/min
GET /api/newsletter/confirm/{token} Confirm subscription (302 → frontend)
GET /api/newsletter/unsubscribe/{token} One-click unsubscribe (302 → frontend)
POST /api/newsletter/unsubscribe Legacy unsubscribe by email
PATCH /api/newsletter/preferences Update topic tags for a subscriber

1. Subscribe — POST /api/newsletter/subscribe

  • Body (JSON):
    • email (string, required)
    • name (string, optional)
    • source (string, optional)
    • tags (string[], optional) — topic tags
  • Responses:
    • 201: Confirmation email sent — body e.g. { "message": "...", "id": "clx1234567890" }
    • 400: Invalid tags
    • 409: Email already subscribed
    • 429: Too many requests (5 per minute)
  • Behavior: Sends confirmation email (double opt-in). Rate limit: 5 requests per minute.

2. Confirm subscription — GET /api/newsletter/confirm/{token}

  • Path: token = confirmation token from email.
  • Responses:
    • 302: Redirect to frontend confirmation page
    • 400: Token expired
    • 404: Invalid/expired token

3. Unsubscribe by token — GET /api/newsletter/unsubscribe/{token}

  • Path: token = unsubscribe token from email footer.
  • Responses:
    • 302: Redirect to frontend unsubscribe confirmation page
    • 404: Invalid link

4. Unsubscribe by email — POST /api/newsletter/unsubscribe

  • Body (JSON): email (string, required).
  • Responses:
    • 200: Success
    • 404: Subscriber not found

5. Update preferences — PATCH /api/newsletter/preferences

  • Body (JSON):
    • email (string, required)
    • tags (string[], required) — valid values: bounties, hackathons, grants, updates
  • Responses:
    • 200: Success
    • 400: Invalid tags
    • 404: Subscriber not found

Tasks

1. Next.js API routes (proxy to backend)

  • 1.1 POST /api/newsletter/subscribe

    • Change proxy target from /api/waitlist/subscribe to /api/newsletter/subscribe.
    • Forward body as-is (email, name, source, tags).
    • Return backend status and body for 201, 400, 409, 429 (no swallowing of 4xx body).
    • Use same backendUrl pattern as in app/api/newsletter/subscribe/route.ts (from NEXT_PUBLIC_API_URL).
  • 1.2 GET /api/newsletter/confirm/[token]

    • Implement route that calls backend GET /api/newsletter/confirm/{token}.
    • If backend returns 302, redirect user to the Location header (frontend confirmation page).
    • If backend returns 400/404, redirect to a frontend page that shows “expired” or “invalid token” (e.g. /newsletter/confirm/error or same page with query param).
  • 1.3 GET /api/newsletter/unsubscribe/[token]

    • Same idea: call backend GET /api/newsletter/unsubscribe/{token}.
    • On 302, redirect to Location.
    • On 404, redirect to an error page or show “invalid unsubscribe link”.
  • 1.4 POST /api/newsletter/unsubscribe

    • Proxy body { email } to backend POST /api/newsletter/unsubscribe.
    • Return 200/404 and backend body as appropriate.
  • 1.5 PATCH /api/newsletter/preferences

    • Proxy body { email, tags } to backend PATCH /api/newsletter/preferences.
    • Return 200/400/404 and backend body.

2. Client API layer

  • 2.1 Types

    • Define request/response types for: subscribe, unsubscribe by email, update preferences.
    • Document allowed tags: bounties, hackathons, grants, updates.
  • 2.2 Subscribe

    • Extend or replace newsletterSubscribe so it:
      • Calls POST /api/newsletter/subscribe with email, name, source, tags.
      • Returns the 201 payload (e.g. message, id).
      • On 400/409/429, throws or returns a structured error so the UI can show specific messages (invalid tags / already subscribed / rate limited).
  • 2.3 Unsubscribe by email

    • New function that calls POST /api/newsletter/unsubscribe with { email }, handles 200/404.
  • 2.4 Update preferences

    • New function that calls PATCH /api/newsletter/preferences with { email, tags }, validates tags client-side against the allowed list, handles 200/400/404.
  • 2.5 Confirm / unsubscribe by token

    • No client call needed if links in emails point to Next.js routes above; user is sent to /api/newsletter/confirm/[token] or /api/newsletter/unsubscribe/[token] and the server redirects.
    • Alternatively, document that backend emails must use app domain links (e.g. {NEXT_PUBLIC_APP_URL}/api/newsletter/confirm/{token}) so the proxy routes are used.

3. Frontend UI

  • 3.1 Subscribe form (components/overview/Newsletter.tsx)

    • Keep required: email, name.
    • Add optional: source (e.g. fixed "website" or from props), tags (e.g. multi-select or checkboxes for bounties, hackathons, grants, updates).
    • On success (201): show “Confirmation email sent. Please check your inbox.” (or backend message).
    • On 409: show “This email is already subscribed.”
    • On 429: show “Too many attempts. Please try again in a minute.”
    • On 400: show “Invalid input” or “Invalid tags” as appropriate.
    • Ensure form is disabled or shows loading while submitting to avoid double submits and rate-limit issues.
  • 3.2 Confirmation success page

    • Add a page (e.g. app/(landing)/newsletter/confirmed/page.tsx) that users land on after clicking the confirmation link (via backend 302 or after proxy redirect).
    • Content: “You’re subscribed” / “Subscription confirmed” and optional link back to home or newsletter CTA.
  • 3.3 Confirmation error page

    • Page (e.g. app/(landing)/newsletter/confirm/error/page.tsx or query on same page) for expired/invalid token: “This confirmation link has expired or is invalid.”
  • 3.4 Unsubscribe success page

    • Page (e.g. app/(landing)/newsletter/unsubscribed/page.tsx) after one-click unsubscribe: “You have been unsubscribed.”
  • 3.5 Unsubscribe by email (optional)

    • If product needs it: small form (email only) that calls POST /api/newsletter/unsubscribe and shows success/not-found message. Could live on a “Manage subscription” or “Unsubscribe” page linked from footer or emails.
  • 3.6 Update preferences (optional)

    • If product needs it: page or modal where user enters email and selects tags (bounties, hackathons, grants, updates), then calls PATCH /api/newsletter/preferences. Show success/400/404.
  • If waitlist is fully replaced by newsletter for this flow, remove lib/api/waitlist.ts newsletterSubscribe and any references so the single source of truth is the new newsletter API and client helpers.


Acceptance criteria

  • POST /api/newsletter/subscribe proxies to backend /api/newsletter/subscribe and returns 201/400/409/429 with correct body.
  • Subscribe form sends email, name, and optionally source and tags; shows success and handles 409/429/400 with clear messages.
  • GET /api/newsletter/confirm/[token] and GET /api/newsletter/unsubscribe/[token] proxy to backend and redirect user to the appropriate frontend page (success or error).
  • Confirmation and unsubscribe success/error pages exist and render correct copy.
  • POST /api/newsletter/unsubscribe and PATCH /api/newsletter/preferences proxy to backend and return correct status/body.
  • Client helpers exist for subscribe, unsubscribe by email, and update preferences, with typed requests and error handling.
  • Valid tags and rate limit are documented; no (or minimal) references to waitlist for newsletter subscribe.

Files to touch (reference)

Area Files
API routes app/api/newsletter/subscribe/route.ts, new: app/api/newsletter/confirm/[token]/route.ts, app/api/newsletter/unsubscribe/[token]/route.ts, app/api/newsletter/unsubscribe/route.ts, app/api/newsletter/preferences/route.ts
Client API lib/api/waitlist.ts and/or new lib/api/newsletter.ts
UI components/overview/Newsletter.tsx, new: app/(landing)/newsletter/confirmed/page.tsx, app/(landing)/newsletter/confirm/error/page.tsx, app/(landing)/newsletter/unsubscribed/page.tsx (and optional manage/unsubscribe/preferences pages)
Config Backend must redirect 302 to NEXT_PUBLIC_APP_URL for confirm/unsubscribe, or use app-domain links to Next.js proxy routes

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions