Skip to main content
RapidDev - Software Development Agency
bolt-ai-integrationsBolt Chat + API Route

How to Integrate Bolt.new with Signal

Signal has no official public API — this is intentional by design for privacy. Direct integration with Signal from Bolt.new is not possible through any official developer program. Your options are: use Twilio for production-grade encrypted messaging, build Signal-inspired end-to-end encrypted chat using Supabase Realtime with the Web Crypto API, or self-host signal-cli behind a REST wrapper (requires a server, not compatible with WebContainers).

What you'll learn

  • Why Signal has no public API and why third-party clients violate Signal's terms of service
  • How to implement production messaging with Twilio as the recommended Signal alternative
  • How to build end-to-end encrypted chat in Bolt using Supabase Realtime and the Web Crypto API
  • Why signal-cli cannot work inside Bolt's WebContainer runtime
  • How to choose between Twilio and in-app encrypted chat based on your use case
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate17 min read30 minutesCommunicationApril 2026RapidDev Engineering Team
TL;DR

Signal has no official public API — this is intentional by design for privacy. Direct integration with Signal from Bolt.new is not possible through any official developer program. Your options are: use Twilio for production-grade encrypted messaging, build Signal-inspired end-to-end encrypted chat using Supabase Realtime with the Web Crypto API, or self-host signal-cli behind a REST wrapper (requires a server, not compatible with WebContainers).

Signal Has No API — Here Are Your Actual Options for Private Messaging in Bolt.new

Signal's privacy-first architecture is intentional and uncompromising: there is no official Signal API, no developer program, and no SDK for third-party apps to send or receive Signal messages. This is not an oversight — it is a deliberate design decision by Signal's nonprofit foundation. Signal's protocol encrypts messages end-to-end in a way that prevents even Signal's servers from reading them. Offering an API would require giving third-party servers access to the message flow, which would fundamentally undermine the privacy model. Signal has explicitly stated they have no plans to offer an API.

There is an open-source project called signal-cli that reverse-engineers Signal's protocol and allows command-line interaction with Signal. Several projects have built REST wrappers around signal-cli. However, signal-cli requires a Linux server with a persistent process, network access to Signal's servers, and the ability to register a phone number with Signal. None of these requirements are compatible with Bolt's WebContainer runtime, which cannot run persistent processes, cannot establish TCP connections, and is sandboxed to browser-accessible protocols only.

The realistic paths forward depend on what you actually need. If you need to send messages to users on their phones (SMS, WhatsApp, or Signal) from your app — use Twilio, which provides an official, battle-tested REST API that works perfectly with Bolt's API routes. If you want private, encrypted messaging between users inside your Bolt app itself — build it using Supabase Realtime for message delivery and the Web Crypto API (built into browsers) for end-to-end encryption. This latter approach gives you a Signal-quality security model implemented with standard browser APIs, all within Bolt's WebContainer architecture.

Integration method

Bolt Chat + API Route

Signal has no official public API, so direct integration is impossible. This guide covers the realistic alternatives: Twilio for production WhatsApp/SMS messaging (official API, works in Bolt), and building a Signal-inspired encrypted chat directly in your Bolt.new app using Supabase Realtime for message delivery and the Web Crypto API for end-to-end encryption. The signal-cli open-source tool exists but requires a Linux server and TCP access — incompatible with Bolt's WebContainer.

Prerequisites

  • A Twilio account at twilio.com for the production messaging alternative (free trial includes $15 credit)
  • Or a Supabase project for the in-app encrypted chat approach
  • A Bolt.new project using Next.js (for server-side API routes to keep Twilio credentials secure)
  • Understanding that Signal's network itself cannot be accessed programmatically from any app
  • A deployed app URL for testing full messaging flows (Netlify or Bolt Cloud)

Step-by-step guide

1

Understand Why Signal Has No API (and What to Use Instead)

