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>
This commit is contained in:
2026-03-26 10:36:38 -05:00
co-authored by Claude Opus 4.6
commit 7fd2438c36
4 changed files with 690 additions and 0 deletions
+211
View File
@@ -0,0 +1,211 @@
# 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
```