Skip to content

Notifications

Two notification channels are integrated: Slack (operational / status alerts to a fixed channel) and SendGrid (transactional email via dynamic templates).


Slack

Used for operational signals from background jobs — color-validation results, social-token expiration alerts, generation completion / errors. Not used for end-user notifications.

Package

slack (npm). Imported per-file:

const Slack = require('slack');
const bot = new Slack({ token });

Configuration

Setting Value Where
Bot token xoxb-3144030948916-... (hardcoded) Inline in each consuming job file
Channel ID C05TS9AHBH6 (hardcoded) Inline

⚠️ The Slack token and channel are hardcoded in source. They're not in .env. Rotating the token requires a code change in every consuming file (see Security → Secrets).

Posting messages

await bot.chat.postMessage({
  channel: 'C05TS9AHBH6',
  text: `[job_color_check] account ${accountId}: contrast adjusted`,
});

There is no shared Slack helper — each job inlines the import, the token, and the channel constant.

Where it fires

File Trigger
job_color_check.js Color contrast validation result on a generated design
job_auto_disconnect.js Social token expired / account requires reconnect
Various generation jobs Job-level errors and completion reports

Operational notes

  • No retries — if Slack is unreachable, the message is silently dropped (the surrounding try/catch typically logs to console.error and moves on).
  • No rate limiting — high-traffic jobs can spam the channel.
  • No structured payload — messages are plain text. No Slack Blocks, no buttons, no thread replies.

When adding a new Slack notification

  1. Don't introduce another hardcoded token. Either move the token to .env first or add it to a shared helper.
  2. Wrap in a try/catch — never let a failed Slack call kill a job.
  3. Include enough context to debug from the message alone (account ID, job name, timestamp).

SendGrid (Email)

All transactional email goes through SendGrid Dynamic Templates — no raw HTML email is generated in code.

Package

const mailService = require('@sendgrid/mail');
mailService.setApiKey(conf.SENDGRID_API_KEY);

Configuration

Env var Purpose
SENDGRID_API_KEY API key, loaded via conf.js

Template IDs are stored as constants in helper/constants.js.

Delivery model

Emails are not sent inline from request handlers. Instead, they are enqueued in tEmailSchedule and processed by job_send_mail.js:

[ Route handler ]                              [ tEmailSchedule ]                  [ job_send_mail.js ]
        │                                              │                                       │
        │ INSERT INTO tEmailSchedule                  │                                       │
        │ (TemplateId, Email, DetailedInfo,           │                                       │
        │  Scheduled_time, Status='Inserted')         │                                       │
        ├─────────────────────────────────────────────►│                                       │
        │                                              │ (every 1 minute via cron)             │
        │                                              │◄──────────────────────────────────────│ poll Status='Inserted'
        │                                              │                                       │ AND TemplateId='d-bf7b...'
        │                                              │                  ┌────────────────────┤ mailService.send()
        │                                              │                  │                    │
        │                                              │                  ▼                    │
        │                                              │           SendGrid API                │
        │                                              │                  │                    │
        │                                              │                  ▼                    │
        │                                              │            Recipient inbox            │
        │                                              │                                       │
        │                                              │ UPDATE Status='Delivered' or 'Pending'│
        │                                              │◄──────────────────────────────────────│

This decoupling means: - A request handler returns immediately after enqueueing. - Email failures don't break user-facing flows. - Scheduled / delayed emails are trivially supported via Scheduled_time.

job_send_mail.js

  • PM2-managed, runs every ~1 minute (*/1 * * * *)
  • Reads tEmailSchedule WHERE Status = 'Inserted' AND TemplateId = 'd-bf7b8ec288304eefba4039d08ccf0cbb' (see ⚠ note below)
  • Calls SendGrid with { templateId, to, dynamic_template_data }
  • On success → Status = 'Delivered'. On failure → Status = 'Pending'.

Two corrections vs. the previous version of this doc:

1. Status values are strings, not integers. The previous doc described a numeric 0 → 1 → 2 state machine. The actual tEmailSchedule.Status column is varchar(50) and uses string values: 'Inserted' (pending), 'Delivered' (sent), 'Pending' (failed — confusingly named). Anyone querying with integer values will match nothing.

