In this blog post, we are going over the project which is called “EventSpark” - a serverless event management app for meetups or conferences. The frontend is done in NextJS and the backend is fully done in AWS.
As mentioned, the frontend was done in NextJS with libraries which are crucial for this project:
- Zod - schema validation used during event creation
- NextAuth.JS - authentication library making it easier to store sensitive token information for accessing the backend
- ShadCN - pre-built component library making it easier to develop more complex UIs on the website
On the other side, we have multiple AWS services which were used to accomplish this project with best practices and makes the project completely serverless. AWS services used are:
- Lambda
- API Gateway
- S3
- CloudFront
- CloudFormation
- DynamoDB
- Cognito
The motivation for this project is to create a fully serverless project with an example of AWS Cognito implementation with NextAuth.JS library. But you’ll see later, that many interesting methods have been implemented to make the project even better than originally planned!
The usage patterns for all of these services will be explained in the later parts of this blog post.
Architecture overview
The application has been seperated into 2 seperate microservices:
- Authentication stack
- Events Stack
Let’s go into more detail for each of the stacks and explain the architecture behind them.
Authentication Stack Deep Dive
The authentication stack consists of:
- 2 seperate Lambdas
- Login Lambda
- Register Lambda
- API Gateway to open up the Lambdas to the Internet
- Cognito User pool
This stack demonstrates the capabilities of AWS Cognito and showcases how straightforward it is to implement user management on any platform. It is important to mention that this stack can be reused in any of the other projects which you may have or can be implemented into any future projects that you may do! Here is the architectural diagram of the Authentication stack.
Our client (NextJS frontend) calls our API gateway URL which forwards the request to either our Login or Register Lambda. Register Lambda takes the email, password and name from the request, registers the user in Cognito and in case of successful registration, the user gets the notification that they have been successfully registered, on the other side, the Login Lambda takes the received credentials, verifies the credentials with Cognito and if the user entered the correct credentials, the client gets the access tokens which are later used for the Event Stack’s Cognito Authorizer (explained later).
It is important to note that the Lambdas are built using Serverless Application Model (SAM) and the build instructions are containted inside the Makefile. We are doing it this way to evade duplication of the requirements.txt
files - the library we are adding is AWS Lambda Powertools for better logging experience.
Let’s see each of the services defined inside this template in more detail.
AWS Cognito
To initialize our User pool, the following CloudFormation template is used:
# Cognito User Pool
EventSparkUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: EventSparkUserPool
AutoVerifiedAttributes:
- email
UsernameAttributes:
- email
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: true
RequireNumbers: true
RequireSymbols: false
RequireUppercase: true
Schema:
- Name: email
AttributeDataType: String
Mutable: true
Required: true
- Name: name
AttributeDataType: String
Mutable: true
Required: false
# User Pool Client
EventSparkUserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref EventSparkUserPool
ClientName: EventSparkWebApp
GenerateSecret: false
ExplicitAuthFlows:
- ALLOW_USER_SRP_AUTH
- ALLOW_REFRESH_TOKEN_AUTH
- ALLOW_USER_PASSWORD_AUTH
PreventUserExistenceErrors: ENABLED
API Gateway
To open up our Lambdas, we use API Gateway resource:
# API Gateway for Auth Services
EventSparkAuthApi:
Type: AWS::Serverless::Api
Properties:
StageName: eventspark
Cors:
AllowMethods: "'*'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
AWS Lambda
Our 2 Lambdas are defined in the following YAML code:
RegisterUserLambda:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: makefile
Properties:
Runtime: python3.12
Handler: lambda_handler.lambda_handler
CodeUri: ./
Policies:
- AWSLambdaBasicExecutionRole
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- cognito-idp:SignUp
- cognito-idp:AdminConfirmSignUp
Resource: !GetAtt EventSparkUserPool.Arn
Environment:
Variables:
POWERTOOLS_SERVICE_NAME: authentication
USER_POOL_ID: !Ref EventSparkUserPool
USER_POOL_CLIENT_ID: !Ref EventSparkUserPoolClient
Events:
RegisterUser:
Type: Api
Properties:
RestApiId: !Ref EventSparkAuthApi
Path: /register
Method: post
LoginUserLambda:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: makefile
Properties:
Runtime: python3.12
Handler: lambda_handler.lambda_handler
CodeUri: ./
Policies:
- AWSLambdaBasicExecutionRole
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- cognito-idp:InitiateAuth
Resource: !GetAtt EventSparkUserPool.Arn
Environment:
Variables:
POWERTOOLS_SERVICE_NAME: authentication
USER_POOL_ID: !Ref EventSparkUserPool
USER_POOL_CLIENT_ID: !Ref EventSparkUserPoolClient
Events:
LoginUser:
Type: Api
Properties:
RestApiId: !Ref EventSparkAuthApi
Path: /login
Method: post
Stack Outputs
You can notice that there are outputs of this stack — these are used for getting more information about the stack after deploying them to AWS:
- AuthApiEndpoint — URL to our Lambda resources
- Cognito User pool information — used in our Events Stack (explained later)
Outputs:
AuthApiEndpoint:
Description: "Auth API Gateway URL"
Value: !Sub "<https://$>{EventSparkAuthApi}.execute-api.${AWS::Region}.amazonaws.com/eventspark"
UserPoolId:
Description: "User Pool ID for EventSpark"
Value: !Ref EventSparkUserPool
UserPoolClientId:
Description: "User Pool Client ID for EventSpark"
Value: !Ref EventSparkUserPoolClient
UserPoolArn:
Description: "User Pool ARN for EventSpark"
Value: !GetAtt EventSparkUserPool.Arn
Interaction of the Authentication Lambdas with AWS Cognito
One of the critical points of this project is to understand how our 2 Lambdas work with Cognito. The following code will be in Python and it will interact with AWS Cognito via the Python’s boto3 library for AWS services.
Here is the code snippet from the Register Lambda to sign up the user — rest of the code (processing the request, logging statements etc.) can be seen inside the Github repository linked at the beginning of this blog post.
# Register the user in Cognito
response = cognito_client.sign_up(
ClientId=USER_POOL_CLIENT_ID,
Username=email,
Password=password,
UserAttributes=[
{
'Name': 'email',
'Value': email
},
{
'Name': 'name',
'Value': name
}
]
)
# Auto confirm the user (for development purposes)
# In production, you might want to use email verification
cognito_client.admin_confirm_sign_up(
UserPoolId=USER_POOL_ID,
Username=email
)
On the other side, we have the Login Lambda, which will with the provided login credentials from our NextJS frontend, communicate with Cognito to verify that the user is existing and has entered the correct password and will receive IdToken, AccessToken, RefreshToken and ExpiresIn fields. All of these fields are very important because the tokens will be used by Cognito Authorizer on Events Stack API Gateway to authenticate and authorize that the user has access to the API endpoints. Getting the mentioned tokens is easy, as you can see in the following code:
# Authenticate user using Cognito
response = cognito_client.initiate_auth(
ClientId=USER_POOL_CLIENT_ID,
AuthFlow='USER_PASSWORD_AUTH',
AuthParameters={
'USERNAME': email,
'PASSWORD': password
}
)
# Extract tokens from the response
id_token = response['AuthenticationResult']['IdToken']
access_token = response['AuthenticationResult']['AccessToken']
refresh_token = response['AuthenticationResult']['RefreshToken']
expires_in = response['AuthenticationResult']['ExpiresIn']
Events Stack Deep Dive
In this part of the blog post, let’s go over the architectural diagram and CloudFormation template first and take the template piece by piece and explain the implementation.
Here is the image of the diagram and YAML CloudFormation template:
Let’s break down this architecture which was made inside a CloudFormation YAML file.
AWS DynamoDB
We have 2 tables, where:
EventSparkEventTable contains the information about the events which are shown on the platform,
EventSparkEventTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: uuid
AttributeType: S
- AttributeName: creator
AttributeType: S
KeySchema:
- AttributeName: uuid
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: CreatorIndex
KeySchema:
- AttributeName: creator
KeyType: HASH
Projection:
ProjectionType: ALL
BillingMode: PAY_PER_REQUEST
while EventSparkEventUserAttendanceTable contains the information which users are attending which event
EventSparkEventUserAttendanceTable:
Type: AWS::DynamoDB::Table
Properties:
AttributeDefinitions:
- AttributeName: uuid
AttributeType: S
- AttributeName: user_email
AttributeType: S
- AttributeName: event_uuid
AttributeType: S
KeySchema:
- AttributeName: uuid
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: UserEmailIndex
KeySchema:
- AttributeName: user_email
KeyType: HASH
Projection:
ProjectionType: ALL
- IndexName: EventUuidIndex
KeySchema:
- AttributeName: event_uuid
KeyType: HASH
Projection:
ProjectionType: ALL
BillingMode: PAY_PER_REQUEST
Both DynamoDB tables have Global Secondary Indexes (GSI) which enables us to use the query method instead of scan method in our Lambdas to gather the information from these tables - this improves data lookup performance, therefore lowering Lambda cost.
S3 + CloudFront
S3 bucket EventSparkBucketcontaining event banners which are images, images are uploaded by the frontend itself by using pre-signed URLs for upload,
EventSparkBucket:
Type: AWS::S3::Bucket
Properties:
PublicAccessBlockConfiguration:
BlockPublicAcls: false
BlockPublicPolicy: false
IgnorePublicAcls: false
RestrictPublicBuckets: false
CorsConfiguration:
CorsRules:
- AllowedHeaders:
- "*"
AllowedMethods:
- PUT
- POST
- DELETE
AllowedOrigins:
- "*"
MaxAge: 3000
and in combination with this S3 bucket we are going to use CloudFront Distribution — using this service enables us to have our event banner images closer to the user globally therefore improving loading times of our media in the browser. You can see my previous blog post about the impact of CloudFront in a full-stack application!
# CloudFront setup
EventSparkCloudFrontOAC:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: EventSparkOAC
Description: OAC for CloudFront to access S3
SigningProtocol: sigv4
SigningBehavior: always
OriginAccessControlOriginType: s3
EventSparkCloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- Id: S3Origin
DomainName: !GetAtt EventSparkBucket.RegionalDomainName
S3OriginConfig:
OriginAccessIdentity: ""
OriginAccessControlId: !Ref EventSparkCloudFrontOAC
Enabled: true
DefaultRootObject: index.html
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
AllowedMethods:
- GET
- HEAD
CachedMethods:
- GET
- HEAD
ForwardedValues:
QueryString: false
Cookies:
Forward: none
ViewerCertificate:
CloudFrontDefaultCertificate: true
API Gateway
To open up our Lambdas to the Internet, we need to use API Gateway. However, we are going to attach Cognito Authorizer — each of the Lambdas which are protected with a Cognito Authorizer will require an Authorization header inside the request which contains the token received from our Login Lambda from the Authentication stack. If the request provides an expired, invalid or no token, our API Gateway will return a 401 Unauthorized error code. This makes protecting our endpoints so much easier since everything is done by Cognito and API Gateway, without needing any additional code! This way, we protect our APIs from unauthorized access and many attacks on our platform. To connect our API Gateway instance and Cognito User Pool, we needed to output our User Pool information and import it into this stack to make it work.
# API Gateway using standard REST API (not HttpApi)
EventSparkRestApi:
Type: AWS::Serverless::Api
Properties:
StageName: eventspark
Auth:
DefaultAuthorizer: CognitoAuthorizer
Authorizers:
CognitoAuthorizer:
UserPoolArn: !Ref UserPoolArn
Identity:
Header: Authorization
AddDefaultAuthorizerToCorsPreflight: false
Cors:
AllowMethods: "'*'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
AWS Lambda
In this section, we are going to go over all Lambdas inside this stack and explain their logic, together with their CF templates and main logic in the code. One important note, all Lambdas, except one, are protected by our Cognito Authorizer
Create Event Lambda
- receives and validates the incoming request
- put the event information inside the EventSparkEventTable DynamoDB table
- create a pre-signed URL which can be used by the client for an image upload to our S3 bucket and return it to the client
Here is the CloudFormation template for this Lambda:
CreateEventLambda:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: makefile
Properties:
Runtime: python3.12
Handler: lambda_handler.lambda_handler
CodeUri: ./
Policies:
- AWSLambdaBasicExecutionRole
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:PutItem
Resource: !GetAtt EventSparkEventTable.Arn
- Effect: Allow
Action:
- s3:PutObject
Resource:
- !Sub "${EventSparkBucket.Arn}/*.jpg"
- !Sub "${EventSparkBucket.Arn}/*.png"
Environment:
Variables:
EventSpark_CLOUDFRONT_URL: !Sub "https://${EventSparkCloudFrontDistribution.DomainName}"
EVENTS_DYNAMODB_TABLE: !Ref EventSparkEventTable
EVENTS_S3_BUCKET: !Ref EventSparkBucket
USER_POOL_ID: !Ref UserPoolId
Events:
CreateEvent:
Type: Api
Properties:
RestApiId: !Ref EventSparkRestApi
Path: /create-event
Method: post
And the code which follows the logic mentioned above:
def create_new_listing(event_id, title, description, date, start_time, end_time,
location_type, location, category, max_attendees, privacy,
creator, s3_key, dynamodb_class, s3_class):
try:
# Save to DynamoDB
dynamodb_class.table.put_item(
Item={
"uuid": event_id,
"title": title,
"description": description,
"date": date,
"startTime": start_time,
"endTime": end_time,
"locationType": location_type,
"location": location,
"category": category,
"maxAttendees": str(max_attendees),
"privacy": privacy,
"creator": creator,
"s3_key": s3_key
}
)
logger.info("Event created successfully", extra={
"event_id": event_id
})
# Generate a presigned URL for uploading
presigned_url = s3_class.client.generate_presigned_url(
"put_object",
Params={"Bucket": s3_class.bucket_name, "Key": s3_key},
ExpiresIn=60 # 1 minute expiration
)
logger.info("Presigned URL generated successfully", extra={
"event_id": event_id
})
return {
"statusCode": 200,
"headers": {
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
'Access-Control-Allow-Credentials': 'true',
'Content-Type': 'application/json'
},
"body": json.dumps({
"event_id": event_id,
"upload_url": presigned_url
})
}
except Exception as e:
logger.error("Failed to create event listing", extra={
"error": str(e),
"event_id": event_id
})
return {"statusCode": 500, "body": json.dumps({"error": "Failed to create listing"})}
Get Events Lambda
- if the event_id is present inside the query params, the provided event_id will be used to fetch data from the EventSparkEventTable together with a CloudFront URL to access the event banner and the data will be returned to the user to render it
- if the event_id is not provided, the Lambda will scan for first 10 items inside the EventSparkEventTable , genereate CloudFront URLs for the collected events banners and return the information to the user.
GetEventsLambda:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: makefile
Properties:
Runtime: python3.12
Handler: lambda_handler.lambda_handler
CodeUri: ./
Policies:
- AWSLambdaBasicExecutionRole
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:Scan
- dynamodb:GetItem
Resource: !GetAtt EventSparkEventTable.Arn
Environment:
Variables:
EVENTSPARK_CLOUDFRONT_URL: !Sub "https://${EventSparkCloudFrontDistribution.DomainName}"
EVENTS_DYNAMODB_TABLE: !Ref EventSparkEventTable
USER_POOL_ID: !Ref UserPoolId
Events:
GetEvents:
Type: Api
Properties:
RestApiId: !Ref EventSparkRestApi
Path: /get-events
Method: get
Auth:
Authorizer: NONE
Notice how, inside the CF template under “Properties -> Events -> GetEvents -> Properties -> Auth” we marked the Authorizer as NONE — the reason is that since our platform is public, we don’t want all users to login to access the data — the available events are shown regardless if the user is logged in/registered or not. This way, the incoming request to the Lambda doesn’t go to the Cognito Authorizer to get checked.
Here is the code:
def get_events_via_cloudfront(event, dynamodb_resource):
query_params = event.get('queryStringParameters', {}) or {}
limit = int(query_params.get('limit', 10))
event_id = query_params.get('event_id', None)
exclusive_start_key = query_params.get('start_key')
logger.info("Processing get_events_via_cloudfront request", extra={
"event_id": event_id,
"limit": limit,
"exclusive_start_key": exclusive_start_key
})
items = []
last_evaluated_key = None
cloudfront_url = environ.get("EVENTSPARK_CLOUDFRONT_URL", "")
# If event_id is provided, use get_item for direct lookup
if event_id:
# Use get_item for direct primary key lookup
response = dynamodb_resource.table.get_item(
Key={
'uuid': event_id
}
)
# Check if item was found
if 'Item' in response:
item = response['Item']
item['file_url'] = f'{cloudfront_url}/{item["s3_key"]}'
items = [item]
else:
# Initialize the scan parameters
scan_kwargs = {
'Limit': limit,
}
if exclusive_start_key:
scan_kwargs['ExclusiveStartKey'] = {'uuid': exclusive_start_key}
response = dynamodb_resource.table.scan(**scan_kwargs)
items = response.get('Items', [])
last_evaluated_key = response.get('LastEvaluatedKey')
# Construct file URLs correctly
for item in items:
item['file_url'] = f'{cloudfront_url}/{item["s3_key"]}'
# Get the ALLOWED_ORIGIN from environment or default to '*'
ALLOWED_ORIGIN = environ.get('ALLOWED_ORIGIN', '*')
logger.info("Returning events", extra={
"items_count": len(items),
"last_evaluated_key": last_evaluated_key
})
return {
'statusCode': 200,
'headers': {
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Content-Type': 'application/json'
},
'body': json.dumps({
'events': items,
'last_evaluated_key': last_evaluated_key
})
}
Get Events That User Created Lambda
This Lambda will query the EventSparkEventUserAttendanceTable with user’s email to fetch the information about the events that the user created and return to the client - used on the user’s dashboard.
GetEventsThatUserCreatedLambda:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: makefile
Properties:
Runtime: python3.12
Handler: lambda_handler.lambda_handler
CodeUri: ./
Policies:
- AWSLambdaBasicExecutionRole
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:Query
Resource:
- !GetAtt EventSparkEventTable.Arn
- !Sub "${EventSparkEventTable.Arn}/index/*"
Environment:
Variables:
EVENTS_DYNAMODB_TABLE: !Ref EventSparkEventTable
EVENTSPARK_CLOUDFRONT_URL: !Sub "https://${EventSparkCloudFrontDistribution.DomainName}"
Events:
GetEventsThatUserCreatedEndpoint:
Type: Api
Properties:
RestApiId: !Ref EventSparkRestApi
Path: /get-events-that-user-created
Method: get
def get_events_that_user_created(event, dynamodb_resource):
query_params = event.get('queryStringParameters', {}) or {}
user_email = query_params.get('user_email', None)
logger.info("Processing get_events_that_user_created request", extra={
"user_email": user_email
})
# Standard response headers
headers = {
'Access-Control-Allow-Origin': environ.get('ALLOWED_ORIGIN', '*'),
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Content-Type': 'application/json'
}
if not user_email:
logger.warning("Missing user_email in query parameters")
return {
'statusCode': 400,
'headers': headers,
'body': json.dumps({'error': 'user_email is required'})
}
try:
# Getting all events user registered for - query by user_email only
response = dynamodb_resource.table.query(
IndexName='CreatorIndex',
KeyConditionExpression='creator = :creator',
ExpressionAttributeValues={
':creator': user_email
}
)
items = response.get('Items', [])
# Add CloudFront URLs to the items
cloudfront_url = environ.get("EVENTSPARK_CLOUDFRONT_URL", "")
for item in items:
if 's3_key' in item:
item['file_url'] = f'{cloudfront_url}/{item["s3_key"]}'
logger.info("Successfully retrieved events created by user", extra={
"user_email": user_email,
"event_count": len(items)
})
return {
'statusCode': 200,
'headers': headers,
'body': json.dumps({'events': items})
}
except Exception as e:
logger.error("Error retrieving events created by user", extra={
"user_email": user_email,
"error": str(e)
})
return {
'statusCode': 500,
'headers': headers,
'body': json.dumps({'error': str(e)})
}
Get Events That User Registered To Lambda
This Lambda will query the EventSparkEventUserAttendanceTable with user’s email to see to which events did the user register to and marked that they will attend the event - used on the user’s dashboard.
GetEventsThatUserRegisteredToLambda:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: makefile
Properties:
Runtime: python3.12
Handler: lambda_handler.lambda_handler
CodeUri: ./
Policies:
- AWSLambdaBasicExecutionRole
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:Query
Resource:
- !GetAtt EventSparkEventUserAttendanceTable.Arn
- !Sub "${EventSparkEventUserAttendanceTable.Arn}/index/*"
- Effect: Allow
Action:
- dynamodb:BatchGetItem
Resource: !GetAtt EventSparkEventTable.Arn
Environment:
Variables:
EVENTSPARK_CLOUDFRONT_URL: !Sub "https://${EventSparkCloudFrontDistribution.DomainName}"
EVENTS_USER_REGISTRATION_TABLE: !Ref EventSparkEventUserAttendanceTable
EVENTS_DYNAMODB_TABLE: !Ref EventSparkEventTable
Events:
GetEventsThatUserRegisteredToEndpoint:
Type: Api
Properties:
RestApiId: !Ref EventSparkRestApi
Path: /get-events-that-user-registered-to
Method: get
def get_events_that_user_registered_to(event, dynamodb_resource):
query_params = event.get('queryStringParameters', {}) or {}
user_email = query_params.get('user_email', None)
event_uuid = query_params.get('event_uuid', None)
logger.info("Processing get_events_that_user_registered_to request", extra={
"user_email": user_email,
"event_uuid": event_uuid
})
# Standard response headers
headers = {
'Access-Control-Allow-Origin': environ.get('ALLOWED_ORIGIN', '*'),
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
'Content-Type': 'application/json'
}
if not user_email:
logger.warning("Missing user_email in query parameters")
return {
'statusCode': 400,
'headers': headers,
'body': json.dumps({'error': 'user_email is required'})
}
try:
# If checking for specific event attendance, query by user_email and filter by event_uuid
if event_uuid:
# Query the UserEmailIndex GSI to find registrations for this user
response = dynamodb_resource.attendance_table.query(
IndexName='UserEmailIndex',
KeyConditionExpression='user_email = :user_email',
FilterExpression='event_uuid = :event_uuid',
ExpressionAttributeValues={
':user_email': user_email,
':event_uuid': event_uuid
}
)
# Check if any registrations exist for this event
is_attending = len(response.get('Items', [])) > 0
logger.info("Checked event attendance", extra={
"user_email": user_email,
"event_uuid": event_uuid,
"is_attending": is_attending
})
return {
'statusCode': 200,
'headers': headers,
'body': json.dumps({'isAttending': is_attending})
}
# Getting all events user registered for - query by user_email only
response = dynamodb_resource.attendance_table.query(
IndexName='UserEmailIndex',
KeyConditionExpression='user_email = :user_email',
ExpressionAttributeValues={
':user_email': user_email
}
)
items = response.get('Items', [])
# Get the event UUIDs from the items
event_uuids = [item['event_uuid'] for item in items if 'event_uuid' in item]
if not event_uuids:
logger.info("No events found for user", extra={
"user_email": user_email
})
return {
'statusCode': 200,
'headers': headers,
'body': json.dumps({'events': []})
}
# Use BatchGetItem to retrieve multiple events by their UUIDs
request_items = {
dynamodb_resource.events_table_name: {
'Keys': [{'uuid': uuid} for uuid in event_uuids]
}
}
max_retries = 5
retries = 0
event_items = []
while request_items and retries < max_retries:
response = dynamodb_resource.resource.batch_get_item(RequestItems=request_items)
event_items.extend(response.get('Responses', {}).get(dynamodb_resource.events_table_name, []))
request_items = response.get('UnprocessedKeys', {})
retries += 1
if request_items:
logger.warning("UnprocessedKeys still present after retries", extra={
"unprocessed_keys": request_items
})
# If no events found, return an empty list
if not event_items:
logger.info("No events found for user", extra={
"user_email": user_email
})
return {
'statusCode': 200,
'headers': headers,
'body': json.dumps({'events': []})
}
# Add CloudFront URLs to the items
cloudfront_url = environ.get("EVENTSPARK_CLOUDFRONT_URL", "")
for item in event_items:
if 's3_key' in item:
item['file_url'] = f'{cloudfront_url}/{item["s3_key"]}'
logger.info("Successfully retrieved events registered by user", extra={
"user_email": user_email,
"event_count": len(event_items)
})
return {
'statusCode': 200,
'headers': headers,
'body': json.dumps({'events': event_items})
}
except Exception as e:
logger.error("Error retrieving events registered by user", extra={
"user_email": user_email,
"error": str(e)
})
return {
'statusCode': 500,
'headers': headers,
'body': json.dumps({'error': str(e)})
}
Register User To Event Lambda
Finally, this Lambda will add a new entry inside the EventSparkEventUserAttendanceTable with event_id and user’s email to mark them as attending and registered.
RegisterUserToEventLambda:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: makefile
Properties:
Runtime: python3.12
Handler: lambda_handler.lambda_handler
CodeUri: ./
Policies:
- AWSLambdaBasicExecutionRole
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:PutItem
Resource: !GetAtt EventSparkEventUserAttendanceTable.Arn
Environment:
Variables:
EVENTS_USER_REGISTRATION_TABLE: !Ref EventSparkEventUserAttendanceTable
Events:
RegisterUserToEvent:
Type: Api
Properties:
RestApiId: !Ref EventSparkRestApi
Path: /register-user-to-event
Method: post
def register_user_to_event(
event_uuid,
user_email,
dynamodb_class
):
try:
# Save to DynamoDB
dynamodb_class.table.put_item(
Item={
"uuid": str(uuid.uuid4()),
"event_uuid": event_uuid,
"user_email": user_email
}
)
logger.info("User registered successfully", extra={
"event_uuid": event_uuid,
"user_email": user_email
})
return {
"statusCode": 200,
"headers": {
'Access-Control-Allow-Origin': environ.get('ALLOWED_ORIGIN', '*'),
'Access-Control-Allow-Credentials': 'true',
'Content-Type': 'application/json'
},
"body": json.dumps({
"message": "User registered successfully"
})
}
except Exception as e:
logger.error("Error saving to DynamoDB", extra={
"event_uuid": event_uuid,
"user_email": user_email,
"error": str(e)
})
return {"statusCode": 500, "body": json.dumps({"error": "Failed to register user to event"})}
Very important to mention is that all Lambdas have the best IAM practices — and that is the least privilege practice — they cannot do any actions on any resources which are not defined inside the CloudFormation template.
You may notice that CORS has been put as ‘*’ — that is made on purpose since this project is only used for learning purposes and this is not a production environment.
How does the application look and what are the flows?
Let’s go over the application and see how it looks after connecting it with our AWS backend!
After clicking “Explore Events” button present on the home page, you will get all available events on the platform — the following page communicates with our GetEvents Lambda:
Logging in and signing up is easy when using AWS Cognito! The User pool configured for this project requires the user’s name, email and a password, here is the form which communicates with our Login and Register Lambdas, depending if the user is signing up or signing in:
After signing up, you will be redirected to the dashboard which is not available to the users which are not logged in. The dashboard page communicates with GetEventsThatUserCreated and GetEventsThatUserRegisteredTo Lambdas and those are shown inside the “My Events” and “Registered Events” tabs respectively. Here is how the dashboard looks:
Finally, if the user wants to create an event, this is how the flow would look like — after filling out the form, the information about the event is sent to our CreateEvent Lambda:
Finally, if a user wants to attend and register to an event, here is the flow the user needs to do — of course, in this flow, we communicate with our last Lambda RegisterUserToEvent Lambda.
And that would be it for this blog post where we’ve gone over a fully serverless application architecture, used best practices and created a fully working full-stack application! The lessons you can take from this project are:
- how to implement AWS Cognito for easy user management and implementation of the Cognito Authorizer inside other stack’s API Gateway
- how to implement Global Secondary Indexes (GSI) to make your DynamoDB data fetching process faster
- implementation of S3 + CloudFront for faster fetching of static data
- Least Privilege access to AWS resources
Thank you so much for reading! See you in the next post!
Top comments (0)