# 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!**