Production-Grade, Event-Driven Architecture for Automated Customer Subscriptions, Payments, and Lifecycle Management.
The SCAI Billing & Subscription System is a production-ready backend designed to automate the entire customer billing lifecycle for the SEO Content AI (SCAI) platform. It handles everything from initial subscriptions and one-time payments to plan upgrades, payment method management, and cancellations. Built on a modern serverless architecture using AWS and Stripe, this system provides a secure, scalable, and cost-effective foundation for SCAI's revenue operations, enabling a seamless self-service billing portal for end-users and minimizing manual intervention.
This project delivered a mission-critical system that directly translates technical excellence into business value.
Eliminated all manual billing tasks, from subscription creation to cancellations, freeing up team resources to focus on core product development.
The serverless architecture on AWS ensures the system can handle growth from ten to ten million users seamlessly, with costs directly tied to usage.
By leveraging Stripe for all payment processing and webhooks for state management, the system ensures PCI compliance and isolates sensitive data from the core application.
The system is built on an event-driven, serverless model, ensuring exceptional scalability, resilience, and cost-efficiency. All infrastructure is defined and deployed using AWS SAM (Serverless Application Model), enabling consistent and repeatable environments.
Core compute engine executing all business logic in a scalable, event-driven manner.
Provides a secure, public-facing HTTP API endpoint for all frontend interactions.
High-performance, scalable NoSQL database for storing user and subscription data.
Handles all payment processing, PCI compliance, and recurring subscription scheduling.
Defines the entire cloud infrastructure as code for automated and reliable deployments.
The event-driven nature is best illustrated by the new subscription process, which decouples the user action from the backend processing via Stripe Webhooks.
[User] -> [Frontend] -> [1. API Gateway] -> [2. Lambda] -> [3. Stripe Checkout]
^ |
| v
+---- [7. UI Updated] <--- [6. Lambda] <--- [5. API Gateway] <--- [4. Stripe Webhook]
The system interacts with two DynamoDB tables, ensuring a clear separation of concerns between user identity and billing data.
This table is the source of truth for user identity. The billing & subscription system interacts with it cautiously:
This table is the source of truth for all billing-related information and is managed exclusively by this system.
The project is encapsulated in a single repository, managed and deployed with AWS SAM. The core logic resides in `app.py`, and the cloud resources are defined in `template.yaml`.
# SCAI Billing Lambda - Stripe payment processing and webhooks
import json
import stripe
from datetime import datetime, timezone
import asyncio
import os
import traceback
import boto3
import requests
from pydantic import BaseModel, ValidationError
from typing import Optional, List, Dict
import logging
# Configure logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# --- CONFIGURATION (from environment variables) ---
STRIPE_SECRET_KEY = os.getenv("STRIPE_SECRET_KEY")
STRIPE_WEBHOOK_SECRET = os.getenv("STRIPE_WEBHOOK_SECRET")
# ... other price IDs and URLs from environment ...
# Initialize Stripe
stripe.api_key = STRIPE_SECRET_KEY
# --- SCHEMAS ---
class CurrentUserResponse(BaseModel): userId: str; email: Optional[str] = None; firstName: Optional[str] = None; lastName: Optional[str] = None; hasPaidForSelfHosted: Optional[bool] = False
class CurrentSubscriptionResponse(BaseModel): status: str; currentPlanId: str; currentPeriodEnd: datetime; cancelAtPeriodEnd: bool
# ... other Pydantic models for request/response validation ...
# --- DATABASE INITIALIZATION ---
USERS_TABLE_NAME = os.environ['USERS_TABLE_NAME']
SUBSCRIPTIONS_TABLE_NAME = os.environ['SUBSCRIPTIONS_TABLE_NAME']
dynamodb = boto3.resource('dynamodb')
UsersTable = dynamodb.Table(USERS_TABLE_NAME)
SubscriptionsTable = dynamodb.Table(SUBSCRIPTIONS_TABLE_NAME)
# --- DATABASE HELPERS ---
def get_user_by_id(user_id: str):
"""Get user by ID from DynamoDB using the IdIndex."""
# ... implementation ...
def upsert_subscription(user_id: str, stripe_sub: dict):
"""Upsert subscription data to DynamoDB, creating a flat, comprehensive record."""
# ... implementation ...
# ... other database helpers ...
# --- API HELPERS ---
def create_response(status_code, body):
"""Create a standardized API Gateway response with CORS headers."""
# ... implementation ...
# --- HANDLERS ---
async def get_dashboard_data_handler(event):
"""Asynchronously get all billing dashboard data for a user."""
# ... implementation ...
def create_checkout_session_handler(event):
"""Create a Stripe Checkout session for a new recurring subscription."""
# ... implementation ...
def create_one_time_payment_session_handler(event):
"""Create a Stripe Checkout session for a one-time purchase."""
# ... implementation ...
def webhook_handler(event):
"""Handle inbound Stripe webhooks, verifying signatures and processing events."""
payload = event.get('body')
sig_header = event.get('headers', {}).get('stripe-signature')
try:
stripe_event = stripe.Webhook.construct_event(
payload=payload, sig_header=sig_header, secret=STRIPE_WEBHOOK_SECRET
)
event_type = stripe_event["type"]
data_object = stripe_event["data"]["object"]
logger.info(f"Webhook received: {event_type}")
if event_type == "checkout.session.completed":
# Handle successful payment and subscription creation/update
pass
elif event_type.startswith("customer.subscription."):
# Handle subscription updates, cancellations
pass
# ... other event handling ...
except Exception as e:
logger.error(f"Webhook error: {e}")
return create_response(400, {"detail": str(e)})
return create_response(200, {"status": "success"})
# --- MAIN LAMBDA HANDLER ---
def lambda_handler(event, context):
"""Main Lambda handler with routing logic to map API requests to functions."""
try:
path = event.get('rawPath', '')
method = event.get('requestContext', {}).get('http', {}).get('method')
logger.info(f"Request: {method} {path}")
# Simple router to map method and path to the correct handler function
# ... routing logic ...
if handler:
return handler(event)
else:
return create_response(404, {"detail": f"Route not found: {method} {path}"})
except ValidationError as e:
return create_response(422, {"detail": json.loads(e.json())})
except Exception as e:
logger.error(f"Unhandled exception: {e}")
return create_response(500, {"detail": "Internal server error"})
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
SCAI Billing & Subscription System: A serverless backend for Stripe subscriptions, payments, and customer management, using AWS Lambda, API Gateway, and DynamoDB.
Resources:
# 1. API Gateway Definition
BillingHttpApi:
Type: AWS::Serverless::HttpApi
Properties:
CorsConfiguration:
AllowOrigins: ["*"] # For production, restrict this to your frontend domain
AllowHeaders: ["*"]
AllowMethods: [GET, POST, PUT, DELETE, OPTIONS]
# 2. DynamoDB Table for Subscriptions
SubscriptionsTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: user_scai_subscriptions
AttributeDefinitions:
- AttributeName: "_id"
AttributeType: "S"
KeySchema:
- AttributeName: "_id"
KeyType: "HASH"
BillingMode: PAY_PER_REQUEST
# 3. The Main Lambda Function
BillingFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: .
Handler: app.lambda_handler
Runtime: python3.10
Architectures: [x86_64]
Events:
ApiEvent:
Type: HttpApi
Properties:
Path: /{proxy+}
Method: ANY
ApiId: !Ref BillingHttpApi
Environment:
Variables:
USERS_TABLE_NAME: "users_scai"
SUBSCRIPTIONS_TABLE_NAME: !Ref SubscriptionsTable
DEPLOYMENT_API_URL: "https://[REDACTED].execute-api.us-east-1.amazonaws.com/dev"
# --- Environment variables are securely set in AWS, not hardcoded ---
STRIPE_WEBHOOK_SECRET: "[REDACTED]"
STRIPE_SECRET_KEY: "[REDACTED]"
STRIPE_PUBLIC_KEY: "[REDACTED]"
STRIPE_STANDARD_PLAN_YEARLY_PRICE_ID: "price_[REDACTED]"
# ... other price IDs ...
Policies:
# Policy for accessing the production users table and its indexes
- Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:UpdateItem
- dynamodb:Query
Resource:
- !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/users_scai"
- !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/users_scai/index/*"
# Policy to fully manage the new subscriptions table
- DynamoDBCrudPolicy:
TableName: !Ref SubscriptionsTable
Outputs:
BillingApiUrl:
Description: "API Gateway endpoint URL for the SCAI Billing API"
Value: !Sub "https://{BillingHttpApi}.execute-api.${AWS::Region}.amazonaws.com"
The system exposes a secure and well-documented API for all billing operations. All endpoints are routed through a single Lambda function.
Base URL: https://[REDACTED].execute-api.us-east-1.amazonaws.com
---
#### `GET /config`
- **Description:** Retrieves public configuration for the frontend.
- **Success Response (200 OK):**
{
"publicKey": "pk_test_[REDACTED]",
"standardPriceId": "price_[REDACTED]",
"agencyPriceId": "price_[REDACTED]",
...
}
---
#### `GET /dashboard/{userId}`
- **Description:** Fetches all data to render a user's billing dashboard.
- **Success Response (200 OK):**
{
"currentUser": { "userId": "...", "email": "...", ... },
"subscription": { "status": "active", ... } | null,
"paymentMethods": [ { "id": "pm_...", ... } ],
"billingHistory": [ { "date": "...", "amount": 99.0, ... } ],
"availablePlans": [ ... ]
}
- **Failure Response (404 Not Found):** { "detail": "User not found" }
---
#### `POST /create-checkout-session`
- **Description:** Creates a Stripe Checkout session for a new recurring subscription.
- **Request Body:** { "priceId": "...", "userId": "...", "metadata": {...} }
- **Success Response (200 OK):** { "sessionId": "cs_test_...", "url": "https://checkout.stripe.com/..." }
---
#### `POST /create-one-time-payment-session`
- **Description:** Creates a Stripe Checkout session for a one-time purchase.
- **Request Body:** { "priceId": "...", "userId": "...", "metadata": {...} }
- **Success Response (200 OK):** { "sessionId": "cs_test_...", "url": "https://checkout.stripe.com/..." }
---
#### `POST /update-subscription`
- **Description:** Upgrades or downgrades a user's existing subscription.
- **Request Body:** { "userId": "...", "newPriceId": "..." }
- **Success Response (200 OK):** { "status": "success", "message": "..." }
---
#### `POST /cancel-subscription`
- **Description:** Schedules a subscription to cancel at the end of the billing period.
- **Request Body:** { "userId": "..." }
- **Success Response (200 OK):** { "status": "success", "message": "..." }
---
#### Payment Method Endpoints
- `POST /create-setup-intent`: Initiates adding a new payment method.
- `POST /attach-payment-method`: Attaches a new payment method to the customer.
- `POST /update-default-payment-method`: Changes the default payment method.
- `POST /detach-payment-method`: Removes a stored payment method.
---
#### `POST /webhook`
- **Description:** Inbound-only endpoint for receiving real-time events from Stripe.
- **Security:** Validates incoming requests using the `STRIPE_WEBHOOK_SECRET`.
- **Success Response (200 OK):** { "status": "success" }
The system is deployed and managed using the AWS SAM CLI, ensuring automated and reliable updates. The terminal output below shows a successful deployment cycle.
> sam build
Building codeuri: . runtime: python3.10 ...
Build Succeeded
Built Artifacts : .aws-sam/build
Built Template : .aws-sam/build/template.yaml
> sam deploy
Deploying with following values
===============================
Stack name : scai-subscription-billing-api
Region : us-east-1
Confirm changeset : False
Deployment s3 bucket : aws-sam-cli-managed-default-[REDACTED]
Capabilities : ["CAPABILITY_IAM"]
Waiting for changeset to be created..
CloudFormation stack changeset
-------------------------------------------------------------------------------------------------
Operation LogicalResourceId ResourceType Replacement
-------------------------------------------------------------------------------------------------
* Modify BillingFunction AWS::Lambda::Function False
* Modify BillingHttpApi AWS::ApiGatewayV2::Api False
-------------------------------------------------------------------------------------------------
Waiting for stack create/update to complete...
CloudFormation events from stack operations
-------------------------------------------------------------------------------------------------
ResourceStatus ResourceType LogicalResourceId ResourceStatusReason
-------------------------------------------------------------------------------------------------
UPDATE_IN_PROGRESS AWS::CloudFormation::Stack scai-subscription-billing-api User Initiated
UPDATE_IN_PROGRESS AWS::Lambda::Function BillingFunction -
UPDATE_COMPLETE AWS::Lambda::Function BillingFunction -
UPDATE_COMPLETE AWS::CloudFormation::Stack scai-subscription-billing-api -
-------------------------------------------------------------------------------------------------
CloudFormation outputs from deployed stack
-------------------------------------------------------------------------------------------------
Outputs
-------------------------------------------------------------------------------------------------
Key BillingApiUrl
Description API Gateway endpoint URL for the SCAI Billing API
Value https://random_API_identifier.execute-api.us-east-1.amazonaws.com
-------------------------------------------------------------------------------------------------
Successfully created/updated stack - scai-subscription-billing-api in us-east-1
This project showcases my ability to deliver enterprise-grade, scalable, and maintainable cloud solutions, with skills directly applicable to organizations building modern software systems.
This project represents my passion for building secure, scalable, and efficient backend systems that solve real-world business problems. It showcases my ability to translate complex requirements into a robust cloud architecture. If you have questions or wish to collaborate on a project, please reach out via the Contact section.
Let's build something great together.
Best regards,
Damilare Lekan Adekeye