DEV Community

Cover image for Send Transactional Emails in Node.js with Convex and AutoSend API
Debajyati Dey
Debajyati Dey

Posted on

Send Transactional Emails in Node.js with Convex and AutoSend API

Every business needs to send transactional emails at some point.
Transactional emails are the very foundation of an online communication framework that works towards users from a business.

What are transactional emails?
Well, in simple terms, Transactional emails are just some automated system generated emails which are sent in response to user actions, such as account verification, password resets, order confirmations, and other critical online communications.

If compared to marketing emails, transactional emails have higher discoverability requirements and often contain urgently needed information.

AutoSend is a great email platform I have recently discovered which is so easy to setup for sending transactional emails and I found it really amazing for a relatively new email platform to have so nice of a developer experience including polished docs, helpful guides, and an friendly community of encouraging developers.

AutoSend: Email for Developers and Marketers

AutoSend is a lightweight SendGrid alternative for transactional and marketing emails. Simple, modern, and built to scale.

favicon autosend.com

I know you say it compares to resend and mailgun and you have complaints why it does not have a free tier. But you need to understand that it automates and abstracts away lot of tedius email domain configurations and provides you a easy headstart with a very intuitive web api. And, most importantly the hobby tier (lowest paid tier) starts from just $1/month, which is affordable for almost everybody with you being able to send 3000 emails/month (no max limit for daily emails).

In this guide, we will go through how to implement transactional email functionality in a Node.js application using Convex as the backend database and AutoSend API for email delivery. We'll use a complete email OTP (One-Time Password) authentication system as our practical example.

Key Tools and Technologies We'll Be Using

Node.js

Node.js will be the runtime environment for our server-side application. It will handle all HTTP requests and coordinate between different services.

Convex

Convex | The backend platform that keeps your app in sync

Convex is the backend platform that keeps your app in sync. Everything you need to build your full-stack project.

favicon convex.dev

Convex is a really good BaaS(Backend-as-a-Service) platform. It provides:
  • Real-time database with auto-sync,
  • Serverless functions (queries, mutations, actions),
  • Built-in authentication,
  • Type-safe API generation, etc.

AutoSend API

What is AutoSend? - AutoSend Documentation

AutoSend is an email platform for developers and marketers to send and track transactional and marketing emails.

favicon docs.autosend.com

AutoSend is an emerging dev freindly REST API based email service that offers:
  • High deliverability rates,
  • Dedicated IPs,
  • Both transactional and marketing email services,
  • Template support with dynamic data (handlebars syntax),
  • Delivery tracking and analytics,
  • Compliance with email regulations, etc.

Demo Project Setup

Prerequisites

Before we begin, ensure you have:

  • Node.js (v20 or higher, v22 LTS recommended),
  • A Convex account and project,
  • An active AutoSend billing account and an API key

Project Structure

otp-demo/
├── convex/
│   ├── _generated/
│   ├── otp.js
│   ├── schema.js
│   ├── sessions.js
│   └── users.js
├── public/
│   └── index.html
├── server.js
├── package.json
└── .env
Enter fullscreen mode Exit fullscreen mode

Dependencies

Install the required packages:

npm install express cors body-parser dotenv convex node-fetch
Enter fullscreen mode Exit fullscreen mode

Brief Overview

Before jumping to code you can refer to this full sequence diagram for a quick look on what the complete workflow will be like -

Sequence Diagram of Whole Workflow

Database Schema Design

First, we need to define our database schema in convex/schema.js:

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  users: defineTable({
    email: v.string(),
    verified: v.boolean(),
    createdAt: v.number(),
  }).index("by_email", ["email"]),

  otpTokens: defineTable({
    email: v.string(),
    otp: v.string(),
    expiresAt: v.number(),
    attempts: v.number(),
    createdAt: v.number(),
  }).index("by_email", ["email"]),

  sessions: defineTable({
    userId: v.id("users"),
    token: v.string(),
    expiresAt: v.number(),
    createdAt: v.number(),
  }).index("by_token", ["token"]),
});
Enter fullscreen mode Exit fullscreen mode

This schema supports:

  • users: Store user information and verification status.
  • otpTokens: Temporary OTP storage with expiration and attempt tracking.
  • sessions: Session management for authenticated users.

