puffin-app/PRODUCTION-SETUP.md
Matt bc9e2d3782
All checks were successful
Build and Push Docker Images / docker (push) Successful in 1m22s
Implement comprehensive Stripe security fixes and production deployment
CRITICAL SECURITY FIXES:
- Add webhook secret validation to prevent signature bypass
- Implement idempotency protection across all webhook handlers
- Add atomic database updates to prevent race conditions
- Improve CORS security with origin validation and logging
- Remove .env from git tracking to protect secrets

STRIPE INTEGRATION:
- Add support for checkout.session.expired webhook event
- Add Stripe publishable key to environment configuration
- Fix webhook handlers with proper idempotency checks
- Update Order model with atomic updatePaymentAndStatus method
- Add comprehensive logging for webhook processing

DEPLOYMENT ARCHITECTURE:
- Split into two Docker images (frontend-latest, backend-latest)
- Update CI/CD to build separate frontend and backend images
- Configure backend on port 3801 (internal 3001)
- Add production-ready docker-compose.yml
- Remove redundant docker-compose.portainer.yml
- Update nginx configuration for both frontend and backend

DOCUMENTATION:
- Add PRODUCTION-SETUP.md with complete deployment guide
- Add docs/stripe-security-fixes.md with security audit details
- Add docs/stripe-checkout-sessions.md with integration docs
- Add docs/stripe-webhooks.md with webhook configuration
- Update .env.example with all required variables including Stripe publishable key

CONFIGURATION:
- Consolidate to single .env.example template
- Update .gitignore to protect all .env variants
- Add server/Dockerfile for backend container
- Update DEPLOYMENT.md with new architecture

🔒 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 12:18:57 +01:00

259 lines
6.2 KiB
Markdown

