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
- User submits form on nself.org
- ping_api inserts subscriber with
is_verified=falseand sends T1 confirmation email - User clicks the confirmation link (valid for 48 hours)
- ping_api sets
is_verified=trueand sends W1 welcome email - Days 3, 7, 14, 21: W2–W5 welcome series (sent manually or via cron, see Welcome Series below)
- Bi-weekly: N1 issue sent to all
is_verified=true, status=activesubscribers
Required Environment Variables
| Variable | Description |
|---|---|
ELASTIC_EMAIL_API_KEY | Elastic Email API key (in vault as ELASTIC_EMAIL_ADMIN_API_KEY) |
ELASTIC_EMAIL_FROM | Sender address, noreplynself |
NEWSLETTER_CONFIRM_SECRET | HMAC secret for confirmation tokens |
NEWSLETTER_UNSUBSCRIBE_SECRET | HMAC secret for unsubscribe tokens |
ELASTIC_EMAIL_LIST_ID_NEWSLETTER | Elastic 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
| Template | When |
|---|---|
T1-confirm.mjml | Double opt-in confirmation |
T2-unsub.mjml | Unsubscribe confirmation |
T3-license-alert.mjml | License renewal -7 days |
W1-welcome.mjml | Immediately on confirm |
W2-first-plugin.mjml | Day 3 |
W3-case-study.mjml | Day 7 |
W4-nclaw.mjml | Day 14 |
W5-cloud-preview.mjml | Day 21 |
N1-biweekly.mjml | Every 2 weeks |
Sending a Bi-Weekly Issue
-
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}}
-
Compile the template:
pnpm email:compile -
Upload to Elastic Email template library via API or dashboard
-
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_subscribersonly, 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