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)
Provisioned SMS-enabled phone number in your target region
Verified recipient numbers for testing (required during trial/development)
Dependencies:
@sinch/sdk-core v1.2.1
@sinch/sms v1.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_at parameter
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_at parameter 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_at timestamp
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:
Bash
mkdir sinch-appointment-reminder
cd sinch-appointment-reminder
Initialize Node.js Project:
Bash
npm init -y
Create Project Structure:
Bash
# Create directoriesmkdir 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
Note: Avoid sessionstorage-for-nodejs in production – data is lost on server restarts. Use express-session for temporary data and a database for persistent storage (see Section 6).
# .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 compliance
PORT: Express application port
SESSION_SECRET: Cryptographically secure random string (32+ bytes) for session cookie signing
DEFAULT_COUNTRY_CODE: Prepends country code to phone numbers without one
Set Up nodemon Script:
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 dev to start the development server.
Configure Application Entry Point (app.js):
JavaScript
// app.jsconst express = require('express');
const session = require('express-session');
const flash = require('connect-flash');
const path = require('path');
const routes = require('./routes'); // Import routesrequire('dotenv').config(); // Load .env variables earlyconst 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 varresave: false, // Don't save session if unmodifiedsaveUninitialized: false, // Don't create session until something storedcookie: {
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 accesssameSite: '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.static and express.urlencoded middleware
Set up express-session with 1-hour cookie lifetime
Make flash messages available globally in EJS views via res.locals
Mount routes from routes.js
Start server on configured PORT
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.
JavaScript
// routes.jsconst express = require('express');
const router = express.Router();
const { DateTime } = require('luxon'); // For date/time handlingconst { 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 = newSinchClient({
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 ---asyncfunctionscheduleReminder(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 requestreturn { 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}`;
} elseif (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 appointmentconst 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 futureif (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 bodyconst 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 = awaitscheduleReminder(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 displayconst appointmentDetails = req.session.appointmentDetails;
// Clear temporary session dataif (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 .env with validation for missing variables
scheduleReminder Function: Calls the Sinch API (sinchClient.sms.batches.send) with UTC ISO 8601 for send_at
Routes:
/: Redirects to appointment form
/appointment (GET): Renders patient_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 scheduleReminder to 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:
HTML
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Schedule Appointment Reminder</title><linkrel="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) { %>
<divclass="alert alert-danger"><%= error_msg %></div>
<% } %>
<formaction="/appointment"method="POST"><div><labelfor="patientName">Patient Name:</label><inputtype="text"id="patientName"name="patientName"required></div><div><labelfor="doctorName">Doctor Name:</label><inputtype="text"id="doctorName"name="doctorName"required></div><div><labelfor="appointmentDate">Appointment Date:</label><inputtype="date"id="appointmentDate"name="appointmentDate"required></div><div><labelfor="appointmentTime">Appointment Time:</label><inputtype="time"id="appointmentTime"name="appointmentTime"required></div><div><labelfor="patientPhone">Patient Phone Number:</label><inputtype="tel"id="patientPhone"name="patientPhone"requiredplaceholder="+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><buttontype="submit">Schedule Reminder</button></form></body></html>
views/success.ejs:
HTML
<!DOCTYPE html><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>Reminder Scheduled</title><linkrel="stylesheet"href="/css/style.css"></head><body><h1>Appointment Reminder Scheduled Successfully!</h1>
<% if (typeof success_msg !== 'undefined' && success_msg && success_msg.length > 0) { %>
<divclass="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><ahref="/appointment">Schedule another reminder</a></p></body></html>
4. Sinch SMS API Integration and Credentials Setup
Integration happens primarily in routes.js:
Credentials: Securely loaded from .env
Client Initialization:SinchClient instantiated using Project ID, Key ID, Key Secret, and Region
Scheduling API Call:scheduleReminder function uses sinchClient.sms.batches.send, passing recipient (to), sender (from), message body, and the send_at parameter (UTC ISO 8601 timestamp)
Click Create Key and give it a name (e.g., "NodeReminderApp")
Immediately copy the Key ID and Key Secret – the Secret is only shown once. Store these securely in your .env file
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 as FROM_NUMBER in .env
Note the Region (us, eu, etc.) associated with your service plan/project and set SMS_REGION in .env
5. Production Error Handling and Logging Best Practices
The current implementation includes basic error handling:
Missing Environment Variables:routes.js checks for essential Sinch variables on startup and exits if missing
API Call Errors:scheduleReminder function uses try...catch around sinchClient.sms.batches.send, logs errors, and returns failure status
Input Validation: Basic checks for empty fields and appointment time validity in POST /appointment handler, with flash messages for user feedback
General Errors: Fallback try...catch in POST /appointment handler catches unexpected errors during processing
Logging: Currently uses console.log and console.error with consistent formatting
Production Enhancements:
Structured Logging: Use winston or pino for structured JSON logs
Bash
npm install winston
Replace console.log/console.error with 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.data from 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.js to 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):
SQL
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 DEFAULTCURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULTCURRENT_TIMESTAMP
);
-- Optional: Index for querying by phone or appointment timeCREATE 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
MongoDB:npm install mongodb (driver) or npm install mongoose (ODM)
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 /appointment handler, after successfully scheduling with Sinch (scheduleResult.success), save appointment details (including patientName, 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 /success handler, retrieve appointmentId from session and query database for full appointment details. Render success.ejs using database data. Clear req.session.appointmentId afterwards
Phone Number Validation: Implement robust validation using libphonenumber-js before 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.
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.
Frequently asked questions
Use the Sinch SMS API's send_at parameter within the @sinch/sdk-core Node.js SDK. Provide the recipient's number, message content, and the desired send time in UTC ISO 8601 format. Sinch's system handles the scheduling and sending, eliminating the need for local scheduling libraries or background processes for the sending mechanism.
The Sinch SMS API allows developers to integrate SMS messaging into applications. In this example, it schedules and sends appointment reminders, demonstrating its reliability and robustness for a production-ready reminder system.
Sinch API credentials authenticate your application, authorizing it to access and utilize Sinch's services like sending SMS messages. These credentials include Project ID, Key ID, and Key Secret, which are vital for secure communication between your app and Sinch. Never expose the secret key publicly.
Luxon is especially useful when working with dates and times in different time zones, as it provides robust tools for managing and converting between them. It's crucial for accurately scheduling reminders using Sinch's send_at parameter, which requires UTC ISO 8601 format.
Session storage is inadequate for production as data is lost upon server restart. For scheduled jobs, persistent storage like a database (PostgreSQL, MySQL, MongoDB) is essential for maintaining appointment details.
You need Node.js, npm or yarn, a Sinch account with API credentials, a verified recipient number for initial testing, and basic familiarity with Node.js, Express, JavaScript, HTML, REST APIs.
Install the official Sinch Node.js SDK using npm with the command: npm install @sinch/sdk-core. This package provides the necessary functions for interacting with Sinch's services, including SMS.
The .env file stores sensitive information, like your Sinch API credentials and database connection strings. Dotenv loads these variables into process.env. Never commit this file to version control.
The user submits appointment data through a web form. The Node.js/Express app processes the data, using the Sinch SMS API to schedule a reminder. Sinch then sends the SMS at the designated time to the patient.
Environment variables offer a secure method to store sensitive information like API keys without directly embedding them in your code. This protects your credentials and makes it easy to manage configurations across different environments (development, testing, production).
While the example shows basic formatting, for production use a library like libphonenumber-js to validate and format phone numbers reliably into E.164 format, essential for international compatibility with Sinch.
Express.js acts as the web framework, handling routing, requests, and responses, serving static files like HTML and CSS, allowing us to create web applications easily using Node.js.
Create directories for public assets (CSS, JS), views (EJS templates), and routes. Configure app.js as the entry point, handling middleware, and routes.js for request processing.
Always use a database for production reminder systems. Databases (like PostgreSQL, MySQL, or MongoDB) persist data across sessions and server restarts, ensuring the reminders are sent as scheduled even if there are interruptions or downtime.
The send_at parameter in the Sinch SMS API lets you specify when to send the SMS message in the future. It's essential for scheduling reminders and must be in UTC ISO 8601 format.
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_atparameter—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:
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:
Sinch Account & Credentials:
Dependencies:
@sinch/sdk-corev1.2.1@sinch/smsv1.2.1Technical Knowledge:
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:
send_atparameterSystem Architecture:
send_atparameter to schedule deliverysend_attimestamp1. Project Setup: Installing Sinch Node.js SDK and Dependencies
Create Project Directory:
mkdir sinch-appointment-reminder cd sinch-appointment-reminderInitialize Node.js Project:
Create Project Structure:
# 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 templatesInstall Dependencies:
express: 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 engineNote: 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:
Configure
.gitignore:Configure Environment Variables (
.env):Replace placeholder values with your actual credentials. Generate
SESSION_SECRETusing a cryptographically secure method:# Generate secure session secret node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"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 oneSet Up
nodemonScript:// 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):// 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:
dotenv.config()express.staticandexpress.urlencodedmiddlewareexpress-sessionwith 1-hour cookie lifetimesecure(HTTPS-only),httpOnly(XSS prevention),sameSite(CSRF protection)res.localsroutes.jsPORTSource: 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_atparameter 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;.envwith validation for missing variablesscheduleReminderFunction: Calls the Sinch API (sinchClient.sms.batches.send) with UTC ISO 8601 forsend_at/: Redirects to appointment form/appointment(GET): Renderspatient_details.ejs/appointment(POST):scheduleReminderto schedule via Sinch/success(GET): Retrieves and displays confirmation from session, then clears temporary data3. 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:.envSinchClientinstantiated using Project ID, Key ID, Key Secret, and RegionscheduleReminderfunction usessinchClient.sms.batches.send, passing recipient (to), sender (from), message body, and thesend_atparameter (UTC ISO 8601 timestamp)Obtaining Credentials:
Key IDandKey Secret– the Secret is only shown once. Store these securely in your.envfile.env+1xxxxxxxxxx) and add it asFROM_NUMBERin.envus,eu, etc.) associated with your service plan/project and setSMS_REGIONin.env5. Production Error Handling and Logging Best Practices
The current implementation includes basic error handling:
routes.jschecks for essential Sinch variables on startup and exits if missingscheduleReminderfunction usestry...catcharoundsinchClient.sms.batches.send, logs errors, and returns failure status/appointmenthandler, with flash messages for user feedbacktry...catchin POST/appointmenthandler catches unexpected errors during processingconsole.logandconsole.errorwith consistent formattingProduction Enhancements:
Structured Logging: Use
winstonorpinofor structured JSON logsReplace
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 gracefullyRetry 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-sessionis 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:
npm install pg(driver) ornpm install sequelize/npm install @prisma/client(ORMs)npm install mysql2(driver) ornpm install sequelize/npm install @prisma/client(ORMs)npm install mongodb(driver) ornpm install mongoose(ODM)routes.js:/appointmenthandler, after successfully scheduling with Sinch (scheduleResult.success), save appointment details (includingpatientName,doctorName,formattedPhone,appointmentDateTimeLocal.toJSDate(),reminderDateTimeLocal.toJSDate(),scheduleResult.batchId) to databasereq.session.appointmentId = newAppointmentRecord.id/successhandler, retrieveappointmentIdfrom session and query database for full appointment details. Rendersuccess.ejsusing database data. Clearreq.session.appointmentIdafterwardslibphonenumber-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_atparameter 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-corev1.2.1 and@sinch/smsv1.2.1 (latest as of January 2025). These versions provide full support for thesend_atscheduling 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), andsameSite: 'strict'(CSRF protection). Also setapp.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_atparameter. 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-jslibrary for robust phone number validation:npm install libphonenumber-js. Parse numbers withparsePhoneNumber(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. CallsinchClient.sms.batches.update()with the batch ID to change the message or send time, orsinchClient.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
appointmentstable 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), andcreated_at/updated_attimestamps. Add indexes onpatient_phoneandappointment_datetimefor efficient queries.