# Production Setup Guide
## Quick Start
### 1. Create Your .env File
Copy the template and fill in your secrets:
```bash
cp .env.example .env
```
Then edit `.env` with your actual values:
```bash
# === Frontend Variables ===
VITE_API_BASE_URL=https://puffinoffset.com/api
VITE_WREN_API_TOKEN=35c025d9-5dbb-404b-85aa-19b09da0578d
VITE_FORMSPREE_CONTACT_ID=xkgovnby
VITE_FORMSPREE_OFFSET_ID=xvgzbory
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51SJc52Pdj1mnVT5k8a2NsdyywF6jlpR2VTAMeHOSoXskOQBNRyKpA35G6sJ2ckgv6UPXq9LbiIspFC6E4Yrppk9m00yAMX8K9Z
# === Backend Variables ===
NODE_ENV=production
PORT=3001
FRONTEND_URL=https://puffinoffset.com
# === Stripe Configuration (Test Mode) ===
STRIPE_SECRET_KEY=sk_test_51SJc52Pdj1mnVT5kkkJQgPpjQPkrf8D6Ik0yvdHgCYHOjZXwdRo3wCMZ4YjqaMDEQL0gyNhUgZZ0sAo4YIGTn6f500Or1vuuxJ
STRIPE_WEBHOOK_SECRET=whsec_6hNtwjRPUvxY3MKOJYfyOZRDZW7mlIsB
# === Wren API Configuration ===
WREN_API_TOKEN=35c025d9-5dbb-404b-85aa-19b09da0578d
WREN_DRY_RUN=true
# === Database Configuration ===
DATABASE_PATH=/app/data/orders.db
```
### 2. Deploy with Docker Compose
```bash
# Pull latest images
docker compose pull
# Start containers
docker compose up -d
# View logs
docker compose logs -f
# Check status
docker compose ps
```
### 3. Configure Nginx on Host
Update your `/etc/nginx/sites-available/puffinoffset.com` configuration:
```nginx
# Frontend
server {
listen 443 ssl http2;
server_name puffinoffset.com;
ssl_certificate /etc/letsencrypt/live/puffinoffset.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/puffinoffset.com/privkey.pem;
location / {
proxy_pass http://localhost:3800;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Backend API
server {
listen 443 ssl http2;
server_name puffinoffset.com;
ssl_certificate /etc/letsencrypt/live/puffinoffset.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/puffinoffset.com/privkey.pem;
# Stripe webhooks (MUST use raw body)
location /api/webhooks/stripe {
proxy_pass http://localhost:3801;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CRITICAL: Don't buffer request body for webhook signature verification
proxy_request_buffering off;
proxy_buffering off;
client_max_body_size 10m;
}
# All other API routes
location /api/ {
proxy_pass http://localhost:3801;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# HTTP redirect
server {
listen 80;
server_name puffinoffset.com;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
location / {
return 301 https://$host$request_uri;
}
}
```
Reload nginx:
```bash
sudo nginx -t
sudo systemctl reload nginx
```
## Architecture
```
Internet
Host Nginx (SSL) :443
├─→ Frontend Container :3800
└─→ Backend Container :3801
```
**Port Mapping:**
- Frontend: Host 3800 → Container 3000
- Backend: Host 3801 → Container 3001
## Stripe Configuration
### Required Stripe Keys:
1. **Publishable Key** (Frontend): `pk_test_*` for test mode
2. **Secret Key** (Backend): `sk_test_*` for test mode
3. **Webhook Secret** (Backend): `whsec_*` from Stripe Dashboard
### Configure Webhooks in Stripe Dashboard:
1. Go to: https://dashboard.stripe.com/test/webhooks
2. Click "Add endpoint"
3. URL: `https://puffinoffset.com/api/webhooks/stripe`
4. Select these events:
- `checkout.session.completed`
- `checkout.session.async_payment_succeeded`
- `checkout.session.async_payment_failed`
- `checkout.session.expired`
### Test Mode Configuration:
- ✅ Using `sk_test_*` and `pk_test_*` keys
- ✅ Using `WREN_DRY_RUN=true` (no real offsets purchased)
- ✅ Test card: 4242 4242 4242 4242
- ✅ No real charges will occur
### Going Live:
When ready for production:
1. Get live Stripe keys (`sk_live_*` and `pk_live_*`)
2. Configure production webhook endpoint
3. Set `WREN_DRY_RUN=false`
4. Update `.env` with live keys
## Testing
### Test Stripe Integration:
```bash
# Use test card in checkout:
Card: 4242 4242 4242 4242
Date: Any future date
CVC: Any 3 digits
ZIP: Any 5 digits
```
### Verify Webhooks:
```bash
# Check backend logs
docker compose logs -f backend
# Should see:
# ✅ Checkout session completed
# 💳 Payment confirmed
# 🌱 Fulfilling order via Wren API
# ⚠️ DRY RUN MODE: No real offset will be created
```
### Check Health:
```bash
curl https://puffinoffset.com/api/health
```
## Troubleshooting
### Container won't start:
```bash
docker compose logs backend
docker compose logs web
```
### Webhook errors:
- Verify `STRIPE_WEBHOOK_SECRET` matches Stripe Dashboard
- Check nginx is not buffering webhook requests
- View webhook attempts in Stripe Dashboard
### Database issues:
```bash
# Check database volume
docker volume ls
docker volume inspect puffin-app_puffin-data
# Access database
docker compose exec backend sh
sqlite3 /app/data/orders.db
```
## Security Checklist
-`.env` excluded from git
- ✅ Webhook signature verification enabled
- ✅ CORS restricted to allowed origins
- ✅ Idempotency protection on webhooks
- ✅ SSL/TLS on host nginx
- ⚠️ Rotate Wren API token (it was previously committed to git)
## CI/CD Pipeline
Gitea Actions automatically builds and pushes images on push to `main`:
- Frontend: `code.puffinoffset.com/matt/puffin-app:frontend-latest`
- Backend: `code.puffinoffset.com/matt/puffin-app:backend-latest`
To deploy updates:
```bash
docker compose pull
docker compose up -d
```
## Files Overview
| File | Purpose | In Git? |
|------|---------|---------|
| `.env.example` | Template with placeholders | ✅ Yes |
| `.env` | Your actual secrets | ❌ No (gitignored) |
| `docker-compose.yml` | Production deployment | ✅ Yes |
| `nginx-host.conf` | Example nginx config | ✅ Yes |
**NEVER commit `.env` with real secrets!**