a tool for shared writing and social publishing
0

Configure Feed

Select the types of activity you want to include in your feed.

link loose email subscriptions

Jared Pereira (May 6, 2026, 6:20 PM EDT) 4156a3e4 cfc66283

+42
+3
actions/emailAuth.tsx
··· 11 11 import { supabaseServerClient } from "supabase/serverClient"; 12 12 import { LeafletConfirmEmail } from "emails/leafletConfirmEmail"; 13 13 import { sendConfirmationEmail } from "src/utils/confirmationEmail"; 14 + import { linkOrphanedEmailSubscribers } from "src/utils/linkOrphanedEmailSubscribers"; 14 15 15 16 async function sendAuthCode(email: string, code: string) { 16 17 await sendConfirmationEmail({ ··· 112 113 } else { 113 114 identityID = identity.id; 114 115 } 116 + 117 + await linkOrphanedEmailSubscribers(identityID, token.email); 115 118 116 119 const [confirmedToken] = await db 117 120 .update(email_auth_tokens)
+8
actions/login.ts
··· 11 11 import { redirect } from "next/navigation"; 12 12 import { pool } from "supabase/pool"; 13 13 import { supabaseServerClient } from "supabase/serverClient"; 14 + import { linkOrphanedEmailSubscribers } from "src/utils/linkOrphanedEmailSubscribers"; 14 15 15 16 export async function loginWithEmailToken( 16 17 localLeaflets: { token: { id: string }; added_at: string }[], ··· 51 52 .where(eq(identities.email, token.email)); 52 53 53 54 let identity = existingIdentity; 55 + let newlyOwnedEmail = false; 54 56 if (!existingIdentity) { 55 57 let identityCookie = (await cookies()).get("identity"); 56 58 if (identityCookie) { ··· 69 71 .set({ email: token.email }) 70 72 .where(eq(identities.id, existingIdentityFromCookie.id)); 71 73 identity = existingIdentityFromCookie; 74 + newlyOwnedEmail = true; 72 75 } 73 76 } else { 74 77 const { data: newIdentity } = await supabaseServerClient ··· 77 80 .select() 78 81 .single(); 79 82 identity = newIdentity!; 83 + newlyOwnedEmail = true; 80 84 } 85 + } 86 + 87 + if (newlyOwnedEmail && identity) { 88 + await linkOrphanedEmailSubscribers(identity.id, token.email); 81 89 } 82 90 83 91 await tx
+6
actions/publications/subscribeEmail.tsx
··· 24 24 } from "app/lish/subscribeToPublication"; 25 25 import type { OAuthSessionError } from "src/atproto-oauth"; 26 26 import { normalizePublicationRecord } from "src/utils/normalizeRecords"; 27 + import { linkOrphanedEmailSubscribers } from "src/utils/linkOrphanedEmailSubscribers"; 27 28 28 29 type RequestError = 29 30 | "invalid_email" ··· 367 368 console.error("[subscribeEmail] attach email failed:", error); 368 369 return Err("database_error"); 369 370 } 371 + await linkOrphanedEmailSubscribers(current.id, email); 370 372 await backfillAtprotoSubscriptionsForIdentity(current.id, current.atp_did); 371 373 return Ok(current.id); 372 374 } ··· 397 399 console.error("[subscribeEmail] identity upsert failed:", identityError); 398 400 return null; 399 401 } 402 + 403 + // Cover any sibling subscriber rows (e.g. CSV-imported entries on other 404 + // publications) so this confirmation also adopts them under the new identity. 405 + await linkOrphanedEmailSubscribers(identity.id, email); 400 406 401 407 const { data: token, error: tokenError } = await supabaseServerClient 402 408 .from("email_auth_tokens")
+25
src/utils/linkOrphanedEmailSubscribers.ts
··· 1 + import { supabaseServerClient } from "supabase/serverClient"; 2 + 3 + // Link any pre-existing email-subscriber rows for `email` to `identityId` if 4 + // they're not already linked. Call this whenever an identity newly comes to 5 + // own an email so that imported / pre-confirmation-flow subscribers become 6 + // visible to their owner. Idempotent — safe to call on every signup/login. 7 + export async function linkOrphanedEmailSubscribers( 8 + identityId: string, 9 + email: string, 10 + ) { 11 + const normalized = email.trim().toLowerCase(); 12 + if (!normalized) return; 13 + const { error } = await supabaseServerClient 14 + .from("publication_email_subscribers") 15 + .update({ identity_id: identityId }) 16 + .eq("email", normalized) 17 + .is("identity_id", null); 18 + if (error) { 19 + console.error( 20 + "[linkOrphanedEmailSubscribers] failed:", 21 + error.message, 22 + { identityId, email: normalized }, 23 + ); 24 + } 25 + }