Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions Caddyfile

This file was deleted.

265 changes: 80 additions & 185 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -1,235 +1,130 @@
# ConFam — Deployment Guide
# ConFam — Deployment Guide (Render)

This document covers deploying ConFam to a single EC2 instance for pilot testing (~5 merchants). This is not a production-grade multi-instance setup — that comes later when there's a real reason to need it.
Pilot deployment for ~5 WhatsApp testers. Free tier. Not for real merchant traffic.

---

## Architecture on the instance
## Architecture on Render

One Docker web service serves all three internal services on a single port via `app.py`.
Migrations run at container start via `db/render_bootstrap.sh` before uvicorn.

```
Internet
│
443/80 (HTTPS/HTTP)
Internet → HTTPS (Render-managed TLS)
│
Caddy (reverse proxy, auto-TLS via Let's Encrypt)
confam-xxxx.onrender.com
│
├── /webhooks/whatsapp* ──→ messaging:8002 (Meta webhook)
├── /webhooks/paystack ──→ settlement-engine:8000 (Paystack webhook only)
└── /{everything else} ──→ checkout:8001 (buyer checkout pages)

Internal Docker network only (not reachable from internet):
settlement-engine:8000 — /merchants, /links, /internal/*
postgres:5432
├── /webhooks/whatsapp — messaging (Meta webhook, public)
├── /webhooks/paystack — settlement engine (Paystack webhook, public)
├── /pay/{link_id} — checkout pages (buyers, public)
└── /links, /merchants — admin endpoints (X-Admin-Key required)
```

OQ-022 network isolation is preserved: settlement-engine's internal endpoints are never reachable from the public internet.

---

## Step 1: Launch an EC2 instance
### Single-role DB difference from local setup

1. In the AWS Console (or CLI), launch an EC2 instance:
- **AMI**: Ubuntu 24.04 LTS
- **Instance type**: `t3.small` (2 vCPU, 2GB RAM — adequate for ~5 testers)
- **Storage**: 20GB gp3
- **Security group** — inbound rules:
| Port | Source | Purpose |
|------|--------|---------|
| 22 | Your IP only (`x.x.x.x/32`) | SSH |
| 80 | 0.0.0.0/0 | Let's Encrypt HTTP challenge |
| 443 | 0.0.0.0/0 | HTTPS (Caddy) |
- No other ports open publicly.
Render's managed Postgres provides one DB user — not a superuser.
The confam_app/confam_migrator role split cannot be created.
Migration 012 adapts RLS policies to work with a single user.

2. **Elastic IP (recommended):** Allocate an Elastic IP and associate it with the instance. Without this, the instance gets a new public IP on every restart, which means:
- The sslip.io hostname changes
- You must update Meta and Paystack webhook URLs again
- An Elastic IP costs ~$0/month while attached to a running instance
What is preserved: append-only trigger on ledger_entries (fires for all users),
FORCE ROW LEVEL SECURITY on payout_accounts (owner still subject to policies),
DELETE still restricted to active_from > now().

If you skip the Elastic IP, accept that a restart requires a one-time webhook URL update in both dashboards (documented in Step 6 below).

3. Note the public IP address.
What is weaker: the running app connects as the full DB owner, not the restricted
confam_app role. Known gap for the pilot. Restore the role split when upgrading
to a dedicated Postgres instance.

---

## Step 2: Derive the sslip.io hostname

Replace dots in the IP with dashes and append `.sslip.io`:

```
IP: 54.123.45.67
Hostname: 54-123-45-67.sslip.io
```

This hostname resolves to your IP automatically — no signup, no DNS config. Let's Encrypt will issue a real TLS certificate for it.
## Step 1: Connect GitHub repo to Render

Test it resolves correctly:
```bash
dig 54-123-45-67.sslip.io +short # should return 54.123.45.67
```
1. dashboard.render.com → New → Blueprint
2. Connect GitHub → select DevSolex/ConFam
3. Render detects render.yaml automatically → click Apply

---

## Step 3: Provision the instance

SSH in and run the setup script:

```bash
ssh -i your-key.pem ubuntu@<instance-ip>
## Step 2: Set secrets

# Option A: run directly from GitHub
curl -fsSL https://raw.githubusercontent.com/DevSolex/ConFam/main/scripts/ec2_setup.sh | bash
After the Blueprint deploys, go to the confam service → Environment.
Fill in every sync:false variable:

# Option B: if repo is already cloned
bash /home/ubuntu/ConFam/scripts/ec2_setup.sh
```

