DEV Community

Cover image for Create a simple OTP system with AWS Serverless
Pubudu Jayawardana for AWS Community Builders

Posted on


Create a simple OTP system with AWS Serverless

(This blog post first appeared in


This post describes how to implement a simple One Time Password (OTP) system with AWS Serverless services which can be used as a part of two-step verification.

Below tools and technologies used to build this application.


Simple OTP Architecture

How it works

  1. In this scenario, I used a login form, which is developed with VueJS and hosted using Amplify static web hosting.

  2. User will enter their email and password and once the credentials are validated, an API endpoint is called to execute "Generate OTP" Lambda function which generates a 6 digit code along with a session id.

  3. Once the code and session id is generated, "Generate OTP" Lambda will save these data into a DynamoDB table.

  4. Then only the session id will be returned as the response of the API endpoint.

  5. DynamoDB streams are enabled in the table. So, once the data are saved, it will trigger the "Send Email" Lambda function.

  6. Within the "Send Email" Lambda function, it will call the Simple Email Service (SES) to send out an email with the generated code to the email address provided.

  7. Meanwhile on the frontend side, once the session id is received from the API, 2nd form is presented to enter the code, which is emailed to the given address.

  8. Once the user enters the code and submits it, it will validate the code along with the session id using another API gateway endpoint that proxy to "Verify OTP" Lambda function.

  9. In "Verify OTP" Lambda function, it queries the DynamoDB table with the given session-id and code and returns the success or error responses.

Key points/Lessons learned

  1. Here I enabled DynamoDB TTL to delete the entries after a specific time to prevent fill out the table very quickly. However, DynamoDB will not delete your record immediately when the TTL is expired. It is deleted eventually and AWS only guarantees it to be deleted within 48 hours. Because of this, when verifying the OTP code, it has to consider the same "expiredAt" field which was used to set TTL.

  2. When design the DynamoDB table, I used sessionId + OTP code as a primary key to easily query the required record. So, when verifying the code, I query by primary key with this combination in the DynamoDB table.

  3. When using DynamoDB steam as a trigger for Lambda, it gets triggers for all the DynamoDB events (ex: insert, delete, update). So, within the Lambda function, had to filter out only the INSERT events using "eventName" of the record.

  4. To send out emails, I have used AWS's own Simple Email Service (SES). However, you need to first verify your sending email address to send emails to any address. This can be done with a support request.

  5. Here, I used Amplify static web hosting to host the frontend of the application. I have used Amplify features - the auto deployments when Github repository modified and custom domain name set up only with few button clicks.

  6. I have used AWS SAM to deploy the backend resources. The expiry time of the OTP and no of digits in the OTP code can be configured at the deployment time.

How to set up


  • Set up and verified SES send email address


  1. Clone the repository:

  2. Run

sam init && sam deploy -g
Enter fullscreen mode Exit fullscreen mode

After providing your stack information and AWS environment parameters, this will create the backend stack. Copy the ApiBaseUrl output value.


  1. Copy the .env.example into .env file and add the ApiBaseUrl value as VUE_APP_API_BASE_URL.

  2. You may zip the whole frontend directory and use that in Amplify web hosting, or authorize your GitHub repository to automatically deploy the application when a git push is made.

  3. If you need to run the frontend in local, navigate to frontend directory and run npm run serve

To Delete the stack

To remove the backend, run sam delete.


Demo version of this application is available at :


Your valuable feedback on this project is mostly welcome! I would like you to play around with this and if you have any questions or general comments, please reach out to me via Personal Blog, LinkedIn, Twitter or Github.

Keep building, keep sharing!

Top comments (12)

karanpratapsingh profile image
Karan Pratap Singh

Great post! what tool did you use for the architecture diagram, Lucidcharts?

pubudusj profile image
Pubudu Jayawardana

Thanks @karanpratapsingh for the feedback.
I use for these diagrams.

karanpratapsingh profile image
Karan Pratap Singh


avinashdalvi_ profile image
Avinash Dalvi

Very explained post. Thanks for this. 👍🏻👍🏻

pubudusj profile image
Pubudu Jayawardana

Thanks for the feedback @aviboy2006

3much profile image

Great and useful post. 💯✌

pubudusj profile image
Pubudu Jayawardana

Thanks @3much for the feedback!

aaronbrighton profile image
Aaron Brighton

FYI, the Github repo link is 404, may need to make it public?

pubudusj profile image
Pubudu Jayawardana

Just made it public. Thanks for pointing this out @aaronbrighton .

eliasibgerardo profile image
Gerardo Eliasib

Excellent post!
I wonder if this implementation limits the number of OTP's generated by email to avoid unnecessary consumption?

pubudusj profile image
Pubudu Jayawardana

Thanks for the feedback @eliasibgerardo
In this implementation there is no limitation enforced. However, there are several ways to protect the unnecessary consumptions.
Since this OTP functionality meant to be consumed by already authenticated users, that will reduce the unnecessary usage since we can track the users who are actually using the system.
Also, in the infrastructure level, we can use Web Application Firewall (WAF) rules with throttling to protect the API end points per IP for example.
Further, in the code level, we can implement our own rate limits per email address using the email address and expiryAt field values.

pawan_kumar profile image
pawan kumar

Hi, its a great post, but send-email function gives error that
event.Records is not iterable