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:
@@ -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 |
|
||||
Reference in New Issue
Block a user