An open API service indexing awesome lists of open source software.

https://github.com/mfkimbell/aws-saas-webapp-template

AWS SaaS DevOps Webapp Template: Fully automated DevOps template for deploying a SaaS web application on AWS using Terraform, GitHub Actions, and ECS. It includes a Next.js frontend and a FastAPI backend with PostgreSQL (RDS), featuring a JWT-based authen
https://github.com/mfkimbell/aws-saas-webapp-template

decorator-pattern docker ecs fastapi hashicorp-cloud nexjs postgresql react redux repository-pattern sqlalchemy terraform

Last synced: about 1 year ago
JSON representation

AWS SaaS DevOps Webapp Template: Fully automated DevOps template for deploying a SaaS web application on AWS using Terraform, GitHub Actions, and ECS. It includes a Next.js frontend and a FastAPI backend with PostgreSQL (RDS), featuring a JWT-based authen

Awesome Lists containing this project

README

          

# AWS SaaS DevOps Webapp Template with Auth v1

logo

## Overview
This is a template for a SaaS application with authentication.

The frontend is built with Next.js and Tailwind CSS.

The backend is built with FastAPI and SQLAlchemy.

This template allows for a user to sign up, sign in, and sign out. It comes with a prebuilt user store made in redux, which persists across refreshes. All of the session logic is handled in the backend.

This template focuses on a credit system where users can purchase credits to use the application, with the ability to make an API key to use the application without the frontend. Though this can be easily modified to use a subscription model, by forcing the `requires_credit` function to not decrement the user's credit balance, to act as a subscription.

This template allows the user to use GitHub Actions to trigger Terraform to deploy their containers to AWS ECS, ensuring fault tolerance, high availability, and seamless scalability for authentication in the cloud.

## Architechture
saas-arch

## Demo

