Files
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

7.4 KiB

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

#!/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:

#!/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