Skip to content

Commit 93baa83

Browse files
authored
feat: short base62 tokens for email tracking links (#2691)
* fix: keep campaign sender daily limit default at 500 in UI Use a frontend constant for senderDailyLimit default and stop overriding it from sender-options payload. Also render toast CTA on a new line under the message text. * shorten email tracking links via nuxt server routes * refactor: move tracking redirect helper to shared edge utils * fix: remove unnecessary async in short-link routes * feat: switch email tracking links to short base62 tokens
1 parent 3693351 commit 93baa83

File tree

9 files changed

+519
-42
lines changed

9 files changed

+519
-42
lines changed

deno.lock

Lines changed: 51 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Shared HTTP Redirect Helper (Design)
2+
3+
Goal
4+
5+
- Centralize redirect response construction for edge functions so CORS headers are always included.
6+
7+
Scope
8+
9+
- Move buildRedirectResponse into shared utilities.
10+
- Update email-campaigns to import shared helper.
11+
- No behavior changes to redirect status or destination URLs.
12+
13+
Architecture
14+
15+
- New shared module: `supabase/functions/_shared/http.ts`.
16+
- Helper uses existing `cors.ts` and returns a 302 with Location.
17+
- Edge functions call helper instead of inline Response construction.
18+
19+
Components & Data Flow
20+
21+
- `_shared/http.ts`: `buildRedirectResponse(location)`.
22+
- `email-campaigns/index.ts`: uses helper in `/track/click/:token` and `/unsubscribe/:token` paths.
23+
- Future edge functions can reuse the helper to avoid CORS drift.
24+
25+
Error Handling
26+
27+
- No new error behavior; redirects remain 302 and preserve target URL.
28+
29+
Testing
30+
31+
- Deno unit test validates status, Location, and CORS headers for helper.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Shared HTTP Redirect Helper Implementation Plan
2+
3+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4+
5+
**Goal:** Move the redirect helper into shared edge-function utilities so multiple functions can reuse it consistently.
6+
7+
**Architecture:** Create `_shared/http.ts` with `buildRedirectResponse` and update email-campaigns to import it. Keep redirect behavior unchanged.
8+
9+
**Tech Stack:** Supabase Edge Functions (Deno), Hono
10+
11+
---
12+
13+
### Task 1: Move helper into shared utilities
14+
15+
**Files:**
16+
17+
- Create: `supabase/functions/_shared/http.ts`
18+
- Modify: `supabase/functions/email-campaigns/http.ts`
19+
- Modify: `supabase/functions/email-campaigns/http.test.ts`
20+
21+
**Step 1: Write the failing test**
22+
23+
Update `supabase/functions/email-campaigns/http.test.ts` to import from shared:
24+
25+
```ts
26+
import { buildRedirectResponse } from "../_shared/http.ts";
27+
```
28+
29+
Expected failure until shared helper exists.
30+
31+
**Step 2: Run test to verify it fails**
32+
33+
Run: `deno test -A supabase/functions/email-campaigns/http.test.ts`
34+
Expected: FAIL with module not found `../_shared/http.ts`
35+
36+
**Step 3: Implement shared helper**
37+
38+
Create `supabase/functions/_shared/http.ts`:
39+
40+
```ts
41+
import corsHeaders from "./cors.ts";
42+
43+
export const buildRedirectResponse = (location: string): Response => {
44+
const headers = new Headers(corsHeaders);
45+
headers.set("Location", location);
46+
47+
return new Response(null, {
48+
status: 302,
49+
headers,
50+
});
51+
};
52+
```
53+
54+
**Step 4: Re-export from email-campaigns (temporary)**
55+
56+
Update `supabase/functions/email-campaigns/http.ts` to re-export:
57+
58+
```ts
59+
export { buildRedirectResponse } from "../_shared/http.ts";
60+
```
61+
62+
**Step 5: Run test to verify it passes**
63+
64+
Run: `deno test -A supabase/functions/email-campaigns/http.test.ts`
65+
Expected: PASS
66+
67+
**Step 6: Commit**
68+
69+
```bash
70+
git add supabase/functions/_shared/http.ts supabase/functions/email-campaigns/http.ts supabase/functions/email-campaigns/http.test.ts
71+
git commit -m "refactor: share redirect response helper"
72+
```
73+
74+
---
75+
76+
### Task 2: Update email-campaigns imports to shared helper
77+
78+
**Files:**
79+
80+
- Modify: `supabase/functions/email-campaigns/index.ts`
81+
82+
**Step 1: Write the failing test**
83+
84+
No new unit test required; rely on existing `http.test.ts` and lint/type-check.
85+
86+
**Step 2: Update imports**
87+
88+
Change import to:
89+
90+
```ts
91+
import { buildRedirectResponse } from "../_shared/http.ts";
92+
```
93+
94+
**Step 3: Run tests**
95+
96+
Run: `deno test -A supabase/functions/email-campaigns/http.test.ts`
97+
Expected: PASS
98+
99+
**Step 4: Commit**
100+
101+
```bash
102+
git add supabase/functions/email-campaigns/index.ts
103+
git commit -m "refactor: use shared redirect helper"
104+
```
105+
106+
---
107+
108+
### Task 3: Remove email-campaigns helper shim
109+
110+
**Files:**
111+
112+
- Delete: `supabase/functions/email-campaigns/http.ts`
113+
114+
**Step 1: Update test import to shared helper**
115+
116+
Ensure `supabase/functions/email-campaigns/http.test.ts` already imports from `../_shared/http.ts`.
117+
118+
**Step 2: Remove file**
119+
120+
Delete `supabase/functions/email-campaigns/http.ts`.
121+
122+
**Step 3: Run tests**
123+
124+
Run: `deno test -A supabase/functions/email-campaigns/http.test.ts`
125+
Expected: PASS
126+
127+
**Step 4: Commit**
128+
129+
```bash
130+
git add supabase/functions/email-campaigns/http.test.ts
131+
git rm supabase/functions/email-campaigns/http.ts
132+
git commit -m "refactor: remove email-campaigns redirect shim"
133+
```
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Short URLs for Email Tracking (Design)
2+
3+
Goal
4+
5+
- Replace long Supabase function URLs in email tracking (open/click/unsubscribe) with short frontend-domain paths.
6+
- Keep existing tokens and tracking data model unchanged.
7+
8+
Scope
9+
10+
- Nuxt server routes for /o/:token, /c/:token, /u/:token to proxy to the email-campaigns edge function.
11+
- Email-campaigns edge function generates short links using the frontend domain.
12+
- No new database columns or tokens.
13+
14+
Architecture
15+
16+
- Nuxt/Nitro handles public short routes and forwards requests to Supabase edge function paths.
17+
- Edge function remains the source of truth for tracking, logging events, and redirects.
18+
- URL base for short links: FRONTEND_HOST (preferred) or public Supabase URL fallback.
19+
20+
Data Flow
21+
22+
- Email send flow generates tracking URLs using buildOpenTrackingUrl/buildClickTrackingUrl/buildUnsubscribeUrl.
23+
- Email HTML includes short links and pixel URL.
24+
- Nuxt routes proxy to:
25+
- /track/open/:token (returns 1x1 GIF)
26+
- /track/click/:token (logs click, 302 to original URL)
27+
- /unsubscribe/:token (updates consent, 302 to frontend success/failure)
28+
29+
Error Handling
30+
31+
- Missing token in Nuxt route returns 400.
32+
- Missing SAAS_SUPABASE_PROJECT_URL in frontend config returns 500.
33+
- Invalid tracking token returns 404 from edge function.
34+
- Redirect responses include CORS headers for consistency.
35+
36+
Risks and Mitigations
37+
38+
- Misconfigured FRONTEND_HOST could break unsubscribe redirects: ensure env is set in edge function runtime.
39+
- Proxy failures return explicit errors to aid debugging.
40+
- No new persistence changes reduces migration risk.
41+
42+
Testing
43+
44+
- Local end-to-end: create campaign, verify /o, /c, /u links resolve and log events.
45+
- Verify redirects preserve status codes and headers.
46+
- Spot-check unsubscribe success/failure paths.

0 commit comments

Comments
 (0)