After writing the file, you must run npx convex dev for uploading the database schema into your convex development server.

Managing OTP Tokens Using Convex Functions

Convex functions are 3 types - queries, mutations and actions.
Queries and Mutations are both transactional while actions are not.
Queries are used to read data from convex database while mutations are used to write data to convex database. Queries are subscribable.

Actions are used for making third party requests like making a fetch API call. Such as http-actions can be used to make an openai api call in your web app, etc.

Create a convex/otp.js for all OTP-related operations:
ANything you perform in a convex backend is done by creating and using convex functions.

import { v } from "convex/values";
import { mutation, query } from "./_generated/server";

// Store OTP token
export const createOTP = mutation({
  args: {
    email: v.string(),
    otp: v.string(),
    expiresAt: v.number(),
  },
  handler: async (ctx, args) => {
    // Delete any existing OTP for this email
    const existing = await ctx.db
      .query("otpTokens")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .collect();

    for (const token of existing) {
      await ctx.db.delete(token._id);
    }

    // Create new OTP
    const otpId = await ctx.db.insert("otpTokens", {
      email: args.email,
      otp: args.otp,
      expiresAt: args.expiresAt,
      attempts: 0,
      createdAt: Date.now(),
    });

    // Return the stored OTP data including the actual expiresAt
    const storedOtp = await ctx.db.get(otpId);
    return {
      id: otpId,
      expiresAt: storedOtp.expiresAt,
    };
  },
});

// Verify OTP
export const verifyOTP = mutation({
  args: {
    email: v.string(),
    otp: v.string(),
  },
  handler: async (ctx, args) => {
    const token = await ctx.db
      .query("otpTokens")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .first();

    if (!token) {
      return { success: false, error: "No OTP found" };
    }

    // Check if expired (with 30 second buffer for timing issues)
    const currentTime = Date.now();
    if (currentTime > token.expiresAt + 30000) { // 30 second grace period
      await ctx.db.delete(token._id);
      return { success: false, error: "OTP expired" };
    }

    // Check attempts
    if (token.attempts >= 5) {
      await ctx.db.delete(token._id);
      return { success: false, error: "Too many attempts" };
    }

    // Verify OTP
    if (token.otp !== args.otp) {
      await ctx.db.patch(token._id, { attempts: token.attempts + 1 });
      return { success: false, error: "Invalid OTP" };
    }

    // Success - delete token
    await ctx.db.delete(token._id);
    return { success: true };
  },
});
Enter fullscreen mode Exit fullscreen mode

Create a convex/users.js file for user operations:

import { v } from "convex/values";
import { mutation, query } from "./_generated/server";

// Create or get user
export const createUser = mutation({
  args: { email: v.string() },
  handler: async (ctx, args) => {
    const existing = await ctx.db
      .query("users")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .first();

    if (existing) {
      return existing._id;
    }

    return await ctx.db.insert("users", {
      email: args.email,
      verified: false,
      createdAt: Date.now(),
    });
  },
});

// Mark user as verified
export const verifyUser = mutation({
  args: { email: v.string() },
  handler: async (ctx, args) => {
    const user = await ctx.db
      .query("users")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .first();

    if (!user) {
      throw new Error("User not found");
    }

    await ctx.db.patch(user._id, { verified: true });
    return user;
  },
});

// Get user by email
export const getUserByEmail = query({
  args: { email: v.string() },
  handler: async (ctx, args) => {
    return await ctx.db
      .query("users")
      .withIndex("by_email", (q) => q.eq("email", args.email))
      .first();
  },
});
Enter fullscreen mode Exit fullscreen mode

Session Management

Create convex/sessions.js for managing user sessions:

import { v } from "convex/values";
import { mutation, query } from "./_generated/server";

function generateSessionToken() {
  return crypto.randomUUID();
}

