A modern monorepo template for building AI-powered SaaS applications with Next.js, TypeScript, and AWS CDK.
saas-template/
├── apps/
│ ├── web/ # Next.js 14 application (App Router)
│ └── api/ # Node.js API server with Hono
├── packages/
│ └── shared/ # Shared types and constants
├── infra/ # AWS CDK v2 infrastructure definitions
├── package.json # Root package.json with workspace configuration
├── tsconfig.base.json # Base TypeScript configuration
├── .editorconfig # Editor configuration
├── .gitignore # Git ignore rules
└── .nvmrc # Node.js version specification (22)
- apps/web: Next.js 14 application with App Router, TypeScript, and Tailwind CSS
- apps/api: Node.js API server with Hono, JWT authentication, and in-memory data store
- packages/shared: Shared TypeScript types, constants, and utilities
- infra: AWS CDK v2 infrastructure as code definitions
- Node.js 22 (specified in
.nvmrc) - pnpm package manager
- Clerk account (for authentication)
# Install dependencies for all workspaces
pnpm install-
Create a Clerk account:
- Go to https://dashboard.clerk.com
- Sign up or sign in to your account
-
Create a new application:
- Click "Add application"
- Choose "Next.js" as the framework
- Copy the API keys
-
Configure environment variables:
For the web app (
apps/web/.env.local):# Clerk configuration NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_your_actual_key_here CLERK_SECRET_KEY=sk_test_your_actual_secret_here # API configuration API_BASE_URL=http://localhost:4000
For the API server (
apps/api/.env.local):# Clerk JWKS configuration CLERK_JWKS_URL=https://your-clerk-domain.clerk.accounts.dev/.well-known/jwks.json CLERK_JWT_ISSUER=https://your-clerk-domain.clerk.accounts.dev -
Test the authentication:
- Run
pnpm run dev(starts both web app on :3000 and API server on :4000) - Visit
http://localhost:3000 - Click "Sign Up" to create an account
- Click "Protected App" to test authentication
- Test the Notes CRUD functionality (requires authentication)
- Test the Billing functionality (requires authentication)
- Run
# Start both web app and API server concurrently
pnpm run dev
# Build all workspaces
pnpm run build
# Run linting across all workspaces
pnpm run lint
# Run type checking across all workspaces
pnpm run typecheck
# Run tests across all workspaces
pnpm run test
# Run tests in watch mode
pnpm run test:watch
# Run tests with coverage
pnpm run test:coverage# Web application
cd apps/web
pnpm dev # Start development server
pnpm build # Build for production
pnpm start # Start production server
pnpm lint # Run ESLint
pnpm typecheck # Run TypeScript type checking
pnpm test # Run web app tests
pnpm test:watch # Run tests in watch mode
pnpm test:coverage # Run tests with coverage
# API server
cd apps/api
pnpm dev # Start API server on port 4000
pnpm build # Build TypeScript to JavaScript
pnpm typecheck # Run TypeScript type checking
pnpm test # Run API tests
pnpm test:watch # Run tests in watch mode
pnpm test:coverage # Run tests with coverage
# Shared package
cd packages/shared
pnpm build # Build shared types and utilities
pnpm typecheck # Run TypeScript type checking
# Infrastructure
cd infra
pnpm build # Build CDK definitions
pnpm typecheck # Run TypeScript type checking
pnpm synth # Synthesize CDK templates
pnpm cdk:bootstrap # Bootstrap CDK (first time only)
pnpm cdk:deploy # Deploy infrastructure
pnpm cdk:destroy # Destroy infrastructure- Frontend: Next.js 14, React 19, TypeScript, Tailwind CSS
- Authentication: Clerk (Next.js integration)
- Backend: Node.js 22, TypeScript, Hono HTTP server
- Database: DynamoDB (production) / In-memory (development)
- CDN: CloudFront distribution for global content delivery
- Billing: Stripe (subscriptions, checkout, customer portal)
- Infrastructure: AWS CDK v2, TypeScript
- Package Management: pnpm workspaces
- Code Quality: ESLint, Prettier, TypeScript strict mode
- Make changes to the appropriate workspace
- Run
pnpm run typecheckto ensure type safety - Run
pnpm run lintto check code quality - Run
pnpm run buildto build all workspaces - Test the web application with
pnpm run dev
The API server (apps/api) includes:
- Hono HTTP server running on port 4000
- JWT authentication using Clerk JWKS verification
- Protected endpoints for Notes and Billing operations
- In-memory data store for development (notes persist during server session)
- CORS enabled for web app integration
- Health check endpoint (
/health)
Health & Auth:
GET /health- Health check (public)
Notes Management (protected):
GET /notes- Get all notesPOST /notes- Create noteGET /notes/:id- Get specific notePATCH /notes/:id- Update noteDELETE /notes/:id- Delete note
Billing Management (protected):
POST /billing/checkout- Create Stripe checkout sessionPOST /billing/portal- Create Stripe customer portal session
The web application includes:
- Sign In/Sign Up pages (
/sign-in,/sign-up) using Clerk components - Protected route (
/app) that redirects unauthenticated users to sign-in - User session management with automatic redirects
- JWT token integration with API server
- Responsive design with Tailwind CSS
The infrastructure is defined using AWS CDK v2 and includes two stacks:
- DynamoDB Table: On-demand billing with PK/SK and three GSIs for org-scoped data access
- S3 Bucket: Private bucket for file attachments with encryption and block public access
- API Gateway: HTTP API with CORS for web domain
- Lambda Functions: Node.js functions for notes, billing, and attachments
- Permissions: Least-privilege access to DynamoDB and S3
- AWS CLI configured with appropriate credentials
- AWS CDK v2 installed globally:
npm install -g aws-cdk
Important: Deploy stacks in this specific order:
- Deploy CoreStack first:
pnpm -C infra cdk:deploy SaasTemplateCore- Deploy ApiStack:
pnpm -C infra cdk:deploy SaasTemplateApi- Update web app environment:
- Get the API URL from the ApiStack outputs
- Update
NEXT_PUBLIC_API_BASE_URLinapps/web/.env.local - Rebuild the web app:
pnpm -C apps/web build
# Bootstrap CDK (only needed once per AWS account/region)
pnpm -C infra cdk:bootstrap
# Deploy CoreStack
pnpm -C infra cdk:deploy SaasTemplateCore
# Deploy ApiStack
pnpm -C infra cdk:deploy SaasTemplateApi-
DynamoDB Table:
saas-template-core- Primary Key: PK (string), SK (string)
- GSI1: Subject → Notes (
ORG#<orgId>#SUBJECT#<subjectId>→NOTE#<noteId>#<createdAtISO>) - GSI2: User → Notes (
ORG#<orgId>#USER#<userId>→NOTE#<noteId>#<createdAtISO>) - GSI3: Role/Indexing (for future admin features)
- Billing: On-demand (pay-per-request)
- Point-in-time recovery: Disabled (free tier optimization)
- Server-side encryption enabled
- Removal policy: DESTROY (for template)
-
S3 Bucket:
saas-template-attachments-<account>-<region>- Private access only
- Server-side encryption
- Versioning: Disabled (free tier optimization)
- Removal policy: DESTROY (for template)
-
Lambda Functions:
- Runtime: Node.js 22.x
- CloudWatch log retention: 7 days (free tier optimization)
- Pay-per-request pricing
- Auto-scaling based on demand
-
API Gateway:
- HTTP API (v2) for better performance and cost
- CORS enabled for web app integration
- Pay-per-request pricing
- Auto-scaling based on demand
The infrastructure is optimized for AWS free tier usage:
- DynamoDB: On-demand billing with PITR disabled
- S3: Versioning disabled to minimize storage costs
- Lambda: 7-day log retention to stay within free tier limits
- API Gateway: HTTP API v2 for lower costs than REST API
- CloudWatch: Minimal log retention to avoid charges
The template includes full Stripe billing integration with subscription management:
- Subscription Checkout: Create Stripe checkout sessions for plan upgrades
- Customer Portal: Manage subscriptions, payment methods, and billing history
- Plan Management: Free, Pro, and Business tiers with environment-configurable pricing
- Development Mode: Works without Stripe configuration using mock responses
-
Create a Stripe account:
- Go to https://dashboard.stripe.com
- Sign up or sign in to your account
- Complete the account setup process
-
Get your API keys:
- In the Stripe Dashboard, go to Developers → API keys
- Copy your Publishable key (starts with
pk_test_) - Copy your Secret key (starts with
sk_test_) - Important: Keep your secret key secure and never commit it to version control
-
Navigate to Products:
- In the Stripe Dashboard, go to Products → Products
- Click "Add product"
-
Create Free Plan (Optional - for consistency):
- Name: "Free Plan"
- Description: "Perfect for getting started"
- Pricing model: One-time
- Price: $0.00
- Click "Save product"
- Copy the Price ID (starts with
price_)
-
Create Pro Plan:
- Name: "Pro Plan"
- Description: "For growing teams"
- Pricing model: Recurring
- Price: $29.00
- Billing period: Monthly
- Click "Save product"
- Copy the Price ID (starts with
price_)
-
Create Business Plan:
- Name: "Business Plan"
- Description: "For large organizations"
- Pricing model: Recurring
- Price: $99.00
- Billing period: Monthly
- Click "Save product"
- Copy the Price ID (starts with
price_)
-
Create Webhook Endpoint:
- In the Stripe Dashboard, go to Developers → Webhooks
- Click "Add endpoint"
- Endpoint URL:
https://your-api-domain.com/billing/webhook - Events to send: Select these events:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deleted
- Click "Add endpoint"
-
Get Webhook Secret:
- Click on your newly created webhook endpoint
- In the Signing secret section, click "Reveal"
- Copy the Signing secret (starts with
whsec_)
For the API server (apps/api/.env.local):
# Stripe configuration
STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key_here
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret_here
STRIPE_PRICE_FREE=price_free_plan_id
STRIPE_PRICE_PRO=price_pro_plan_id
STRIPE_PRICE_BUSINESS=price_business_plan_id
# Optional: Custom URLs (defaults to localhost)
STRIPE_SUCCESS_URL=http://localhost:3000/app/billing?success=true
STRIPE_CANCEL_URL=http://localhost:3000/app/billing?canceled=true
STRIPE_RETURN_URL=http://localhost:3000/app/billingFor the web app (apps/web/.env.local):
# Stripe Price IDs (public)
NEXT_PUBLIC_STRIPE_PRICE_FREE=price_free_plan_id
NEXT_PUBLIC_STRIPE_PRICE_PRO=price_pro_plan_id
NEXT_PUBLIC_STRIPE_PRICE_BUSINESS=price_business_plan_id-
Start the development server:
pnpm run dev
-
Test the billing flow:
- Visit
http://localhost:3000/app/billing - You should see the pricing plans without the "Stripe not configured" warning
- Click on a paid plan to test the checkout flow
- Use Stripe's test card numbers:
- Success:
4242 4242 4242 4242 - Decline:
4000 0000 0000 0002 - 3D Secure:
4000 0025 0000 3155
- Success:
- Visit
-
Test webhook events (Development):
- Install Stripe CLI:
brew install stripe/stripe-cli/stripe(macOS) or see Stripe CLI docs - Login to Stripe:
stripe login - Forward webhooks to local server:
stripe listen --forward-to localhost:4000/billing/webhook - Copy the webhook signing secret from the CLI output
- Update your
STRIPE_WEBHOOK_SECRETinapps/api/.env.local - Test webhook events by triggering actions in your app
- Monitor webhook events in the Stripe Dashboard
- Install Stripe CLI:
-
Switch to Live Mode:
- In the Stripe Dashboard, toggle to Live mode
- Get your live API keys and webhook secrets
- Update your environment variables with live keys
-
Update Webhook URL:
- Update your webhook endpoint URL to your production API URL
- Ensure your production server can receive webhook events
-
Test with Real Cards:
- Use real payment methods in test mode first
- Verify all webhook events are processed correctly
- Test subscription lifecycle (create, update, cancel)
Common Issues:
-
"Stripe not configured" warning:
- Ensure all environment variables are set correctly
- Check that Price IDs are valid and active in Stripe Dashboard
- Restart your development server after adding environment variables
-
Webhook signature verification failed:
- Verify the webhook secret is correct
- Ensure the webhook endpoint URL is accessible
- Check that the webhook is receiving the correct events
-
Checkout session creation fails:
- Verify the Price ID exists and is active
- Check that the success/cancel URLs are accessible
- Ensure the Stripe secret key has the correct permissions
-
Customer portal not working:
- Verify the customer exists in Stripe
- Check that the customer has an active subscription
- Ensure the return URL is accessible
Useful Stripe Resources:
- Stripe Dashboard - Manage your account and view events
- Stripe API Documentation - Complete API reference
- Stripe Webhooks Guide - Webhook setup and testing
- Stripe Test Cards - Test card numbers for different scenarios
Required Environment Variables:
| Variable | Location | Description | Example |
|---|---|---|---|
STRIPE_SECRET_KEY |
apps/api/.env.local |
Stripe secret key | sk_test_... |
STRIPE_WEBHOOK_SECRET |
apps/api/.env.local |
Webhook signing secret | whsec_... |
STRIPE_PRICE_PRO |
apps/api/.env.local |
Pro plan price ID | price_... |
STRIPE_PRICE_BUSINESS |
apps/api/.env.local |
Business plan price ID | price_... |
NEXT_PUBLIC_STRIPE_PRICE_PRO |
apps/web/.env.local |
Pro plan price ID (public) | price_... |
NEXT_PUBLIC_STRIPE_PRICE_BUSINESS |
apps/web/.env.local |
Business plan price ID (public) | price_... |
Test Card Numbers:
- Success:
4242 4242 4242 4242 - Decline:
4000 0000 0000 0002 - 3D Secure:
4000 0025 0000 3155 - Requires Authentication:
4000 0025 0000 3155
Webhook Events Handled:
checkout.session.completed- Store customer informationcustomer.subscription.created- Update plan entitlementcustomer.subscription.updated- Update plan entitlementcustomer.subscription.deleted- Update plan entitlement
POST /billing/checkout- Create checkout session for plan upgradePOST /billing/portal- Create customer portal session for subscription managementGET /billing/entitlement- Get current billing entitlement and plan statusPOST /billing/webhook- Stripe webhook handler for subscription events
Local Development (pnpm run dev):
- Uses HTTP server on port 4000
- Direct database access (DynamoDB or in-memory)
- Real-time development with hot reload
- No AWS Lambda cold starts
Production Deployment (Lambda + API Gateway):
- Serverless Lambda functions
- API Gateway for HTTP routing
- DynamoDB and S3 integration
- Auto-scaling and pay-per-request
Billing Configuration:
Development Mode (no Stripe config):
- Shows "Stripe not configured" message
- Plan buttons are disabled with tooltips
- Mock responses for testing billing flow
pnpm run devworks out of the box
Production Mode (with Stripe config):
- Real Stripe checkout and portal sessions
- Customer management by organization ID
- Full subscription lifecycle management
- Webhook signature verification for secure event processing
- Real-time entitlement tracking and plan status updates
The system includes a secure webhook handler that processes Stripe events:
Supported Events:
checkout.session.completed- Stores customer informationcustomer.subscription.created/updated/deleted- Updates plan entitlements
Security:
- Webhook signature verification using
STRIPE_WEBHOOK_SECRET - Event validation and error handling
- Graceful fallback for missing Stripe configuration
Entitlement System:
- Real-time plan status tracking
- Current plan display with billing dates
- Upgrade/downgrade CTAs based on current entitlement
- Mock mode support for development without Stripe
-
Without Stripe (default):
pnpm run dev # Visit http://localhost:3000/app/billing # See mock billing interface
-
With Stripe (configured):
# Set environment variables in both apps pnpm run dev # Visit http://localhost:3000/app/billing # Test real Stripe checkout flow
To stay within AWS free tier limits during development:
✅ Development Mode:
- Keep
DAL=inmemoryinapps/api/.env.localto avoid AWS calls during development - Use small test data sets (limit to 10-20 notes per test)
- Run
pnpm run devfor local development (no AWS resources used)
✅ Infrastructure Optimizations:
- DynamoDB: On-demand billing with PITR disabled
- S3: Versioning disabled, minimal storage usage
- Lambda: 7-day CloudWatch log retention
- API Gateway: Pay-per-request pricing
✅ Cleanup Commands:
# Clean up S3 bucket before destroying infrastructure
aws s3 rm s3://your-bucket-name --recursive
# Destroy stacks in order (ApiStack first, then CoreStack)
pnpm -C infra cdk:destroy SaasTemplateApi
pnpm -C infra cdk:destroy SaasTemplateCore| Component | Development | Production |
|---|---|---|
| Database | In-memory (DAL=inmemory) | DynamoDB (DAL=dynamo) |
| Storage | Local filesystem | S3 bucket |
| Authentication | Clerk test keys | Clerk live keys |
| Billing | Mock/Stripe test | Stripe live |
| Logs | Console output | CloudWatch (7-day retention) |
| API | Local server (port 4000) | Lambda + API Gateway |
| Frontend | Next.js dev server | S3 + CloudFront CDN |
Local Development (Free tier friendly):
# Single command to start everything
pnpm run dev
# This starts:
# - Next.js app on http://localhost:3000
# - API server on http://localhost:4000
# - Uses in-memory database (no AWS calls)
# - Uses mock billing (no Stripe calls)Production Deployment:
# Deploy infrastructure
pnpm -C infra cdk:deploy SaasTemplateCore
pnpm -C infra cdk:deploy SaasTemplateApi
# Build and deploy web app
pnpm -C apps/web build
# Deploy to your hosting provider (Vercel, Netlify, etc.)The project includes comprehensive testing setup with Jest and React Testing Library.
apps/
├── api/
│ ├── src/
│ │ ├── __tests__/
│ │ │ ├── setup.ts # Test setup and mocks
│ │ │ ├── lambda.test.ts # Lambda function tests
│ │ │ ├── dal.test.ts # Data access layer tests
│ │ │ └── integration.test.ts # Integration tests
│ │ └── ...
│ └── jest.config.js
└── web/
├── src/
│ ├── __tests__/
│ │ ├── billing.test.tsx # Billing page tests
│ │ └── components.test.tsx # Component tests
│ └── ...
├── jest.config.js
└── jest.setup.js
- API Tests: Lambda functions, DAL operations, authentication, billing flows
- Web Tests: React components, pages, user interactions, API integration
- Integration Tests: End-to-end flows, error handling, data persistence
# Run all tests
pnpm run test
# Run tests in watch mode (development)
pnpm run test:watch
# Run tests with coverage report
pnpm run test:coverage
# Run tests for specific workspace
pnpm -C apps/api test
pnpm -C apps/web test- Mocking: AWS SDK, Stripe, Clerk authentication
- Coverage: Comprehensive coverage reporting
- Type Safety: Full TypeScript support in tests
- Isolation: Each test runs in isolation with clean state
- CI Ready: Configured for continuous integration
The project includes comprehensive deployment automation with GitHub Actions and deployment scripts.
- Trigger: Push to
mainbranch or manual dispatch - Steps: Test → Deploy Infrastructure → Deploy Web App
- Environment: Production AWS + Vercel
- Trigger: Push to
developorfeature/*branches, PRs, or manual dispatch - Steps: Test → Deploy Infrastructure → Deploy Web App (Preview)
- Environment: Development AWS + Vercel Preview
- Trigger: Manual dispatch only
- Actions: Deploy, Destroy, Diff, Synthesize
- Environments: Production or Development
# Deploy to development
./scripts/deploy.sh --environment development
# Deploy to production
./scripts/deploy.sh --environment production
# Dry run (see what would be deployed)
./scripts/deploy.sh --dry-run
# Skip tests (faster deployment)
./scripts/deploy.sh --environment development --skip-tests# Set up development environment
./scripts/setup-env.sh development
# Set up production environment
./scripts/setup-env.sh productionSee .github/SECRETS.md for complete setup instructions.
Key Changes from Vercel to AWS CDN:
- Web app now deploys to S3 + CloudFront instead of Vercel
- No Vercel secrets required
- Additional AWS S3 and CloudFront permissions needed
- CloudFront provides global CDN with custom domain support
Essential Secrets:
- AWS credentials (production + development)
- Clerk authentication keys
- Stripe price IDs (optional)
- Vercel deployment tokens
# Install dependencies
pnpm install
# Build infrastructure
pnpm -C infra build
# Deploy CoreStack first
pnpm -C infra cdk:deploy SaasTemplateCore
# Deploy ApiStack
pnpm -C infra cdk:deploy SaasTemplateApi# Build web app
NEXT_PUBLIC_API_BASE_URL=https://your-api-url.com pnpm -C apps/web build
# Deploy to Vercel
vercel --prod| Component | Development | Production |
|---|---|---|
| AWS Stack | SaasTemplateDev* |
SaasTemplate* |
| Vercel | Preview deployment | Production deployment |
| Clerk | Test keys | Live keys |
| Stripe | Test mode | Live mode |
| Domain | *.vercel.app |
Custom domain |
- Go to GitHub Actions tab
- View workflow runs and logs
- Check AWS CloudFormation stacks
- Verify Vercel deployments
- AWS Permissions: Ensure IAM user has required policies
- Environment Variables: Verify all secrets are set correctly
- Stack Dependencies: Deploy CoreStack before ApiStack
- Build Failures: Check logs for missing dependencies or configuration
- Add user profile management
- Implement role-based access control
- Add webhook handlers for Stripe events
- Implement usage tracking and plan limits
- Add admin dashboard for subscription management