Files
fosterflow/docs/SAAS.md
T
justinandClaude Opus 4.6 7fd2438c36 Initial paw❤️print repo with SaaS architecture docs
README, multi-tenant SaaS architecture (database-per-tenant),
automated onboarding flow (signup → 60 seconds → live site),
and template extraction plan from AHCR codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:36:38 -05:00

212 lines
7.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PawPrint SaaS Architecture
## Multi-Tenant Strategy
### Phase 1: Database-per-Tenant (MVP)
Each rescue gets their own database on shared infrastructure. The app code is identical across tenants — only the database connection and subdomain differ.
```
┌─────────────────────────────────────────────┐
│ Caddy Reverse Proxy │
│ *.pawprint.app → route by subdomain │
├──────────┬──────────┬──────────┬────────────┤
│ almosthome│ happypaws│ furever │ ... │
│ :3001 │ :3002 │ :3003 │ │
├──────────┼──────────┼──────────┼────────────┤
│ DB: ahcr │ DB: hpaws│ DB: furev│ │
└──────────┴──────────┴──────────┴────────────┘
│ MariaDB Server │
└─────────────────────────────────────────────┘
```
**Why this approach:**
- Zero code changes to the rescue app itself
- Complete data isolation between tenants (no org_id bugs)
- Each tenant can be backed up, migrated, or deleted independently
- Can scale horizontally by adding more VPS nodes
- Simple to reason about — each rescue is just "another install"
**How it works:**
1. Tenant signs up at `pawprint.app`
2. Provisioning script creates: database, .env file, systemd service, Caddy route
3. App boots on a unique port, connects to its own DB
4. Caddy routes `{slug}.pawprint.app` → `localhost:{port}`
### Phase 2: Shared Database (Scale)
When we hit 500+ tenants and per-DB overhead matters:
- Add `orgId` column to every table
- Single app instance, single DB
- Row-level security via middleware
- **Only do this when Phase 1 becomes painful**
## Infrastructure
### Single Server (0-100 tenants)
One beefy VPS handles everything:
- **Hetzner CX31** ($15/mo): 4 vCPU, 8GB RAM, 80GB SSD
- MariaDB: all tenant databases
- Node.js: one process per tenant (low memory with Node adapter)
- Caddy: wildcard SSL via Let's Encrypt
- Each tenant uses ~50MB RAM idle, ~200MB under load
100 tenants × 50MB = 5GB RAM — fits on an 8GB server with headroom.
### Multi Server (100+ tenants)
- Separate DB server (managed MariaDB or dedicated VPS)
- Multiple app servers behind a load balancer
- Shared file storage (Cloudflare R2 or mounted volume)
- Caddy on each app server or a dedicated proxy
### Image Storage
| Scale | Strategy | Cost |
|-------|----------|------|
| 0-50 tenants | Local disk | $0 (included in VPS) |
| 50-200 tenants | Cloudflare R2 | ~$1-5/mo (zero egress) |
| 200+ tenants | R2 + CDN | ~$5-20/mo |
Average rescue: ~300MB of images. 100 rescues = 30GB = $0.45/mo on R2.
## Tenant Management
### Admin Dashboard (`admin.pawprint.app`)
The PawPrint operator (you) gets a meta-dashboard:
- **Tenants list** — name, subdomain, plan, status, created date, last active
- **Provisioning** — create new tenant (runs automation)
- **Billing** — Stripe subscription status per tenant
- **Health** — systemd service status, DB size, disk usage
- **Maintenance** — run migrations across all tenants, restart services
### Database Schema (Meta)
The admin dashboard has its own database (`pawprint_admin`):
```sql
CREATE TABLE tenants (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL, -- "Almost Home Canine Rescue"
slug VARCHAR(100) NOT NULL UNIQUE, -- "almosthome"
subdomain VARCHAR(100) NOT NULL UNIQUE, -- "almosthome.pawprint.app"
custom_domain VARCHAR(255), -- "almosthomecaninerescue.com"
db_name VARCHAR(100) NOT NULL, -- "pp_almosthome"
port INT NOT NULL, -- 3001
admin_email VARCHAR(255) NOT NULL,
admin_name VARCHAR(255) NOT NULL,
plan ENUM('free_trial', 'cloud', 'self_hosted') DEFAULT 'free_trial',
stripe_customer_id VARCHAR(255),
stripe_subscription_id VARCHAR(255),
status ENUM('provisioning', 'active', 'suspended', 'deleted') DEFAULT 'provisioning',
trial_ends_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
CREATE TABLE tenant_events (
id INT PRIMARY KEY AUTO_INCREMENT,
tenant_id INT NOT NULL REFERENCES tenants(id),
event VARCHAR(50) NOT NULL, -- "provisioned", "migrated", "suspended", etc.
details JSON,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
## Billing
### Stripe Integration
- **Product:** PawPrint Cloud ($19/mo)
- **Trial:** 14 days free (no card required)
- **Webhook events:**
- `customer.subscription.created` → activate tenant
- `customer.subscription.deleted` → suspend tenant (grace period)
- `invoice.payment_failed` → send warning email, suspend after 3 failures
- **Dunning:** Stripe handles retry logic
### Lifecycle
```
Signup → Trial (14 days) → Active ($19/mo) → ...
→ Expired → Suspended (7 days) → Deleted
```
Suspended tenants:
- App still running but shows "account suspended" page
- Data preserved for 30 days
- Reactivate by paying
## Custom Domains
Tenants can optionally use their own domain:
1. Tenant sets custom domain in their settings
2. They add a CNAME record: `www.theirrescue.com → almosthome.pawprint.app`
3. Caddy auto-provisions SSL via ACME
4. We add the domain to the Caddy config
```
theirrescue.com {
reverse_proxy localhost:3001
}
```
## Security Considerations
- Each tenant's DB user should only have access to their own database
- .env files are per-tenant with unique SESSION_SECRET
- Admin dashboard requires separate auth (not tenant auth)
- Tenant data is never mixed — complete isolation
- Backups are per-tenant (can restore one without affecting others)
- Rate limiting is per-tenant (one rescue getting hammered doesn't affect others)
## Migration Path
### From WordPress
1. Export pets from WP (CSV or WP REST API)
2. Map fields to PawPrint schema
3. Import via PawPrint's CSV import tool
4. Download and re-upload photos
### From Shelterluv / RescueGroups
1. Export data as CSV
2. Map to PawPrint schema
3. Import tool handles the rest
### From Petfinder
1. If they have API access: use REST API to pull pets
2. If not: manual CSV export or widget scraping
3. Note: Petfinder killed their API in 2025, so most rescues can't use it
## Development Workflow
### Updating the App
When we push updates to the PawPrint app:
```bash
# On the server
for tenant in $(cat /etc/pawprint/tenants.list); do
cd /var/www/pawprint/$tenant
git pull
npm install
npm run build
systemctl restart pawprint-$tenant
done
```
Or better: blue-green deploys per tenant. Build once, symlink to each tenant.
### Running Migrations
```bash
for tenant in $(cat /etc/pawprint/tenants.list); do
DB_NAME=$(grep DATABASE_URL /var/www/pawprint/$tenant/.env | cut -d/ -f4)
mysql $DB_NAME < migrations/0009_new_feature.sql
done
```