← back to Govarbitrage
Subscriber model: double-opt-in newsletter (PENDING/CONFIRMED/UNSUBSCRIBED, unique tokens)
562ea5d2a8a8c7357cb415814eb5a388781d066d · 2026-07-13 01:19:22 -0700 · Steve Abrams
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files touched
Diff
commit 562ea5d2a8a8c7357cb415814eb5a388781d066d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Jul 13 01:19:22 2026 -0700
Subscriber model: double-opt-in newsletter (PENDING/CONFIRMED/UNSUBSCRIBED, unique tokens)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
prisma/schema.prisma | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index da4a4a2..870ab84 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -110,6 +110,15 @@ enum Tier {
PREMIUM
}
+// Double-opt-in newsletter lifecycle. PENDING until the confirm link is
+// clicked; UNSUBSCRIBED rows are kept (suppression list) so a re-subscribe
+// re-issues a fresh confirm rather than silently re-adding.
+enum SubscriberStatus {
+ PENDING
+ CONFIRMED
+ UNSUBSCRIBED
+}
+
enum EventType {
IMPORTED
RESEARCH_STARTED
@@ -410,6 +419,22 @@ model User {
auditLogs AuditLog[]
}
+// Public newsletter subscriber for the twice-daily Top-10 digest.
+// email is stored lowercase; tokens are single-purpose URL-safe secrets.
+model Subscriber {
+ id String @id @default(cuid())
+ email String @unique
+ status SubscriberStatus @default(PENDING)
+ confirmToken String @unique
+ unsubscribeToken String @unique
+ source String @default("newsletter_page")
+ createdAt DateTime @default(now())
+ confirmedAt DateTime?
+ unsubscribedAt DateTime?
+
+ @@index([status])
+}
+
model ApiCredential {
id String @id @default(cuid())
userId String
← 687fd82 Reprice tiers to research-backed audience-building points: S
·
back to Govarbitrage
·
Newsletter API: subscribe (honeypot+rate-limit, enumeration- d242730 →