Build in Public

Trading Platform Notification System: From Database to Email Push

ClawDUX TeamApril 13, 20266 min read0 views

Trading Platform Notification System: From Database to Email Push

Every transaction on ClawDUX triggers notifications — in-app and via email. Here's the architecture behind it.

The Notification Service Pattern

typescript
// services/notification.ts
import { PrismaClient } from '@prisma/client';
import { sendEmail } from './email';

const prisma = new PrismaClient();

type NotificationType =
  | 'WELCOME'
  | 'DEPOSIT'
  | 'ORDER_CONFIRMED'
  | 'ORDER_DISPUTED'
  | 'OFFER_RECEIVED'
  | 'OFFER_ACCEPTED'
  | 'ARBITRATION_RESULT';

interface NotifyParams {
  userId: string;
  type: NotificationType;
  title: string;
  message: string;
  metadata?: Record<string, any>;
}

export async function notify(params: NotifyParams): Promise<void> {
  const { userId, type, title, message, metadata } = params;

  // 1. Create in-app notification (always)
  await prisma.notification.create({
    data: {
      userId,
      type,
      title,
      message,
      metadata: metadata ? JSON.stringify(metadata) : null,
    },
  });

  // 2. Send email (if user has email)
  const user = await prisma.user.findUnique({
    where: { id: userId },
    select: { email: true, username: true },
  });

  if (user?.email) {
    await sendEmail({
      to: user.email,
      subject: title,
      template: type,
      data: {
        username: user.username || 'Trader',
        message,
        ...metadata,
      },
    }).catch((err) => {
      // Fire-and-forget: don't fail the main operation
      console.error('Email send failed:', err);
    });
  }
}

Usage in Business Logic

typescript
// In GraphQL resolvers
async confirmOrder(_, { orderId }, context) {
  const order = await prisma.order.update({
    where: { id: orderId },
    data: { status: 'CONFIRMED' },
    include: { listing: true, buyer: true },
  });

  // Notify buyer
  notify({
    userId: order.buyerId,
    type: 'ORDER_CONFIRMED',
    title: 'Purchase Confirmed',
    message: `Your purchase of "${order.listing.title}" is confirmed.`,
    metadata: {
      orderId: order.id,
      amount: order.amount.toString(),
      listingTitle: order.listing.title,
    },
  }).catch(() => {});
  // ^^^^ Fire-and-forget — notification failure
  //      should never block the main flow

  // Notify seller
  notify({
    userId: order.listing.sellerId,
    type: 'ORDER_CONFIRMED',
    title: 'Sale Confirmed',
    message: `Your strategy "${order.listing.title}" sale is confirmed.`,
    metadata: { orderId: order.id },
  }).catch(() => {});

  return order;
}

Frontend: The Notification Bell

tsx
function NotificationBell() {
  const [unread, setUnread] = useState(0);
  const [notifications, setNotifications] = useState([]);
  const [open, setOpen] = useState(false);

  // Poll for unread count
  useEffect(() => {
    const poll = setInterval(async () => {
      const count = await api.query(UNREAD_COUNT);
      setUnread(count);
    }, 10000);
    return () => clearInterval(poll);
  }, []);

  return (
    <div className="relative">
      <button onClick={() => setOpen(!open)}>
        <span className="material-symbols-outlined">
          notifications
        </span>
        {unread > 0 && (
          <span className="absolute -top-1 -right-1 w-5 h-5
                          bg-red-500 text-white text-xs rounded-full
                          flex items-center justify-center">
            {unread > 9 ? '9+' : unread}
          </span>
        )}
      </button>

      {open && (
        <div className="absolute right-0 mt-2 w-80
                       bg-surface-card border border-surface-border
                       rounded-xl shadow-xl overflow-hidden">
          {notifications.map((n) => (
            <div
              key={n.id}
              className={`p-3 border-b border-surface-border
                         ${!n.isRead ? 'bg-blue-600/5' : ''}`}
            >
              <p className="text-sm text-gray-300">{n.title}</p>
              <p className="text-xs text-gray-500 mt-1">
                {n.message}
              </p>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

Design Decisions

Decision Why
Fire-and-forget Notification failure should never block transactions
Database-first Notifications persist even if email fails
Polling (not WebSocket) Simpler, good enough for 10s latency
JSON metadata Flexible — each notification type carries different data

This notification architecture handles thousands of events daily on ClawDUX, keeping both buyers and sellers informed throughout the escrow lifecycle.

The core logic discussed in this article has been integrated into the ClawDUX API. Access ClawDUX-core for full permissions, or browse the marketplace to discover verified trading strategies.

#notifications#email#architecture#real-time#backend

Related Articles