![Untitled-ezgif com-speed copy 5](https://github.com/user-attachments/assets/36e796f5-a3c4-4367-844d-e34f3e43e195)

## Local Development Steps
1. Change `Dockerfile.frontend` to end with
```
CMD ["npm", "run", "dev"]
```
To enable hot reloading

2. Setup enviornment variables

`./env`

```
JWT_SECRET=
APP_MODE=dev
DB_URL= # this can be left empty and it will default to SQLite
```

`./frontend/env`

```
API_URL=http://localhost:8000
JWT_SECRET=
NEXTAUTH_SECRET=
```
3. Run `make build up` to build and start the containers
4. Visit `http://localhost:3000` to view the frontend

## Production Deployment steps

1. You need to go to app.terraform.io and setup an **Organization** and a **Workspace**

Go to the **Variables Tab** set the following **Workspace Variables**:

| Key | Value | Category | Actions |
|-----------------------|------------------------------|----------|---------------------|
| AWS_ACCESS_KEY_ID | | env | |
| AWS_REGION | us-east-1 | env | |
| AWS_SECRET_ACCESS_KEY | | env | Sensitive - write only |

2. You need to set the following Github Secrets:

| Secret | Description |
|-------------------------|---------------------------------------------------------------------------------------------------|
| `AWS_ACCESS_KEY_ID` | You need to create this in AWS. Create an IAM user or use a Root Access Key. |
| `AWS_SECRET_ACCESS_KEY` | You need to create this in AWS. Create an IAM user or use a Root Access Key. |
| `DOCKERHUB_USERNAME` | Your DockerHub username. (e.g., mfkimbell). |
| `DOCKERHUB_TOKEN` | Go to DockerHub -> Account Settings -> Personal Access Tokens -> Generate New Token |
| `DOCKERHUB_REPO` | The repository name (e.g., aws-saas-template). |
| `TF_API_TOKEN` | Go to Terraform -> Account Settings -> Tokens -> Create an API Token |

3. To alter the landing page `frontend/src/components/pages/landing-page.tsx` and go to `localhost:3000`.

To alter the backend go to `src/app.py` and it can be accessed at `localhost:8000`.

4. Commit your code to `/main` to trigger the auto-deployment to AWS

Check your Github Actions to get the Frontend and Backend ALB URLs:

Apply completet Resources 4 asded, 2 changed, 4 destroyed

5. To delete resources run the following:

`cd terraform`

`terraform login`

`terraform init`

`terraform destroy`

Screenshot 2025-02-06 at 8 28 55 PM

## Authentication

This application proxies api calls from NextJS's local api to the FastAPI, but only if they're authenticated (unless we are calling login, which requires no prior authorization). We use the same JWT secret in both the frontend and backend .env files to encode and decode our JWT tokens. Next.js knows the hashing algorithm because the backend includes it in the JWT header. If an attacker modifies the token payload (e.g., changing "id": 1 to "id": 999), the signature will no longer match.

We use the `Repository Pattern` in order to grab user data on the backend and we use a NextJS Session and `Redux` in order to grab and persist user data on the frontend.

Instead of directly writing db.query(User).filter(User.id == 1), we call:

```python
user = UserManager.get_user_from_access_token(token)
```
This way, we decouple the application from the database.

## Credit System

Each user starts with 0 credits and can be assigned credits in order to use backend routes created by the saas provider.

The `Decorator Pattern` is a structural pattern that allows you to dynamically modify functions or objects without changing their original code.
In Python, decorators are implemented using higher-order functions (functions that take other functions as arguments).
They "wrap" another function to add extra behavior before or after it runs.

A paid route would be defined with an `@requires_credit` decorator:
```Python
@router.get("/generate-text")
@requires_credit(decrement=True) # This decorator is applied
async def generate_text(user=Depends(UserManager.get_user_from_header)):
return {"generated_text": "AI-generated text!"}
```
Here's what the function looks like:
```Python
def requires_credit(decrement: bool = True) -> Callable[[F], F]:
def decorator(func: F) -> F:
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> Any:
...
if user.credits == 0:
raise HTTPException(status_code=403, detail="User has no credits")
result = await func(*args, **kwargs) # actually calls function
if decrement:
UserManager.decrement_user_credits(user.id)
result["credits"] = user.credits - 1
else:
result["credits"] = user.credits
return JSONResponse(content=result)
return wrapper # pyright: ignore[reportReturnType]
return decorator
```

## Request proxying
There is a custom fetch function in the React Frontend in lib/utils that adds "/api" in front of all calls and then adds the "" in front of that. So "/login" will go to NextJS's api as "/api/login" and then to the backend as "/login". To be clear, this is NOT used in the authentication logic, it is for developers to call their backend without manually calling the frontend api with a token.
```typescript
export const nextApi = axios.create({
baseURL: "/api",
});
```
```typescript
export async function fetch(url: string, options?: AxiosRequestConfig)
{ ...
const response: AxiosResponse = await nextApi.get(url, options);
}
```
```typescript
const api = axios.create({
baseURL: process.env.API_URL,
});
```
```typescript
response = await api.request({
method: method,
url: forwardPath,
headers: headers,
data: forwardedBody,
});
```

## Unit Testing

Unit tests are run automatically during the `test` job in the `Test Python` github action.

This template using `pytest` to validate various backend routes. It uses an override for the "get_db()" function to perform its tests on a test database, while still testing production code.

FastAPI has a built in "TestClient" is a wrapper around requests that allows sending HTTP requests directly to FastAPI applications in memory, without actually running a server.

It tests the register, login, and api-key logic.

## Roadblocks I Faced

* Had to set `NEXTAUTH_URL` to the frontend loadbalancer endpoint to enable proper signout functionality in the cloud
* Needed a Cloud-Based `.tfstate` file for Terraform so we could undo changes deployed by the github action
* To enable health checks for my containers, I had to manually add `curl` to both containers so they could be asked to ping themselves
* Auth logic was difficult to work through because of the server side NextAPI being used as a proxy
* Needed to add a custom session refresh hook `useRefreshSession` to enable refreshing credits without logging in and out
* Had to make the `DB_URL` secret change on every release, this is because AWS Secret deletes take up to a week
* Github Actions makes it difficult to pass secrets between jobs, so you have to dynamically build the `DOCKER_URL` in the job, not pass it in as an output from another job
* Load balancers are required in conjuction with Target Groups because Load balancers can have public endpoints but target groups do not
* If I update the Readme in main it WILL put all my resources in the cloud, so I need to be careful about then when developing