Skip to main content

Newsletter

How to manage the ɳSelf newsletter, send schedule, template updates, and subscriber management.

Newsletter Operations

The ɳSelf newsletter sends every two weeks. Infrastructure is built on Elastic Email via the ping_api service at ping.nself.org. Subscriber data lives in the newsletter_subscribers Postgres table, no third-party CRM.

Architecture

Web form → POST /api/newsletter (Next.js proxy)
         → POST /newsletter/subscribe (ping_api)
         → Insert newsletter_subscribers (is_verified=false)
         → Send T1 confirmation email (Elastic Email)

User clicks confirm link → GET /newsletter/confirm?token=&email=
                         → Verify HMAC token
                         → Update is_verified=true
                         → Send W1 welcome email

Subscriber Flow

  1. User submits form on nself.org
  2. ping_api inserts subscriber with is_verified=false and sends T1 confirmation email
  3. User clicks the confirmation link (valid for 48 hours)
  4. ping_api sets is_verified=true and sends W1 welcome email
  5. Days 3, 7, 14, 21: W2–W5 welcome series (sent manually or via cron, see Welcome Series below)
  6. Bi-weekly: N1 issue sent to all is_verified=true, status=active subscribers

Required Environment Variables

VariableDescription
ELASTIC_EMAIL_API_KEYElastic Email API key (in vault as ELASTIC_EMAIL_ADMIN_API_KEY)
ELASTIC_EMAIL_FROMSender address, noreplynself
NEWSLETTER_CONFIRM_SECRETHMAC secret for confirmation tokens
NEWSLETTER_UNSUBSCRIBE_SECRETHMAC secret for unsubscribe tokens
ELASTIC_EMAIL_LIST_ID_NEWSLETTERElastic Email list ID (created on first deploy)

Generate secrets with:

openssl rand -hex 32

Email Templates

Templates are MJML sources at web/org/src/email-templates/. Compile to HTML with:

cd web/org
pnpm email:preview   # renders to localhost for preview
pnpm email:compile   # compiles all MJML to HTML
TemplateWhen
T1-confirm.mjmlDouble opt-in confirmation
T2-unsub.mjmlUnsubscribe confirmation
T3-license-alert.mjmlLicense renewal -7 days
W1-welcome.mjmlImmediately on confirm
W2-first-plugin.mjmlDay 3
W3-case-study.mjmlDay 7
W4-nclaw.mjmlDay 14
W5-cloud-preview.mjmlDay 21
N1-biweekly.mjmlEvery 2 weeks

Sending a Bi-Weekly Issue

  1. Fill in the variables in N1-biweekly.mjml:

    • {{ISSUE_NUMBER}}, {{HEADLINE}}, {{RELEASE_HEADLINE}}, {{RELEASE_SUMMARY}}
    • {{CLI_SNIPPET}}, {{CHANGELOG_URL}}
    • {{COMMUNITY_SHOWCASE}}, {{DOCS_URL}}, {{DOCS_TITLE}}, {{DOCS_TEASER}}
    • {{ROADMAP_TEASER}}
  2. Compile the template: pnpm email:compile

  3. Upload to Elastic Email template library via API or dashboard

  4. Send to list via Elastic Email campaigns UI or API

Subscriber Management

Query active confirmed subscribers:

SELECT email, subscribed_at
FROM newsletter_subscribers
WHERE is_verified = true AND status = 'active'
ORDER BY subscribed_at DESC;

Check unconfirmed (pending double opt-in):

SELECT email, created_at, verification_expires_at
FROM newsletter_subscribers
WHERE is_verified = false
  AND verification_expires_at > NOW();

Clean up expired unconfirmed:

DELETE FROM newsletter_subscribers
WHERE is_verified = false
  AND verification_expires_at < NOW()
  AND created_at < NOW() - INTERVAL '7 days';

Rate Limiting

The subscribe endpoint enforces 3 attempts per IP per hour via in-memory rate limiting in ping_api. This is sufficient for protecting against form spam without a Redis dependency.

GDPR

  • Subscriber data is stored in newsletter_subscribers only, no third-party CRM sync
  • Unsubscribe link is included in every email footer
  • Unsubscribe takes effect within 10 seconds (direct DB update)
  • Double opt-in is required, unconfirmed subscribers never receive marketing emails