How I Replaced Firebase with AWS Cognito, Lambda, DynamoDB in a Serverless Application

In one of my recent projects, we started off using Firebase for authentication and storage. But as the app became more complex, we found ourselves needing more sophisticated authentication logic, custom user roles, and a scalable multi-tenant architecture. So, I made the switch from Firebase to a serverless AWS stack.

  • Cognito (authorization)

  • Lambda(Business logic, autherization)

  • DynamoDB (single table design)

  • amazon s3 + CloudFront( file storage and delivery)

In this post, I will discuss how we design and what our benefits are

Why we moved from Firebase

We decided to move away from Firebase for a few reasons.

  • While it’s fantastic for whipping up a quick MVP, we ran into some limitations that just didn’t work for us:

  • We found ourselves with limited control over the authentication flow, especially when it came to custom roles and permissions.

  • Managing fine-grained access logic became quite a challenge.

  • It also didn’t scale automatically, which was a big drawback for us.

  • Lastly, there wasn’t a straightforward way to cascade entity deletions based on user or organization relationships.

Backend Architecture (Serverless)

Let’s take a quick look at the backend architecture, specifically focusing on a serverless setup:

  • Cognito takes care of user logins and issues tokens.

  • API Gateway is where we handle authenticated requests.

  • Lambda functions are in charge of performing CRUD operations.

  • DynamoDB is our go-to for storing app data, supporting multi-tenancy and role-based access.

  • S3 is used for securely storing files like GLB and images.

  • CloudFront helps us deliver assets through OAI, ensuring private access.

We’ve deployed the same Lambda code for both development and production environments, but we’re using different tables, buckets, and configurations for each.

Authentication Flow with Cognito

Signup Flow:

Users can register using Cognito, either with their email/password or through Google.

  • If the email is verified, ✅ → We store the user in DynamoDB.

  • If it’s not verified, ❌ → The user is marked as "unconfirmed" and isn’t added to the database.

Login Flow:

After logging in, a token (ID token) is issued.

We utilize a Lambda Authorizer (or middleware) to check the validity of the token.

Depending on the user's status (like whether they were created by an admin or are unconfirmed), we take different actions, such as:

  • Redirecting to a password change page

  • Redirecting to email verification

  • Proceeding to the app/dashboard

We also implemented pre-/post-auth Lambda triggers to fill the database either before or after signup, but only when certain conditions are met.

Lambda Functions

Each entity(user, org, directory, etc) has its lambda handler. These functions: We have replaced most of the Firebase functions with Lambda functions. Each function checks first on a role-based permission basis, like whether that function will be executed or not to that particular user. These functions perform CRUD operations.

DynamoDB: Single Table Design (Multi-Tenant)

We used a clean, scalable single table design. we will have Primary key is OrgId, and we will have a sort key that is entity path(variable).

PK(OrgId)SK(Entity Path)Data
org#ORG123organization#ORG123{ name: "Acme Inc", users: [...] }
org#ORG123directories#DIR123{ name: "Assets", type: "directory" }
org#ORG123material-directories#MATDIR123{ name: ..., type: "directory" }

Benefits:

  • All org data is neatly scoped

  • Easy to query by prefix (e.g., all directories in an org)

File Upload with S3 + CloudFront

We keep user-uploaded files, like images and previews, safely stored in private S3 buckets:

  • Bucket: myapp-static-dev / myapp-static-prod.

  • These files are accessed through CloudFront, using OAI and a JWT-based access system.

  • Uploads are handled directly via the Lambda API.

Cost Perspective

One of the surprising perks of switching to this architecture was how cost-effective the serverless model turned out to be:

Cognito: It's super budget-friendly for thousands of monthly active users. You only start paying once you go beyond the generous free tier.

Lambda: You get charged based on requests and execution time. For most scenarios, it usually stays within the free tier or just costs a few bucks a month.

DynamoDB: The pay-per-request model helped us steer clear of overprovisioning. Even with frequent reads and writes, our costs stayed low.

S3 and CloudFront: Storing static files is inexpensive, and CloudFront enhances delivery speed without adding significant costs.

When we compared it to Firebase, where scaling costs can shoot up with usage or Firestore reads, AWS offered us much more predictability and control.

Conclusion

It took some hard work, but now the backend is much easier to understand, expand, and secure. If you're considering doing something similar, don’t hesitate to reach out or leave your questions in the comments. I’d love to share more!

0
Subscribe to my newsletter

Read articles from Bama Charan Chhandogi directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Bama Charan Chhandogi
Bama Charan Chhandogi

I am a Dev from Kolkata who loves to write about technology.