Before spending time trying to integrate Signal directly, it is important to understand why it cannot be done and choose the right alternative for your use case. Signal Foundation has never offered a public API. Their position is unambiguous: 'We're a nonprofit. We're not here to build an ecosystem of integrations.' Signal's end-to-end encryption is designed so that only the sender and recipient devices hold the keys. An API would require routing messages through a third-party server, which is architecturally incompatible with this model. signal-cli is a community project that uses Signal's protocol without authorization. Using it violates Signal's Terms of Service, which explicitly prohibit 'using Signal for any commercial purpose without our express consent' and 'using any automated system to access Signal.' Signal has rate-limited and blocked signal-cli registrations in the past. Additionally, signal-cli requires a Linux server running as a persistent daemon, which is incompatible with both Bolt's WebContainer and serverless API routes. For sending messages to users outside your app: use Twilio. Twilio supports SMS, WhatsApp (encrypted like Signal, widely adopted), and voice. It has an official REST API with an official Node.js SDK, production-grade reliability, and straightforward pricing. WhatsApp specifically offers end-to-end encryption and is used by many of the same privacy-conscious users who prefer Signal. For encrypted messaging between users inside your app: use Supabase Realtime for message transport and the Web Crypto API (window.crypto.subtle) for encryption. This approach gives you genuine end-to-end encryption — messages are encrypted in the sender's browser and cannot be read by your server. This is the architectural equivalent of Signal's security model, built with standard browser APIs.

Bolt.new Prompt

Create an information page that explains the messaging options available. Show three cards: (1) Twilio WhatsApp/SMS — for reaching users on their existing messaging apps, with a 'Setup Twilio' button; (2) In-App Encrypted Chat — for private messaging between users within this app, with a 'Build Encrypted Chat' button; (3) signal-cli (Not Recommended) — explain it requires a Linux server and violates Signal ToS. Make the first two options prominent and the third clearly labeled as impractical.

Paste this in Bolt.new chat

Pro tip: WhatsApp Business API via Twilio is often the best replacement for Signal in production apps. WhatsApp has 2 billion users, provides end-to-end encryption identical to Signal (both use the Signal Protocol), and has official business API support that Signal deliberately lacks.

Expected result: You have a clear understanding of why Signal cannot be integrated and have chosen either Twilio (for external messaging) or Supabase Realtime + Web Crypto (for in-app encrypted messaging) based on your use case.

2

Option A: Integrate Twilio for Production Messaging

