messaging channels
messaging channels
Sinch SMS Scheduling with Node.js: Build Automated Appointment Reminders (2025 Guide)
Build production-ready SMS appointment reminders with Sinch SMS API and Node.js. Complete tutorial with send_at parameter scheduling, Luxon timezone handling, database persistence, and secure Express implementation. No cron jobs needed.
Learn how to build a production-ready SMS appointment reminder system using Sinch SMS API, Node.js, and Express. This comprehensive guide shows you how to schedule SMS messages with the send_at parameter—no cron jobs or background workers required. Sinch handles all message scheduling server-side, simplifying your infrastructure.
The hospital appointment reminder example adapts to multiple use cases:
- Meeting reminders for business scheduling
- Event notifications for conferences or webinars
- Subscription renewal alerts for SaaS applications
- Service maintenance notifications
Sinch's built-in scheduling eliminates complex local scheduling libraries and persistent background processes. The API handles message delivery server-side, reducing your infrastructure complexity.
Prerequisites:
Environment:
- Node.js v18 or v20 LTS (v18 supported until May 2025)
- npm or yarn package manager
- Download Node.js
Sinch Account & Credentials:
- Active Sinch account (Sign up)
- Project ID (from Sinch Customer Dashboard)
- API Key ID and Secret (from Access Keys section)
- Provisioned SMS-enabled phone number in your target region
- Verified recipient numbers for testing (required during trial/development)
Dependencies:
@sinch/sdk-corev1.2.1@sinch/smsv1.2.1- Express v4.18+
- Luxon v3.7.2+
Technical Knowledge:
- Node.js and Express fundamentals
- JavaScript async/await patterns
- HTML forms and REST APIs
Source: Sinch Node.js SDK npm repository (@sinch/sdk-core v1.2.1, @sinch/sms v1.2.1); Node.js LTS release schedule; Sinch SMS API documentation (send_at parameter accepts ISO 8601 format, defaults to UTC if no offset specified)
What You'll Build:
- Express web application with appointment scheduling UI
- Backend validation and SMS scheduling via Sinch
send_atparameter - Secure credential management with environment variables
- Error handling and structured logging
- Production-ready database persistence and deployment strategies
System Architecture:
graph LR
A[User Browser] -- HTTP Request (Submit Form) --> B(Node.js/Express App);
B -- Schedule SMS (API Call w/ send_at) --> C(Sinch SMS API);
C -- Scheduled Time --> D(Sends SMS);
D -- SMS --> E(Recipient Phone);
B -- HTTP Response (Success/Error Page) --> A;
B -- Store Appointment (DB Write) --> F[(Database)];
B -- Retrieve Appointment (DB Read) --> F;- User Browser: Submits appointment details via HTML form
- Node.js/Express App:
- Serves HTML form
- Validates input data
- Calls Sinch API with
send_atparameter to schedule delivery - Persists appointment data to database (production requirement)
- Renders confirmation or error page
- Sinch SMS API:
- Receives API request
- Stores message with scheduled
send_attimestamp - Sends SMS at specified time
- Recipient Phone: Receives SMS reminder
- Database: Stores appointment details, ensuring persistence across server restarts
1. Project Setup: Installing Sinch Node.js SDK and Dependencies
-
Create Project Directory:
bashmkdir sinch-appointment-reminder cd sinch-appointment-reminder -
Initialize Node.js Project:
bashnpm init -y -
Create Project Structure:
bash# Create directories mkdir public public/css views # Create files (use `touch` on macOS/Linux or `echo. >` on Windows) touch .env .gitignore app.js routes.js public/css/style.css views/patient_details.ejs views/success.ejs.env: Environment variables (API keys, secrets) – never commit to Git.gitignore: Files and folders Git should ignoreapp.js: Express application entry pointroutes.js: Application routing and request logicpublic/: Static assets (CSS, JavaScript, images)views/: EJS templates
-
Install Dependencies:
bashnpm install express @sinch/sdk-core dotenv luxon express-session connect-flash ejsexpress: Web framework for Node.js@sinch/sdk-core: Official Sinch Node.js SDKdotenv: Loads environment variables from.envluxon: Date/time library (v3.7.2) with native timezone supportexpress-session: Session management middlewareconnect-flash: Temporary message display middlewareejs: HTML templating engine
Note: Avoid
sessionstorage-for-nodejsin production – data is lost on server restarts. Useexpress-sessionfor temporary data and a database for persistent storage (see Section 6).
Source: Luxon npm package (v3.7.2); Luxon documentation (native timezone support)
-
Install Development Dependency:
bashnpm install --save-dev nodemon -
Configure
.gitignore:plaintext# .gitignore # Dependencies node_modules # Environment variables .env # Logs logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # OS generated files .DS_Store Thumbs.db -
Configure Environment Variables (
.env):Replace placeholder values with your actual credentials. Generate
SESSION_SECRETusing a cryptographically secure method:bash# Generate secure session secret node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"dotenv# .env # Sinch API Credentials & Configuration # Obtain from Sinch Customer Dashboard -> Access Keys -> Create API Key KEY_ID='YOUR_SINCH_KEY_ID' KEY_SECRET='YOUR_SINCH_KEY_SECRET' PROJECT_ID='YOUR_SINCH_PROJECT_ID' # Sinch Phone Number (Must be SMS enabled and assigned to your Project) # Obtain from Sinch Customer Dashboard -> Numbers -> Your Numbers FROM_NUMBER='+1xxxxxxxxxx' # Use E.164 format (e.g., +12025550181) # Sinch SMS API Region (e.g., 'us' or 'eu') # Determines the API endpoint and potentially default country code logic SMS_REGION='us' # Application Configuration PORT=3000 # Generate a strong random string for production! SESSION_SECRET='replace_with_a_strong_random_secret_string' # Optional: Default Country Code (Used for basic phone formatting logic) # Example for US DEFAULT_COUNTRY_CODE='+1' # Example for UK # DEFAULT_COUNTRY_CODE='+44'KEY_ID,KEY_SECRET,PROJECT_ID: Access Keys page in Sinch Customer Dashboard (treat Secret like a password)FROM_NUMBER: SMS-enabled virtual number from Sinch (E.164 format required)SMS_REGION: Sinch API region (us,eu) affecting endpoints and compliancePORT: Express application portSESSION_SECRET: Cryptographically secure random string (32+ bytes) for session cookie signingDEFAULT_COUNTRY_CODE: Prepends country code to phone numbers without one
-
Set Up
nodemonScript:json// package.json ("scripts" section) "scripts": { "start": "node app.js", "dev": "nodemon app.js", "test": "echo \"Error: no test specified\" && exit 1" },Run
npm run devto start the development server. -
Configure Application Entry Point (
app.js):javascript// app.js const express = require('express'); const session = require('express-session'); const flash = require('connect-flash'); const path = require('path'); const routes = require('./routes'); // Import routes require('dotenv').config(); // Load .env variables early const app = express(); const port = process.env.PORT || 3000; // View engine setup (EJS) app.set('views', path.join(__dirname, 'views')); app.set('view engine', 'ejs'); // Middleware // Serve static files (CSS, client-side JS) from 'public' directory app.use(express.static(path.join(__dirname, 'public'))); // Parse URL-encoded bodies (as sent by HTML forms) app.use(express.urlencoded({ extended: false })); // Session middleware configuration app.use( session({ secret: process.env.SESSION_SECRET || 'default_fallback_secret', // Use env var resave: false, // Don't save session if unmodified saveUninitialized: false, // Don't create session until something stored cookie: { maxAge: 3600000, // Cookie expires in 1 hour (in milliseconds) secure: process.env.NODE_ENV === 'production', // Use secure cookies in production (HTTPS only) httpOnly: true, // Helps prevent XSS attacks by blocking client-side JS access sameSite: 'strict' // CSRF protection } }) ); // Flash message middleware (depends on session) app.use(flash()); // Global variables for views (e.g., making flash messages available) app.use((req, res, next) => { res.locals.success_msg = req.flash('success_msg'); res.locals.error_msg = req.flash('error_msg'); // Make environment variables available to templates if needed (use with caution) res.locals.process = { env: { DEFAULT_COUNTRY_CODE: process.env.DEFAULT_COUNTRY_CODE } }; next(); }); // Mount the routes defined in routes.js app.use('/', routes); // Basic Error Handler (optional but recommended) app.use((err, req, res, next) => { console.error(err.stack); res.status(500).send('Something broke!'); }); // Start the server app.listen(port, () => { console.log(`Server started on http://localhost:${port}`); console.log(`Sinch Project ID: ${process.env.PROJECT_ID}`); console.log(`Sinch SMS Region: ${process.env.SMS_REGION}`); });Key configurations:
- Load environment variables with
dotenv.config() - Configure
express.staticandexpress.urlencodedmiddleware - Set up
express-sessionwith 1-hour cookie lifetime - Enable security options:
secure(HTTPS-only),httpOnly(XSS prevention),sameSite(CSRF protection) - Make flash messages available globally in EJS views via
res.locals - Mount routes from
routes.js - Start server on configured
PORT
- Load environment variables with
Source: Express security best practices documentation (secure cookies require HTTPS, httpOnly prevents client-side JS access, sameSite provides CSRF protection)
2. Implementing SMS Scheduling with Sinch send_at Parameter
The Sinch SMS API's send_at parameter is the key to serverless SMS scheduling. It accepts an ISO 8601 UTC timestamp and handles all message delivery server-side. When you make the API call, Sinch stores the message in a queue and automatically sends it at the specified time—no cron jobs, no background workers, no node-cron library needed. This architecture dramatically simplifies scheduled SMS implementation compared to traditional polling-based systems.
// routes.js
const express = require('express');
const router = express.Router();
const { DateTime } = require('luxon'); // For date/time handling
const { SinchClient } = require('@sinch/sdk-core');
require('dotenv').config(); // Ensure env vars are loaded
// --- Sinch Client Initialization ---
if (!process.env.PROJECT_ID || !process.env.KEY_ID || !process.env.KEY_SECRET || !process.env.SMS_REGION || !process.env.FROM_NUMBER) {
console.error("FATAL ERROR: Missing required Sinch environment variables in .env file.");
console.error("Check KEY_ID, KEY_SECRET, PROJECT_ID, SMS_REGION, FROM_NUMBER.");
process.exit(1);
}
const sinchClient = new SinchClient({
projectId: process.env.PROJECT_ID,
keyId: process.env.KEY_ID,
keySecret: process.env.KEY_SECRET,
region: process.env.SMS_REGION, // Use region from .env
});
// --- Helper Function to Send Scheduled SMS ---
async function scheduleReminder(to, message, sendAtIsoString) {
console.log('Scheduling SMS:');
console.log(` To: ${to}`);
console.log(` Message: "${message}"`);
console.log(` Send Time (UTC): ${sendAtIsoString}`);
try {
const response = await sinchClient.sms.batches.send({
sendSMSRequestBody: {
to: [to],
from: process.env.FROM_NUMBER,
body: message,
send_at: sendAtIsoString, // ISO 8601 UTC timestamp
},
});
console.log('Sinch API Response:', JSON.stringify(response, null, 2));
// Successful response means Sinch accepted the scheduling request
return { success: true, batchId: response.id };
} catch (error) {
console.error('Error scheduling SMS:', error.response ? JSON.stringify(error.response.data, null, 2) : error.message);
let errorMessage = 'Failed to schedule reminder.';
if (error.response?.data?.text) {
errorMessage += ` Details: ${error.response.data.text}`;
} else if (error.message) {
errorMessage += ` Details: ${error.message}`;
}
return { success: false, error: errorMessage };
}
}
// --- Route Definitions ---
// GET / : Redirect to the appointment form
router.get('/', (req, res) => {
res.redirect('/appointment');
});
// GET /appointment : Display the appointment scheduling form
router.get('/appointment', (req, res) => {
// Render the form. Flash messages are available globally
res.render('patient_details');
});
// POST /appointment : Handle form submission, validate, and schedule SMS
router.post('/appointment', async (req, res, next) => {
const { patientName, doctorName, appointmentDate, appointmentTime, patientPhone } = req.body;
// --- 1. Input Validation ---
if (!patientName || !doctorName || !appointmentDate || !appointmentTime || !patientPhone) {
req.flash('error_msg', 'Please fill in all fields.');
return res.redirect('/appointment');
}
// --- 2. Date/Time Processing with Luxon ---
let appointmentDateTimeLocal;
try {
// Combine date and time, parse using Luxon in local timezone
appointmentDateTimeLocal = DateTime.fromISO(`${appointmentDate}T${appointmentTime}`, { zone: 'local' });
if (!appointmentDateTimeLocal.isValid) {
req.flash('error_msg', `Invalid date or time format. Error: ${appointmentDateTimeLocal.invalidReason || 'Unknown reason'}`);
return res.redirect('/appointment');
}
} catch (parseError) {
console.error('Error parsing date/time:', parseError);
req.flash('error_msg', 'Could not parse the provided date or time.');
return res.redirect('/appointment');
}
// --- 3. Scheduling Validation ---
try {
// Ensure appointment is reasonably in the future
// Reminder is set 2 hours before appointment
const reminderLeadTime = { hours: 2 };
const minimumBookingLeadTime = { hours: 2, minutes: 5 };
const nowLocal = DateTime.local();
const reminderDateTimeLocal = appointmentDateTimeLocal.minus(reminderLeadTime);
// Check if appointment is far enough in the future
if (appointmentDateTimeLocal < nowLocal.plus(minimumBookingLeadTime)) {
req.flash('error_msg', `Appointment must be at least ${minimumBookingLeadTime.hours} hours and ${minimumBookingLeadTime.minutes} minutes from now.`);
return res.redirect('/appointment');
}
// --- 4. Prepare Data for Sinch ---
// Format reminder time as UTC ISO 8601 string (required by Sinch send_at)
const reminderDateTimeUtcISO = reminderDateTimeLocal.toUTC().toISO();
// Format phone number (basic implementation)
let formattedPhone = patientPhone.trim();
if (!formattedPhone.startsWith('+')) {
const defaultCountryCode = process.env.DEFAULT_COUNTRY_CODE || '+1';
formattedPhone = defaultCountryCode + formattedPhone.replace(/[^0-9]/g, '');
}
// PRODUCTION WARNING: Use libphonenumber-js for robust phone validation
// npm install libphonenumber-js
// Construct SMS message body
const messageBody = `Hi ${patientName}, this is a reminder for your appointment with Dr. ${doctorName} scheduled for ${appointmentDateTimeLocal.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY)}.`;
// --- 5. Schedule SMS via Sinch ---
const scheduleResult = await scheduleReminder(formattedPhone, messageBody, reminderDateTimeUtcISO);
if (!scheduleResult.success) {
req.flash('error_msg', `Failed to schedule reminder. ${scheduleResult.error || ''}`);
return res.redirect('/appointment');
}
// --- 6. Persist Data (Database Required for Production!) ---
// IMPORTANT: Save to persistent database (See Section 6)
// Store details in session temporarily for success page display
req.session.appointmentDetails = {
patient: patientName,
doctor: doctorName,
phone: formattedPhone,
appointmentDateTimeDisplay: appointmentDateTimeLocal.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY),
reminderDateTimeDisplay: reminderDateTimeLocal.toLocaleString(DateTime.DATETIME_MED_WITH_WEEKDAY),
sinchBatchId: scheduleResult.batchId || 'N/A'
};
// In database implementation, store record and save ID: req.session.appointmentId = dbRecord.id
req.flash('success_msg', 'Appointment reminder scheduled successfully!');
res.redirect('/success');
} catch (error) {
console.error('Error processing appointment:', error);
req.flash('error_msg', 'An unexpected error occurred while processing the appointment.');
res.redirect('/appointment');
}
});
// GET /success : Display confirmation page
router.get('/success', (req, res) => {
// Retrieve data from session for display
const appointmentDetails = req.session.appointmentDetails;
// Clear temporary session data
if (req.session.appointmentDetails) {
delete req.session.appointmentDetails;
}
// PRODUCTION NOTE: In production, retrieve from database using ID
// Example: const appointmentDetails = await prisma.appointment.findUnique({ where: { id: req.session.appointmentId } })
if (!appointmentDetails) {
req.flash('error_msg', 'Confirmation details expired or unavailable. Please check your schedule or contact support.');
return res.redirect('/appointment');
}
res.render('success', {
patient: appointmentDetails.patient,
doctor: appointmentDetails.doctor,
phone: appointmentDetails.phone,
appointmentTimeDisplay: appointmentDetails.appointmentDateTimeDisplay,
reminderTimeDisplay: appointmentDetails.reminderDateTimeDisplay,
batchId: appointmentDetails.sinchBatchId
});
});
module.exports = router;- Sinch Client: Initialized using credentials from
.envwith validation for missing variables scheduleReminderFunction: Calls the Sinch API (sinchClient.sms.batches.send) with UTC ISO 8601 forsend_at- Routes:
/: Redirects to appointment form/appointment(GET): Renderspatient_details.ejs/appointment(POST):- Validates required fields
- Parses and validates date/time using Luxon
- Calculates reminder time (2 hours prior) and validates future scheduling
- Formats reminder time to UTC ISO 8601 and constructs message
- Calls
scheduleReminderto schedule via Sinch - Stores confirmation details temporarily in session (database required for production)
/success(GET): Retrieves and displays confirmation from session, then clears temporary data
3. Building the Appointment Scheduling Interface
Create HTML forms for user interaction using EJS templates.
views/patient_details.ejs:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Schedule Appointment Reminder</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<h1>Schedule Sinch Hospital Appointment Reminder</h1>
<% if (typeof error_msg !== 'undefined' && error_msg && error_msg.length > 0) { %>
<div class="alert alert-danger"><%= error_msg %></div>
<% } %>
<form action="/appointment" method="POST">
<div>
<label for="patientName">Patient Name:</label>
<input type="text" id="patientName" name="patientName" required>
</div>
<div>
<label for="doctorName">Doctor Name:</label>
<input type="text" id="doctorName" name="doctorName" required>
</div>
<div>
<label for="appointmentDate">Appointment Date:</label>
<input type="date" id="appointmentDate" name="appointmentDate" required>
</div>
<div>
<label for="appointmentTime">Appointment Time:</label>
<input type="time" id="appointmentTime" name="appointmentTime" required>
</div>
<div>
<label for="patientPhone">Patient Phone Number:</label>
<input type="tel" id="patientPhone" name="patientPhone" required placeholder="+15551234567 or 5551234567">
<small>Enter number with country code (e.g., +1555…) or without if in the default region (<%= process.env.DEFAULT_COUNTRY_CODE || '+1' %>).</small>
</div>
<button type="submit">Schedule Reminder</button>
</form>
</body>
</html>views/success.ejs:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reminder Scheduled</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<h1>Appointment Reminder Scheduled Successfully!</h1>
<% if (typeof success_msg !== 'undefined' && success_msg && success_msg.length > 0) { %>
<div class="alert alert-success"><%= success_msg %></div>
<% } %>
<% if (typeof patient !== 'undefined' && patient) { %>
<p>An SMS reminder has been scheduled for:</p>
<ul>
<li><strong>Patient:</strong> <%= patient %></li>
<li><strong>Doctor:</strong> <%= doctor %></li>
<li><strong>Appointment Time:</strong> <%= appointmentTimeDisplay %></li>
<li><strong>Patient Phone:</strong> <%= phone %></li>
<li><strong>Reminder Scheduled Time:</strong> <%= reminderTimeDisplay %> (approx, local time)</li>
<li><strong>Sinch Batch ID:</strong> <%= batchId %></li>
</ul>
<p><em>Note: This confirmation shows details based on submission. Ensure data is saved persistently for reliability.</em></p>
<% } else { %>
<p>Confirmation details could not be retrieved (session might have expired or data was unavailable).</p>
<% } %>
<p><a href="/appointment">Schedule another reminder</a></p>
</body>
</html>public/css/style.css:
/* public/css/style.css */
body {
font-family: sans-serif;
padding: 20px;
line-height: 1.6;
}
h1 {
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
form div {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"],
input[type="date"],
input[type="time"],
input[type="tel"] {
width: 95%;
max-width: 400px;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
}
small {
display: block;
font-size: 0.8em;
color: #666;
margin-top: 3px;
}
button {
padding: 10px 15px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
}
button:hover {
background-color: #0056b3;
}
.alert {
padding: 15px;
margin-bottom: 20px;
border: 1px solid transparent;
border-radius: 4px;
}
.alert-danger {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
}
.alert-success {
color: #155724;
background-color: #d4edda;
border-color: #c3e6cb;
}
ul {
list-style: none;
padding: 0;
}
li {
margin-bottom: 8px;
}4. Sinch SMS API Integration and Credentials Setup
Integration happens primarily in routes.js:
- Credentials: Securely loaded from
.env - Client Initialization:
SinchClientinstantiated using Project ID, Key ID, Key Secret, and Region - Scheduling API Call:
scheduleReminderfunction usessinchClient.sms.batches.send, passing recipient (to), sender (from), message body, and thesend_atparameter (UTC ISO 8601 timestamp)
Obtaining Credentials:
- Log in to your Sinch Customer Dashboard
- Navigate to Access Keys in the left menu
- Click Create Key and give it a name (e.g., "NodeReminderApp")
- Immediately copy the
Key IDandKey Secret– the Secret is only shown once. Store these securely in your.envfile - Your Project ID is visible at the top of the dashboard or under API settings. Add it to
.env - Navigate to Numbers → Your Numbers. Copy your active SMS-enabled number (E.164 format, e.g.,
+1xxxxxxxxxx) and add it asFROM_NUMBERin.env - Note the Region (
us,eu, etc.) associated with your service plan/project and setSMS_REGIONin.env
5. Production Error Handling and Logging Best Practices
The current implementation includes basic error handling:
- Missing Environment Variables:
routes.jschecks for essential Sinch variables on startup and exits if missing - API Call Errors:
scheduleReminderfunction usestry...catcharoundsinchClient.sms.batches.send, logs errors, and returns failure status - Input Validation: Basic checks for empty fields and appointment time validity in POST
/appointmenthandler, with flash messages for user feedback - General Errors: Fallback
try...catchin POST/appointmenthandler catches unexpected errors during processing - Logging: Currently uses
console.logandconsole.errorwith consistent formatting
Production Enhancements:
-
Structured Logging: Use
winstonorpinofor structured JSON logsbashnpm install winstonReplace
console.log/console.errorwith logger instances (e.g.,logger.info(),logger.error()). Configure transports to write logs to files or external logging services. -
Detailed API Error Parsing: Check
error.response.datafrom Sinch API errors for specific error codes to provide more informative feedback or trigger specific actions (e.g., retries for transient network issues) -
Centralized Error Handling Middleware: Implement more robust Express error-handling middleware in
app.jsto catch unhandled errors gracefully -
Retry Mechanisms: For transient network errors, consider implementing a simple retry strategy (1-2 retries with short delay). However, since Sinch handles the sending schedule, retrying the scheduling call might result in duplicate scheduled messages if the first call succeeded but the response was lost. Log failures and alert administrators instead.
6. Database Persistence for Scheduled SMS Appointments
Using only express-session is NOT persistent storage. Scheduled jobs require data that survives server restarts and deployments. If the server restarts between scheduling via the API and rendering the success page (or if the user's session expires), confirmation data will be lost. The application has no record of scheduled appointments after server restarts.
A database is mandatory for production.
Conceptual Database Schema (PostgreSQL or MySQL):
CREATE TABLE appointments (
id SERIAL PRIMARY KEY, -- Or UUID for distributed systems
patient_name VARCHAR(255) NOT NULL,
doctor_name VARCHAR(255) NOT NULL,
patient_phone VARCHAR(30) NOT NULL, -- Store in E.164 format ideally
appointment_datetime TIMESTAMPTZ NOT NULL, -- Store with time zone (UTC recommended)
reminder_datetime TIMESTAMPTZ NOT NULL, -- Store scheduled reminder time (UTC recommended)
sinch_batch_id VARCHAR(100) UNIQUE, -- Store ID returned by Sinch API for tracking
status VARCHAR(50) DEFAULT 'scheduled', -- e.g., 'scheduled', 'sent', 'failed', 'cancelled'
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
-- Optional: Index for querying by phone or appointment time
CREATE INDEX idx_appointments_phone ON appointments(patient_phone);
CREATE INDEX idx_appointments_datetime ON appointments(appointment_datetime);Data Layer Implementation Steps:
- Choose a Database: Select PostgreSQL, MySQL, or MongoDB
- Install ORM/Driver:
- PostgreSQL:
npm install pg(driver) ornpm install sequelize/npm install @prisma/client(ORMs) - MySQL:
npm install mysql2(driver) ornpm install sequelize/npm install @prisma/client(ORMs) - MongoDB:
npm install mongodb(driver) ornpm install mongoose(ODM)
- PostgreSQL:
- Configure Connection: Set up database connection details using environment variables
- Define Model/Schema: Create the table/collection schema using your chosen tool's syntax
- Modify
routes.js:- Import your database client/model
- In POST
/appointmenthandler, after successfully scheduling with Sinch (scheduleResult.success), save appointment details (includingpatientName,doctorName,formattedPhone,appointmentDateTimeLocal.toJSDate(),reminderDateTimeLocal.toJSDate(),scheduleResult.batchId) to database - Store only the database record's ID in session:
req.session.appointmentId = newAppointmentRecord.id - In GET
/successhandler, retrieveappointmentIdfrom session and query database for full appointment details. Rendersuccess.ejsusing database data. Clearreq.session.appointmentIdafterwards
- Phone Number Validation: Implement robust validation using
libphonenumber-jsbefore saving to database and sending to Sinch. Ensure numbers are stored consistently (E.164 format).
This ensures appointment data persists reliably even after application restarts, providing a foundation for future features like viewing scheduled appointments, cancellation, or status tracking.
Frequently Asked Questions
How does Sinch SMS scheduling work with the send_at parameter?
The Sinch SMS API's send_at parameter accepts an ISO 8601 timestamp in UTC format. When you include this parameter in your API call, Sinch stores the message and automatically sends it at the specified future time. You don't need local cron jobs or background workers – Sinch handles the entire scheduling mechanism server-side.
What version of the Sinch Node.js SDK should I use for SMS scheduling?
Use @sinch/sdk-core v1.2.1 and @sinch/sms v1.2.1 (latest as of January 2025). These versions provide full support for the send_at scheduling parameter and are compatible with Node.js v16+ (v18 or v20 LTS recommended for production as of 2025).
How do I handle timezones when scheduling SMS reminders with Luxon?
Luxon (v3.7.2) provides native timezone support. Parse user input in local time with DateTime.fromISO(), perform calculations (like subtracting 2 hours for reminders), then convert to UTC using .toUTC().toISO() before sending to Sinch. Always store appointment times with timezone information (TIMESTAMPTZ in PostgreSQL) for accurate scheduling across regions.
Why is express-session not suitable for production appointment storage?
The default MemoryStore for express-session loses all data when your server restarts, which means scheduled appointments would be lost. For production, you must use a persistent database (PostgreSQL, MySQL, MongoDB) to store appointment details. Use express-session only for temporary data like flash messages or passing confirmation IDs between requests.
What secure cookie settings should I use for Express session in production?
Enable three critical security options: secure: true (HTTPS-only cookies), httpOnly: true (prevents XSS attacks by blocking client-side JavaScript access), and sameSite: 'strict' (CSRF protection). Also set app.set('trust proxy', 1) if running behind a load balancer or reverse proxy.
Can I schedule SMS messages more than 24 hours in advance with Sinch?
Yes, Sinch supports scheduling SMS messages days or even weeks in advance using the send_at parameter. There's no documented hard limit, but for very long-term scheduling (months), consider storing appointments in your database and implementing a daily or weekly job to schedule upcoming reminders within a reasonable window (e.g., 7 days ahead).
How do I validate international phone numbers before sending to Sinch?
Use the libphonenumber-js library for robust phone number validation: npm install libphonenumber-js. Parse numbers with parsePhoneNumber(input, defaultCountry), validate with .isValid(), and format to E.164 using .format('E.164'). This ensures all numbers are correctly formatted (e.g., +442071234567) before sending to Sinch.
What happens if my Express server restarts after scheduling an SMS?
If you only store appointment data in express-session, it's lost on restart. However, the SMS message itself is already scheduled with Sinch's servers and will send at the specified time. For production reliability, persist all appointment details (including Sinch batch IDs) to a database so you can track, modify, or cancel scheduled messages even after server restarts.
How do I cancel or update a scheduled SMS reminder in Sinch?
Use the Sinch batch ID returned when scheduling (stored in response.id) to modify or cancel messages. Call sinchClient.sms.batches.update() with the batch ID to change the message or send time, or sinchClient.sms.batches.cancel() to prevent delivery. Store batch IDs in your database alongside appointment records for full management capabilities.
Example:
// Cancel a scheduled batch
await sinchClient.sms.batches.cancel({ batch_id: 'your-batch-id' });
// Update a scheduled batch
await sinchClient.sms.batches.update({
batch_id: 'your-batch-id',
sendSMSRequestBody: {
to: ['+15551234567'],
from: process.env.FROM_NUMBER,
body: 'Updated message content',
send_at: 'new-iso-timestamp'
}
});What database schema should I use for storing scheduled appointment reminders?
Create an appointments table with: id (primary key), patient_name, doctor_name, patient_phone (E.164 format), appointment_datetime (TIMESTAMPTZ in UTC), reminder_datetime (TIMESTAMPTZ in UTC), sinch_batch_id (unique, for tracking), status (scheduled/sent/failed/cancelled), and created_at/updated_at timestamps. Add indexes on patient_phone and appointment_datetime for efficient queries.