Resend Audience Contact Sync
Resend Audience Contact Sync
User lifecycle → Resend audience — how Bloqr keeps the Resend “Bloqr Users” audience in sync with the D1/Neon user database.
Overview
When a user signs up or is deleted, Better Auth databaseHooks trigger ResendContactService to add or remove a contact from the configured Resend audience. This happens fire-and-forget via ctx.waitUntil() — the user-facing response is never delayed by audience sync.
Architecture
flowchart TD
SU["User sign-up"] --> UCA["Better Auth\nuser.create.after hook"]
UCA --> SYC["ResendContactService\n.syncUserCreated()"]
SYC --> CAP["ResendApiService\n.createContact(audienceId, { email, firstName, lastName })"]
CAP --> PSTC["POST https://api.resend.com\n/audiences/{id}/contacts"]
UD["User deletion"] --> UDA["Better Auth\nuser.delete.after hook"]
UDA --> SYD["ResendContactService\n.syncUserDeleted()"]
SYD --> DAP["ResendApiService\n.deleteContact(audienceId, email)"]
DAP --> DLTC["DELETE https://api.resend.com\n/audiences/{id}/contacts/{email}"]
Services
ResendApiService (worker/services/resend-api-service.ts)
Typed REST wrapper for the Resend Contacts/Audiences API. This is the only place in the codebase that calls https://api.resend.com/audiences/* — all other code uses this service.
- Zod-validates all request payloads before sending
- Validates all API responses against typed Zod schemas
- Throws
ResendApiError(typed, carriesstatusCode+errorName) on non-2xx responses - Uses
fetch()directly — no Resend SDK dependency
ResendContactService (worker/services/resend-contact-service.ts)
Business logic layer for user lifecycle sync.
syncUserCreated(user)— callsResendApiService.createContact()syncUserDeleted(user)— callsResendApiService.deleteContact()- All errors are caught and logged as warnings — never rethrown
- Name splitting:
"Alice Smith"→firstName: "Alice",lastName: "Smith"
NullResendContactService
No-op implementation returned by createResendContactService() when either RESEND_API_KEY or RESEND_AUDIENCE_ID is absent. Implements IResendContactService so call sites are unconditional — no if (contactSvc) guards needed.
Configuration
Production secrets
# Set the Resend API key (shared with email send path)wrangler secret put RESEND_API_KEY# Value: re_xxxxxxxxxxxx (from https://resend.com/api-keys)
# Set the audience IDwrangler secret put RESEND_AUDIENCE_ID# Value: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx# (from https://resend.com/audiences → create "Bloqr Users" → copy UUID)Local development
Add to .dev.vars:
RESEND_API_KEY=re_test_xxxxxxxxxxxxRESEND_AUDIENCE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxResend dashboard setup
- Go to resend.com/audiences
- Create an audience named “Bloqr Users”
- Copy the audience UUID — this becomes
RESEND_AUDIENCE_ID - Ensure
bloqr.devis a verified domain in resend.com/domains
Sequence Diagrams
User sign-up
sequenceDiagram
participant U as User browser
participant W as Cloudflare Worker
participant BA as Better Auth
participant DB as Neon (Prisma)
participant CS as ResendContactService
participant RA as ResendApiService
participant R as Resend API
U->>W: POST /api/auth/sign-up
W->>BA: createAuth(env, origin, ctx)
BA->>DB: INSERT users row
BA->>CS: user.create.after(user) via databaseHooks
W->>W: ctx.waitUntil(syncPromise)
W-->>U: 200 OK (response sent immediately)
CS->>RA: createContact(audienceId, { email, firstName, lastName })
RA->>R: POST /audiences/{id}/contacts
R-->>RA: 200 { id: "contact-uuid" }
User deletion
sequenceDiagram
participant A as Admin / system
participant W as Cloudflare Worker
participant BA as Better Auth
participant DB as Neon (Prisma)
participant CS as ResendContactService
participant RA as ResendApiService
participant R as Resend API
A->>W: DELETE /api/admin/users/{id}
W->>BA: auth.api.deleteUser(...)
BA->>DB: DELETE FROM users WHERE id = ?
BA->>CS: user.delete.after(user) via databaseHooks
W->>W: ctx.waitUntil(syncPromise)
CS->>RA: deleteContact(audienceId, email)
RA->>R: DELETE /audiences/{id}/contacts/{email}
R-->>RA: 204 No Content
Error Handling
All sync errors are caught inside ResendContactService and logged at console.warn level. They never propagate to the auth response:
[ResendContactService] syncUserCreated failed: ResendApiError 422 (validation_error): Contact already exists[ResendContactService] syncUserDeleted failed: ResendApiError 404 (not_found): Contact not foundA ResendApiError 422 on syncUserCreated is normal for re-registrations (same email, new account). It is non-fatal.
A ResendApiError 404 on syncUserDeleted means the contact was already removed or never synced. It is non-fatal.
Testing
# Unit tests (no HTTP calls — uses stub ResendApiService)deno test worker/services/resend-contact-service.test.ts
# API service tests (patches globalThis.fetch)deno test worker/services/resend-api-service.test.tsSee Also
worker/services/resend-api-service.ts— API wrapper implementationworker/services/resend-contact-service.ts— contact sync serviceworker/lib/auth.ts— Better AuthdatabaseHooksintegration- Email Architecture — full email system reference
- Resend Contacts API
Bloqr AI™ — The privacy you didn't know you needed.
© 2026 Bloqr AI™, a trademark of Bloqr Systems™. Created by Bloqr Systems™, founded by Jayson Knight.
Internet Hygiene (n.) — the ongoing practices that keep your digital life clean, private, and safe.
Our product repos live in the BloqrAI org, part of the Bloqr Systems GitHub Enterprise. Product repos are internal-visibility — enterprise membership is required to view them.