If your goal is to send messages to users on their phones, Twilio is the correct tool. Create a Twilio account at twilio.com — the free trial gives you $15 credit, enough for hundreds of test messages. Note your Account SID and Auth Token from the Twilio console dashboard. For WhatsApp specifically (the privacy-focused alternative most similar to Signal's demographic), you need to request access to the Twilio WhatsApp sandbox for development testing, or submit your app for WhatsApp Business API approval for production use. For SMS (simpler to set up), buy a Twilio phone number from the Twilio console under 'Phone Numbers → Manage → Buy a number'. Install the Twilio Node.js SDK (@twilio/twilio in npm). Create a Next.js API route that accepts a recipient number and message body, initializes the Twilio client with your credentials from environment variables, and sends the message. The Twilio SDK communicates over HTTPS and works perfectly inside Bolt's WebContainer — this is a standard outbound API call with no TCP socket requirement. Store TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN in your .env file, never in client-side code. Twilio credentials with access to send messages are sensitive — a leaked Auth Token allows anyone to send messages and incur charges on your account. Always call the Twilio SDK from API routes only.

Bolt.new Prompt

Install the twilio npm package and create a /api/messaging/send/route.ts that accepts { to, message } in the request body. Initialize a Twilio client with TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN from environment variables. Send an SMS using the Twilio client to the provided number from TWILIO_PHONE_NUMBER. Return the message SID on success. Build a simple messaging form UI with a phone number input and message textarea, with a Send button that calls this API route. Show the message SID in a success state.

Paste this in Bolt.new chat

app/api/messaging/send/route.ts
1// app/api/messaging/send/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import twilio from 'twilio';
4
5export async function POST(request: NextRequest) {
6 const { to, message } = await request.json();
7
8 if (!to || !message) {
9 return NextResponse.json(
10 { error: 'Both to (phone number) and message are required' },
11 { status: 400 }
12 );
13 }
14
15 const accountSid = process.env.TWILIO_ACCOUNT_SID;
16 const authToken = process.env.TWILIO_AUTH_TOKEN;
17 const fromNumber = process.env.TWILIO_PHONE_NUMBER;
18
19 if (!accountSid || !authToken || !fromNumber) {
20 return NextResponse.json(
21 { error: 'Twilio credentials not configured' },
22 { status: 500 }
23 );
24 }
25
26 try {
27 const client = twilio(accountSid, authToken);
28 const msg = await client.messages.create({
29 body: message,
30 from: fromNumber,
31 to: to,
32 });
33
34 return NextResponse.json({
35 success: true,
36 messageSid: msg.sid,
37 status: msg.status,
38 });
39 } catch (err: any) {
40 return NextResponse.json(
41 { error: err.message, code: err.code },
42 { status: 400 }
43 );
44 }
45}

Pro tip: Twilio trial accounts can only send messages to verified phone numbers. Go to Twilio console → Phone Numbers → Verified Caller IDs to add your test phone number. Remove this restriction by upgrading your Twilio account.

Expected result: A working /api/messaging/send endpoint sends SMS messages via Twilio. The form UI allows sending test messages during Bolt development. Twilio credentials are stored in .env and never exposed to the client.

3

Option B: Build End-to-End Encrypted Chat with Supabase + Web Crypto

For in-app private messaging between your users — with the same end-to-end encryption model that Signal uses — combine Supabase Realtime for message delivery with the Web Crypto API for encryption. This is the architecturally correct way to build a Signal-quality messaging system without relying on Signal's network. The encryption scheme uses Elliptic Curve Diffie-Hellman (ECDH) key exchange. Each user generates an ECDH key pair in their browser using window.crypto.subtle.generateKey(). The public key is shared openly (stored in your users table in Supabase). When two users start a conversation, each derives a shared secret from their own private key and the other person's public key — this shared secret is mathematically identical on both sides but was never transmitted over the network. Messages are encrypted with AES-GCM (a symmetric cipher) using the shared secret as the encryption key. The encrypted ciphertext is what gets stored in Supabase and transmitted via Supabase Realtime. Only the two participants who derived the same shared secret can decrypt it — not your server, not Supabase, and not anyone who intercepts the network traffic. This is exactly how Signal's protocol works at a high level. Bolt's WebContainer fully supports the Web Crypto API (window.crypto.subtle) — it is a standard browser API, not a Node.js module. This encryption approach requires no external dependencies and no native modules.

Bolt.new Prompt

Build an end-to-end encrypted chat feature. Create a src/lib/crypto.ts helper with functions: generateKeyPair() that uses window.crypto.subtle.generateKey with ECDH P-256, exportPublicKey(keyPair) that exports the public key as base64, deriveSharedSecret(myPrivateKey, theirPublicKeyBase64) that imports their key and calls deriveKey with AES-GCM, encryptMessage(sharedSecret, text) that encodes the text, generates a random IV, and returns base64-encoded { iv, ciphertext }, and decryptMessage(sharedSecret, encryptedData) that reverses the process. Create a Supabase table 'messages' with columns: id, conversation_id, sender_id, encrypted_content (text), created_at. Build a chat UI that uses Supabase Realtime to subscribe to new messages in a conversation and decrypts them client-side.

Paste this in Bolt.new chat

src/lib/crypto.ts
1// src/lib/crypto.ts
2// End-to-end encryption using Web Crypto API (built into browsers — no npm needed)
3
4export async function generateKeyPair(): Promise<CryptoKeyPair> {
5 return window.crypto.subtle.generateKey(
6 { name: 'ECDH', namedCurve: 'P-256' },
7 true, // extractable (so we can export/import)
8 ['deriveKey']
9 );
10}
11
12export async function exportPublicKey(publicKey: CryptoKey): Promise<string> {
13 const exported = await window.crypto.subtle.exportKey('spki', publicKey);
14 return btoa(String.fromCharCode(...new Uint8Array(exported)));
15}
16
17export async function deriveSharedSecret(
18 myPrivateKey: CryptoKey,
19 theirPublicKeyBase64: string
20): Promise<CryptoKey> {
21 const keyBytes = Uint8Array.from(atob(theirPublicKeyBase64), c => c.charCodeAt(0));
22 const theirPublicKey = await window.crypto.subtle.importKey(
23 'spki',
24 keyBytes,
25 { name: 'ECDH', namedCurve: 'P-256' },
26 false,
27 []
28 );
29
30 return window.crypto.subtle.deriveKey(
31 { name: 'ECDH', public: theirPublicKey },
32 myPrivateKey,
33 { name: 'AES-GCM', length: 256 },
34 false,
35 ['encrypt', 'decrypt']
36 );
37}
38
39export async function encryptMessage(
40 sharedSecret: CryptoKey,
41 plaintext: string
42): Promise<string> {
43 const iv = window.crypto.getRandomValues(new Uint8Array(12));
44 const encoded = new TextEncoder().encode(plaintext);
45 const ciphertext = await window.crypto.subtle.encrypt(
46 { name: 'AES-GCM', iv },
47 sharedSecret,
48 encoded
49 );
50 const combined = new Uint8Array(iv.length + ciphertext.byteLength);
51 combined.set(iv, 0);
52 combined.set(new Uint8Array(ciphertext), iv.length);
53 return btoa(String.fromCharCode(...combined));
54}
55
56export async function decryptMessage(
57 sharedSecret: CryptoKey,
58 encryptedBase64: string
59): Promise<string> {
60 const combined = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0));
61 const iv = combined.slice(0, 12);
62 const ciphertext = combined.slice(12);
63 const decrypted = await window.crypto.subtle.decrypt(
64 { name: 'AES-GCM', iv },
65 sharedSecret,
66 ciphertext
67 );
68 return new TextDecoder().decode(decrypted);
69}

