
24時間前までの履歴を保持するSMS受信システムを Twilio + Momento Cache で構築する
はじめに
Twilio Messaging API を使用することで、SMS 受信機能を簡単にアプリケーションに組み込むことができます。Twilio では Twilio Functions という Serverless function の仕組みも提供されており、受信した SMS に対する基本的な処理は Twilio 内で完結させることも可能です。
一方で、Twilio には DB のような永続化の仕組みが存在しない という制約があります。受信した SMS を一時保存して履歴として参照したり、複数のクライアントに配信したりする用途では、外部のストレージサービスとの連携が必要になります。
今回この制約を解決するため、24 時間の TTL 機能を持つ Momento Cache を採用し、SMS 受信データの一時保存と履歴管理を実現しました。本記事では、SMS 受信とリアルタイム表示、履歴取得機能を持つシステムの実装内容を紹介します。
Twilio とは
Twilio は、SMS、音声通話、ビデオ通話などの通信機能を API で提供するクラウドサービスです。開発者は簡単に通信機能をアプリケーションに組み込むことができます。今回は Messaging API を使用して SMS の受信機能と Webhook による外部システムとの連携を実装します。
Momento とは
Momento は、フルマネージドなインメモリキャッシュサービスです。従来の Redis や Memcached と異なり、インフラの管理や設定が不要で、API を通じて即座に利用開始できます。TTL (Time To Live) 機能により、データの自動削除も可能で、一時的なデータ保存に最適です。
対象読者
- Next.js の基本的な知識がある方
- Twilio の Messaging API に興味がある方
- リアルタイム通信の実装について学びたい方
- サーバーレス環境でのデータ永続化について課題を感じている方
参考
- Twilio Messaging API Documentation
- Momento Cache Documentation
- Next.js App Router Documentation
- Server-Sent Events (MDN)
- Vercel Deployment Documentation
システム概要
- SMS 受信と Webhook 送信: 外部電話番号から Twilio 購入番号に SMS が送信されると、Twilio が設定された Webhook URL (/api/sms) に POST リクエストを送信
- データ永続化とリアルタイム配信: サーバーが SMS データを Momento Cache に保存し、同時に Server-Sent Events で接続中のクライアントにリアルタイム通知を配信
- 履歴表示: フロントエンドでは初回読み込み時に履歴 API (/api/sms/history) から過去 24 時間の SMS データを取得し、リアルタイム通知と重複排除しながら表示
前提となる環境
- Twilio アカウント
- Momento アカウント
- Vercel アカウント (GitHub アカウント連携済み)
- Node.js のローカル環境
Momento Cache 作成
Momento コンソール で twilio-sms キャッシュを作成し、 API キーを作成します。
- Type of key: Fine-Grained Access Key
- Cache Name: twilio-sms
- Role Type: readwrite
Next.js プロジェクト作成
Next.js プロジェクトを作成し、必要なライブラリをインストールします。
# Next.js プロジェクト作成
npx create-next-app@latest sms-receiver-poc --typescript --tailwind --app
cd sms-receiver-poc
# Momento SDK のインストール
npm install @gomomento/sdk
# 開発サーバー起動
npm run dev
ファイル構成
プロジェクトの主要なファイル構成は以下の通りです。
src/
├── app/
│ ├── api/
│ │ └── sms/
│ │ ├── route.ts # 統合API(SSE + Webhook)
│ │ └── history/
│ │ └── route.ts # 履歴取得API
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx # メインページ
├── components/
│ ├── SmsMessageList.tsx # SMS一覧表示コンポーネント
│ └── SystemStatus.tsx # システム状態表示コンポーネント
├── contexts/
│ └── SmsContext.tsx # SMS状態管理Context
├── lib/
│ └── momento.ts # Momento接続設定
└── types/
└── sms.ts # 型定義
バックエンド実装
Momento 接続設定
まず、Momento Cache への接続設定を作成します。
lib/momento.ts
import { CacheClient, Configurations, CredentialProvider } from '@gomomento/sdk';
let cacheClient: CacheClient | null = null;
export function getMomentoClient(): CacheClient {
if (!cacheClient) {
cacheClient = new CacheClient({
configuration: Configurations.Laptop.v1(),
credentialProvider: CredentialProvider.fromString({
apiKey: process.env.MOMENTO_API_KEY!
}),
defaultTtlSeconds: 86400, // 24時間
});
}
return cacheClient;
}
export const CACHE_NAME = process.env.MOMENTO_CACHE_NAME!;
- シングルトンパターンでクライアントを管理し、TTL を 24 時間に設定
型定義
SMS データの型定義を作成します。
src/types/sms.ts
export interface SmsMessage {
messageId: string;
from: string;
to: string;
body: string;
timestamp: string;
source: 'notification' | 'history';
}
export interface SmsNotification {
type: 'new_sms';
data: SmsMessage;
timestamp: number;
}
export interface ConnectionNotification {
type: 'connection';
message: string;
timestamp: number;
}
export interface HeartbeatNotification {
type: 'heartbeat';
timestamp: number;
}
export type NotificationData = SmsNotification | ConnectionNotification | HeartbeatNotification;
source
フィールドでデータの出所を識別し、重複排除
統合 API エンドポイント
SSE 接続と Webhook 処理を単一エンドポイントで実現します。
app/api/sms/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { getMomentoClient, CACHE_NAME } from '@/lib/momento';
import { CacheSet, CacheListPushBack } from '@gomomento/sdk';
import { SmsMessage } from '@/types/sms';
// SSE 接続中のクライアントを管理するセット
const clients = new Set<ReadableStreamDefaultController>();
/**
* SSE (Server-Sent Events) エンドポイント
* GET リクエストでクライアントとの接続を確立
*/
export async function GET() {
const stream = new ReadableStream({
start(controller) {
// 新しいクライアントを接続リストに追加
clients.add(controller);
console.log(`SSE client connected. Total clients: ${clients.size}`);
// 接続確認メッセージを送信
const connectMsg = JSON.stringify({ type: 'connected', timestamp: Date.now() });
controller.enqueue(`data: ${connectMsg}\n\n`);
},
cancel(controller) {
// 接続が切断された際にクライアントをリストから削除
clients.delete(controller);
console.log(`SSE client disconnected. Total clients: ${clients.size}`);
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
},
});
}
/**
* Twilio Webhook エンドポイント
* POST リクエストで SMS データを受信・処理
*/
export async function POST(request: NextRequest) {
try {
// Twilio からの Form データを取得
const formData = await request.formData();
// SMS データの構築(UTC タイムスタンプで保存)
const smsData: SmsMessage = {
messageId: formData.get('MessageSid') as string,
from: formData.get('From') as string,
to: formData.get('To') as string,
body: formData.get('Body') as string,
timestamp: new Date().toISOString(), // UTC で保存
source: 'notification'
};
console.log('SMS received:', smsData);
// Momento Cache への保存
const momento = getMomentoClient();
const cacheKey = `sms:${smsData.timestamp}:${smsData.messageId}`;
// 1. SMS データを保存
const setResponse = await momento.set(CACHE_NAME, cacheKey, JSON.stringify(smsData));
if (setResponse instanceof CacheSet.Error) {
console.error('Momento set error:', setResponse.message());
}
// 2. インデックス用のキーリストに追加
const listResponse = await momento.listPushBack(CACHE_NAME, 'sms-keys', cacheKey);
if (listResponse instanceof CacheListPushBack.Error) {
console.warn('Failed to add to key list:', listResponse.message());
}
// 3. リアルタイム通知を全クライアントに配信
const message = {
type: 'new_sms',
data: smsData,
timestamp: Date.now()
};
const data = `data: ${JSON.stringify(message)}\n\n`;
console.log(`Broadcasting to ${clients.size} clients`);
clients.forEach(controller => {
try {
controller.enqueue(data);
} catch {
// 送信失敗したクライアントは削除
clients.delete(controller);
}
});
// Twilio への応答
return new NextResponse(
'<?xml version="1.0" encoding="UTF-8"?><Response></Response>',
{ headers: { 'Content-Type': 'text/xml' } }
);
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json({ error: 'Error' }, { status: 500 });
}
}
clients
Set で SSE 接続を管理- Momento への保存と SSE 配信を同期実行
履歴取得 API
過去 24 時間の SMS 履歴を取得する API です。
app/api/sms/history/route.ts
import { NextResponse } from 'next/server';
import { getMomentoClient, CACHE_NAME } from '@/lib/momento';
import { CacheListFetch, CacheGet } from '@gomomento/sdk';
import { SmsMessage } from '@/types/sms';
export async function GET() {
try {
const momento = getMomentoClient();
// インデックスリストからキー一覧を取得
const keysResponse = await momento.listFetch(CACHE_NAME, 'sms-keys');
if (keysResponse instanceof CacheListFetch.Error) {
// キーリストが存在しない場合は空配列を返す
return NextResponse.json([]);
}
const messages: SmsMessage[] = [];
if (keysResponse instanceof CacheListFetch.Hit) {
const keys = keysResponse.valueListString();
// 各キーから SMS データを取得
for (const key of keys) {
const getResponse = await momento.get(CACHE_NAME, key);
if (getResponse instanceof CacheGet.Hit) {
try {
const smsData = JSON.parse(getResponse.valueString()) as SmsMessage;
messages.push({
...smsData,
source: 'history' // 履歴データとして識別
});
} catch (parseError) {
console.error(`Failed to parse SMS data for key ${key}:`, parseError);
}
}
}
}
// タイムスタンプで降順ソート
messages.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
return NextResponse.json(messages);
} catch (error) {
console.error('History fetch error:', error);
return new Response(
JSON.stringify({ error: 'Internal server error' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
フロントエンド実装
SMS 状態管理 Context
React Context で SMS データとリアルタイム接続を管理します。
src/contexts/SmsContext.tsx
'use client';
import React, { createContext, useContext, useReducer, useEffect } from 'react';
import { SmsMessage } from '@/types/sms';
interface SmsState {
messages: Map<string, SmsMessage>;
isLoading: boolean;
error: string | null;
connectionStatus: 'disconnected' | 'connecting' | 'connected' | 'error';
}
type SmsAction =
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'ADD_MESSAGE'; payload: SmsMessage }
| { type: 'SET_MESSAGES'; payload: SmsMessage[] }
| { type: 'CLEAR_MESSAGES' }
| { type: 'SET_CONNECTION_STATUS'; payload: SmsState['connectionStatus'] };
const initialState: SmsState = {
messages: new Map(),
isLoading: false,
error: null,
connectionStatus: 'disconnected',
};
// Reducer で状態を管理
function smsReducer(state: SmsState, action: SmsAction): SmsState {
switch (action.type) {
case 'SET_LOADING':
return { ...state, isLoading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload };
case 'ADD_MESSAGE':
// Map で重複排除
const newMessages = new Map(state.messages);
newMessages.set(action.payload.messageId, action.payload);
return { ...state, messages: newMessages };
case 'SET_MESSAGES':
const messageMap = new Map();
action.payload.forEach(msg => messageMap.set(msg.messageId, msg));
return { ...state, messages: messageMap };
case 'CLEAR_MESSAGES':
return { ...state, messages: new Map() };
case 'SET_CONNECTION_STATUS':
return { ...state, connectionStatus: action.payload };
default:
return state;
}
}
const SmsContext = createContext<{
state: SmsState;
dispatch: React.Dispatch<SmsAction>;
loadHistory: () => Promise<void>;
addMessage: (message: SmsMessage) => void;
} | null>(null);
export function SmsProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(smsReducer, initialState);
// 履歴データの読み込み
const loadHistory = async () => {
dispatch({ type: 'SET_LOADING', payload: true });
dispatch({ type: 'SET_ERROR', payload: null });
try {
const response = await fetch('/api/sms/history');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const messages: SmsMessage[] = await response.json();
dispatch({ type: 'SET_MESSAGES', payload: messages });
} catch (error) {
console.error('Failed to load SMS history:', error);
dispatch({ type: 'SET_ERROR', payload: 'Failed to load SMS history' });
} finally {
dispatch({ type: 'SET_LOADING', payload: false });
}
};
const addMessage = (message: SmsMessage) => {
dispatch({ type: 'ADD_MESSAGE', payload: message });
};
// SSE 接続を管理
useEffect(() => {
dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'connecting' });
const eventSource = new EventSource('/api/sms');
eventSource.onopen = () => {
console.log('SSE connected to /api/sms');
dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'connected' });
};
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'connected') {
console.log('SSE connection confirmed');
} else if (data.type === 'new_sms') {
// リアルタイムで新しいメッセージを追加
console.log('New SMS received via SSE:', data.data);
addMessage(data.data);
}
} catch (error) {
console.error('Error parsing SSE message:', error);
}
};
eventSource.onerror = (error) => {
console.error('SSE error:', error);
dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'error' });
};
// クリーンアップ
return () => {
console.log('Closing SSE connection');
eventSource.close();
dispatch({ type: 'SET_CONNECTION_STATUS', payload: 'disconnected' });
};
}, []);
// 初回履歴読み込み
useEffect(() => {
loadHistory();
}, []);
return (
<SmsContext.Provider value={{ state, dispatch, loadHistory, addMessage }}>
{children}
</SmsContext.Provider>
);
}
export function useSms() {
const context = useContext(SmsContext);
if (!context) {
throw new Error('useSms must be used within SmsProvider');
}
return context;
}
- Map データ構造で
messageId
ベースの重複排除 - SSE 接続の自動管理
- 履歴読み込みとリアルタイム更新の統合
SMS 一覧表示コンポーネント
src/components/SmsMessageList.tsx
'use client';
import { useSms } from '@/contexts/SmsContext';
export default function SmsMessageList() {
const { state, loadHistory } = useSms();
// Map を配列に変換してソート
const messages = Array.from(state.messages.values()).sort(
(a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
// 日時フォーマット(UTC → 日本時間変換)
const formatDate = (timestamp: string) => {
try {
const date = new Date(timestamp);
if (isNaN(date.getTime())) {
return '時刻不明';
}
return date.toLocaleString('ja-JP', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
timeZone: 'Asia/Tokyo' // UTC から日本時間に変換
});
} catch (error) {
console.error('Date formatting error:', error);
return '時刻エラー';
}
};
// 電話番号フォーマット
const formatPhoneNumber = (phone: string) => {
return phone.replace(/^\+1(\d{3})(\d{3})(\d{4})$/, '+1 ($1) $2-$3');
};
if (state.isLoading) {
return (
<div className="flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-2">読み込み中...</span>
</div>
);
}
if (state.error) {
return (
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="text-red-600">
<span className="font-medium">エラー:</span> {state.error}
</div>
<button
onClick={loadHistory}
className="mt-2 px-3 py-1 text-sm bg-red-600 text-white rounded hover:bg-red-700"
>
再読み込み
</button>
</div>
);
}
if (messages.length === 0) {
return (
<div className="text-center py-8 text-gray-500">
<p>SMS メッセージがありません</p>
<button
onClick={loadHistory}
className="mt-2 px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700"
>
再読み込み
</button>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h2 className="text-xl font-semibold">受信 SMS 履歴</h2>
<button
onClick={loadHistory}
className="px-3 py-1 text-sm bg-gray-600 text-white rounded hover:bg-gray-700"
>
更新
</button>
</div>
<div className="space-y-3">
{messages.map((message) => (
<div
key={message.messageId}
className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm hover:shadow-md transition-shadow"
>
<div className="flex justify-between items-start mb-2">
<div className="flex-1">
<div className="text-sm text-gray-600">
<span className="font-medium">送信者:</span> {formatPhoneNumber(message.from)}
</div>
<div className="text-sm text-gray-600">
<span className="font-medium">宛先:</span> {formatPhoneNumber(message.to)}
</div>
</div>
<div className="text-xs text-gray-500">
{formatDate(message.timestamp)}
</div>
</div>
<div className="bg-gray-50 rounded p-3">
<p className="text-gray-800 whitespace-pre-wrap">{message.body}</p>
</div>
<div className="flex justify-between items-center mt-2">
<span className={`text-xs px-2 py-1 rounded ${
message.source === 'notification'
? 'bg-green-100 text-green-800'
: 'bg-blue-100 text-blue-800'
}`}>
{message.source === 'notification' ? 'リアルタイム' : '履歴'}
</span>
<span className="text-xs text-gray-400">
ID: {message.messageId}
</span>
</div>
</div>
))}
</div>
</div>
);
}
システム状態表示コンポーネント
src/components/SystemStatus.tsx
'use client';
import { useState, useEffect } from 'react';
import { useSms } from '@/contexts/SmsContext';
export default function SystemStatus({ className = '' }) {
const { state } = useSms();
const [apiStatus, setApiStatus] = useState<'checking' | 'online' | 'error'>('checking');
const [lastCheck, setLastCheck] = useState<Date | null>(null);
// API 状態チェック
const checkApiStatus = async () => {
setApiStatus('checking');
try {
const response = await fetch('/api/sms/history');
setApiStatus(response.ok ? 'online' : 'error');
} catch {
setApiStatus('error');
}
setLastCheck(new Date());
};
useEffect(() => {
checkApiStatus();
const interval = setInterval(checkApiStatus, 30000);
return () => clearInterval(interval);
}, []);
// 総合ステータスの判定
const getOverallStatus = () => {
if (apiStatus === 'error' || state.connectionStatus === 'error') return 'error';
if (apiStatus === 'checking' || state.connectionStatus === 'connecting') return 'checking';
if (apiStatus === 'online' && state.connectionStatus === 'connected') return 'online';
return 'checking';
};
const getStatusColor = () => {
const status = getOverallStatus();
switch (status) {
case 'online': return 'text-green-600 bg-green-50 border-green-200';
case 'error': return 'text-red-600 bg-red-50 border-red-200';
case 'checking': return 'text-yellow-600 bg-yellow-50 border-yellow-200';
}
};
const getStatusText = () => {
const status = getOverallStatus();
switch (status) {
case 'online': return 'システム正常';
case 'error': return 'システムエラー';
case 'checking': return '確認中...';
}
};
const getConnectionStatusText = () => {
switch (state.connectionStatus) {
case 'connected': return 'リアルタイム接続中';
case 'connecting': return '接続中...';
case 'error': return '接続エラー';
case 'disconnected': return '未接続';
}
};
return (
<div className={`flex flex-col space-y-1 ${className}`}>
<div className={`inline-flex items-center px-3 py-1 border rounded-full text-sm ${getStatusColor()}`}>
<div className={`w-2 h-2 rounded-full mr-2 ${
getOverallStatus() === 'online' ? 'bg-green-600' :
getOverallStatus() === 'error' ? 'bg-red-600' : 'bg-yellow-600'
} ${getOverallStatus() === 'checking' ? 'animate-pulse' : ''}`} />
<span>{getStatusText()}</span>
{lastCheck && (
<span className="ml-2 text-xs opacity-75">
{lastCheck.toLocaleTimeString('ja-JP')}
</span>
)}
</div>
<div className="text-xs text-gray-500 text-center">
{getConnectionStatusText()}
</div>
</div>
);
}
メインページ
src/app/page.tsx
'use client';
import SmsMessageList from '@/components/SmsMessageList';
import SystemStatus from '@/components/SystemStatus';
import { SmsProvider } from '@/contexts/SmsContext';
export default function HomePage() {
return (
<SmsProvider>
<div className="min-h-screen bg-gray-50">
<header className="bg-white shadow-sm border-b">
<div className="max-w-4xl mx-auto px-4 py-6">
<div className="flex justify-between items-start">
<div>
<h1 className="text-2xl font-bold text-gray-900">
SMS 受信システム PoC
</h1>
<p className="mt-2 text-sm text-gray-600">
Twilio + Momento Cache を使用した SMS 受信・履歴管理システム
</p>
</div>
<SystemStatus />
</div>
</div>
</header>
<main className="max-w-4xl mx-auto px-4 py-8">
<div className="bg-white rounded-lg shadow-sm border p-6">
<SmsMessageList />
</div>
</main>
<footer className="mt-16 bg-white border-t">
<div className="max-w-4xl mx-auto px-4 py-4">
<p className="text-center text-sm text-gray-500">
SMS Receiver PoC - Powered by Next.js, Twilio & Momento
</p>
</div>
</footer>
</div>
</SmsProvider>
);
}
layout
src/app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'SMS Receiver PoC',
description: 'Twilio + Momento Cache を使用した SMS 受信システム',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body className={inter.className}>
{children}
</body>
</html>
);
}
ローカルでの事前確認
Webhook エンドポイントが正常に動作するかテストします。
.env.local
ファイルを作成し、 Momento の API key と cache name をセットします。
MOMENTO_API_KEY=your_momento_api_key_here
MOMENTO_CACHE_NAME=twilio-sms
下記のコマンドでサーバーを起動します。
npm run dev
CURL コマンドを実行し、想定したレスポンスが得られることを確認します。
curl -X POST http://localhost:3000/api/sms \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "MessageSid=SM1234567890abcdef&From=%2B1234567890&To=%2B0987654321&Body=Test%20message&DateCreated=2024-01-20T10%3A30%3A00Z"
正常な場合、以下のレスポンスが返されます。
<?xml version="1.0" encoding="UTF-8"?><Response></Response>
Vercel へのデプロイ
プロジェクトを GitHub リポジトリに push し、 Vercel で Import します。
デプロイ完了後、環境変数を設定して Redeploy します。
Name | Value |
---|---|
MOMENTO_API_KEY |
先ほど作成した Momento API キー |
MOMENTO_CACHE_NAME |
twilio-sms |
デプロイが完了すると、以下のような URL が発行されます。この URL を控えておきます。
https://sms-receiver-poc-xxxx.vercel.app
デプロイ後の動作確認
実際の SMS 送信前に、デプロイしたエンドポイントが正常に動作するかテストします。
curl -X POST https://your-vercel-app.vercel.app/api/sms \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "MessageSid=SM1234567890abcdef&From=%2B1234567890&To=%2B0987654321&Body=Test%20message&DateCreated=2024-01-20T10%3A30%3A00Z"
正常な場合、以下のレスポンスが返されます。
<?xml version="1.0" encoding="UTF-8"?><Response></Response>
Twilio コンソールでの設定
Phone Numbers > Manage > Active numbers で購入した番号をクリックし、Messaging Configuration セクションで以下を設定します。設定したら Save configuration をクリックするのを忘れないようにしてください。
- A message comes in: Webhook
- URL:
https://sms-receiver-poc-xxxx.vercel.app/api/sms
- HTTP: POST
フロントエンドの動作確認
https://sms-receiver-poc-xxxx.vercel.app
にアクセス- 外部電話番号から Twilio 購入番号に SMS を送信
- フロントエンドに即座に SMS が表示されることを確認
- ページをリロードしても履歴が残ることを確認
まとめ
本記事では、Twilio、Momento Cache を組み合わせた SMS 受信システムを構築しました。Twilio の永続化機能の制約を Momento Cache の TTL 機能で補完し、Server-Sent Events によるリアルタイム通知を実現することで、SMS 受信から履歴管理まで一貫したシステムを構築できました。IoT デバイスからの通知システム、チャットアプリケーション、リアルタイムダッシュボードなど、様々なリアルタイム通信要件に応用可能です。