{"id":25412982,"url":"https://github.com/mfkimbell/aws-saas-webapp-template","last_synced_at":"2025-04-19T23:16:21.597Z","repository":{"id":276234755,"uuid":"923117889","full_name":"mfkimbell/aws-saas-webapp-template","owner":"mfkimbell","description":"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","archived":false,"fork":false,"pushed_at":"2025-03-05T19:37:24.000Z","size":616,"stargazers_count":27,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-19T23:16:15.965Z","etag":null,"topics":["decorator-pattern","docker","ecs","fastapi","hashicorp-cloud","nexjs","postgresql","react","redux","repository-pattern","sqlalchemy","terraform"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mfkimbell.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-01-27T17:13:09.000Z","updated_at":"2025-04-08T16:00:44.000Z","dependencies_parsed_at":"2025-03-05T20:38:50.120Z","dependency_job_id":"ffefe084-678e-4afe-be0e-382353fd54ba","html_url":"https://github.com/mfkimbell/aws-saas-webapp-template","commit_stats":null,"previous_names":["mfkimbell/aws-saas-webapp-template"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mfkimbell%2Faws-saas-webapp-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mfkimbell%2Faws-saas-webapp-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mfkimbell%2Faws-saas-webapp-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mfkimbell%2Faws-saas-webapp-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mfkimbell","download_url":"https://codeload.github.com/mfkimbell/aws-saas-webapp-template/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":249826726,"owners_count":21330676,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["decorator-pattern","docker","ecs","fastapi","hashicorp-cloud","nexjs","postgresql","react","redux","repository-pattern","sqlalchemy","terraform"],"created_at":"2025-02-16T13:50:17.699Z","updated_at":"2025-04-19T23:16:21.573Z","avatar_url":"https://github.com/mfkimbell.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# AWS SaaS DevOps Webapp Template with Auth v1\n\n\n  \u003cimg src=\"https://github.com/user-attachments/assets/61a2145d-1623-4e11-b0cd-2a1ace6b566f\" alt=\"logo\" width=\"200\"\u003e\n\n## Overview \nThis is a template for a SaaS application with authentication.\n\nThe frontend is built with Next.js and Tailwind CSS.\n\nThe backend is built with FastAPI and SQLAlchemy.\n\nThis 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.\n\nThis 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.\n\nThis 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.\n\n## Architechture\n\u003cimg width=\"1158\" alt=\"saas-arch\" src=\"https://github.com/user-attachments/assets/364298da-b926-4416-a27b-d6145f2cc5c3\" /\u003e\n\n## Demo\n\n![Untitled-ezgif com-speed copy 5](https://github.com/user-attachments/assets/36e796f5-a3c4-4367-844d-e34f3e43e195)\n\n\n\n\n## Local Development Steps\n1. Change `Dockerfile.frontend` to end with\n```\nCMD [\"npm\", \"run\", \"dev\"]\n```\n To enable hot reloading\n\n2. Setup enviornment variables\n   \n`./env`\n\n```\nJWT_SECRET=\u003cMATCHING PASSWORD\u003e\nAPP_MODE=dev\nDB_URL= # this can be left empty and it will default to SQLite\n```\n\n`./frontend/env`\n\n```\nAPI_URL=http://localhost:8000\nJWT_SECRET=\u003cMATCHING PASSWORD\u003e\nNEXTAUTH_SECRET=\u003cMATCHING PASSWORD\u003e\n```\n3. Run `make build up` to build and start the containers\n4. Visit `http://localhost:3000` to view the frontend\n\n## Production Deployment steps\n\n1. You need to go to app.terraform.io and setup an **Organization** and a **Workspace**\n\nGo to the **Variables Tab** set the following **Workspace Variables**:\n\n| Key                   | Value                        | Category | Actions             |\n|-----------------------|------------------------------|----------|---------------------|\n| AWS_ACCESS_KEY_ID     | \u003cyour access key\u003e            | env      |                     |\n| AWS_REGION            | us-east-1                    | env      |                     |\n| AWS_SECRET_ACCESS_KEY | \u003cyour secret\u003e                | env      | Sensitive - write only |\n\n2. You need to set the following Github Secrets:\n\n\n| Secret                  | Description                                                                                       |\n|-------------------------|---------------------------------------------------------------------------------------------------|\n| `AWS_ACCESS_KEY_ID`     | You need to create this in AWS. Create an IAM user or use a Root Access Key.                      |\n| `AWS_SECRET_ACCESS_KEY` | You need to create this in AWS. Create an IAM user or use a Root Access Key.                      |\n| `DOCKERHUB_USERNAME`    | Your DockerHub username. (e.g., mfkimbell).                                                       |\n| `DOCKERHUB_TOKEN`       | Go to DockerHub -\u003e Account Settings -\u003e Personal Access Tokens -\u003e Generate New Token                 |\n| `DOCKERHUB_REPO`        | The repository name (e.g., aws-saas-template).                                                   |\n| `TF_API_TOKEN`          | Go to Terraform -\u003e Account Settings -\u003e Tokens -\u003e Create an API Token                                |\n\n3. To alter the landing page `frontend/src/components/pages/landing-page.tsx` and go to `localhost:3000`.\n\nTo alter the backend go to `src/app.py` and it can be accessed at `localhost:8000`.\n\n4. Commit your code to `/main` to trigger the auto-deployment to AWS\n\nCheck your Github Actions to get the Frontend and Backend ALB URLs:\n\n\u003cimg width=\"459\" alt=\"Apply completet Resources 4 asded, 2 changed, 4 destroyed\" src=\"https://github.com/user-attachments/assets/e364e21e-a8a9-4a63-83aa-d61ed9fe3c89\" /\u003e\n\n5. To delete resources run the following:\n\n`cd terraform`\n\n`terraform login`\n\n`terraform init`\n\n`terraform destroy`\n\n\u003cimg width=\"464\" alt=\"Screenshot 2025-02-06 at 8 28 55 PM\" src=\"https://github.com/user-attachments/assets/31cc7bec-dce5-41b2-8580-291ace070e0a\" /\u003e\n\n\n## Authentication\n\nThis 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.\n\nWe 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. \n\nInstead of directly writing db.query(User).filter(User.id == 1), we call:\n\n```python\nuser = UserManager.get_user_from_access_token(token)\n```\nThis way, we decouple the application from the database. \n\n## Credit System\n\nEach user starts with 0 credits and can be assigned credits in order to use backend routes created by the saas provider. \n\nThe `Decorator Pattern` is a structural pattern that allows you to dynamically modify functions or objects without changing their original code.\nIn Python, decorators are implemented using higher-order functions (functions that take other functions as arguments).\nThey \"wrap\" another function to add extra behavior before or after it runs.\n\nA paid route would be defined with an `@requires_credit` decorator:\n```Python\n@router.get(\"/generate-text\")\n@requires_credit(decrement=True)  # This decorator is applied\nasync def generate_text(user=Depends(UserManager.get_user_from_header)):\n    return {\"generated_text\": \"AI-generated text!\"}\n```\nHere's what the function looks like:\n```Python\ndef requires_credit(decrement: bool = True) -\u003e Callable[[F], F]:\n    def decorator(func: F) -\u003e F:\n        @wraps(func)\n        async def wrapper(*args: Any, **kwargs: Any) -\u003e Any:\n            ...\n            if user.credits == 0:\n                raise HTTPException(status_code=403, detail=\"User has no credits\")\n            result = await func(*args, **kwargs) # actually calls function\n            if decrement:\n                UserManager.decrement_user_credits(user.id)\n                result[\"credits\"] = user.credits - 1\n            else:\n                result[\"credits\"] = user.credits\n            return JSONResponse(content=result)\n        return wrapper  # pyright: ignore[reportReturnType]\n    return decorator\n```\n\n## Request proxying\nThere is a custom fetch function in the React Frontend in lib/utils that adds \"/api\" in front of all calls and then adds the \"\u003cAPI_URL\u003e\" in front of that. So \"/login\" will go to NextJS's api as \"/api/login\" and then to the backend as \"\u003cAPI_URL\u003e/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. \n```typescript\nexport const nextApi = axios.create({\n  baseURL: \"/api\",\n});\n```\n```typescript\nexport async function fetch\u003cT\u003e(url: string, options?: AxiosRequestConfig)\n{ ... \nconst response: AxiosResponse\u003cT\u003e = await nextApi.get(url, options);\n}\n```\n```typescript\nconst api = axios.create({\n  baseURL: process.env.API_URL,\n});\n```\n```typescript\nresponse = await api.request({\n        method: method,\n        url: forwardPath,\n        headers: headers,\n        data: forwardedBody,\n      });\n```\n\n## Unit Testing\n\nUnit tests are run automatically during the `test` job in the `Test Python` github action.\n\nThis 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. \n\nFastAPI 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.\n\nIt tests the register, login, and api-key logic. \n\n\n## Roadblocks I Faced\n\n* Had to set `NEXTAUTH_URL` to the frontend loadbalancer endpoint to enable proper signout functionality in the cloud\n* Needed a Cloud-Based `.tfstate` file for Terraform so we could undo changes deployed by the github action\n* To enable health checks for my containers, I had to manually add `curl` to both containers so they could be asked to ping themselves\n* Auth logic was difficult to work through because of the server side NextAPI being used as a proxy\n* Needed to add a custom session refresh hook `useRefreshSession` to enable refreshing credits without logging in and out\n* Had to make the `DB_URL` secret change on every release, this is because AWS Secret deletes take up to a week\n* 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\n* Load balancers are required in conjuction with Target Groups because Load balancers can have public endpoints but target groups do not\n* 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\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmfkimbell%2Faws-saas-webapp-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmfkimbell%2Faws-saas-webapp-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmfkimbell%2Faws-saas-webapp-template/lists"}