
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.

- Change
Dockerfile.frontend
to end with
CMD ["npm", "run", "dev"]
To enable hot reloading
- Setup enviornment variables
./env
JWT_SECRET=<MATCHING PASSWORD>
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=<MATCHING PASSWORD>
NEXTAUTH_SECRET=<MATCHING PASSWORD>
- Run
make build up
to build and start the containers - Visit
http://localhost:3000
to view the frontend
- 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 |
- 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 |
- To alter the landing page
frontend/src/components/pages/landing-page.tsx
and go tolocalhost:3000
.
To alter the backend go to src/app.py
and it can be accessed at localhost:8000
.
- Commit your code to
/main
to trigger the auto-deployment to AWS
Check your Github Actions to get the Frontend and Backend ALB URLs:

- To delete resources run the following:
cd terraform
terraform login
terraform init
terraform destroy

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:
user = UserManager.get_user_from_access_token(token)
This way, we decouple the application from the database.
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:
@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:
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
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 "<API_URL>" in front of that. So "/login" will go to NextJS's api as "/api/login" and then to the backend as "<API_URL>/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.
export const nextApi = axios.create({
baseURL: "/api",
});
export async function fetch<T>(url: string, options?: AxiosRequestConfig)
{ ...
const response: AxiosResponse<T> = await nextApi.get(url, options);
}
const api = axios.create({
baseURL: process.env.API_URL,
});
response = await api.request({
method: method,
url: forwardPath,
headers: headers,
data: forwardedBody,
});
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.
- 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