code examples
code examples
Developer Guide: Building a RedwoodJS Scheduling & Reminder System with AWS SNS & EventBridge
A step-by-step guide to creating a RedwoodJS application for scheduling reminders using AWS SNS for notifications and AWS EventBridge Scheduler for event triggering.
This guide provides a comprehensive, step-by-step walkthrough for building a production-ready scheduling and reminder application using RedwoodJS, powered by AWS SNS for notifications and AWS EventBridge Scheduler for triggering events. We'll cover everything from initial setup to deployment and monitoring.
Version: 1.0
Project Overview and Goals
What We're Building:
We will build a RedwoodJS application that enables users to schedule reminders. At the scheduled time, the system will automatically send a notification (e.g., email, SMS) via AWS Simple Notification Service (SNS). The scheduling itself will be managed reliably by AWS EventBridge Scheduler, triggering a backend process to dispatch the reminders.
Problems Solved:
- Automated Reminders: Offload the task of remembering events or tasks from users.
- Scalable Notifications: Leverage AWS SNS to reliably deliver notifications to potentially large numbers of users across various channels (Email, SMS, Push Notifications).
- Reliable Scheduling: Utilize AWS EventBridge Scheduler for robust, serverless cron-like job execution, avoiding the complexities of managing self-hosted schedulers.
- Decoupled Architecture: Separate the core application logic from the notification and scheduling mechanisms, improving maintainability and resilience.
Technologies Used:
- RedwoodJS: A full-stack JavaScript/TypeScript framework for the web. Provides structure, conventions, a GraphQL API, and ORM integration (Prisma). Chosen for its integrated full-stack experience and developer productivity features.
- Node.js: The underlying runtime environment for RedwoodJS.
- PostgreSQL: Our relational database, managed by Prisma via RedwoodJS. (Other SQL databases supported by Prisma can also be used).
- AWS Simple Notification Service (SNS): A fully managed messaging service for decoupling microservices, distributed systems, and serverless applications. Chosen for its scalability, reliability, and support for multiple endpoint types.
- AWS EventBridge Scheduler: A serverless scheduler to create, run, and manage scheduled tasks at scale. Chosen for its fine-grained scheduling capabilities (cron expressions), reliability, and native AWS integration.
- AWS Lambda: A serverless compute service used to run the code triggered by EventBridge Scheduler. It will query the database and publish messages to SNS.
- AWS SDK for JavaScript v3: Used to interact with AWS services (SNS, potentially others) programmatically from our Lambda function and potentially the RedwoodJS API side.
- Prisma: The ORM used by RedwoodJS for database access.
System Architecture:
+-------------+ +-----------------+ +-------------------+ +-------------+
| User |<----->| RedwoodJS Web |<----->| RedwoodJS API |<----->| Database |
| (Browser) | | (React Frontend)| | (GraphQL, Services)| | (PostgreSQL)|
+-------------+ +-----------------+ +---------+---------+ +------+------+
| |
| Creates/Updates | Stores
| Reminders | Reminders
v v
+-------------------------+ +-----------------------+ +---------------+ +-----------------+
| AWS EventBridge Scheduler |------>| AWS Lambda Function |------>| AWS SNS Topic |------>| User Endpoint |
| (Checks for due | | (Queries DB, Formats, | | (e.g., email) | | (Email, SMS...) |
| reminders on schedule) | | Publishes to SNS) | +---------------+ +-----------------+
+-------------------------+ +-----------------------+
^ Triggered by Cron | Reads Due Reminders
| | Updates Status
+--------------------------------------------------+
Prerequisites:
- Node.js: Version 20.x recommended (Check RedwoodJS/deployment platform docs for specifics). Use
nvmto manage versions. - Yarn: Version 1.22.21 or higher.
- AWS Account: With appropriate permissions to create IAM users/roles, SNS topics, Lambda functions, and EventBridge schedules.
- AWS CLI: Configured with credentials (
aws configure). - RedwoodJS CLI:
yarn global add redwoodjs-cli(or usenpx). - Basic Git knowledge.
- Docker (Optional): For running PostgreSQL locally easily.
Final Outcome:
A functional web application where users can create, view, update, and delete reminders. Scheduled reminders will trigger email notifications (as an example) sent via AWS SNS at the specified time. The system will be robust, scalable, and ready for further feature development.
1. Setting up the Project
Let's initialize our RedwoodJS project and configure the basic environment.
-
Check Prerequisites:
bashnode -v # Should be 20.x yarn -v # Should be >= 1.22.21 aws --version # Verify AWS CLI is installedIf Node version is incorrect, use
nvm:bashnvm install 20 nvm use 20Upgrade Yarn if needed:
npm install --global yarn -
Create RedwoodJS App: We'll use TypeScript for better type safety. Use single quotes for the commit message to avoid potential shell issues with nested double quotes.
bashyarn create redwood-app redwood-sns-scheduler --typescript --git-init --commit-message 'Initial project setup' cd redwood-sns-scheduler--typescript: Initializes the project with TypeScript.--git-init: Initializes a Git repository.--commit-message: Sets the initial commit message.yarn installwill likely run automatically if using Yarn v1. If not, runyarn install.
-
Project Structure Overview:
api/: Contains the backend code (GraphQL API, services, database schema, functions).web/: Contains the frontend React code (components, pages, layouts).scripts/: For utility scripts (like database seeding).redwood.toml: Project configuration file.package.json: Project dependencies.
-
Environment Variables (
.env): Create a.envfile in the project root. This file stores secrets and configuration and should not be committed to Git. Add it to your.gitignoreif it's not already there. IMPORTANT: The values below are placeholders. Replace them with your actual credentials and resource identifiers.dotenv# .env # --- Database --- # Use Docker or local install. Example for Docker: DATABASE_URL=""postgresql://postgres:password@localhost:5432/scheduler_dev?schema=public"" # --- AWS Credentials (for local development/testing API interactions) --- # Obtain these by creating an IAM User with programmatic access (details later) # For production, use environment variables provided by your deployment host. AWS_ACCESS_KEY_ID=""YOUR_AWS_ACCESS_KEY_ID"" # Replace with your actual key ID AWS_SECRET_ACCESS_KEY=""YOUR_AWS_SECRET_ACCESS_KEY"" # Replace with your actual secret key AWS_REGION=""us-east-1"" # Replace with your preferred AWS region # --- AWS SNS --- # Obtain this after creating the SNS topic (details later) SNS_TOPIC_ARN=""arn:aws:sns:us-east-1:123456789012:redwood-reminders-topic"" # Replace with actual ARN # --- Scheduler Lambda --- # We'll define this Lambda later SCHEDULER_LAMBDA_ARN=""arn:aws:lambda:us-east-1:123456789012:function:redwood-reminder-scheduler"" # Replace with actual ARN- Purpose:
DATABASE_URLconnects Prisma to your database. AWS variables allow the SDK to authenticate.SNS_TOPIC_ARNtells our code which SNS topic to publish to.SCHEDULER_LAMBDA_ARNmight be needed if managing EventBridge from the API side (optional).
- Purpose:
-
Database Setup (Local PostgreSQL using Docker): If you have Docker installed, this is the easiest way:
bashdocker run --name postgres-scheduler -e POSTGRES_PASSWORD=password -e POSTGRES_DB=scheduler_dev -p 5432:5432 -d postgres- This starts a PostgreSQL container named
postgres-scheduler, sets the password, creates the initial database, maps port 5432, and runs it in the background. Ensure the credentials match yourDATABASE_URLin.env. - Alternatively, install PostgreSQL directly on your OS.
- This starts a PostgreSQL container named
-
Install AWS SDK v3: We'll need the SDK in our API side (potentially) and definitely in our Lambda function later. Let's add it to the API workspace.
bashcd api yarn add @aws-sdk/client-sns @aws-sdk/client-scheduler @aws-sdk/client-lambda # Add others as needed cd ..
2. Creating Database Schema and Data Layer
We need a way to store the reminders.
-
Define Prisma Schema: Edit
api/db/schema.prisma. Add aRemindermodel.prisma// api/db/schema.prisma datasource db { provider = "postgresql" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" binaryTargets = ["native"] } // Optional: Add user model if you implement authentication // model User { // id Int @id @default(autoincrement()) // email String @unique // // ... other fields // reminders Reminder[] // } model Reminder { id Int @id @default(autoincrement()) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt message String scheduledAt DateTime // Time the reminder should be sent (Store in UTC!) status String @default("PENDING") // PENDING, SENT, ERROR recipient String // E.g., email address, phone number, user ID // Optional: Link to a user if implementing multi-user support // userId Int? // user User? @relation(fields: [userId], references: [id]) @@index([scheduledAt, status]) // Index for efficient querying by the scheduler }- Explanation: We define fields for the reminder message, the scheduled time (
scheduledAt), the recipient identifier (e.g., email), and a status (PENDING,SENT,ERROR).createdAtandupdatedAtare standard timestamp fields. scheduledAt: Crucially, store this in UTC. Prisma handles JavaScriptDateobjects, which are timezone-aware, but ensure consistency.status: Tracks the lifecycle of the reminder.recipient: How we know where to send the notification.@@index: Creates a database index onscheduledAtandstatus. This is vital for the performance of the Lambda function that queries for pending reminders.
- Explanation: We define fields for the reminder message, the scheduled time (
-
Apply Database Migrations: Create and apply the database migration using Prisma Migrate.
bashyarn rw prisma migrate dev --name create_reminders- This command generates SQL migration files in
api/db/migrations/and applies them to your database defined inDATABASE_URL. It will also update your Prisma Client.
- This command generates SQL migration files in
-
Generate Prisma Client: Migrations usually handle this, but you can run it manually if needed:
bashyarn rw prisma generate -
(Optional) Seed Sample Data: Create
api/db/seeds.jsto add initial data for testing.javascript// api/db/seeds.js import { db } from './index' // Adjusted path if needed // Calculates a date/time 5 minutes from now in UTC const fiveMinutesFromNow = () => { const date = new Date() date.setMinutes(date.getMinutes() + 5) return date.toISOString() // Ensure it's UTC string for consistency } export default async () => { try { console.log('Seeding database...') // Add example reminders await db.reminder.createMany({ data: [ { message: 'Test Reminder 1 (due in 5 mins)', scheduledAt: fiveMinutesFromNow(), recipient: 'test@example.com', // Replace with a real test email status: 'PENDING', }, { message: 'Test Reminder 2 (already sent)', scheduledAt: new Date(Date.now() - 3600 * 1000).toISOString(), // 1 hour ago recipient: 'another@example.com', status: 'SENT', }, { message: 'Test Reminder 3 (due far future)', scheduledAt: new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString(), // 1 week from now recipient: 'future@example.com', status: 'PENDING', }, ], skipDuplicates: true, // Avoid errors if you run seed multiple times }) console.log('Database seeded successfully!') } catch (error) { console.error('Error seeding database:', error) } }Run the seed script:
bashyarn rw exec seed
3. Implementing Core Functionality (Redwood Service)
We need backend logic to manage reminders (Create, Read, Update, Delete). Redwood services are perfect for this.
-
Generate Reminder Service & SDL: Redwood generators simplify creating the necessary files for a CRUD interface.
bashyarn rw g sdl Reminder --crud- This command does two things:
- Creates
api/src/services/reminders/reminders.ts: Contains functions (resolvers) to interact with theRemindermodel. - Creates
api/src/graphql/reminders.sdl.ts: Defines the GraphQL schema (types, queries, mutations) for reminders. - Updates
api/src/directives/requireAuth/requireAuth.tsandapi/src/directives/skipAuth/skipAuth.ts(if not already present). - Creates basic tests in
api/src/services/reminders/reminders.test.ts.
- Creates
- This command does two things:
-
Review Generated Service (
api/src/services/reminders/reminders.ts): The generated code provides basic CRUD operations using Prisma.typescript// api/src/services/reminders/reminders.ts (simplified example) import type { QueryResolvers, MutationResolvers, ReminderRelationResolvers } from 'types/graphql' import { db } from 'src/lib/db' // Optional: Add authentication checks if needed // import { requireAuth } from 'src/lib/auth' export const reminders: QueryResolvers['reminders'] = () => { // requireAuth() // Uncomment if users should only see their own reminders return db.reminder.findMany() // Modify to filter by userId if needed } export const reminder: QueryResolvers['reminder'] = ({ id }) => { // requireAuth() // Add logic to ensure user owns this reminder if needed return db.reminder.findUnique({ where: { id }, }) } export const createReminder: MutationResolvers['createReminder'] = ({ input }) => { // requireAuth() // Add userId: context.currentUser.id if using auth // --- Input Validation --- if (new Date(input.scheduledAt) <= new Date()) { throw new Error('Scheduled time must be in the future.') } // Add more validation as needed (e.g., recipient format) return db.reminder.create({ data: { ...input, scheduledAt: new Date(input.scheduledAt), // Ensure it's a Date object status: 'PENDING', // Explicitly set status on creation }, }) } // updateReminder, deleteReminder... (review and adjust as needed) // ...- Key Points:
- The service functions directly interact with the Prisma client (
db). - Basic CRUD is provided. You'll likely need to customize this, especially if adding authentication.
- Input Validation: Added a simple check for
scheduledAtbeing in the future withincreateReminder. Robust validation (e.g., using Zod) is recommended for production.
- The service functions directly interact with the Prisma client (
- Key Points:
-
Review Generated GraphQL SDL (
api/src/graphql/reminders.sdl.ts): This file defines the API shape.graphql# api/src/graphql/reminders.sdl.ts export const schema = gql` type Reminder { id: Int! createdAt: DateTime! updatedAt: DateTime! message: String! scheduledAt: DateTime! status: String! recipient: String! # userId: Int # user: User } type Query { reminders: [Reminder!]! @requireAuth # Adjust auth directive if needed reminder(id: Int!): Reminder @requireAuth # Adjust auth directive if needed } input CreateReminderInput { message: String! scheduledAt: DateTime! recipient: String! # userId: Int } input UpdateReminderInput { message: String scheduledAt: DateTime recipient: String status: String # Allow updating status manually? Maybe not. # userId: Int } type Mutation { createReminder(input: CreateReminderInput!): Reminder! @requireAuth # Adjust auth directive if needed updateReminder(id: Int!, input: UpdateReminderInput!): Reminder! @requireAuth # Adjust auth directive if needed deleteReminder(id: Int!): Reminder! @requireAuth # Adjust auth directive if needed } `- Key Points:
- Defines the
Remindertype matching the Prisma model. - Defines standard CRUD queries and mutations.
- Uses Redwood's
@requireAuthdirective by default. You might change this to@skipAuthfor public access during development or implement actual authentication.
- Defines the
- Key Points:
-
Test the API: Start the development server:
bashyarn rw devNavigate to the GraphQL Playground:
http://localhost:8911/graphql- Create Reminder:
graphql
mutation CreateNewReminder { createReminder(input: { message: ""Check GraphQL Playground"", recipient: ""dev@example.com"", scheduledAt: ""2025-04-21T10:00:00.000Z"" # Use ISO 8601 UTC format }) { id message scheduledAt status recipient } } - Query Reminders:
graphql
query GetReminders { reminders { id message scheduledAt status } } - Experiment with
updateReminderanddeleteReminder.
- Create Reminder:
4. Setting up AWS Infrastructure (IAM, SNS, Lambda, EventBridge)
This is the core AWS setup for scheduling and notification.
Security First: IAM Setup
We need specific permissions for our different components. Remember to replace placeholders like YOUR_REGION, YOUR_ACCOUNT_ID, and example resource names with your actual values.
-
IAM User for Local Dev/API (Optional but Recommended):
- Navigate to IAM in the AWS Console -> Users -> Create user.
- Username:
redwood-scheduler-api-user(or similar). - Select Access key - Programmatic access.
- Attach policies directly or create a group. For simplicity now, attach directly.
- Click Attach existing policies directly -> Create policy.
- Go to the JSON tab and paste the following policy (adjust region, account ID, and topic name):
json
{ "Version": "2012-10-17", "Statement": [ { "Sid": "SNSPublishAccessForRemindersTopic", "Effect": "Allow", "Action": "sns:Publish", "Resource": "arn:aws:sns:YOUR_REGION:YOUR_ACCOUNT_ID:redwood-reminders-topic" } // Add other permissions if needed (e.g., EventBridge PutRule if managing schedules from API) ] } - Review policy, give it a name (e.g.,
RedwoodSchedulerAPIPolicy), and create it. - Back on the user creation screen, refresh and attach the newly created policy.
- Complete user creation. Crucially, copy the Access key ID and Secret access key. Add these to your
.envfile (AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY).
-
IAM Role for Lambda Function: The Lambda function needs permission to access the database (via network/secrets) and publish to SNS.
- Go to IAM -> Roles -> Create role.
- Trusted entity type: AWS service.
- Use case: Lambda.
- Permissions:
- Search for and add
AWSLambdaBasicExecutionRole(for CloudWatch Logs). - Click Create policy. Use the JSON editor:
json
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowSNSPublishReminders", "Effect": "Allow", "Action": "sns:Publish", "Resource": "arn:aws:sns:YOUR_REGION:YOUR_ACCOUNT_ID:redwood-reminders-topic" } // Add permissions to access Secrets Manager if storing DB creds there // Example: // { // "Sid": "AllowReadDBSecret", // "Effect": "Allow", // "Action": "secretsmanager:GetSecretValue", // "Resource": "arn:aws:secretsmanager:YOUR_REGION:YOUR_ACCOUNT_ID:secret:your-db-secret-name-?????" // } // Add VPC permissions if Lambda needs to be in a VPC for DB access // Example (adjust actions based on needs): // { // "Sid": "AllowVPCAccess", // "Effect": "Allow", // "Action": [ // "ec2:CreateNetworkInterface", // "ec2:DescribeNetworkInterfaces", // "ec2:DeleteNetworkInterface", // "ec2:AssignPrivateIpAddresses", // "ec2:UnassignPrivateIpAddresses" // ], // "Resource": "*" // } ] } - Name it
RedwoodSchedulerLambdaSNSPolicy(or similar), create it. - Back on the role creation screen, attach the new policy (
RedwoodSchedulerLambdaSNSPolicy).
- Search for and add
- Give the role a name (e.g.,
RedwoodSchedulerLambdaRole) and create it. Note the ARN.
-
IAM Role for EventBridge Scheduler: EventBridge needs permission to invoke our Lambda function.
- Go to IAM -> Roles -> Create role.
- Trusted entity type: AWS service.
- Use case: Scroll down and choose Scheduler.
- Permissions: Click Create policy. Use the JSON editor:
json
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AllowInvokeReminderLambda", "Effect": "Allow", "Action": "lambda:InvokeFunction", "Resource": "arn:aws:lambda:YOUR_REGION:YOUR_ACCOUNT_ID:function:redwood-reminder-scheduler" } ] } - Name it
RedwoodEventBridgeInvokeLambdaPolicy, create it. - Back on the role creation screen, attach the new policy.
- Give the role a name (e.g.,
RedwoodEventBridgeSchedulerRole) and create it. Note the ARN.
SNS Topic Setup
- Navigate to Simple Notification Service (SNS) in the AWS Console.
- Go to Topics -> Create topic.
- Type: Standard.
- Name:
redwood-reminders-topic. - Leave other settings as default for now (Access policy can be refined later).
- Click Create topic.
- Note the ARN. Update the
SNS_TOPIC_ARNvalue in your.envfile (remembering it's a placeholder there). - Create a Subscription for Testing:
- On the topic page, click Create subscription.
- Protocol: Email.
- Endpoint: Enter an email address you can access for testing.
- Click Create subscription.
- Check your email inbox for a confirmation message from AWS Notification and click the link to confirm the subscription. The status should change to
Confirmed.
Lambda Function Setup
This function will be triggered by EventBridge and will handle sending reminders.
-
Create Lambda Function Directory: Outside your RedwoodJS project, create a new directory for the Lambda function code. This separates concerns and allows for independent dependency management for the Lambda function.
bashmkdir reminder-scheduler-lambda cd reminder-scheduler-lambda npm init -y npm install @aws-sdk/client-sns @prisma/client pg # Optional: npm install @aws-sdk/client-secrets-manager (if fetching DB URL from Secrets Manager)- We need the SNS client, Prisma client (to talk to the DB), and the
pgdriver.
- We need the SNS client, Prisma client (to talk to the DB), and the
-
Copy Prisma Schema and Generate Client: The Lambda needs the schema to generate the correct Prisma client. Copy
schema.prismafrom your Redwood project (api/db/schema.prisma) into thereminder-scheduler-lambdadirectory.bashcp ../redwood-sns-scheduler/api/db/schema.prisma . # Generate client. Choose ONE strategy: # Strategy 1: Prisma Data Proxy (Simpler setup, potential latency/cost) npx prisma generate --data-proxy # Strategy 2: Native Binaries (Better performance, requires matching Lambda architecture) # npx prisma generate --binary-targets linux-arm64-openssl-3.0.x # For Graviton (arm64) Lambda # npx prisma generate --binary-targets linux-x64-openssl-3.0.x # For x86_64 Lambda # Ensure the corresponding binaryTarget is also listed in schema.prisma generator block if using this.- Important: Generating the client is crucial. Using
--data-proxyconnects through Prisma's managed service, avoiding binary compatibility issues but requiring network access and a Prisma Cloud account setup. Using--binary-targetsrequires specifying the exact Linux architecture and OpenSSL version your Lambda runtime uses (check AWS Lambda documentation) and ensuring these binaries are included in your deployment package. The example code below assumesDATABASE_URLis directly available, but--data-proxyrequires specific connection string format and potentially API key environment variables. Consult Prisma's serverless deployment documentation for details on your chosen strategy. This guide proceeds assuming--data-proxyfor simplicity in the example command, but the code uses a standardDATABASE_URL.
- Important: Generating the client is crucial. Using
-
Write Lambda Handler Code (
index.js):javascript// reminder-scheduler-lambda/index.js import { PrismaClient } from '@prisma/client' import { SNSClient, PublishCommand } from '@aws-sdk/client-sns' // Optional: Uncomment if using Secrets Manager // import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; // --- Client Initialization --- // NOTE: If using Secrets Manager, the PrismaClient initialization needs to happen *after* fetching the secret. // Consider initializing prismaClient inside the handler or using an async initialization pattern. // The current approach assumes DATABASE_URL is directly in the environment. const prisma = new PrismaClient() // Reads DATABASE_URL from env by default const snsClient = new SNSClient({ region: process.env.AWS_REGION || 'us-east-1' }) // Ensure AWS_REGION env var is set // Optional: Secrets Manager client // const secretsClient = new SecretsManagerClient({ region: process.env.AWS_REGION || 'us-east-1' }); const SNS_TOPIC_ARN = process.env.SNS_TOPIC_ARN // Ensure SNS_TOPIC_ARN env var is set // Optional: Secret ARN from environment // const DATABASE_URL_SECRET_ARN = process.env.DATABASE_URL_SECRET_ARN; // --- Helper Function (Optional: For Secrets Manager) --- // async function getDatabaseUrlFromSecretsManager(secretArn) { // if (!secretArn) { // throw new Error("DATABASE_URL_SECRET_ARN environment variable is not set."); // } // try { // const command = new GetSecretValueCommand({ SecretId: secretArn }); // const data = await secretsClient.send(command); // if ('SecretString' in data) { // // Assuming the secret stores the URL directly or as JSON with a specific key // const secretData = JSON.parse(data.SecretString); // // Adjust key based on how you stored it in Secrets Manager // return secretData.DATABASE_URL || secretData.dbConnectionString; // } // throw new Error("SecretString not found in secret value."); // } catch (error) { // console.error("Failed to retrieve database secret:", error); // throw error; // Re-throw to indicate critical failure // } // } export const handler = async (event) => { console.log('Scheduler Lambda triggered:', JSON.stringify(event, null, 2)) // --- Configuration Check --- if (!SNS_TOPIC_ARN) { console.error('SNS_TOPIC_ARN environment variable is not set.') return { statusCode: 500, body: 'Internal configuration error (SNS Topic).' } } // NOTE: Add check for DATABASE_URL or DATABASE_URL_SECRET_ARN depending on your setup // --- Database Connection (Handles Secrets Manager if implemented) --- // let currentPrismaClient = prisma; // Use pre-initialized client by default // try { // if (DATABASE_URL_SECRET_ARN) { // console.log('Fetching DB URL from Secrets Manager...'); // const dbUrl = await getDatabaseUrlFromSecretsManager(DATABASE_URL_SECRET_ARN); // // Initialize Prisma Client here if needed, or ensure the pre-initialized one picks up the fetched URL // // This might require adjusting PrismaClient instantiation if URL is dynamic per invocation // console.log('Successfully fetched DB URL.'); // // If PrismaClient needs explicit URL: // // currentPrismaClient = new PrismaClient({ datasources: { db: { url: dbUrl } } }); // } else if (!process.env.DATABASE_URL) { // console.error('DATABASE_URL environment variable is not set.'); // return { statusCode: 500, body: 'Internal configuration error (DB URL).' } // } // } catch (configError) { // console.error('Failed to configure database connection:', configError); // return { statusCode: 500, body: 'Database configuration failed.' }; // } // Use currentPrismaClient for subsequent operations const now = new Date() // Look for reminders due in the near past/future to catch slight timing discrepancies const windowStart = new Date(now.getTime() - 60 * 1000) // 1 minute ago const windowEnd = new Date(now.getTime() + 10 * 1000) // 10 seconds from now let remindersToSend = [] try { console.log(`Querying for reminders between ${windowStart.toISOString()} and ${windowEnd.toISOString()}`) // Use the potentially re-initialized client: currentPrismaClient remindersToSend = await prisma.reminder.findMany({ where: { status: 'PENDING', scheduledAt: { gte: windowStart, lte: windowEnd, }, }, take: 100, // Limit batch size to avoid overwhelming resources/hitting limits }) } catch (dbError) { console.error('Error querying database:', dbError) // Depending on the error, you might want to retry or just report failure return { statusCode: 500, body: 'Database query failed.' } } if (remindersToSend.length === 0) { console.log('No pending reminders found in the current window.') return { statusCode: 200, body: 'No reminders to process.' } } console.log(`Found ${remindersToSend.length} reminders to process.`) const publishPromises = [] const sentReminderIds = [] const errorReminderIds = [] for (const reminder of remindersToSend) { const messagePayload = { // Customize the message structure as needed by your subscribers default: `Reminder: ${reminder.message}`, // Fallback message email: `Subject: Your Reminder!\n\nHi there,\n\nThis is a reminder for: ${reminder.message}\n\nScheduled at: ${reminder.scheduledAt.toISOString()}`, // sms: `Reminder: ${reminder.message}` // Example for SMS } const publishParams = { TopicArn: SNS_TOPIC_ARN, Message: JSON.stringify(messagePayload), MessageStructure: 'json', // Important for sending different messages per protocol // Optional: Add MessageAttributes for filtering subscriptions // MessageAttributes: { // 'recipientType': { DataType: 'String', StringValue: 'email' }, // Example // 'recipientId': { DataType: 'String', StringValue: reminder.recipient } // } } // Push the promise to the array publishPromises.push( snsClient.send(new PublishCommand(publishParams)) .then(publishResult => { console.log(`Successfully published message for reminder ID ${reminder.id}. Message ID: ${publishResult.MessageId}`) sentReminderIds.push(reminder.id) }) .catch(snsError => { console.error(`Error publishing message for reminder ID ${reminder.id}:`, snsError) errorReminderIds.push(reminder.id) // Don't throw here, allow other messages to be processed }) ) } // Wait for all SNS publish operations to complete await Promise.allSettled(publishPromises) console.log(`Finished processing batch. Sent: ${sentReminderIds.length}, Errors: ${errorReminderIds.length}`) // --- Update Reminder Statuses in Database --- try { if (sentReminderIds.length > 0) { console.log(`Updating status to SENT for IDs: ${sentReminderIds.join(', ')}`) // Use currentPrismaClient await prisma.reminder.updateMany({ where: { id: { in: sentReminderIds } }, data: { status: 'SENT' }, }) } if (errorReminderIds.length > 0) { console.log(`Updating status to ERROR for IDs: ${errorReminderIds.join(', ')}`) // Use currentPrismaClient await prisma.reminder.updateMany({ where: { id: { in: errorReminderIds } }, data: { status: 'ERROR' }, // Mark as error for potential retry or investigation }) } } catch (updateError) { console.error('Error updating reminder statuses in database:', updateError) // This is problematic - notifications might have been sent but status not updated. // Consider adding monitoring/alerting here. return { statusCode: 500, body: 'Failed to update reminder statuses after sending.' } } finally { // --- Disconnect Prisma Client --- // Important in serverless environments to prevent connection pool exhaustion // await currentPrismaClient.$disconnect() // Disconnect the client used // console.log('Prisma client disconnected.') } return { statusCode: 200, body: `Processed batch. Sent: ${sentReminderIds.length}, Errors: ${errorReminderIds.length}`, } }
Frequently Asked Questions
How to schedule reminders using RedwoodJS?
You can schedule reminders in a RedwoodJS application by leveraging AWS EventBridge Scheduler to trigger events at specific times, which then activate a backend process to send notifications via AWS SNS. This setup ensures reliable, scalable scheduling and notification delivery. The reminder details are stored in a PostgreSQL database managed by Prisma, RedwoodJS's ORM.
What is AWS EventBridge Scheduler used for in this project?
AWS EventBridge Scheduler is used to trigger the reminder notifications. It acts as a serverless cron job system, reliably executing tasks at scheduled times. This removes the need for managing a separate scheduling infrastructure, simplifying the application's architecture.
Why use AWS SNS for sending reminder notifications?
AWS SNS provides a scalable and reliable way to send notifications across multiple channels, including email, SMS, and push notifications. Its decoupled nature ensures that notification delivery doesn't impact the core application logic, improving resilience and maintainability.
When should I use Prisma in RedwoodJS reminder application?
Prisma is RedwoodJS's default ORM and is used throughout the application lifecycle to interact with the PostgreSQL database. It simplifies database operations by providing a type-safe and convenient way to create, read, update, and delete reminder records.
How to create a new RedwoodJS project for reminders?
Create a new project by running the command `yarn create redwood-app redwood-sns-scheduler --typescript --git-init --commit-message 'Initial project setup'`. The `--typescript` flag initializes a TypeScript project, `--git-init` creates a Git repository, and `--commit-message` sets the initial commit message.
What database is used in RedwoodJS reminder tutorial?
The tutorial uses PostgreSQL as the database. It is managed by Prisma, RedwoodJS's built-in Object-Relational Mapper (ORM), enabling easy interaction between the application and the database.
How to send email notifications with AWS SNS and RedwoodJS?
The RedwoodJS application interacts with AWS SNS to send email notifications. The Lambda function, triggered by EventBridge Scheduler, queries the database for due reminders and publishes JSON-formatted messages to an SNS topic. This topic is subscribed to an email endpoint, ensuring delivery to the specified recipient.
What AWS services are used in this RedwoodJS scheduler?
The RedwoodJS scheduler uses AWS SNS for sending notifications, EventBridge Scheduler for triggering events, Lambda for running the notification logic, and potentially Secrets Manager for storing database credentials securely. These services provide a serverless architecture for the application.
How to setup AWS Lambda to send reminders?
The Lambda function needs access to the database credentials and permission to publish to the designated SNS topic. An IAM role specifically for the Lambda function is created with these permissions, ensuring secure access to resources.
What is the purpose of the `status` field in Reminder model?
The status field in the `Reminder` model tracks the reminder's lifecycle, using values like `PENDING`, `SENT`, and `ERROR`. This allows for efficient management and monitoring of the reminder's delivery status.
Why is scheduledAt stored as UTC in the database?
Storing `scheduledAt` as UTC in the database ensures consistency and avoids timezone-related issues, especially when dealing with users across different geographical locations. This guarantees the reminder is triggered at the correct time regardless of the user's local timezone.
How to configure environment variables for RedwoodJS and AWS?
Environment variables are stored in a `.env` file in the project root. This file contains sensitive information like database credentials, AWS access keys, and SNS topic ARNs. For the Lambda function, environment variables are configured through the AWS Lambda console.
What are the prerequisites for building this RedwoodJS application?
You'll need Node.js (v20.x recommended), Yarn (v1.22.21+), an AWS account with necessary permissions, AWS CLI, RedwoodJS CLI, basic Git knowledge, and optionally Docker for local PostgreSQL.
How does the reminder system handle multiple users in RedwoodJS?
Although not explicitly implemented in the basic tutorial, multi-user support can be added by associating reminders with users in the database schema. This allows filtering reminders based on the current user and enhances data privacy.