
Building Airhealth on AWS: S3, CloudFront, Elastic Beanstalk and RDS for a worldwide air quality app
How our team deployed a Spring Boot and React air quality app on AWS, from a private S3 bucket behind CloudFront to Elastic Beanstalk on Amazon Corretto 21 and Amazon RDS for PostgreSQL, plus the CloudFront routing mistakes that cost us the most time.
Air quality can change sharply from one day to the next, during crop-burning season, wildfire smoke or an industrial episode, but most people only find out after they have been outside in it. We built Airhealth for a hackathon to fix that: look up the air quality of any place on a map, see nearby satellite-detected heat spots, and get an in-app alert when the air gets worse.
This article walks through how we deployed it on AWS and what tripped us up along the way.
What Airhealth does
- Search any place worldwide, click anywhere on the map, or use your current location.
- See the live US AQI with a colour-coded health category, plus six pollutants: PM2.5, PM10, NO2, SO2, O3 and CO.
- See how many NASA FIRMS heat spots (often crop burning) are within 50 km, and how close the nearest one is.
- Subscribe to a place, name it, label it Home, School, Office, Playground or Other, and set your own AQI limit. Each place type gets its own advice, for example moving recess indoors at a school.
- Get in-app alerts through a notification bell. Alerts are checked while the app is open, with at most one per place every six hours.
Accounts need only a name, email and password. Air and fire data are never stored: they are fetched on request and cached in memory.
The stack
- Backend: Java 21, Spring Boot, Spring Security with JWT, Spring Data JPA
- Database: PostgreSQL
- Frontend: React with Leaflet for the map
- Data: Open-Meteo (air quality and geocoding), NASA FIRMS (active fires), OpenStreetMap Nominatim (area names)
- Tests: 137 backend tests with JUnit 5, Mockito and Spring Security Test
Everything runs in the Asia Pacific (Mumbai) region.
Frontend: private S3 behind CloudFront
The React build lives in a private Amazon S3 bucket. CloudFront reads it through origin access control, so the bucket is never public. CloudFront also gives us HTTPS on its default domain, which matters more than it sounds: the browser's "Use my current location" feature only works on a secure connection.
We also configured CloudFront to return
/index.html for 403 and 404 responses. Without that, refreshing a client-side route such as /app fails, because S3 has no file by that name.Backend: Spring Boot on Elastic Beanstalk
We uploaded the Spring Boot jar to AWS Elastic Beanstalk on the Amazon Corretto 21 platform. Beanstalk provisioned the EC2 instance, stored the application version in S3, and gave us health status and logs in the console.
Beanstalk's environment properties suited our app well. The database URL, username and password, JWT secret, NASA FIRMS key and port are all environment variables, so the same build deployed with no code changes and no secrets in the repository.
Database: Amazon RDS for PostgreSQL
The database is Amazon RDS for PostgreSQL. Its security group accepts connections on port 5432 only from the application's security group, and the JDBC URL uses
sslmode=require . When the login endpoint returned a normal 401 for an unknown user instead of a server error, we knew the connection worked.One domain, no CORS
CloudFront routes API paths to Elastic Beanstalk, so the browser sees a single origin for both the frontend and the API. That removed the need for any CORS configuration.
What cost us the most time
CloudFront was the hardest part of the setup. Four things stood out.
- The default Beanstalk URL is HTTP only. That is why we put CloudFront in front of it, which meant an S3 origin, a second origin for the backend, separate behaviors for API paths, and error pages.
- The Beanstalk environment did not appear in the origin dropdown. We typed its URL by hand.
- The origin protocol defaulted to HTTPS only. That fails against a plain HTTP backend until you change it to HTTP only.
-
/user/*does not match/user. Our signup is aPOST /user, so it fell through to the S3 behavior and returned a 403 that gave no hint about the path pattern. The fix was a separate behavior for the bare/userpath.
If you are putting a static frontend and an API behind one CloudFront distribution, check every path your API uses, including the bare ones, against your behaviors.
Design decisions we are happy with
- Frontend on S3 + CloudFront, backend only on Beanstalk: HTTPS on the default domain, one origin, no CORS.
- Our own JWT login instead of Amazon Cognito: it keeps the project independent of a single AWS service.
- Nothing air-related stored: the data covers the whole world and is cheap to refetch, so we cache it in memory and keep the database for accounts and subscriptions.
- Alerts checked on request: no scheduler, which was the simplest working version for the deadline.
What we would improve next
- Scheduled alert checks, for example with Amazon EventBridge, so alerts do not depend on the app being open.
- Cleaner-route suggestions between places.
Try it
- Live demo: https://d2m0b3oibu3wv6.cloudfront.net
- Source code: https://github.com/Atharv3221/airhealth
Air quality values are modeled estimates from the Copernicus Atmosphere Monitoring Service via Open-Meteo, not sensor readings, and they are not medical advice.
The AWS architecture
Path | Goes to |
|---|---|
Default (the React app) | Amazon S3, a private bucket, through CloudFront origin access control |
/api/* , /user , /user/* | AWS Elastic Beanstalk (Spring Boot on Amazon Corretto 21) |
Database | Amazon RDS for PostgreSQL, reached from Beanstalk over JDBC with sslmode=require |
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article