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
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
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 |
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
components/overview/Newsletter.tsxusesnewsletterSubscribe()fromlib/api/waitlist.ts. The Next.js routeapp/api/newsletter/subscribe/route.tscurrently proxies to waitlist (${backendUrl}/api/waitlist/subscribe), not the newsletter API.NewsletterSubscribeRequestinlib/api/waitlist.tsonly hasemailandname; the new API addssourceandtags.Backend API contract (reference)
Base URL:
NEXT_PUBLIC_API_URL(e.g.http://localhost:8000orhttps://staging-api.boundlessfi.xyz). All paths below are relative to the backend.POST/api/newsletter/subscribeGET/api/newsletter/confirm/{token}GET/api/newsletter/unsubscribe/{token}POST/api/newsletter/unsubscribePATCH/api/newsletter/preferences1. Subscribe —
POST /api/newsletter/subscribeemail(string, required)name(string, optional)source(string, optional)tags(string[], optional) — topic tags201: Confirmation email sent — body e.g.{ "message": "...", "id": "clx1234567890" }400: Invalid tags409: Email already subscribed429: Too many requests (5 per minute)2. Confirm subscription —
GET /api/newsletter/confirm/{token}token= confirmation token from email.302: Redirect to frontend confirmation page400: Token expired404: Invalid/expired token3. Unsubscribe by token —
GET /api/newsletter/unsubscribe/{token}token= unsubscribe token from email footer.302: Redirect to frontend unsubscribe confirmation page404: Invalid link4. Unsubscribe by email —
POST /api/newsletter/unsubscribeemail(string, required).200: Success404: Subscriber not found5. Update preferences —
PATCH /api/newsletter/preferencesemail(string, required)tags(string[], required) — valid values:bounties,hackathons,grants,updates200: Success400: Invalid tags404: Subscriber not foundTasks
1. Next.js API routes (proxy to backend)
1.1
POST /api/newsletter/subscribe/api/waitlist/subscribeto/api/newsletter/subscribe.email,name,source,tags).backendUrlpattern as inapp/api/newsletter/subscribe/route.ts(fromNEXT_PUBLIC_API_URL).1.2
GET /api/newsletter/confirm/[token]GET /api/newsletter/confirm/{token}.Locationheader (frontend confirmation page)./newsletter/confirm/erroror same page with query param).1.3
GET /api/newsletter/unsubscribe/[token]GET /api/newsletter/unsubscribe/{token}.Location.1.4
POST /api/newsletter/unsubscribe{ email }to backendPOST /api/newsletter/unsubscribe.1.5
PATCH /api/newsletter/preferences{ email, tags }to backendPATCH /api/newsletter/preferences.2. Client API layer
2.1 Types
bounties,hackathons,grants,updates.2.2 Subscribe
newsletterSubscribeso it:POST /api/newsletter/subscribewithemail,name,source,tags.message,id).2.3 Unsubscribe by email
POST /api/newsletter/unsubscribewith{ email }, handles 200/404.2.4 Update preferences
PATCH /api/newsletter/preferenceswith{ email, tags }, validates tags client-side against the allowed list, handles 200/400/404.2.5 Confirm / unsubscribe by token
/api/newsletter/confirm/[token]or/api/newsletter/unsubscribe/[token]and the server redirects.{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)email,name.source(e.g. fixed"website"or from props),tags(e.g. multi-select or checkboxes for bounties, hackathons, grants, updates).message).3.2 Confirmation success page
app/(landing)/newsletter/confirmed/page.tsx) that users land on after clicking the confirmation link (via backend 302 or after proxy redirect).3.3 Confirmation error page
app/(landing)/newsletter/confirm/error/page.tsxor query on same page) for expired/invalid token: “This confirmation link has expired or is invalid.”3.4 Unsubscribe success page
app/(landing)/newsletter/unsubscribed/page.tsx) after one-click unsubscribe: “You have been unsubscribed.”3.5 Unsubscribe by email (optional)
POST /api/newsletter/unsubscribeand shows success/not-found message. Could live on a “Manage subscription” or “Unsubscribe” page linked from footer or emails.3.6 Update preferences (optional)
PATCH /api/newsletter/preferences. Show success/400/404.If waitlist is fully replaced by newsletter for this flow, remove
lib/api/waitlist.tsnewsletterSubscribeand any references so the single source of truth is the new newsletter API and client helpers.Acceptance criteria
POST /api/newsletter/subscribeproxies to backend/api/newsletter/subscribeand returns 201/400/409/429 with correct body.email,name, and optionallysourceandtags; shows success and handles 409/429/400 with clear messages.GET /api/newsletter/confirm/[token]andGET /api/newsletter/unsubscribe/[token]proxy to backend and redirect user to the appropriate frontend page (success or error).POST /api/newsletter/unsubscribeandPATCH /api/newsletter/preferencesproxy to backend and return correct status/body.Files to touch (reference)
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.tslib/api/waitlist.tsand/or newlib/api/newsletter.tscomponents/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)NEXT_PUBLIC_APP_URLfor confirm/unsubscribe, or use app-domain links to Next.js proxy routes