This guide provides a complete walkthrough for building a production-ready bulk SMS broadcasting system using the RedwoodJS framework and Amazon Simple Notification Service (AWS SNS).
We'll build an application that enables authenticated users to send a single message to a list of contacts stored in a database. This solves the common need for businesses to efficiently distribute notifications, alerts, or marketing messages via SMS.
Technologies Used:
- RedwoodJS: A full-stack JavaScript/TypeScript framework for building modern web applications. Chosen for its integrated GraphQL API, Prisma ORM, and developer experience.
- AWS Simple Notification Service (SNS): A fully managed messaging service for both application-to-application (A2A) and application-to-person (A2P) communication. Chosen for its scalability, reliability, and direct SMS sending capabilities.
- PostgreSQL: A powerful, open-source object-relational database system. (Other databases supported by Prisma can also be used).
- Prisma: A next-generation Node.js and TypeScript ORM for database access.
- AWS SDK for JavaScript v3: Used to interact with AWS SNS programmatically.
System Architecture:
+-----------------+ +---------------------+ +-----------------+ +-------------------+
| RedwoodJS | ---> | RedwoodJS API | ---> | AWS SDK Client | ---> | AWS SNS Service |
| Web Interface | | (GraphQL Mutation) | | (@aws-sdk/...) | | (PublishBatch API)|
| (React) | +---------------------+ +-----------------+ +-------------------+
+-----------------+ |
| Uses
v
+-----------------+ +-------------------+
| Prisma Client | ---> | PostgreSQL DB |
| (Data Access) | | (Contacts Store) |
+-----------------+ +-------------------+
Final Outcome:
By the end of this guide, you will have a RedwoodJS application with:
- A database schema for storing contacts (name, phone number).
- A secure GraphQL API endpoint for triggering bulk SMS messages.
- Integration with AWS SNS to send messages efficiently using batching.
- Basic error handling and logging.
Prerequisites:
- Node.js (v18 or later recommended)
- Yarn (v1 or later)
- An AWS account with permissions to manage SNS and IAM.
- Basic understanding of RedwoodJS, GraphQL, and AWS concepts.
- AWS CLI configured locally (optional but helpful for verification).
1. Setting up the RedwoodJS Project
Let's initialize a new RedwoodJS project and install necessary dependencies.
-
Create RedwoodJS App: Open your terminal and run the following command. Choose TypeScript when prompted.
yarn create redwood-app redwood-sns-bulk-sms
-
Navigate to Project Directory:
cd redwood-sns-bulk-sms
-
Install AWS SDK v3 for SNS: We need the specific client package for SNS.
yarn workspace api add @aws-sdk/client-sns
Why
@aws-sdk/client-sns
? The AWS SDK v3 is modular. Installing only the SNS client keeps our dependency footprint smaller compared to installing the entire SDK. -
Setup Database: RedwoodJS uses Prisma. By default, it's configured for SQLite. Let's switch to PostgreSQL (adjust connection string for your setup).
-
Edit
api/db/schema.prisma
: Change theprovider
in thedatasource
block.// api/db/schema.prisma datasource db { provider = ""postgresql"" // Changed from ""sqlite"" url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" binaryTargets = ""native"" }
-
Create a
.env
file in the project root (if it doesn't exist) and add your PostgreSQL connection string:# ./.env DATABASE_URL=""postgresql://user:password@host:port/database?schema=public"" # Replace with your actual connection details
-
Explanation: The
schema.prisma
file defines our database connection and data models. The.env
file stores sensitive information like database credentials, keeping them out of version control.
-
-
Initial Database Migration: Apply the initial schema setup to your database.
yarn rw prisma migrate dev # Follow prompts to name the migration (e.g., ""init"")
Your basic RedwoodJS project structure is now ready. The api
directory contains backend code (GraphQL API, services, database schema), and the web
directory contains frontend code (React components, pages).
2. Implementing Core Functionality (Backend Service & SNS Client)
We'll create a reusable SNS client and a service function to handle the logic of sending bulk messages.
-
Configure AWS Credentials Securely: The AWS SDK needs credentials to interact with your account. We'll use environment variables, which RedwoodJS automatically loads from the
.env
file in development. Never hardcode credentials in your source code.-
Add the following to your
.env
file (obtain these from your AWS IAM user setup - see Section 4):# ./.env # Add these lines AWS_ACCESS_KEY_ID=""YOUR_AWS_ACCESS_KEY_ID"" AWS_SECRET_ACCESS_KEY=""YOUR_AWS_SECRET_ACCESS_KEY"" AWS_REGION=""us-east-1"" # Or your preferred AWS region
-
Explanation: The SDK automatically detects
AWS_ACCESS_KEY_ID
,AWS_SECRET_ACCESS_KEY
, andAWS_REGION
(orAWS_DEFAULT_REGION
) environment variables. Storing them here keeps them secure and environment-specific.
-
-
Create an SNS Client Library: This centralizes the SNS client initialization.
-
Create a new file:
api/src/lib/snsClient.js
// api/src/lib/snsClient.js import { SNSClient } from ""@aws-sdk/client-sns""; import { logger } from ""src/lib/logger""; // Import Redwood logger // The AWS SDK v3 automatically reads credentials from environment variables // (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION) // or IAM roles if deployed on AWS infrastructure. const region = process.env.AWS_REGION; if (!region) { logger.warn( ""AWS_REGION environment variable not set. SNS Client might not initialize correctly."" ); // Consider throwing an error in production if region is mandatory } export const snsClient = new SNSClient({ region }); logger.info(""AWS SNS Client initialized."");
-
Explanation: We import the
SNSClient
, instantiate it (optionally passing the region), and export it. The Redwood logger is used for basic initialization feedback.
-
-
Define the Contact Data Model: We need a way to store the phone numbers we want to message.
-
Edit
api/db/schema.prisma
and add theContact
model:// api/db/schema.prisma // Keep the datasource and generator blocks from before model Contact { id Int @id @default(autoincrement()) name String? // Optional name phone String @unique // E.164 format recommended, ensure uniqueness createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
-
Explanation: Defines a simple
Contact
table with an ID, an optional name, a unique phone number, and timestamps. Using@unique
onphone
prevents duplicate entries.
-
-
Apply Database Migration: Create and apply a migration for the new
Contact
model.yarn rw prisma migrate dev # Name the migration (e.g., ""add contact model"")
-
Generate Contact Scaffolding (Optional but helpful): Redwood can generate basic CRUD operations (Create, Read, Update, Delete) for the
Contact
model. This gives us a service and SDL definitions automatically.yarn rw generate scaffold contact
- This creates:
api/src/graphql/contacts.sdl.js
(GraphQL schema definitions)api/src/services/contacts/contacts.js
(Service functions for DB interaction)- Frontend pages/components in
web/src
(we won't focus on the UI here, but they are generated)
- This creates:
-
Create the Messaging Service: This service will contain the core logic for fetching contacts and sending the bulk SMS via SNS.
-
Generate a new service:
yarn rw generate service messaging
-
Edit
api/src/services/messaging/messaging.js
:// api/src/services/messaging/messaging.js import { PublishBatchCommand } from ""@aws-sdk/client-sns""; import { UserInputError } from ""@redwoodjs/graphql-server""; import { db } from ""src/lib/db""; import { logger } from ""src/lib/logger""; import { snsClient } from ""src/lib/snsClient""; // Import our initialized client import { requireAuth } from ""src/lib/auth""; // We'll add auth later // E.164 regex (basic example; for robust validation, consider using libphonenumber-js as discussed in Section 8) const E164_REGEX = /^\+[1-9]\d{1,14}$/; // Max batch size for SNS PublishBatch const MAX_BATCH_SIZE = 10; export const sendBulkSms = async ({ message }) => { requireAuth(); // Ensure user is logged in if (!message || message.trim().length === 0) { throw new UserInputError(""Message content cannot be empty.""); } // Consider adding length checks for SMS here (e.g., 160 chars for standard SMS) logger.info(""Initiating bulk SMS send job...""); let contacts = []; // Initialize contacts array try { contacts = await db.contact.findMany({ select: { id: true, phone: true }, // Only select necessary fields }); if (!contacts || contacts.length === 0) { logger.warn(""No contacts found in the database. Aborting SMS job.""); return { successCount: 0, failureCount: 0, failedNumbers: [], message: ""No contacts found to send messages to."", }; } logger.info(`Found ${contacts.length} contacts. Preparing batches...`); let successfulSends = 0; let failedSends = 0; const failedNumbersDetails = []; // Store { number, reason } // Process contacts in batches of MAX_BATCH_SIZE for (let i = 0; i < contacts.length; i += MAX_BATCH_SIZE) { const batch = contacts.slice(i_ i + MAX_BATCH_SIZE); const batchEntries = batch .map((contact_ index) => { // Validate phone number format before adding to batch if (!E164_REGEX.test(contact.phone)) { logger.warn( `Invalid phone number format skipped: ${contact.phone}` ); failedSends++; failedNumbersDetails.push({ number: contact.phone, reason: ""Invalid E.164 Format"", }); return null; // Skip this entry } return { Id: `msg_${contact.id}_${index}`, // Unique ID within the batch PhoneNumber: contact.phone, // Optional: Add MessageAttributes if needed // MessageAttributes: { // 'AWS.SNS.SMS.SMSType': { DataType: 'String', StringValue: 'Transactional' }, // Or 'Promotional' // // Add other attributes if necessary // } }; }) .filter((entry) => entry !== null); // Remove null entries (invalid numbers) if (batchEntries.length === 0) { logger.warn(`Skipping empty batch starting at index ${i}.`); continue; // Nothing to send in this batch } const command = new PublishBatchCommand({ PublishBatchRequestEntries: batchEntries, Message: message, // The actual SMS content }); try { logger.info(`Sending batch of ${batchEntries.length} messages...`); const result = await snsClient.send(command); // Process results if (result.Successful) { successfulSends += result.Successful.length; // Optional: Log successful IDs: result.Successful.map(s => s.Id) } if (result.Failed) { failedSends += result.Failed.length; result.Failed.forEach((failure) => { // Find the original phone number from the batch using failure.Id const originalEntry = batchEntries.find( (entry) => entry.Id === failure.Id ); const failedNumber = originalEntry ? originalEntry.PhoneNumber : ""Unknown ID: "" + failure.Id; logger.error( `Failed to send to ${failedNumber}: ${failure.Code} - ${failure.Message} (SenderFault: ${failure.SenderFault})` ); failedNumbersDetails.push({ number: failedNumber, reason: `${failure.Code}: ${failure.Message}`, }); }); } logger.info( `Batch finished. Success: ${ result.Successful?.length || 0 }, Failed: ${result.Failed?.length || 0}` ); } catch (batchError) { // Handle errors sending the entire batch logger.error( `Error sending SMS batch starting at index ${i}:`, batchError ); // Increment failure count for all intended recipients in this batch const attemptedCount = batchEntries.length; failedSends += attemptedCount; batchEntries.forEach((entry) => { failedNumbersDetails.push({ number: entry.PhoneNumber, reason: `Batch Send Error: ${batchError.message || batchError.name}`, }); }); } } // End of batch loop logger.info( `Bulk SMS job completed. Total Success: ${successfulSends}, Total Failed: ${failedSends}` ); return { successCount: successfulSends, failureCount: failedSends, failedNumbers: failedNumbersDetails, // Return detailed failures message: `SMS Job Finished. Sent: ${successfulSends}, Failed: ${failedSends}.`, }; } catch (error) { logger.error(""Error during bulk SMS processing:"", error); // Generic error for the entire process return { successCount: 0, failureCount: contacts?.length || 0, // Assume all failed if error before/during loop failedNumbers: [], message: `An unexpected error occurred: ${error.message}`, error: true, // Indicate a top-level error occurred }; } };
-
Explanation:
- Imports necessary modules:
PublishBatchCommand
from AWS SDK,db
for database access,logger
, oursnsClient
, andUserInputError
for GraphQL validation errors. - Defines
MAX_BATCH_SIZE
(SNS limit is 10 forPublishBatch
). - The
sendBulkSms
function takes themessage
content as input. - It fetches all contacts from the DB (selecting only
id
andphone
). - It iterates through contacts in batches.
- Phone Number Validation: Includes a basic check using
E164_REGEX
before adding a number to a batch. Invalid numbers are skipped and logged. (Reminds user about Section 8 for better validation). - It constructs
PublishBatchRequestEntries
for each valid contact in the batch, ensuring a uniqueId
for each entry. - It creates and sends a
PublishBatchCommand
usingsnsClient.send()
. - Batch Result Handling: It processes the
Successful
andFailed
arrays from the SNS response, logging details about failures and updating counters. It attempts to map failure IDs back to phone numbers for better reporting. - Error Handling: Includes
try...catch
blocks for both individual batch sends and the overall process. - Returns a summary object with success/failure counts and details on failed numbers.
requireAuth()
is added (we'll set up auth next).
- Imports necessary modules:
-
3. Building the API Layer (GraphQL Mutation)
We need a way for the frontend (or external clients) to trigger the sendBulkSms
service function. We'll use a GraphQL mutation.
-
Define the GraphQL Mutation:
-
Edit
api/src/graphql/messaging.sdl.js
:# api/src/graphql/messaging.sdl.js type SendBulkSmsFailure { number: String! reason: String! } type SendBulkSmsResponse { successCount: Int! failureCount: Int! message: String! failedNumbers: [SendBulkSmsFailure!] # Return details error: Boolean # Optional flag for top-level errors } type Mutation { # Mutation to trigger sending SMS to all contacts sendBulkSms(message: String!): SendBulkSmsResponse! @requireAuth }
-
Explanation:
- Defines a custom
SendBulkSmsFailure
type to structure failure details. - Defines a
SendBulkSmsResponse
type to structure the return value of our mutation, including success/failure counts and the detailed failures list. - Defines the
sendBulkSms
mutation within theMutation
type. It accepts a requiredmessage
string. - Applies the
@requireAuth
directive to ensure only authenticated users can call this mutation.
- Defines a custom
-
-
Implement Basic Authentication (dbAuth): Redwood makes setting up simple database authentication straightforward.
-
Run the setup command:
yarn rw setup auth dbAuth
-
This command:
- Adds necessary auth models (
User
,UserCredential
,UserRole
) toschema.prisma
. - Creates auth-related service (
api/src/services/users
) and GraphQL definitions (api/src/graphql/users.sdl.js
). - Sets up auth functions (
api/src/lib/auth.js
). - Adds frontend pages for login/signup (
web/src/pages/LoginPage
,web/src/pages/SignupPage
, etc.) and updates routing.
- Adds necessary auth models (
-
Apply Auth Migrations: Run the migration to add the new auth tables to your database.
yarn rw prisma migrate dev # Name the migration (e.g., ""add dbAuth models"")
-
Create a Test User: You'll need a user to test the authenticated mutation. You can either:
- Start the dev server (
yarn rw dev
) and navigate to the signup page (/signup
) in your browser. - Or, use
yarn rw prisma studio
to add a user directly to the database (remember to hash the password correctly if adding manually, or use the signup page).
- Start the dev server (
-
-
Link SDL to Service: Redwood automatically maps the
sendBulkSms
mutation inmessaging.sdl.js
to thesendBulkSms
function exported fromapi/src/services/messaging/messaging.js
because the names match. No extra resolver code is needed here. -
Testing the Mutation (GraphQL Playground):
-
Start the Redwood development server:
yarn rw dev
-
Open your browser to
http://localhost:8911/graphql
. -
Authentication Header: You need to simulate being logged in.
- Log in via the web interface (
/login
). - Open your browser's developer tools (Network tab).
- Find a GraphQL request (e.g., one made after logging in).
- Copy the value of the
auth-provider
header (should bedbAuth
). - Copy the value of the
authorization
header (should beBearer <session-token>
). - In the GraphQL Playground, click the ""HTTP HEADERS"" tab at the bottom left.
- Add the headers:
{ ""auth-provider"": ""dbAuth"", ""authorization"": ""Bearer YOUR_COPIED_SESSION_TOKEN"" }
- Log in via the web interface (
-
Execute the Mutation: Run the following mutation in the Playground:
mutation SendTestBulkSms { sendBulkSms(message: ""Hello from Redwood SNS Bulk Sender!"") { successCount failureCount message failedNumbers { number reason } } }
-
Check the response in the Playground and the logs in your terminal (
yarn rw dev
output) to see the outcome. You should see logs indicating contacts being fetched, batches being sent, and the final result.
-
4. Integrating with AWS SNS (IAM & Setup)
Properly configuring AWS permissions is crucial for security and functionality.
-
Create an IAM User for Your Application: It's best practice to create a dedicated IAM user with the minimum necessary permissions rather than using your root AWS account credentials.
- Navigate to the IAM console in AWS.
- Go to Users -> Create user.
- Enter a User name (e.g.,
redwood-sns-app-user
). - Select Provide user access to the AWS Management Console - Optional. If you select this, choose a password option.
- Click Next.
- Choose Attach policies directly.
- Search for and select the
AmazonSNSFullAccess
policy. Note: For production, create a custom policy granting onlysns:Publish
andsns:PublishBatch
permissions, potentially restricted to specific regions or topics if applicable.AmazonSNSFullAccess
is simpler for this guide but too permissive for production.// Example Custom Policy (More Secure) { ""Version"": ""2012-10-17"", ""Statement"": [ { ""Effect"": ""Allow"", ""Action"": [ ""sns:Publish"", ""sns:PublishBatch"" ], ""Resource"": ""*"" // Restrict further by ARN if possible: ""arn:aws:sns:REGION:ACCOUNT_ID:*"" } ] }
- Click Next.
- Review the user details and policy, then click Create user.
-
Generate Access Keys:
- After the user is created, click on the username to go to the user's summary page.
- Go to the Security credentials tab.
- Scroll down to Access keys and click Create access key.
- Select Application running outside AWS as the use case.
- Click Next.
- (Optional) Add a description tag (e.g.,
redwood-sns-bulk-sms-key
). - Click Create access key.
- CRITICAL: Copy the Access key ID and Secret access key immediately and store them securely. You cannot retrieve the secret key again after closing this screen.
-
Add Credentials to
.env
:-
Open your project's
.env
file. -
Paste the copied keys into the respective variables:
# ./.env DATABASE_URL=""..."" # Keep existing vars AWS_ACCESS_KEY_ID=""PASTE_YOUR_ACCESS_KEY_ID_HERE"" AWS_SECRET_ACCESS_KEY=""PASTE_YOUR_SECRET_ACCESS_KEY_HERE"" AWS_REGION=""us-east-1"" # Ensure this matches your desired region
-
-
Restart Development Server: If your dev server (
yarn rw dev
) is running, stop it (Ctrl+C) and restart it to ensure it picks up the new environment variables from.env
.yarn rw dev
Your application is now configured to authenticate with AWS SNS using the dedicated IAM user's credentials.
5. Implementing Error Handling, Logging, and Retries
Robust error handling and logging are essential for production systems.
-
Error Handling (Already Implemented):
- Input Validation: The service function (
sendBulkSms
) already includes checks for an empty message (UserInputError
) and basic E.164 phone number format validation. - SNS Batch Errors: The code iterates through the
Failed
array in thePublishBatch
response, logs each specific failure, and includes details in the mutation response. - General Exceptions: The main
try...catch
block insendBulkSms
catches unexpected errors during the process (e.g., database connection issues, SDK client errors). - Authentication: The
@requireAuth
directive handles unauthorized access attempts.
- Input Validation: The service function (
-
Logging (Already Implemented):
- RedwoodJS's built-in logger (
api/src/lib/logger.js
) is used throughout thesendBulkSms
service. - We log:
- Job initiation and completion.
- Number of contacts found.
- Batch processing progress.
- Specific send failures with reasons from SNS.
- Invalid phone numbers skipped.
- Overall success/failure counts.
- Major exceptions.
- Configuration: You can configure Redwood's logger levels and destinations (e.g., console, file, third-party services) in
api/src/lib/logger.js
. For production, you'd typically set the level toinfo
orwarn
and potentially log to a file or a log aggregation service.
- RedwoodJS's built-in logger (
-
Retry Mechanisms:
-
SNS Internal Retries: AWS SNS itself has built-in retry mechanisms for transient delivery issues when sending SMS messages. You generally don't need to implement complex retry logic for the final delivery attempt from SNS to the carrier.
-
Application-Level Retries (for
PublishBatch
): ThePublishBatch
API call itself might fail due to transient network issues or AWS throttling. Our current code logs batch errors but doesn't automatically retry the entire batch. -
Implementing Retries (Optional Enhancement): To add retries for failed
PublishBatch
calls, you could wrap thesnsClient.send(command)
call within a loop with exponential backoff:// Inside the batch loop in api/src/services/messaging/messaging.js const MAX_RETRIES = 3; let attempt = 0; let batchResult = null; let lastBatchError = null; while (attempt < MAX_RETRIES && !batchResult) { try { logger.info(`Sending batch (Attempt ${attempt + 1}/${MAX_RETRIES})...`); batchResult = await snsClient.send(command); // Success_ break loop } catch (error) { lastBatchError = error; logger.warn(`Batch send attempt ${attempt + 1} failed: ${error.name} - ${error.message}`); if (attempt + 1 < MAX_RETRIES) { // Exponential backoff: wait 1s_ 2s_ 4s... const delay = Math.pow(2_ attempt) * 1000; logger.info(`Retrying batch in ${delay / 1000}s...`); await new Promise(resolve => setTimeout(resolve, delay)); } attempt++; } } if (!batchResult && lastBatchError) { // Handle final failure after retries (log, increment counters as before) logger.error(`Error sending SMS batch starting at index ${i} after ${MAX_RETRIES} attempts:`, lastBatchError); // ... update failedSends and failedNumbersDetails for the whole batch ... continue; // Move to next batch } // Process the successful batchResult as before... if (batchResult.Successful) { /* ... */ } if (batchResult.Failed) { /* ... */ }
-
Consideration: Adding retries increases complexity. Evaluate if the potential benefits outweigh the added code, especially given SNS's own reliability. For critical messages, retrying might be necessary. Also consider idempotency if retrying.
-
6. Creating Database Schema and Data Layer
This was largely covered during the setup and core functionality implementation.
-
Schema Definition (
api/db/schema.prisma
): We defined theContact
model and thedbAuth
models.// api/db/schema.prisma recap datasource db { provider = ""postgresql"" url = env(""DATABASE_URL"") } generator client { provider = ""prisma-client-js"" binaryTargets = ""native"" } model Contact { id Int @id @default(autoincrement()) name String? phone String @unique // E.164 format recommended createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // --- dbAuth Models --- // RedwoodJS automatically adds these via `yarn rw setup auth dbAuth` // model User { ... } // model UserCredential { ... } // model UserRole { ... }
-
Data Access Layer:
- RedwoodJS uses Prisma Client as the data access layer. The
db
object imported fromsrc/lib/db
provides typed access to your database models. - Example usage in
messaging.js
:await db.contact.findMany(...)
- The
contacts
service (api/src/services/contacts/contacts.js
) generated by the scaffold command provides standard CRUD operations.
- RedwoodJS uses Prisma Client as the data access layer. The
-
Migrations:
- We used
yarn rw prisma migrate dev
to apply schema changes. This command:- Compares the
schema.prisma
file to the database state. - Generates SQL migration files in
api/db/migrations
. - Applies the migration to the development database.
- Updates the Prisma Client types.
- Compares the
- For production deployments, you'll typically use
yarn rw prisma migrate deploy
.
- We used
-
Sample Data Population (Seeding): To easily add test contacts, you can use Prisma's seeding feature.
-
Create a seed file:
api/db/seed.js
// api/db/seed.js import { db } from 'src/lib/db' // Import any other libraries you need // Use ""+COUNTRYCODE AREACODE NUMBER"" format (E.164) const CONTACTS_TO_SEED = [ { name: 'Test User One', phone: '+15551234567' }, { name: 'Test User Two', phone: '+15559876543' }, { name: 'Test User Three', phone: '+447700900123' }, // Example UK number { name: 'Invalid Number User', phone: '555-INVALID'}, // To test validation ] export default async () => { try { console.log('Seeding contacts...') // Use Prisma Promise API for potential performance improvement with many records await db.$transaction( CONTACTS_TO_SEED.map((contact) => db.contact.upsert({ where: { phone: contact.phone }, // Avoid duplicates based on phone update: { name: contact.name }, // Update name if phone exists create: contact, }) ) ) console.log(`Seeded ${CONTACTS_TO_SEED.length} contacts (or updated existing).`); // Add other seed data if needed (e.g., default UserRoles) } catch (error) { console.error('Error seeding database:', error) // Avoid exiting with error code 1 in dev to prevent Prisma migrate prompts // process.exit(1) } }
-
Run the seed command (this automatically runs after
migrate dev
or can be run manually):yarn rw prisma db seed
-
7. Adding Security Features
Security is paramount, especially when dealing with potentially sensitive contact information and incurring costs via AWS services.