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>
275 lines
7.4 KiB
Markdown
275 lines
7.4 KiB
Markdown
# 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 |
|