[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Notification Template Engine

Build a Notification Template Engine

Build a notification template engine with multi-channel rendering, variable interpolation, conditional blocks, localization, preview mode, and A/B testing for consistent messaging across email, SMS, push, and in-app.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Lena leads product at a 20-person SaaS sending 500K notifications monthly across email, SMS, push, and in-app. Each channel has hardcoded templates in different codebases — email in the backend, push in the mobile app, SMS in a lambda. Changing "Your order #{{orderId}} has shipped" requires 4 deploys. Templates aren't translated — French users get English push notifications. Marketing wants to A/B test subject lines but there's no infrastructure. They need a template engine: one template per notification type, rendered for each channel, with variables, conditionals, localization, and A/B testing.

Step 1: Build the Template Engine

typescript
import { pool } from "../db";
import { Redis } from "ioredis";
import { createHash } from "node:crypto";
const redis = new Redis(process.env.REDIS_URL!);

interface NotificationTemplate {
  id: string;
  event: string;
  channel: "email" | "sms" | "push" | "in_app";
  locale: string;
  subject?: string;
  body: string;
  variant?: string;
  active: boolean;
  version: number;
}

interface RenderResult {
  channel: string;
  subject?: string;
  body: string;
  plainText?: string;
}

// Render notification for all channels
export async function renderNotification(
  event: string,
  variables: Record<string, any>,
  options?: { locale?: string; channels?: string[]; userId?: string }
): Promise<RenderResult[]> {
  const locale = options?.locale || "en";
  const channels = options?.channels || ["email", "sms", "push", "in_app"];
  const results: RenderResult[] = [];

  for (const channel of channels) {
    const template = await getTemplate(event, channel, locale, options?.userId);
    if (!template) continue;

    const rendered: RenderResult = { channel };
    if (template.subject) rendered.subject = renderTemplate(template.subject, variables);
    rendered.body = renderTemplate(template.body, variables);
    if (channel === "email") rendered.plainText = stripHtml(rendered.body);

    results.push(rendered);
  }

  return results;
}

async function getTemplate(event: string, channel: string, locale: string, userId?: string): Promise<NotificationTemplate | null> {
  const cacheKey = `tmpl:${event}:${channel}:${locale}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  // Check for A/B variant
  let variant: string | null = null;
  if (userId) {
    const hash = parseInt(createHash("md5").update(userId + event).digest("hex").slice(0, 8), 16);
    variant = hash % 2 === 0 ? "A" : "B";
  }

  // Try locale-specific, then fallback to 'en'
  const { rows: [template] } = await pool.query(
    `SELECT * FROM notification_templates WHERE event = $1 AND channel = $2 AND locale = $3 AND active = true
     ${variant ? "AND (variant = $4 OR variant IS NULL) ORDER BY variant DESC NULLS LAST" : "AND variant IS NULL"}
     LIMIT 1`,
    variant ? [event, channel, locale, variant] : [event, channel, locale]
  );

  if (!template) {
    // Fallback to English
    const { rows: [fallback] } = await pool.query(
      "SELECT * FROM notification_templates WHERE event = $1 AND channel = $2 AND locale = 'en' AND active = true AND variant IS NULL LIMIT 1",
      [event, channel]
    );
    if (fallback) { await redis.setex(cacheKey, 300, JSON.stringify(fallback)); return fallback; }
    return null;
  }

  await redis.setex(cacheKey, 300, JSON.stringify(template));
  return template;
}

function renderTemplate(template: string, variables: Record<string, any>): string {
  let result = template;

  // Variable interpolation: {{variableName}}
  result = result.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_, path) => {
    const parts = path.split(".");
    let value: any = variables;
    for (const part of parts) value = value?.[part];
    return value !== undefined ? String(value) : `{{${path}}}`;
  });

  // Conditional blocks: {{#if condition}}...{{/if}}
  result = result.replace(/\{\{#if (\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, condition, content) => {
    return variables[condition] ? content : "";
  });

  // Loops: {{#each items}}...{{/each}}
  result = result.replace(/\{\{#each (\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_, arrayName, content) => {
    const items = variables[arrayName];
    if (!Array.isArray(items)) return "";
    return items.map((item: any) => {
      return content.replace(/\{\{this\.(\w+)\}\}/g, (_: any, key: string) => String(item[key] ?? ""));
    }).join("");
  });

  return result;
}

function stripHtml(html: string): string {
  return html.replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim();
}

// Preview template with sample data
export async function preview(event: string, channel: string, locale: string, sampleData: Record<string, any>): Promise<RenderResult> {
  const template = await getTemplate(event, channel, locale);
  if (!template) throw new Error("Template not found");
  return {
    channel,
    subject: template.subject ? renderTemplate(template.subject, sampleData) : undefined,
    body: renderTemplate(template.body, sampleData),
  };
}

// Create or update template
export async function saveTemplate(params: Omit<NotificationTemplate, "id" | "version">): Promise<void> {
  const { rows: [existing] } = await pool.query(
    "SELECT version FROM notification_templates WHERE event = $1 AND channel = $2 AND locale = $3 AND variant IS NOT DISTINCT FROM $4 ORDER BY version DESC LIMIT 1",
    [params.event, params.channel, params.locale, params.variant || null]
  );
  const version = (existing?.version || 0) + 1;

  await pool.query(
    `INSERT INTO notification_templates (event, channel, locale, subject, body, variant, active, version, created_at)
     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())`,
    [params.event, params.channel, params.locale, params.subject, params.body, params.variant || null, params.active, version]
  );

  // Invalidate cache
  await redis.del(`tmpl:${params.event}:${params.channel}:${params.locale}`);
}

Results

  • Template change: 4 deploys → 0 — update template in DB; all channels use new wording in <5 minutes via cache refresh; no code changes
  • French push notifications — locale-specific templates for FR, DE, ES, JA; fallback to EN if translation missing; French users get French notifications
  • A/B testing subject lines — variant A: "Your order shipped!" vs B: "{{name}}, your package is on its way!"; 50/50 split by userId hash; B wins with 23% higher open rate
  • Conditional content{{#if isPremium}} shows premium features; {{#each items}} renders order line items; dynamic content without code
  • Preview before send — marketing team previews rendered template with sample data; catches broken variables and formatting; no test emails to production