Pro tip: Private keys generated with window.crypto.subtle should never be stored on a server. Store them only in browser memory (React state) for the session, or persist them in localStorage with an additional encryption layer. If a user clears their browser data, they lose access to their private key and cannot decrypt past messages.

Expected result: A src/lib/crypto.ts module provides generateKeyPair, exportPublicKey, deriveSharedSecret, encryptMessage, and decryptMessage functions using only browser-native Web Crypto APIs. Messages stored in Supabase are ciphertext only — your server never sees plaintext.

4

Wire Up Supabase Realtime for Encrypted Message Delivery

With the crypto utilities in place, connect them to Supabase Realtime for live message delivery. Create a conversations table and a messages table in Supabase. The messages table stores conversation_id, sender_id, and encrypted_content (the base64-encoded ciphertext from encryptMessage). Plaintext is never stored. When a user opens a conversation, subscribe to new messages using Supabase's realtime channel. When a new message arrives, decrypt it client-side using the pre-derived shared secret for that conversation. When the user sends a message, encrypt it client-side before inserting it into Supabase. This architecture guarantees that if your Supabase database is breached or subpoenaed, the attacker only gets ciphertext. They cannot decrypt the messages without the recipient's private key, which only exists in the recipient's browser. This is functionally equivalent to Signal's security guarantees within the scope of your app's user base. During Bolt development in the WebContainer preview, Supabase Realtime (WebSocket-based) works fully — you can test the full encrypted chat flow including real-time message delivery without deploying first.

Bolt.new Prompt

Build the encrypted chat UI at app/chat/[conversationId]/page.tsx. On mount: load the user's private key from React state (or generate a new key pair if none exists), fetch the conversation partner's public key from the Supabase users table, derive the shared secret using deriveSharedSecret from src/lib/crypto.ts. Subscribe to the 'messages' Supabase realtime channel filtered by conversation_id. When a new message arrives, decrypt it using decryptMessage and add it to the message list. For sending: encrypt the input text with encryptMessage, insert the encrypted_content into Supabase messages table. Show a green lock icon in the chat header with 'End-to-End Encrypted'. Display '🔒 Messages are end-to-end encrypted' at the top of the conversation.

Paste this in Bolt.new chat

