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
nvm
to 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:
node -v # Should be 20.x yarn -v # Should be >= 1.22.21 aws --version # Verify AWS CLI is installed
If Node version is incorrect, use
nvm
:nvm install 20 nvm use 20
Upgrade 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.
yarn 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 install
will 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.env
file in the project root. This file stores secrets and configuration and should not be committed to Git. Add it to your.gitignore
if it's not already there. IMPORTANT: The values below are placeholders. Replace them with your actual credentials and resource identifiers.# .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_URL
connects Prisma to your database. AWS variables allow the SDK to authenticate.SNS_TOPIC_ARN
tells our code which SNS topic to publish to.SCHEDULER_LAMBDA_ARN
might 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:
docker 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_URL
in.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.
cd 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 aReminder
model.// 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
).createdAt
andupdatedAt
are standard timestamp fields. scheduledAt
: Crucially, store this in UTC. Prisma handles JavaScriptDate
objects, 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 onscheduledAt
andstatus
. 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.
yarn 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:
yarn rw prisma generate
-
(Optional) Seed Sample Data: Create
api/db/seeds.js
to add initial data for testing.// 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:
yarn 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.
yarn rw g sdl Reminder --crud
- This command does two things:
- Creates
api/src/services/reminders/reminders.ts
: Contains functions (resolvers) to interact with theReminder
model. - Creates
api/src/graphql/reminders.sdl.ts
: Defines the GraphQL schema (types, queries, mutations) for reminders. - Updates
api/src/directives/requireAuth/requireAuth.ts
andapi/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.// 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
scheduledAt
being 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.# 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
Reminder
type matching the Prisma model. - Defines standard CRUD queries and mutations.
- Uses Redwood's
@requireAuth
directive by default. You might change this to@skipAuth
for public access during development or implement actual authentication.
- Defines the
- Key Points:
-
Test the API: Start the development server:
yarn rw dev
Navigate to the GraphQL Playground:
http://localhost:8911/graphql
- Create Reminder:
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:
query GetReminders { reminders { id message scheduledAt status } }
- Experiment with
updateReminder
anddeleteReminder
.
- 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):
{ "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
.env
file (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:
{ "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:
{ "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_ARN
value in your.env
file (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.
mkdir 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
pg
driver.
- 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.prisma
from your Redwood project (api/db/schema.prisma
) into thereminder-scheduler-lambda
directory.cp ../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-proxy
connects through Prisma's managed service, avoiding binary compatibility issues but requiring network access and a Prisma Cloud account setup. Using--binary-targets
requires 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_URL
is directly available, but--data-proxy
requires 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-proxy
for 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
):// 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}`, } }