This installs Docker, clones the repo, copies `.env.example` → `.env`, and opens ports 22/80/443 in ufw.
ADMIN_API_KEY — openssl rand -base64 32 (protects admin routes)
PAYSTACK_SECRET_KEY — from Paystack dashboard (test key for pilot)
PAYSTACK_WEBHOOK_SECRET — same as or derived from Paystack secret key
WHATSAPP_PHONE_NUMBER_ID — Meta App → WhatsApp → API Setup
WHATSAPP_ACCESS_TOKEN — permanent System User token (NOT the 24h temp token)
WHATSAPP_APP_SECRET — Meta App → Settings → Basic → App Secret
WHATSAPP_VERIFY_TOKEN — any string you choose
CHECKOUT_BASE_URL — https://confam-xxxx.onrender.com/pay
PAYMENT_LINK_SIGNING_SECRET — openssl rand -base64 32
SENTRY_DSN — optional (sentry.io)

After the script completes, log out and back in (or run `newgrp docker`) so the docker group takes effect.
IMPORTANT: Use a permanent System User token for WHATSAPP_ACCESS_TOKEN.
The temporary dashboard token expires in 24 hours.
Meta Business Suite → Settings → System Users → create user → add app → generate token.

---

## Step 4: Configure `.env` on the instance

```bash
nano /home/ubuntu/ConFam/.env
```

Fill in every `PLACEHOLDER_` value. Key ones:

```bash
# Instance hostname (from Step 2)
PUBLIC_HOSTNAME=54-123-45-67.sslip.io

# Database passwords — choose your own strong passwords
POSTGRES_SUPERUSER_PASSWORD=choose_a_strong_password
CONFAM_APP_PASSWORD=choose_a_strong_password
CONFAM_MIGRATOR_PASSWORD=choose_a_strong_password

# Paystack — use test keys until you're ready for live
PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxx
PAYSTACK_WEBHOOK_SECRET=sk_test_xxxxxxxxxxxx

# Meta WhatsApp Cloud API
WHATSAPP_PHONE_NUMBER_ID=your_phone_number_id
WHATSAPP_ACCESS_TOKEN=your_permanent_system_user_token
WHATSAPP_APP_SECRET=your_app_secret
WHATSAPP_VERIFY_TOKEN=choose_any_string
## Step 3: Deploy

# Checkout URL — must match the public hostname
CHECKOUT_BASE_URL=https://54-123-45-67.sslip.io

# Sentry (optional but recommended for production)
SENTRY_DSN=your_sentry_dsn

# Cooling-off for payout account changes (48h default)
PAYOUT_ACCOUNT_COOLING_OFF_SECONDS=172800
```

**Important — permanent WhatsApp access token:** The temporary token from the Meta dashboard expires in 24 hours. For a stable deployment, create a System User token:
1. Meta Business Suite → Settings → System Users
2. Create a system user, add your WhatsApp app, generate a permanent token
3. Use that token as `WHATSAPP_ACCESS_TOKEN`

Lock down the file:
```bash
chmod 600 /home/ubuntu/ConFam/.env
```

---

## Step 5: Start the stack

```bash
cd /home/ubuntu/ConFam
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
```

Watch the logs to confirm migrations ran and all services started:
```bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f
```

Verify Caddy obtained a TLS certificate (check logs for `certificate obtained successfully`):
```bash
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs caddy
```
Render deploys automatically on push to main.
Watch logs for:
[render-bootstrap] Postgres is ready.
[render-bootstrap] Running Alembic migrations...
[render-bootstrap] Done. Starting application...
INFO: Application startup complete.

Health check:
```bash
curl https://54-123-45-67.sslip.io/health
# {"status":"ok","service":"checkout"}
```
curl https://confam-xxxx.onrender.com/health

---

## Step 6: Update webhook URLs (one-time manual step)
## Step 4: Update webhook URLs (you do this manually)

### Paystack
1. Paystack Dashboard → Settings → API Keys & Webhooks
2. Set webhook URL to:
```
https://54-123-45-67.sslip.io/webhooks/paystack
```
Paystack: Dashboard → Settings → API Keys & Webhooks
https://confam-xxxx.onrender.com/webhooks/paystack