supabase-schema.sql
1// Supabase schema for encrypted messages
2// Run this in Supabase SQL editor
3
4CREATE TABLE conversations (
5 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
6 participant_a UUID REFERENCES auth.users(id),
7 participant_b UUID REFERENCES auth.users(id),
8 created_at TIMESTAMPTZ DEFAULT now()
9);
10
11CREATE TABLE messages (
12 id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
13 conversation_id UUID REFERENCES conversations(id) ON DELETE CASCADE,
14 sender_id UUID REFERENCES auth.users(id),
15 -- Only ciphertext is stored server never sees plaintext
16 encrypted_content TEXT NOT NULL,
17 created_at TIMESTAMPTZ DEFAULT now()
18);
19
20-- Store users' public keys for ECDH key exchange
21ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS
22 public_key TEXT; -- base64-encoded ECDH public key
23
24-- RLS: users can only read messages in their conversations
25ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
26CREATE POLICY "Users see their conversation messages"
27 ON messages FOR SELECT
28 USING (
29 conversation_id IN (
30 SELECT id FROM conversations
31 WHERE participant_a = auth.uid() OR participant_b = auth.uid()
32 )
33 );
34
35-- Enable Realtime on messages table
36ALTER PUBLICATION supabase_realtime ADD TABLE messages;

Pro tip: Enable Supabase Realtime on the messages table (Database → Replication → Add table) for live message delivery. Without this, the Supabase channel subscription will not receive new message events.

Expected result: A working encrypted chat interface where messages are encrypted in the sender's browser, delivered via Supabase Realtime as ciphertext, and decrypted in the recipient's browser. The UI clearly indicates end-to-end encryption. The full chat flow works in Bolt's preview without deployment.

5

Deploy and Handle Incoming Message Webhooks

For the Twilio messaging path, deploy your app to Netlify or Bolt Cloud before setting up incoming message handling. Twilio can POST incoming messages to a webhook URL on your server — this allows you to build two-way messaging where users can reply and your app processes their responses. After deploying, go to your Twilio console → Phone Numbers → your number → Messaging → A message comes in. Set the webhook URL to https://your-app.netlify.app/api/messaging/incoming. Create this API route that parses Twilio's webhook payload and processes the incoming message. Important: Twilio webhooks require a publicly accessible server URL. During development in Bolt's WebContainer, outbound Twilio calls work perfectly, but incoming webhooks from Twilio cannot reach the WebContainer. Deploy first, then configure and test webhook-dependent features on your deployed site. For the in-app encrypted chat path (Supabase Realtime), no additional deployment steps are required — Supabase Realtime uses WebSockets initiated from the client, not incoming webhooks. The encrypted chat feature works identically in the Bolt preview and in production.

Bolt.new Prompt

