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

6.2 KiB

Production Setup Guide

Quick Start

1. Create Your .env File

Copy the template and fill in your secrets:

cp .env.example .env

Then edit .env with your actual values:

# === 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

# 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:

# 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:

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:

# Use test card in checkout:
Card: 4242 4242 4242 4242
Date: Any future date
CVC: Any 3 digits
ZIP: Any 5 digits

Verify Webhooks:

# 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:

curl https://puffinoffset.com/api/health

Troubleshooting

Container won't start:

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:

# 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:

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!