### Meta WhatsApp
1. Meta App Dashboard → WhatsApp → Configuration → Webhook
2. Set callback URL to:
```
https://54-123-45-67.sslip.io/webhooks/whatsapp
```
3. Set verify token to whatever you put in `WHATSAPP_VERIFY_TOKEN`
4. Click "Verify and Save" — Caddy must be running for this to succeed
Meta WhatsApp: App Dashboard → WhatsApp → Configuration → Webhook
Callback URL: https://confam-xxxx.onrender.com/webhooks/whatsapp
Verify token: whatever you set for WHATSAPP_VERIFY_TOKEN
Click "Verify and Save"

---

## Step 7: Verify the full flow

1. Send `REGISTER My Business` to your WhatsApp number → should get a reply
2. Send `ONBOARD 0123456789 058` → Paystack verifies bank, subaccount created
3. Send `PAY 750 Test item` → get a checkout URL back
4. Open the URL in a browser → checkout page renders at the sslip.io domain
5. Complete a test payment → receive "Payment received ✅" WhatsApp message
## Calling admin endpoints

---
curl -X POST https://confam-xxxx.onrender.com/merchants \
-H "X-Admin-Key: your-admin-key" \
-H "Content-Type: application/json" \
-d '{"whatsapp_number": "+234...", "business_name": "My Store"}'

## Redeploying (code updates)

From your local machine:
```bash
./deploy.sh ubuntu@54.123.45.67
```

Or directly on the instance:
```bash
cd /home/ubuntu/ConFam
git pull origin main
docker compose -f docker-compose.yml -f docker-compose.prod.yml build
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
```
curl -X POST https://confam-xxxx.onrender.com/links \
-H "X-Admin-Key: your-admin-key" \
-H "Content-Type: application/json" \
-d '{"merchant_id": "...", "amount_minor_units": 75000, "currency": "NGN", "description": "Item"}'

---

## If the instance is restarted (no Elastic IP)
## Free tier constraints

The public IP changes → the sslip.io hostname changes → you must:
SERVICE SLEEP: Render free services sleep after ~15 min idle. ~60s wake-up time.
Workaround: external pinger (UptimeRobot free, cron-job.org) hitting /health
every 10 minutes. Testing workaround only — not a production setup.

1. Update `PUBLIC_HOSTNAME` in `/home/ubuntu/ConFam/.env`
2. Update `CHECKOUT_BASE_URL` in `.env`
3. Restart Caddy: `docker compose -f docker-compose.yml -f docker-compose.prod.yml restart caddy`
4. Update Paystack webhook URL (Step 6 above)
5. Update Meta webhook URL (Step 6 above)
POSTGRES EXPIRY: Free Postgres expires 30 days after creation (14-day grace).
Before expiry:
Option A: Upgrade to Render paid Postgres ($7/month) — no data loss
Option B: Create new free DB, update DATABASE_URL, re-run migrations,
re-onboard merchants (data loss — acceptable for pilot)

**Recommendation:** Allocate an Elastic IP to avoid this entirely.
INSTANCE HOURS: ~750 free hours/month per workspace = one always-on service.
Do not add a second free web service or the first gets suspended mid-month.

---

## Teardown
## Redeploying

```bash
# Stop the stack (keeps data)
docker compose -f docker-compose.yml -f docker-compose.prod.yml down
git push origin main # triggers auto-deploy

# Full reset including database volume
docker compose -f docker-compose.yml -f docker-compose.prod.yml down -v
```
Or: Render dashboard → confam → Manual Deploy → Deploy latest commit.
19 changes: 19 additions & 0 deletions alembic/versions/0006_render_single_role.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""Alembic revision 0006: Render single-role RLS adaptation."""

from pathlib import Path
from alembic import op

revision = "0006_render_single_role"
down_revision = "0005_payout_accounts_rls"
branch_labels = None
depends_on = None

_SQL = Path(__file__).parent.parent.parent / "db" / "migrations" / "012_render_single_role_adaptation.sql"


def upgrade() -> None:
op.execute(_SQL.read_text())


def downgrade() -> None:
pass # Conditional — no downgrade needed
Loading
Loading