export const createSession = mutation({
  args: { userId: v.id("users") },
  handler: async (ctx, args) => {
    // Delete any existing sessions for this user
    const existing = await ctx.db
      .query("sessions")
      .filter((q) => q.eq(q.field("userId"), args.userId))
      .collect();

    for (const session of existing) {
      await ctx.db.delete(session._id);
    }

    // Create new session (24 hours)
    const token = generateSessionToken();
    const expiresAt = Date.now() + 24 * 60 * 60 * 1000;

    const sessionId = await ctx.db.insert("sessions", {
      userId: args.userId,
      token,
      expiresAt,
      createdAt: Date.now(),
    });

    return { token, expiresAt };
  },
});

export const validateSession = query({
  args: { token: v.string() },
  handler: async (ctx, args) => {
    const session = await ctx.db
      .query("sessions")
      .withIndex("by_token", (q) => q.eq("token", args.token))
      .first();

    if (!session) {
      return null;
    }

    // Check if expired
    if (Date.now() > session.expiresAt) {
      // Clean up expired session
      await ctx.db.delete(session._id);
      return null;
    }

    const user = await ctx.db.get(session.userId);
    if (!user || !user.verified) {
      return null;
    }

    return {
      userId: user._id,
      email: user.email,
      verified: user.verified,
      createdAt: user.createdAt,
    };
  },
});

