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
+69
View File
@@ -0,0 +1,69 @@
# 🐾 paw❤️print
**Open-source rescue management platform — self-host free or use PawPrint Cloud.**
Modern, fast, mobile-first website and management system built for animal rescues. Everything a foster-based rescue needs, nothing it doesn't.
---
## What It Does
- 🐕 **Pet Management** — Profiles, photos, medical records, foster assignments, status tracking
- 📋 **Applications** — Adoption, foster, volunteer, surrender forms with configurable questions
- 🏠 **Foster Portal** — Dedicated dashboard for fosters to submit updates, photos, supply requests
- 💰 **Donations & Expenses** — Stripe payments, multi-source tracking, financial reports
- 📅 **Events** — Listings with optional Facebook auto-posting
- ✏️ **Content CMS** — Editable page sections, newsletter, happy tails / success stories
- 📊 **Analytics** — Human traffic, bot detection, honeypot, AI job tracking
- 🔐 **Role-Based Access** — 9 roles from sysadmin to foster with granular permissions
- 🤖 **Bot Protection** — Dynamic robots.txt, honeypot tarpit, managed blocklist/whitelist
- 🧠 **AI Features** — Pet name generator (Ollama), expandable job queue
## Tech Stack
- **SvelteKit 2** + **Svelte 5** (runes)
- **Tailwind CSS 4**
- **Drizzle ORM** + **MariaDB/MySQL**
- **Node.js** adapter for production
- **Stripe** for payments
- **Nodemailer** for email
- **Ollama** for local AI (optional)
## SaaS Architecture
PawPrint supports multi-tenant deployment:
- **Self-hosted** — One rescue, one server, free forever
- **PawPrint Cloud** — Automated onboarding, subdomain provisioning, managed hosting
See [docs/SAAS.md](docs/SAAS.md) for the multi-tenant architecture.
See [docs/ONBOARDING.md](docs/ONBOARDING.md) for automated provisioning.
## Quick Start (Self-Hosted)
```bash
git clone https://github.com/yourusername/pawprint
cd pawprint
cp .env.example .env
# Edit .env with your database URL, SMTP, etc.
npm install
npx drizzle-kit push
npm run seed
npm run dev
```
## PawPrint Cloud Pricing
| Plan | Price | What You Get |
|------|-------|-------------|
| **Self-Hosted** | Free | Full source, you manage everything |
| **PawPrint Cloud** | $19/mo | Managed hosting, subdomain, SSL, backups, updates |
| **Setup Assist** | $50 one-time | We migrate your data from your old site |
## License
MIT — use it, fork it, save some dogs.
---
Built with ❤️ by [JRei](https://reiners.dev) — started as Almost Home Canine Rescue's website, now open for every rescue.
+274
View File
@@ -0,0 +1,274 @@
# Automated Onboarding
## Flow
```
Rescue signs up at pawprint.app
│
▼
┌─────────────────────┐
│ Signup Form │
│ - Org name │
│ - Admin email │
│ - Admin name │
│ - Subdomain pick │
│ - Password │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ Validate │
│ - Subdomain avail? │
│ - Email unique? │
│ - Spam check │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ Provision │ ← Automated script
│ 1. Create DB │
│ 2. Run migrations │
│ 3. Seed admin user │
│ 4. Generate .env │
│ 5. Create service │
│ 6. Add Caddy route │
│ 7. Start app │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ Welcome Email │
│ - Login URL │
│ - Temp password │
│ - Getting started │
└─────────┬───────────┘
│
▼
Rescue is live at
{slug}.pawprint.app
(~60 seconds total)
```
## Provisioning Script
```bash
#!/bin/bash
# provision-tenant.sh <slug> <org_name> <admin_email> <admin_name>
set -e
SLUG=$1
ORG_NAME=$2
ADMIN_EMAIL=$3
ADMIN_NAME=$4
DB_NAME="pp_${SLUG}"
PORT=$(next_available_port) # Function to find next open port
APP_DIR="/var/www/pawprint/${SLUG}"
SESSION_SECRET=$(openssl rand -hex 32)
TEMP_PASSWORD=$(openssl rand -hex 4)
echo "=== Provisioning ${SLUG} ==="
# 1. Create database
echo "Creating database ${DB_NAME}..."
mysql -e "CREATE DATABASE IF NOT EXISTS ${DB_NAME} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mysql -e "CREATE USER IF NOT EXISTS 'pp_${SLUG}'@'localhost' IDENTIFIED BY '$(openssl rand -hex 16)';"
mysql -e "GRANT ALL PRIVILEGES ON ${DB_NAME}.* TO 'pp_${SLUG}'@'localhost';"
mysql -e "FLUSH PRIVILEGES;"
# 2. Clone app (or symlink to shared build)
echo "Setting up app directory..."
mkdir -p ${APP_DIR}
cp -r /var/www/pawprint/_template/* ${APP_DIR}/
# Or: ln -s /var/www/pawprint/_shared_build/build ${APP_DIR}/build
# 3. Generate .env
echo "Generating .env..."
cat > ${APP_DIR}/.env << EOF
DATABASE_URL=mysql://pp_${SLUG}:${DB_PASS}@localhost:3306/${DB_NAME}
SESSION_SECRET=${SESSION_SECRET}
UPLOAD_DIR=${APP_DIR}/uploads
PUBLIC_SITE_URL=https://${SLUG}.pawprint.app
ORG_NAME=${ORG_NAME}
PORT=${PORT}
EOF
# 4. Run migrations
echo "Running migrations..."
cd ${APP_DIR}
npx drizzle-kit push
# 5. Seed admin user
echo "Seeding admin user..."
node scripts/seed-tenant.js \
--email "${ADMIN_EMAIL}" \
--name "${ADMIN_NAME}" \
--password "${TEMP_PASSWORD}" \
--orgName "${ORG_NAME}"
# 6. Create systemd service
echo "Creating systemd service..."
cat > /etc/systemd/system/pawprint-${SLUG}.service << EOF
[Unit]
Description=PawPrint - ${ORG_NAME}
After=network.target
[Service]
Type=simple
User=www-data
WorkingDirectory=${APP_DIR}
ExecStart=/usr/bin/node build
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
EnvironmentFile=${APP_DIR}/.env
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable pawprint-${SLUG}
systemctl start pawprint-${SLUG}
# 7. Add Caddy route
echo "Configuring Caddy..."
cat >> /etc/caddy/sites/pawprint-tenants.caddy << EOF
${SLUG}.pawprint.app {
reverse_proxy localhost:${PORT}
file_server /uploads/* {
root ${APP_DIR}
}
}
EOF
systemctl reload caddy
# 8. Register in admin DB
mysql pawprint_admin -e "INSERT INTO tenants (name, slug, subdomain, db_name, port, admin_email, admin_name, status, trial_ends_at) VALUES ('${ORG_NAME}', '${SLUG}', '${SLUG}.pawprint.app', '${DB_NAME}', ${PORT}, '${ADMIN_EMAIL}', '${ADMIN_NAME}', 'active', DATE_ADD(NOW(), INTERVAL 14 DAY));"
# 9. Send welcome email
echo "Sending welcome email..."
node scripts/send-welcome.js \
--email "${ADMIN_EMAIL}" \
--name "${ADMIN_NAME}" \
--orgName "${ORG_NAME}" \
--url "https://${SLUG}.pawprint.app" \
--password "${TEMP_PASSWORD}"
echo "=== ${SLUG}.pawprint.app is LIVE ==="
```
## What the Rescue Gets
After ~60 seconds of provisioning:
1. **Live site** at `{slug}.pawprint.app` with SSL
2. **Admin login** with temp password (forced change on first login)
3. **Welcome email** with login URL, getting started guide
4. **Empty but functional** — ready to add pets, customize content, upload photos
5. **14-day free trial** — full features, no credit card required
## Setup Wizard (First Login)
When the admin first logs in, they see a setup wizard instead of the dashboard:
### Step 1: Organization Info
- Logo upload
- Organization name (pre-filled from signup)
- Tagline / mission statement
- Location (city, state)
- Contact email, phone
- Social links (Facebook, Instagram)
### Step 2: Branding
- Primary color (default: teal)
- Accent color
- Font preference (2-3 options)
- Preview of how the site looks
### Step 3: Application Forms
- Toggle which application types to enable (adoption, foster, volunteer, surrender)
- Customize adoption fee default
- Edit agreement items (pre-populated with sensible defaults)
### Step 4: Integrations (Optional)
- Stripe keys (for accepting payments)
- Email SMTP (or use PawPrint's shared sender)
- Facebook page connection
- Petfinder widget (org ID)
### Step 5: Import Data (Optional)
- Upload CSV of existing pets
- Map columns to PawPrint fields
- Preview and confirm import
### Done!
- Redirect to dashboard
- Checklist of "next steps" (add first pet, upload logo, share your site)
## Deprovisioning
When a tenant cancels or trial expires:
```bash
#!/bin/bash
# deprovision-tenant.sh <slug>
SLUG=$1
# 1. Stop service
systemctl stop pawprint-${SLUG}
systemctl disable pawprint-${SLUG}
rm /etc/systemd/system/pawprint-${SLUG}.service
# 2. Remove Caddy route
# (sed out the block from tenants.caddy)
systemctl reload caddy
# 3. Archive data (keep for 30 days)
mysqldump pp_${SLUG} > /backups/tenants/${SLUG}-$(date +%Y%m%d).sql
tar -czf /backups/tenants/${SLUG}-uploads-$(date +%Y%m%d).tar.gz /var/www/pawprint/${SLUG}/uploads/
# 4. Mark as deleted in admin DB
mysql pawprint_admin -e "UPDATE tenants SET status = 'deleted' WHERE slug = '${SLUG}';"
# 5. Schedule hard delete after 30 days
at now + 30 days << EOF
mysql -e "DROP DATABASE IF EXISTS pp_${SLUG};"
mysql -e "DROP USER IF EXISTS 'pp_${SLUG}'@'localhost';"
rm -rf /var/www/pawprint/${SLUG}
rm /backups/tenants/${SLUG}-*.sql
rm /backups/tenants/${SLUG}-*.tar.gz
EOF
echo "=== ${SLUG} deprovisioned, data retained for 30 days ==="
```
## Scaling Considerations
### 0-50 Tenants
- Single VPS ($15-20/mo)
- Everything on one box
- Manual monitoring
### 50-200 Tenants
- Separate DB server
- Shared build (symlink, not copy)
- Automated health checks
- Consider container-per-tenant (Docker)
### 200+ Tenants
- Multiple app servers
- Managed database (PlanetScale, Aiven)
- Cloudflare R2 for images
- Kubernetes or Docker Swarm
- Dedicated ops/monitoring (Grafana, alerts)
### Revenue at Scale
| Tenants | Monthly Revenue | Server Costs | Margin |
|---------|----------------|--------------|--------|
| 10 | $190 | $15 | $175 |
| 50 | $950 | $30 | $920 |
| 100 | $1,900 | $60 | $1,840 |
| 500 | $9,500 | $200 | $9,300 |
+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
```
+136
View File
@@ -0,0 +1,136 @@
# Template Extraction Plan
## What to Pull from AHCR
The AHCR codebase (`almosthomecaninerescue.com-2026/ahcr`) is the source. We extract and genericize.
### Copy As-Is (Core Framework)
- SvelteKit config, vite config, tsconfig
- Drizzle ORM setup and schema (strip AHCR-specific data)
- Auth system (sessions, Argon2, roles)
- Rate limiting
- Audit logging
- File upload handler
- Email system (Nodemailer)
- Stripe integration
- Bot protection (tracking, robots.txt, honeypot)
### Genericize (Replace AHCR-Specific)
| AHCR-Specific | PawPrint Generic |
|---|---|
| "Almost Home Canine Rescue" | `ORG_NAME` env var |
| Sioux Falls, SD references | `ORG_LOCATION` env var |
| SD17 Petfinder org ID | `PETFINDER_ORG_ID` env var (optional) |
| Teal color scheme (#0d9488) | Configurable via setup wizard, stored in site_settings |
| AHCR logo / images | Placeholder logo, upload your own |
| 16 adoption agreement items | Configurable JSON in site_settings |
| Foster application agreements | Configurable JSON |
| info@almosthomecaninerescue.com | `SMTP_FROM` env var |
| GiveButter donation link | Generic donate page with configurable URL |
| Facebook app ID / secrets | Per-tenant config |
| Specific intake sources | Seed with common defaults, editable in admin |
| Content blocks (home, about, etc.) | Seed with placeholder content |
### Remove (AHCR Data)
- database_dump.sql
- decoded_lewodi.php (malware artifact)
- CSV import data and scripts (AHCR-specific)
- Petfinder scraper (API is dead)
- docs/csv-import-data-issues.md
- docs/deleted-junk-pets.md
- All pet data, user data, etc.
### Add (New for Template)
- Setup wizard (first-login onboarding)
- `org_config` table or expand site_settings for org-level config
- Configurable color theme (CSS custom properties)
- Configurable application form questions
- Seed script with placeholder data (example pets, example events)
- Docker Compose file
- Landing page / marketing site (separate from rescue site)
- Health check endpoint (`/api/health`)
- Tenant provisioning scripts
## File-by-File Extraction Checklist
### Config Files
- [ ] package.json — update name, description, remove AHCR references
- [ ] svelte.config.js — keep as-is
- [ ] vite.config.ts — keep as-is
- [ ] tsconfig.json — keep as-is
- [ ] drizzle.config.ts — keep as-is
- [ ] .env.example — genericize all values
- [ ] .gitignore — keep, add uploads/
### Schema
- [ ] src/lib/server/schema.ts — keep all tables, add org_config if needed
- [ ] drizzle/ migrations — regenerate clean from schema
### Auth & Roles
- [ ] src/lib/roles.ts — keep all roles
- [ ] src/lib/server/auth.ts — keep as-is
- [ ] src/lib/server/auth-utils.ts — keep as-is
- [ ] src/hooks.server.ts — keep, genericize any hardcoded strings
### Server Utilities
- [ ] src/lib/server/db.ts — keep as-is
- [ ] src/lib/server/email.ts — genericize from address, org name in templates
- [ ] src/lib/server/stripe.ts — keep as-is
- [ ] src/lib/server/upload.ts — keep as-is
- [ ] src/lib/server/audit.ts — keep as-is
- [ ] src/lib/server/track.ts — keep as-is
- [ ] src/lib/server/rate-limit.ts — keep as-is
- [ ] src/lib/server/content.ts — keep as-is
- [ ] src/lib/server/facebook.ts — keep as-is
- [ ] src/lib/server/contract.ts — genericize org name in contract template
- [ ] src/lib/server/slugify.ts — keep as-is
### Routes — Keep & Genericize
- [ ] All admin routes — keep, genericize any AHCR text
- [ ] All public routes — keep, replace hardcoded content with CMS blocks
- [ ] Foster portal — keep as-is
- [ ] API routes — keep as-is
- [ ] robots.txt — keep dynamic version
- [ ] sitemap.xml — keep, genericize
### Routes — Remove
- [ ] src/lib/server/csv-import.ts — AHCR-specific
- [ ] src/lib/server/run-import.ts — AHCR-specific
- [ ] src/routes/admin/import/ — replace with generic CSV import
### Components
- [ ] All UI components — keep as-is
- [ ] Header/Footer — genericize org name, links
- [ ] Replace any hardcoded "Almost Home" text
### Static Assets
- [ ] Remove AHCR logos and images
- [ ] Add placeholder logo
- [ ] Keep favicon structure (replace with paw icon)
### New Files to Create
- [ ] scripts/provision-tenant.sh
- [ ] scripts/deprovision-tenant.sh
- [ ] scripts/seed-tenant.js
- [ ] scripts/send-welcome.js
- [ ] scripts/migrate-all.sh
- [ ] docker-compose.yml
- [ ] Dockerfile
- [ ] Setup wizard route (/setup)
- [ ] Health check endpoint (/api/health)
- [ ] Landing/marketing page (separate or at /)
## Estimated Effort
| Task | Time |
|------|------|
| Extract and genericize core app | 4-6 hours |
| Setup wizard | 2-3 hours |
| Docker Compose | 1 hour |
| Provisioning scripts | 2-3 hours |
| Admin meta-dashboard | 4-6 hours |
| Landing page | 2-3 hours |
| Stripe billing integration | 2-3 hours |
| Testing & polish | 3-4 hours |
| **Total** | **~20-30 hours** |