code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

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 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 use npx).
  • 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.

  1. Check Prerequisites:

    bash
    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:

    bash
    nvm install 20
    nvm use 20

    Upgrade Yarn if needed: npm install --global yarn

  2. 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.

    bash
    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, run yarn install.
  3. 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.
  4. 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.

    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_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).
  5. Database Setup (Local PostgreSQL using Docker): If you have Docker installed, this is the easiest way:

    bash
    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 your DATABASE_URL in .env.
    • Alternatively, install PostgreSQL directly on your OS.
  6. 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.

    bash
    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.

  1. Define Prisma Schema: Edit api/db/schema.prisma. Add a Reminder model.

    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). createdAt and updatedAt are standard timestamp fields.
    • scheduledAt: Crucially, store this in UTC. Prisma handles JavaScript Date 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 on scheduledAt and status. This is vital for the performance of the Lambda function that queries for pending reminders.
  2. Apply Database Migrations: Create and apply the database migration using Prisma Migrate.

    bash
    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 in DATABASE_URL. It will also update your Prisma Client.
  3. Generate Prisma Client: Migrations usually handle this, but you can run it manually if needed:

    bash
    yarn rw prisma generate
  4. (Optional) Seed Sample Data: Create api/db/seeds.js to 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:

    bash
    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.

  1. Generate Reminder Service & SDL: Redwood generators simplify creating the necessary files for a CRUD interface.

    bash
    yarn rw g sdl Reminder --crud
    • This command does two things:
      • Creates api/src/services/reminders/reminders.ts: Contains functions (resolvers) to interact with the Reminder model.
      • Creates api/src/graphql/reminders.sdl.ts: Defines the GraphQL schema (types, queries, mutations) for reminders.
      • Updates api/src/directives/requireAuth/requireAuth.ts and api/src/directives/skipAuth/skipAuth.ts (if not already present).
      • Creates basic tests in api/src/services/reminders/reminders.test.ts.
  2. 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 scheduledAt being in the future within createReminder. Robust validation (e.g., using Zod) is recommended for production.
  3. 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 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.
  4. Test the API: Start the development server:

    bash
    yarn rw dev

    Navigate 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 updateReminder and deleteReminder.

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.

  1. 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 .env file (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY).
  2. 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).
    • Give the role a name (e.g., RedwoodSchedulerLambdaRole) and create it. Note the ARN.
  3. 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

  1. Navigate to Simple Notification Service (SNS) in the AWS Console.
  2. Go to Topics -> Create topic.
  3. Type: Standard.
  4. Name: redwood-reminders-topic.
  5. Leave other settings as default for now (Access policy can be refined later).
  6. Click Create topic.
  7. Note the ARN. Update the SNS_TOPIC_ARN value in your .env file (remembering it's a placeholder there).
  8. 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.

  1. 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.

    bash
    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.
  2. 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 the reminder-scheduler-lambda directory.

    bash
    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 assumes DATABASE_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 standard DATABASE_URL.
  3. 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.