You share a product demo with a prospect. The analytics show 5 views. But who watched? Did the decision-maker see it, or just the person you sent the link to? View counts can't answer that.
We added an email gate to SendRec. When enabled on a video, viewers enter their email before they can watch. The email shows up in the per-video analytics page alongside view count and completion percentage. You know exactly who watched your video and how much of it they saw.
The gate flow
A video in SendRec can have two gates: a password gate (existing feature) and an email gate (new). They're evaluated in order — password first, then email. If a video has both, the viewer enters the password, then sees the email form.
The email gate is a minimal form matching the password gate's dark theme — just a title, a subtitle ("Enter your email to watch this video"), an email input, and a "Watch Video" button. No name field, no company field. One input, minimal friction.
When the viewer submits their email, a POST request goes to the identify endpoint:
POST /api/watch/{shareToken}/identify
{"email": "alice@example.com"}
The server validates the email, records it in the database, sets a signed cookie, and returns 200. The page reloads, the cookie is present, and the video plays.
Cookie-based authentication
The email gate uses the same pattern as the password gate — an HMAC-signed cookie that proves the viewer has already identified themselves.
The cookie name is eg_ followed by the first 8 characters of the share token. The value is the email address and an HMAC signature, separated by a pipe:
eg_a048914d = alice@example.com|3f8a7b2e...
When the watch page loads, it checks for this cookie and verifies the signature:
func verifyEmailGateCookie(hmacSecret, shareToken, cookieValue string) (string, bool) {
parts := strings.SplitN(cookieValue, "|", 2)
if len(parts) != 2 {
return "", false
}
email := parts[0]
expected := signEmailGateCookie(hmacSecret, shareToken, email)
if !hmac.Equal([]byte(expected), []byte(cookieValue)) {
return "", false
}
return email, true
}
The hmac.Equal comparison is timing-safe — it prevents an attacker from guessing the signature byte by byte by measuring response times. The email is extracted from the cookie value itself, not from a separate source, so the cookie is self-contained.
The cookie lasts 7 days. A viewer who enters their email once can return to the same video without being asked again.
Linking emails to anonymous analytics
SendRec's analytics use an anonymous viewer hash — a SHA-256 of the IP address and User-Agent string. This is what deduplicates views, milestone tracking, and CTA clicks. But it's anonymous. You see "42 unique viewers" without knowing who they are.
The email gate bridges this gap with a video_viewers table:
CREATE TABLE video_viewers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
video_id UUID NOT NULL REFERENCES videos(id) ON DELETE CASCADE,
email TEXT NOT NULL,
viewer_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(video_id, email)
);
When a viewer submits their email, the server computes their viewer hash from the request's IP and User-Agent, then inserts it alongside the email:
ip := clientIP(r)
hash := viewerHash(ip, r.UserAgent())
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
h.db.Exec(ctx,
`INSERT INTO video_viewers (video_id, email, viewer_hash) VALUES ($1, $2, $3)
ON CONFLICT (video_id, email) DO NOTHING`,
videoID, req.Email, hash,
)
}()
The ON CONFLICT DO NOTHING means if the same person enters the same email twice (maybe their cookie expired), the original row is preserved. The viewer hash might differ if their IP changed, but the email remains the stable identifier.
This viewer hash is the same one used in the video_views and view_milestones tables. That means we can join across tables to answer "how many times did alice@example.com watch this video?" and "what percentage did she reach?"
Per-viewer analytics
The analytics endpoint joins the three tables when the email gate is enabled:
SELECT vw.email, vw.created_at,
COUNT(DISTINCT vv.id) as view_count,
COALESCE(MAX(vm.milestone), 0) as max_milestone
FROM video_viewers vw
LEFT JOIN video_views vv ON vv.video_id = vw.video_id
AND vv.viewer_hash = vw.viewer_hash
LEFT JOIN view_milestones vm ON vm.video_id = vw.video_id
AND vm.viewer_hash = vw.viewer_hash
WHERE vw.video_id = $1
GROUP BY vw.email, vw.created_at
ORDER BY vw.created_at DESC
The result is a viewer list in the analytics page — a table showing each email, when they first watched, how many times they returned, and the furthest point they reached in the video.
This turns anonymous analytics into actionable data. Instead of "23 unique views, 60% reached 50%," you see "alice@example.com watched 3 times and finished the whole thing; bob@example.com opened it once and dropped off at 25%."
The embed page
The email gate works identically on the embeddable player at /embed/:token. The form matches the embed's minimal style, and the cookie is shared across the same domain — entering an email on the watch page also unlocks the embed, and vice versa.
One detail that bit us: cookie SameSite policy. We initially set the cookies to SameSite=Strict, which is the most restrictive option. But when an embed is loaded inside an iframe on a different site, the browser considers it a cross-origin context. Strict cookies aren't sent in cross-origin requests, so the cookie set after email submission was silently dropped on page reload.
The fix was switching both the password gate and email gate cookies to SameSite=None (with Secure required by browsers for None). This allows the cookie to work inside third-party iframes — the whole point of having an embeddable player.
What we chose not to build
Email verification. We could send a magic link to confirm the address is real. But that adds significant friction — the viewer has to leave the page, check their inbox, click a link, and come back. For a shared video demo, that's too much. The honesty-based approach trades verification accuracy for conversion.
Name collection. Adding a name field to the form would give richer viewer data. But every extra field reduces completion rates. Email alone is enough to identify who watched.
User-level defaults. The email gate is per-video only. There's no setting to enable it for all videos at once. Most users will only gate specific high-value content — an investor pitch, a client proposal — not every quick screen recording.
Try it
SendRec is open source (AGPL-3.0) and self-hostable. The email gate is live at app.sendrec.eu — upload a video, toggle "Require email" in the Library overflow menu, then share the link. You'll see exactly who watches and how far they get.
Top comments (0)