2. job_send_mail.js filters on a single TemplateId. The actual SELECT in job_send_mail.js:20 filters to a specific TemplateId ('d-bf7b8ec288304eefba4039d08ccf0cbb', the admin-confirmation template). It does not poll all pending emails. This means: - Either there are other email-sending workers we haven't fully traced (look for additional jobs polling tEmailSchedule with different TemplateId filters) - Or other templates accumulate forever in tEmailSchedule with Status = 'Inserted'

This needs investigation. If the second case is true, the email queue has a silent backlog and other email types are not being delivered through this job. If the first case is true, document the other workers here and flag the architecture as having multiple email processors operating on disjoint TemplateId subsets.

Template catalog

The constants are defined in helper/constants.js. Categorized:

Plan welcome / confirmation

Template ID Plan
d-47c22927d5ec4dc082b6ae025d35ae8a Accelerate (monthly)
d-85b484bad1ef47b2b8bb6cccde7e6f97 Launch (monthly)
d-85cdaa9925f942aab0c5df28a72a8a6e Supercharge (monthly)
d-5b4ae3f552d74d4ea3ba76b66d4adccf Accelerate (yearly)
d-c3ffb757a70e420bae4824409085dd69 Launch (yearly)
d-6046f1f1dde94a6bbc03a1c9faff91f7 Supercharge (yearly)

System / lifecycle

Template ID Trigger
d-1cea4e80209a49dca90451333ea578ad Token expired / auto-disconnect
d-c9b699718a224a7ab4ae87e889fc2c80 Reconnection reminder
d-2a5b752104c44c67ac269d1fa619392d Card expired
d-bf7b8ec288304eefba4039d08ccf0cbb Admin confirmation
d-be6b2fe487564f3e95069a66106c12c7 Admin approval
d-96e697a642344c5b9218e02092143720 MFA code sent
d-686730f97d6347b2be1e55a112c878ec Account verification
d-abb8180b15f5499892fb1efaa9d59b04 Generation complete

Template IDs are environment-agnostic at the API level — all environments hit the same SendGrid templates. Be careful when changing template content; non-prod traffic is using the same templates as prod.

Major callers (enqueue path)

These places insert into tEmailSchedule:

  • routes/auth.js — registration, password reset, MFA, plan changes
  • routes/routes.js — generation completion, account verification
  • job_new_auto_disconnect.js — token expiration / reconnection nudges
  • job_generation_complete.js — long-running content generation finished

Operational notes

  • No HTML fallback — if a SendGrid template is broken, the email fails entirely. There's no inline HTML backup.
  • No bounce handling in code — bounces / spam reports must be handled in the SendGrid dashboard.
  • No localization at the queue layer — the language must be encoded into the chosen templateId (one template per language) or via dynamic data fields.
  • Single API key — all environments share one key unless .env is per-environment. Verify in your deployment.

Common Patterns

Adding a new email type

  1. Create the dynamic template in SendGrid; copy the d-... ID.
  2. Add a constant in helper/constants.js:
    exports.TEMPLATE_X = 'd-abcdef...';
    
  3. Enqueue from your handler / job:
    await action.insertDataAsync('tEmailSchedule', {
      templateId: constants.TEMPLATE_X,
      recipient: user.email,
      dynamicData: JSON.stringify({ first_name: user.first_name }),
      scheduledAt: new Date(),
      status: 0,
    });
    
  4. job_send_mail.js picks it up on the next tick. No code change needed in the worker.

Adding a new Slack alert

  1. Move the token to .env (SLACK_BOT_TOKEN) if you haven't already — don't perpetuate the hardcoded pattern.
  2. Wrap in try/catch. Never let a Slack call abort the calling job.
  3. Use a descriptive prefix: [<job_name>] <accountId>: <message>.

  • Configuration ReferenceSENDGRID_API_KEY (and where to add SLACK_BOT_TOKEN)
  • Jobs Inventoryjob_send_mail.js, job_auto_disconnect.js, job_color_check.js, job_generation_complete.js
  • Data ModeltEmailSchedule schema
  • Security — secrets handling concerns including the hardcoded Slack token