From cbe00d6c8546f3ecd47ed2eea1080bbb6dec6b96 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Sat, 30 May 2026 20:28:24 -0500 Subject: [PATCH] feat(signup): T+3 activation nudge for users with zero paired screens Daily sweep (15:00 UTC) emails a warm, personal "checking in" message to users who signed up 3-14 days ago and still have no paired screen, nudging them toward activation. Once per user, reuses the Graph transport (services/email.js) via the existing fromName/rawSubject options. - New service services/activationNudge.js, started from server.js. Self-correcting daily scheduler (recompute next 15:00 UTC each run; no node-cron dependency). - Eligibility (Option B, workspace-aware): created 3-14 days ago, activation_nudge_sent_at IS NULL, COALESCE(email_alerts,1)=1 (only an explicit opt-out of 0 is excluded; NULL/unset still qualify), and ZERO devices owned by the user OR present in any workspace they belong to. The workspace check avoids nudging engaged team members. - Idempotency: activation_nudge_sent_at, stamped after send; paired sentinel-1 backfill so the first sweep can't blast the dormant legacy base. Only genuinely-new signups become eligible. - GATE: HOSTED_INSTANCE=true (positive hosted signal, NOT !selfHosted). A daily bulk sweep would be far worse to leak than a single email, so a self-hoster who configured Graph but missed SELF_HOSTED won't blast their user base. Unset -> neither scheduled nor sent. Documented in .env.example. --- .env.example | 6 ++ server/db/database.js | 9 ++ server/server.js | 4 + server/services/activationNudge.js | 155 +++++++++++++++++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 server/services/activationNudge.js diff --git a/.env.example b/.env.example index 1e8f58b..5fb7d49 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,12 @@ SELF_HOSTED=true # who want to be notified of signups set this to their own address. # ADMIN_NOTIFY_EMAIL=you@example.com +# Marks THIS deployment as the hosted (screentinker.com) instance. Gates the +# daily activation-nudge sweep (the T+3 "haven't paired a screen yet?" email). +# Leave UNSET on self-hosted instances so a daily bulk sweep never emails your +# user base with our onboarding mail. Only the hosted instance sets this true. +# HOSTED_INSTANCE=true + # --- Outbound email (Microsoft Graph, client-credentials flow) --- # Required for ANY email (welcome, offline alerts, admin notify) to actually # send. Leave blank and the app logs "[EMAIL] not configured" instead of sending. diff --git a/server/db/database.js b/server/db/database.js index 902da80..4247e09 100644 --- a/server/db/database.js +++ b/server/db/database.js @@ -153,6 +153,15 @@ const migrations = [ // sent at