// Delete a session (logout)
export const deleteSession = mutation({
  args: { token: v.string() },
  handler: async (ctx, args) => {
    const session = await ctx.db
      .query("sessions")
      .withIndex("by_token", (q) => q.eq("token", args.token))
      .first();

    if (session) {
      await ctx.db.delete(session._id);
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

Now run npx convex dev again to upload the convex functions to your convex project cloud.

Email Service Implementation

AutoSend API Integration

Create the email sending functionality in server.js:
We are strictly enforcing IPv4 DNS order because the convex server does not work well with IPv6 because it faces synchronization issues due to IPv6 timeouts.

import { setDefaultResultOrder } from 'dns';
// Force IPv4 first to avoid IPv6 timeout issues
setDefaultResultOrder('ipv4first');

import express from "express";
import cors from "cors";
import bodyParser from "body-parser";
import dotenv from "dotenv";
import https from 'https';
import http from 'http';
import fetch from 'node-fetch';

// Load environment variables FIRST
dotenv.config();

// Send email via AutoSend API
async function sendOTPEmail(email, otp) {
  try {
    const response = await nodefetch("https://api.autosend.com/v1/mails/send", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${process.env.AUTOSEND_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        from: {
          email: process.env.AUTOSEND_FROM_EMAIL,
          name: process.env.AUTOSEND_FROM_NAME,
        },
        to: {
          email: email,
        },
        subject: "Your Verification Code",
        html: `
          <div style="font-family: Arial, sans-serif; padding: 20px;">
            <h2>Email Verification</h2>
            <p>Your verification code is:</p>
            <h1 style="color: #4F46E5; font-size: 32px; letter-spacing: 5px;">${otp}</h1>
            <p>This code will expire in 10 minutes.</p>
            <p style="color: #666; font-size: 12px;">If you didn't request this code, please ignore this email.</p>
          </div>
        `,
        text: `Your verification code is: ${otp}. This code will expire in 10 minutes.`,
      }),
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || "Failed to send email");
    }

    return await response.json();
  } catch (error) {
    console.error("Error sending email:", error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

API Endpoints

Okay, time to Implement the OTP request endpoint. Paste the following code in the same server.js file:

// Generate 6-digit OTP
function generateOTP() {
  return Math.floor(100000 + Math.random() * 900000).toString();
}

// Request OTP
app.post("/api/request-otp", async (req, res) => {
  try {
    const { email } = req.body;

    if (!email || !email.includes("@")) {
      return res.status(400).json({ error: "Valid email is required" });
    }

    console.log("Requesting OTP for:", email);

    // Generate OTP
    const otp = generateOTP();
    const expiresAt = Date.now() + 10 * 60 * 1000; // 10 minutes

    console.log("Generated OTP:", otp);

    // Create or get user
    console.log("Creating user in Convex...");
    await convex.mutation(api.users.createUser, { email });
    console.log("User created/found");

    // Store OTP in Convex
    console.log("Storing OTP in Convex...");
    const otpResult = await convex.mutation(api.otp.createOTP, {
      email,
      otp,
      expiresAt,
    });
    console.log("OTP stored successfully");

    // Send email
    console.log("Sending email...");
    await sendOTPEmail(email, otp);
    console.log("Email sent successfully");

    res.json({
      success: true,
      message: "OTP sent successfully",
      expiresAt: otpResult.expiresAt, // Use the expiresAt from Convex for consistency
    });
  } catch (error) {
    console.error("Error requesting OTP:", error);
    res.status(500).json({
      error: "Failed to send OTP",
      details: error.message,
    });
  }
});

// Verify OTP
app.post("/api/verify-otp", async (req, res) => {
  try {
    const { email, otp } = req.body;

    if (!email || !otp) {
      return res.status(400).json({ error: "Email and OTP are required" });
    }

    console.log("Verifying OTP for:", email);

    // Verify OTP using Convex
    const result = await convex.mutation(api.otp.verifyOTP, {
      email,
      otp,
    });

    if (!result.success) {
      return res.status(400).json({
        success: false,
        error: result.error,
      });
    }

    // Mark user as verified
    const user = await convex.mutation(api.users.verifyUser, { email });

    // Create session
    const session = await convex.mutation(api.sessions.createSession, { userId: user._id });

    res.json({
      success: true,
      message: "Email verified successfully",
      sessionToken: session.token,
    });
  } catch (error) {
    console.error("Error verifying OTP:", error);
    res.status(500).json({
      error: "Failed to verify OTP",
      details: error.message,
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Building the Frontend

The core logical part is in the backend. So I didn't bother building the frontend all by myself. Hehe. :)

I just prompted to claude and vibe coded a working frontend for the demo.

You can create something like that too, just prompt your cursor or any AI coding tool you use. Here is the responsive UI in my public/index.html (in case you would like to copy paste):

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Email OTP Authentication</title>
  <style>
    /* CSS styles for the authentication forms */
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      margin: 0;
      padding: 20px;
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .container {
      background: white;
      border-radius: 12px;
      box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
      padding: 40px;
      width: 100%;
      max-width: 400px;
    }

    h1 {
      text-align: center;
      color: #333;
      margin-bottom: 30px;
      font-size: 24px;
    }

    .form-group {
      margin-bottom: 20px;
    }

    label {
      display: block;
      margin-bottom: 5px;
      color: #555;
      font-weight: 500;
    }

    input {
      width: 100%;
      padding: 12px 16px;
      border: 2px solid #e1e5e9;
      border-radius: 8px;
      font-size: 16px;
      transition: border-color 0.3s ease;
      box-sizing: border-box;
    }

    input:focus {
      outline: none;
      border-color: #667eea;
    }

    button {
      width: 100%;
      padding: 12px 16px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 16px;
      font-weight: 600;
      cursor: pointer;
      transition: transform 0.2s ease;
    }

    button:hover {
      transform: translateY(-2px);
    }

    button:disabled {
      opacity: 0.6;
      cursor: not-allowed;
      transform: none;
    }

    .message {
      padding: 12px 16px;
      border-radius: 8px;
      margin-bottom: 20px;
      font-weight: 500;
    }

    .success {
      background: #d4edda;
      color: #155724;
      border: 1px solid #c3e6cb;
    }

    .error {
      background: #f8d7da;
      color: #721c24;
      border: 1px solid #f5c6cb;
    }

    .hidden {
      display: none;
    }

    .otp-input {
      text-align: center;
      font-size: 18px;
      letter-spacing: 8px;
    }

    .timer {
      text-align: center;
      margin: 20px 0;
      font-size: 14px;
      color: #666;
    }

    .resend-link {
      text-align: center;
      margin-top: 20px;
    }

    .resend-link a {
      color: #667eea;
      text-decoration: none;
      font-weight: 500;
    }

    .resend-link a:hover {
      text-decoration: underline;
    }
  </style>
</head>
<body>
  <div class="container">
    <!-- Email Form -->
    <div id="emailForm">
      <h1>🔐 Email Verification</h1>
      <p>Enter your email to receive a verification code</p>

      <div id="emailMessage" class="message hidden"></div>

      <form id="requestOtpForm">
        <div class="form-group">
          <label for="email">Email Address</label>
          <input
            type="email"
            id="email"
            placeholder="your@email.com"
            required
          >
        </div>
        <button type="submit" id="sendOtpBtn">Send Code</button>
      </form>
    </div>

    <!-- OTP Form -->
    <div id="otpForm" class="hidden">
      <h1>✉️ Verify Code</h1>
      <p>Enter the 6-digit code sent to <strong id="userEmail"></strong></p>

      <div id="otpMessage" class="message hidden"></div>

      <form id="verifyOtpForm">
        <div class="form-group">
          <label for="otp">Verification Code</label>
          <input
            type="text"
            id="otp"
            class="otp-input"
            placeholder="000000"
            maxlength="6"
            pattern="[0-9]{6}"
            required
          >
        </div>
        <button type="submit" id="verifyOtpBtn">Verify Code</button>
      </form>

      <div class="timer">
        Code expires in <span id="countdown">10:00</span>
      </div>

      <div class="resend-link hidden" id="resendLink">
        <a href="#" id="resendBtn">Resend Code</a>
      </div>
    </div>
  </div>

  <script>
    // JavaScript implementation for OTP authentication
    let countdownInterval;
    let userEmail = '';

    // Request OTP
    document.getElementById('requestOtpForm').addEventListener('submit', async (e) => {
      e.preventDefault();

      const email = document.getElementById('email').value;
      const btn = document.getElementById('sendOtpBtn');
      const message = document.getElementById('emailMessage');

      btn.disabled = true;
      btn.textContent = 'Sending...';

      try {
        const response = await fetch('/api/request-otp', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ email }),
        });

        const data = await response.json();

        if (response.ok) {
          userEmail = email;
          showMessage(message, data.message, 'success');
          setTimeout(() => {
            showOtpForm();
            startCountdown(data.expiresAt);
          }, 1000);
        } else {
          showMessage(message, data.error || 'Failed to send code', 'error');
          btn.disabled = false;
          btn.textContent = 'Send Code';
        }
      } catch (error) {
        showMessage(message, 'Network error. Please try again.', 'error');
        btn.disabled = false;
        btn.textContent = 'Send Code';
      }
    });

    // Verify OTP
    document.getElementById('verifyOtpForm').addEventListener('submit', async (e) => {
      e.preventDefault();

      const otp = document.getElementById('otp').value;
      const btn = document.getElementById('verifyOtpBtn');
      const message = document.getElementById('otpMessage');

      btn.disabled = true;
      btn.textContent = 'Verifying...';

      try {
        const response = await fetch('/api/verify-otp', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ email: userEmail, otp }),
        });

        const data = await response.json();

        if (response.ok && data.success) {
          clearInterval(countdownInterval);
          showMessage(message, 'Code verified successfully!', 'success');
          // Handle successful verification
        } else {
          showMessage(message, data.error || 'Invalid code', 'error');
          btn.disabled = false;
          btn.textContent = 'Verify Code';
        }
      } catch (error) {
        showMessage(message, 'Network error. Please try again.', 'error');
        btn.disabled = false;
        btn.textContent = 'Verify Code';
      }
    });

    // Resend OTP
    document.getElementById('resendLink').addEventListener('click', (e) => {
      e.preventDefault();
      // Resend logic here
    });

    // Helper functions
    function showMessage(element, text, type) {
      element.textContent = text;
      element.className = `message ${type}`;
      element.classList.remove('hidden');
    }

    function showOtpForm() {
      document.getElementById('emailForm').classList.add('hidden');
      document.getElementById('otpForm').classList.remove('hidden');
      document.getElementById('userEmail').textContent = userEmail;
    }

    function startCountdown(expiresAt) {
      const now = Date.now();
      let timeLeft = Math.floor((expiresAt - now) / 1000);

      if (timeLeft <= 0) {
        timeLeft = 0;
      }

      const countdownEl = document.getElementById('countdown');
      const resendLink = document.getElementById('resendLink');

      resendLink.classList.add('hidden');

      countdownInterval = setInterval(() => {
        timeLeft--;

        if (timeLeft <= 0) {
          clearInterval(countdownInterval);
          resendLink.classList.remove('hidden');
          countdownEl.textContent = 'expired';
          return;
        }

        const minutes = Math.floor(timeLeft / 60);
        const seconds = timeLeft % 60;
        countdownEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
      }, 1000);
    }
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

express js will statically serve the html file.

Environment Configuration

.env File Setup

Create a .env file in your project root:

# Convex Configuration
# Deployment used by `npx convex dev`
CONVEX_DEPLOYMENT=dev:convex-deployment # team: debajyati-dey, project: otp-demo
CONVEX_URL=https://your-deployment.convex.cloud

# AutoSend API Configuration
AUTOSEND_API_KEY=your_autosend_api_key_here
AUTOSEND_FROM_EMAIL=noreply@yourdomain.com
AUTOSEND_FROM_NAME=DEYOTP

# Server Configuration
PORT=3000
Enter fullscreen mode Exit fullscreen mode

The Complete Workflow in Action

Our methodology works smooth and is very robust in implementation.
Our demo website looks like this before authentication -
Triggering Sending OTP to my email for authentication process

After the email is sent successfully, the modal dialog will change and wait for user to enter the otp code sent to his email address to verify the user email and let him/her log in.

Auth Modal Dialog waiting for user input- otp token

Now, user must enter the otp code (6-digit token) within 10 minutes, because after that the token will expire.

If everything works properly, the user will recieve the email in his/her email client (gmail or proton or thunderbird or outlook whatever he/she uses).

The recieved email containing the OTP

User typing the otp code copied from email he/she recieved

After successful verification the system will let the user authenticate into his account in the website and TADAA!!

Session logs

Here is the profile page -

User profie page after successful authentication

I know you might get the ick after seeing the emojis and the purple gradient in the webpage but yeah I told you before it is just a demo and didn't care about the the frontend will look like so I generated it with AI.

Best Practices

Email Content Best Practices

  1. Clear Subject Lines: Use descriptive, action-oriented subject lines
  2. Personalization: Include recipient's name when possible
  3. Mobile Optimization: Ensure emails render well on mobile devices
  4. Plain Text Fallback: Always include a plain text version.
  5. Unsubscribe Links: Must include unsubscribe options for marketing emails.

Troubleshooting

Common Issues

Email Not Being Delivered

Symptoms: Emails are not reaching recipients.

Solutions:

  1. Check AutoSend API key and credentials.
  2. Verify sender email is authorized in AutoSend.
  3. Check spam/junk folders.
  4. Review AutoSend delivery logs.

OTP Verification Failing

Symptoms: Valid OTP codes are rejected.

Solutions:

  1. Check server time synchronization between Express server and Convex.
  2. Verify OTP storage and retrieval logic - ensure expiresAt is returned from Convex.
  3. Check for race conditions in OTP creation/deletion.
  4. Review attempt counting logic.
  5. Timing Issues: This system includes a 30-second grace period for expiration checks to account for clock differences and network latency.
  6. Countdown Accuracy: Client countdown uses the exact expiresAt timestamp stored in Convex for synchronization.

Debugging Tips

  1. Enable Detailed Logging: Add comprehensive logging for email operations.
  2. Monitor API Usage: Track API call volumes and error rates.
  3. Database Inspection: Use Convex dashboard to inspect data and uptime.

Conclusion

I hope you learnt something new or at least found a new way to build your own secure auth system by leveraging different services that together do very well.

Now, if you found this article helpful, if this blog added some value to your time and energy, please show some love by giving the article some likes and share it with your dev friends.

Feel free to connect with me. :)

Thanks for reading! 🙏🏻
Written with 💚 by Debajyati Dey
My GitHub My LinkedIn My Daily.dev My Peerlist My Twitter

Follow me on Dev to motivate me so that I can bring more such tutorials like this on here!

Happy coding 🧑🏽‍💻👩🏽‍💻! Have a nice day ahead! 🚀

Further Read For Reference

Top comments (4)

Collapse
 
sammaji profile image
Samyabrata Maji

Definitely checking out autosend 👀

Collapse
 
ddebajyati profile image
Debajyati Dey

Sure! Let me know what you build using it!

Collapse
 
hey_yogini profile image
Yogini Bende

Thanks a lot for writing such a detailed article

Collapse
 
ddebajyati profile image
Debajyati Dey

Glad you liked the article and found it useful!💚💚💚