Create a /api/messaging/incoming/route.ts webhook handler for Twilio incoming messages. Parse the Twilio webhook body (it's URL-encoded, not JSON) to extract From, Body, and MessageSid. Store the incoming message in a database table 'incoming_messages' with fields: from_number, body, message_sid, received_at. Send an auto-reply using the Twilio client: 'Thanks for your message. We will get back to you soon.' Validate the Twilio request signature using twilio.validateExpressRequest to ensure the webhook is genuinely from Twilio. Return a 200 status.

Paste this in Bolt.new chat

app/api/messaging/incoming/route.ts
1// app/api/messaging/incoming/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import twilio from 'twilio';
4
5export async function POST(request: NextRequest) {
6 // Twilio sends URL-encoded form data, not JSON
7 const formData = await request.formData();
8 const from = formData.get('From') as string;
9 const body = formData.get('Body') as string;
10 const messageSid = formData.get('MessageSid') as string;
11
12 // Validate webhook signature (requires deployed URL)
13 const signature = request.headers.get('x-twilio-signature') ?? '';
14 const authToken = process.env.TWILIO_AUTH_TOKEN!;
15 const webhookUrl = `${process.env.NEXT_PUBLIC_APP_URL}/api/messaging/incoming`;
16
17 const isValid = twilio.validateRequest(
18 authToken,
19 signature,
20 webhookUrl,
21 Object.fromEntries(formData.entries()) as Record<string, string>
22 );
23
24 if (!isValid) {
25 return new NextResponse('Forbidden', { status: 403 });
26 }
27
28 // Process the incoming message (save to DB, trigger notifications, etc.)
29 console.log(`Incoming message from ${from}: ${body}`);
30
31 // Send TwiML response (XML for auto-reply)
32 const twiml = `<?xml version="1.0" encoding="UTF-8"?>
33<Response>
34 <Message>Thanks for your message. We'll get back to you soon.</Message>
35</Response>`;
36
37 return new NextResponse(twiml, {
38 status: 200,
39 headers: { 'Content-Type': 'text/xml' },
40 });
41}

Pro tip: Always validate Twilio webhook signatures in production using twilio.validateRequest(). Without validation, anyone who discovers your webhook URL can POST fake messages to it. The validation uses your Auth Token to verify the HMAC-SHA1 signature Twilio includes in every webhook request.

Expected result: Twilio webhooks for incoming messages work on your deployed site. The webhook validates Twilio's signature and responds with TwiML. For the in-app encrypted chat, the Supabase Realtime connection works identically in both the Bolt preview and on the deployed site.

Common use cases

Sending Secure Notifications via Twilio WhatsApp

Use Twilio's WhatsApp Business API to send notifications, alerts, and two-way messages to users on WhatsApp — the messaging platform most commonly mentioned alongside Signal for privacy-conscious users. Twilio provides a production-grade API with official support.

Bolt.new Prompt

Integrate Twilio to send WhatsApp messages from my app. When a user completes an action (like submitting a form or making a purchase), send them a WhatsApp notification. Create a /api/notify/route.ts that uses the Twilio Node.js SDK to send a WhatsApp message. Store TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_WHATSAPP_NUMBER in .env. The message should include the user's name and a summary of the action completed. Show a 'Notify via WhatsApp' button in the UI that calls this API route.

Copy this prompt to try it in Bolt.new

End-to-End Encrypted In-App Chat

Build a private chat feature inside your Bolt app where messages are encrypted in the sender's browser before being transmitted, and decrypted only in the recipient's browser. No server (including yours) can read message content. This mirrors Signal's security model using standard browser Web Crypto APIs and Supabase Realtime for message delivery.

Bolt.new Prompt

Build an end-to-end encrypted chat feature. When two users start a conversation, generate an ECDH key pair for each user using the Web Crypto API (window.crypto.subtle). Exchange public keys through Supabase to derive a shared secret without transmitting it. Encrypt messages client-side with AES-GCM using the shared secret before storing them in Supabase. Decrypt incoming messages client-side. The message stored in Supabase should only be the encrypted ciphertext — the server never sees plaintext. Show a lock icon indicating messages are end-to-end encrypted.

Copy this prompt to try it in Bolt.new

Secure OTP Verification via SMS

Send one-time passwords via SMS for two-factor authentication using Twilio Verify. While not Signal-based, SMS OTP is the most widely adopted form of secure message-based verification and is straightforward to implement with Twilio's purpose-built Verify API.

Bolt.new Prompt

Add SMS two-factor authentication using Twilio Verify. Create /api/auth/send-otp that calls the Twilio Verify API to send a 6-digit OTP to the user's phone number. Create /api/auth/verify-otp that checks the code using Twilio Verify's check endpoint. Show a phone number input and a 'Send Code' button in the login flow. After the user enters the code, verify it and grant session access. Store TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_VERIFY_SERVICE_SID in .env.

Copy this prompt to try it in Bolt.new

Troubleshooting

signal-cli REST API calls fail or return connection errors from within Bolt

Cause: signal-cli requires a Linux server running as a persistent daemon process with access to Signal's servers via TCP connections. Bolt's WebContainer cannot run persistent server processes, cannot open TCP sockets, and cannot access services that require a persistent connection daemon.

Solution: signal-cli is architecturally incompatible with Bolt's WebContainer. Use Twilio for external messaging or the Supabase Realtime + Web Crypto approach for in-app encrypted messaging. Both are officially supported, reliable, and work within Bolt's constraints.

Twilio webhook at /api/messaging/incoming returns 403 and Twilio stops retrying

Cause: Twilio webhook signature validation is failing, likely because the NEXT_PUBLIC_APP_URL environment variable does not exactly match the URL Twilio is posting to, or the signature is being validated against a parsed request body instead of the raw URL-encoded string.

Solution: Ensure NEXT_PUBLIC_APP_URL is set to your exact deployed domain without a trailing slash. The URL passed to twilio.validateRequest must match the full URL Twilio calls, including protocol. Parse the form data using request.formData() and pass Object.fromEntries() to the validation function — Twilio signs URL-encoded bodies, not JSON.

decryptMessage throws DOMException: The operation failed for an operation-specific reason

Cause: The shared secret derived on the decrypting side does not match the one used for encryption. This happens when the ECDH key exchange used different key pairs, or when the encrypted base64 string was corrupted during storage or transmission.

Solution: Verify that the public key stored in Supabase for each user is the exact base64 output of exportPublicKey() with no trimming or encoding changes. Test the full encrypt-decrypt cycle with the same key pair in a unit test before integrating with Supabase. Also ensure the encrypted_content column in Supabase is TEXT type (not VARCHAR with a short limit) to prevent truncation.

typescript
1// Quick test — run in browser console to verify crypto round-trip
2const pair1 = await generateKeyPair();
3const pair2 = await generateKeyPair();
4const pub1 = await exportPublicKey(pair1.publicKey);
5const pub2 = await exportPublicKey(pair2.publicKey);
6
7const secret1 = await deriveSharedSecret(pair1.privateKey, pub2);
8const secret2 = await deriveSharedSecret(pair2.privateKey, pub1);
9
10const encrypted = await encryptMessage(secret1, 'hello world');
11const decrypted = await decryptMessage(secret2, encrypted);
12console.log(decrypted); // should print 'hello world'

Best practices

  • Never attempt to integrate directly with Signal's network — it violates Signal's Terms of Service, will be blocked by Signal's infrastructure, and creates legal risk for your application.
  • Choose Twilio for messaging that reaches users on their existing phone numbers and messaging apps — it is the most straightforward replacement for sending notifications that users might otherwise receive via Signal.
  • For in-app encrypted messaging, store only ciphertext in your database — if you store plaintext even temporarily, your encryption is theater rather than genuine security.
  • Private keys in the Web Crypto encrypted chat approach must never leave the user's browser — store them in memory during the session and warn users that clearing browser data will lose their ability to decrypt past messages.
  • Always validate Twilio webhook signatures before processing incoming messages — skip validation only during initial local testing, never in production.
  • Rate-limit your messaging API routes to prevent abuse — add a per-user limit on how many messages can be sent per minute to protect your Twilio credits.
  • For the ECDH key exchange, use P-256 (secp256r1) — it is universally supported in browsers, hardware-accelerated on most devices, and provides 128-bit security equivalent to AES-256.

Alternatives

Frequently asked questions

Can I use signal-cli to send Signal messages from my Bolt.new app?

Not from within Bolt's WebContainer. signal-cli requires a Linux server running as a persistent process with a registered Signal phone number, and it communicates via TCP connections that WebContainers cannot open. You could theoretically self-host signal-cli on a separate server and expose a REST API, but this violates Signal's Terms of Service and Signal has historically blocked signal-cli connections. Use Twilio for production messaging.

Is there any way to send official Signal messages from a web app?

No — Signal has no public API and no developer program. This is a deliberate product decision by Signal Foundation to protect privacy. Signal has explicitly declined to create business APIs that would introduce third-party servers into the message flow. The only privacy-preserving messaging service with an official API for reaching Signal's demographic is WhatsApp Business (via Twilio), which also uses the Signal Protocol for end-to-end encryption.

How secure is the Web Crypto API approach compared to Signal itself?

The Web Crypto approach described here uses the same mathematical primitives as Signal: ECDH key exchange for establishing a shared secret, and AES-GCM for symmetric encryption. The main security difference is that Signal's full protocol (the Double Ratchet) also provides forward secrecy (compromising one message doesn't expose past messages) and deniability. The simpler ECDH+AES approach is secure but doesn't rotate keys per-message. For most app use cases, ECDH+AES-GCM provides excellent security.

Will Twilio WhatsApp messages appear in the recipient's Signal app?

No — WhatsApp and Signal are separate messaging networks. Twilio WhatsApp messages are delivered to the recipient's WhatsApp account, not their Signal app. They are different services owned by different companies. However, WhatsApp also uses the Signal Protocol for end-to-end encryption, making it a privacy-conscious alternative to Signal for users who already have WhatsApp installed.

Can I receive replies from users who receive my Twilio messages?

Yes, but only after deploying your app. When users reply to your Twilio SMS or WhatsApp messages, Twilio delivers those replies to a webhook URL on your server. Since Bolt's WebContainer cannot receive incoming HTTP connections, you must deploy to Netlify or Bolt Cloud first, then configure your Twilio webhook URL with your deployed domain. Outbound sending works in the Bolt preview; incoming webhooks require deployment.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.