Files
smoothschedule/CLAUDE.md
poduck da508da398 Add database initialization and seeding documentation
- Add comprehensive "Database Initialization & Seeding" section to CLAUDE.md
- Document all 10 steps for fresh database setup
- Include Activepieces platform initialization steps
- Document seed commands for billing, demo data, automation templates
- Add quick reference section with all commands
- Update Activepieces platform/project IDs after fresh setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 02:34:12 -05:00

818 lines
25 KiB
Markdown

# SmoothSchedule - Multi-Tenant Scheduling Platform
## Quick Reference
### Project Structure
```
/home/poduck/Desktop/smoothschedule2/
├── frontend/ # React + Vite + TypeScript frontend
│ └── CLAUDE.md # Frontend-specific docs
├── smoothschedule/ # Django backend (RUNS IN DOCKER!)
│ └── CLAUDE.md # Backend-specific docs
└── legacy_reference/ # Old code for reference (do not modify)
```
### Development URLs
- **Frontend:** `http://demo.lvh.me:5173` (or any business subdomain)
- **Platform Frontend:** `http://platform.lvh.me:5173`
- **Backend API:** `http://lvh.me:8000/api/`
Note: `lvh.me` resolves to `127.0.0.1` - required for subdomain cookies to work.
## CRITICAL: Test-Driven Development (TDD) Required
**All code changes MUST follow TDD.** This is non-negotiable.
### TDD Workflow
1. **Write tests FIRST** before writing any implementation code
2. **Run tests** to verify they fail (red)
3. **Write minimal code** to make tests pass (green)
4. **Refactor** while keeping tests green
5. **Repeat** for each new feature or bug fix
### Coverage Requirements
| Target | Minimum | Goal |
|--------|---------|------|
| Backend (Django) | **80%** | 100% |
| Frontend (React) | **80%** | 100% |
### Running Tests with Coverage
**Backend (Django):**
```bash
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
# Run all tests with coverage
docker compose -f docker-compose.local.yml exec django pytest --cov --cov-report=term-missing
# Run tests for a specific app
docker compose -f docker-compose.local.yml exec django pytest smoothschedule/scheduling/schedule/tests/ --cov=smoothschedule/scheduling/schedule
# Run a single test file
docker compose -f docker-compose.local.yml exec django pytest smoothschedule/path/to/test_file.py -v
# Run tests matching a pattern
docker compose -f docker-compose.local.yml exec django pytest -k "test_create_resource" -v
```
**Frontend (React):**
```bash
cd /home/poduck/Desktop/smoothschedule2/frontend
# Run all tests with coverage
npm test -- --coverage
# Run tests in watch mode during development
npm test
# Run a single test file
npm test -- src/hooks/__tests__/useResources.test.ts
# Run tests matching a pattern
npm test -- -t "should create resource"
```
### Test File Organization
**Backend:**
```
smoothschedule/smoothschedule/{domain}/{app}/
├── models.py
├── views.py
├── serializers.py
└── tests/
├── __init__.py
├── test_models.py # Model unit tests
├── test_serializers.py # Serializer tests
├── test_views.py # API endpoint tests
└── factories.py # Test factories (optional)
```
**Frontend:**
```
frontend/src/
├── hooks/
│ ├── useResources.ts
│ └── __tests__/
│ └── useResources.test.ts
├── components/
│ ├── MyComponent.tsx
│ └── __tests__/
│ └── MyComponent.test.tsx
└── pages/
├── MyPage.tsx
└── __tests__/
└── MyPage.test.tsx
```
### What to Test
**Backend:**
- Model methods and properties
- Model validation (clean methods)
- Serializer validation
- API endpoints (all HTTP methods)
- Permission classes
- Custom querysets and managers
- Signals
- Celery tasks
- Utility functions
**Frontend:**
- Custom hooks (state changes, API calls)
- Component rendering
- User interactions (clicks, form submissions)
- Conditional rendering
- Error states
- Loading states
- API client functions
### TDD Example - Adding a New Feature
**Step 1: Write the test first**
```python
# Backend: test_views.py
def test_create_resource_with_schedule(self, api_client, tenant):
"""New feature: resources can have a default schedule."""
data = {
"name": "Test Resource",
"type": "STAFF",
"default_schedule": {
"monday": {"start": "09:00", "end": "17:00"},
"tuesday": {"start": "09:00", "end": "17:00"},
}
}
response = api_client.post("/api/resources/", data, format="json")
assert response.status_code == 201
assert response.data["default_schedule"]["monday"]["start"] == "09:00"
```
```typescript
// Frontend: useResources.test.ts
it('should create resource with schedule', async () => {
const { result } = renderHook(() => useCreateResource());
await act(async () => {
await result.current.mutateAsync({
name: 'Test Resource',
type: 'STAFF',
defaultSchedule: { monday: { start: '09:00', end: '17:00' } }
});
});
expect(mockApiClient.post).toHaveBeenCalledWith('/resources/', expect.objectContaining({
default_schedule: expect.any(Object)
}));
});
```
**Step 2: Run tests - they should FAIL**
**Step 3: Write minimal implementation to make tests pass**
**Step 4: Refactor if needed while keeping tests green**
### Pre-Commit Checklist
Before committing ANY code:
1. [ ] Tests written BEFORE implementation
2. [ ] All tests pass
3. [ ] Coverage meets minimum threshold (80%)
4. [ ] No skipped or disabled tests without justification
## CRITICAL: Backend Runs in Docker
**NEVER run Django commands directly.** Always use Docker Compose:
```bash
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
# Run migrations
docker compose -f docker-compose.local.yml exec django python manage.py migrate
# Django shell
docker compose -f docker-compose.local.yml exec django python manage.py shell
# View logs
docker compose -f docker-compose.local.yml logs -f django
# Any management command
docker compose -f docker-compose.local.yml exec django python manage.py <command>
```
## Key Configuration Files
### Backend (Django)
| File | Purpose |
|------|---------|
| `smoothschedule/docker-compose.local.yml` | Docker services config |
| `smoothschedule/.envs/.local/.django` | Django env vars (SECRET_KEY, etc.) |
| `smoothschedule/.envs/.local/.postgres` | Database credentials |
| `smoothschedule/config/settings/local.py` | Local Django settings |
| `smoothschedule/config/settings/base.py` | Base Django settings |
| `smoothschedule/config/urls.py` | URL routing |
### Frontend (React)
| File | Purpose |
|------|---------|
| `frontend/.env.development` | Vite env vars |
| `frontend/vite.config.ts` | Vite configuration |
| `frontend/src/api/client.ts` | Axios API client |
| `frontend/src/types.ts` | TypeScript interfaces |
| `frontend/src/i18n/locales/en.json` | Translations |
| `frontend/src/utils/dateUtils.ts` | Date formatting utilities |
## Timezone Architecture (CRITICAL)
All date/time handling follows this architecture to ensure consistency across timezones.
### Core Principles
1. **Database**: All times stored in UTC
2. **API Communication**: Always use UTC (both directions)
3. **API Responses**: Include `business_timezone` field
4. **Frontend Display**: Convert UTC based on `business_timezone`
- If `business_timezone` is set → display in that timezone
- If `business_timezone` is null/blank → display in user's local timezone
### Data Flow
```
FRONTEND (User in Eastern Time selects "Dec 8, 2:00 PM")
Convert to UTC: "2024-12-08T19:00:00Z"
Send to API (always UTC)
DATABASE (stores UTC)
API RESPONSE:
{
"start_time": "2024-12-08T19:00:00Z", // Always UTC
"business_timezone": "America/Denver" // IANA timezone (or null for local)
}
FRONTEND CONVERTS:
- If business_timezone set: UTC → Mountain Time → "Dec 8, 12:00 PM MST"
- If business_timezone null: UTC → User local → "Dec 8, 2:00 PM EST"
```
### Frontend Helper Functions
Located in `frontend/src/utils/dateUtils.ts`:
```typescript
import {
toUTC,
fromUTC,
formatForDisplay,
formatDateForDisplay,
getDisplayTimezone,
} from '../utils/dateUtils';
// SENDING TO API - Always convert to UTC
const apiPayload = {
start_time: toUTC(selectedDateTime), // "2024-12-08T19:00:00Z"
};
// RECEIVING FROM API - Convert for display
const displayTime = formatForDisplay(
response.start_time, // UTC from API
response.business_timezone // "America/Denver" or null
);
// Result: "Dec 8, 2024 12:00 PM" (in business or local timezone)
// DATE-ONLY fields (time blocks)
const displayDate = formatDateForDisplay(
response.start_date,
response.business_timezone
);
```
### API Response Requirements
All endpoints returning date/time data MUST include:
```python
# In serializers or views
{
"start_time": "2024-12-08T19:00:00Z",
"business_timezone": business.timezone, # "America/Denver" or None
}
```
### Backend Serializer Mixin
Use `TimezoneSerializerMixin` from `core/mixins.py` to automatically add the timezone field:
```python
from core.mixins import TimezoneSerializerMixin
class EventSerializer(TimezoneSerializerMixin, serializers.ModelSerializer):
class Meta:
model = Event
fields = [
'id', 'start_time', 'end_time',
# ... other fields ...
'business_timezone', # Provided by mixin
]
read_only_fields = ['business_timezone']
```
The mixin automatically retrieves the timezone from the tenant context.
- Returns the IANA timezone string if set (e.g., "America/Denver")
- Returns `null` if not set (frontend uses user's local timezone)
### Common Mistakes to Avoid
```typescript
// BAD - Uses browser local time, not UTC
date.toISOString().split('T')[0]
// BAD - Doesn't respect business timezone setting
new Date(utcString).toLocaleString()
// GOOD - Use helper functions
toUTC(date) // For API requests
formatForDisplay(utcString, businessTimezone) // For displaying
```
## Django App Organization (Domain-Based)
Apps are organized into domain packages under `smoothschedule/smoothschedule/`:
### Identity Domain
| App | Location | Purpose |
|-----|----------|---------|
| `core` | `identity/core/` | Tenant, Domain, PermissionGrant, middleware, mixins |
| `users` | `identity/users/` | User model, authentication, MFA |
### Scheduling Domain
| App | Location | Purpose |
|-----|----------|---------|
| `schedule` | `scheduling/schedule/` | Resources, Events, Services, Participants |
| `contracts` | `scheduling/contracts/` | Contract/e-signature system |
| `analytics` | `scheduling/analytics/` | Business analytics and reporting |
### Communication Domain
| App | Location | Purpose |
|-----|----------|---------|
| `notifications` | `communication/notifications/` | Notification system |
| `credits` | `communication/credits/` | SMS/calling credits |
| `mobile` | `communication/mobile/` | Field employee mobile app |
| `messaging` | `communication/messaging/` | Email templates and messaging |
### Commerce Domain
| App | Location | Purpose |
|-----|----------|---------|
| `payments` | `commerce/payments/` | Stripe Connect payments bridge |
| `tickets` | `commerce/tickets/` | Support ticket system |
### Platform Domain
| App | Location | Purpose |
|-----|----------|---------|
| `admin` | `platform/admin/` | Platform administration, subscriptions |
| `api` | `platform/api/` | Public API v1 for third-party integrations |
## Core Mixins & Base Classes
Located in `smoothschedule/smoothschedule/identity/core/mixins.py`. Use these to avoid code duplication.
### Permission Classes
```python
from smoothschedule.identity.core.mixins import DenyStaffWritePermission, DenyStaffAllAccessPermission, DenyStaffListPermission
class MyViewSet(ModelViewSet):
# Block write operations for staff (GET allowed)
permission_classes = [IsAuthenticated, DenyStaffWritePermission]
# Block ALL operations for staff
permission_classes = [IsAuthenticated, DenyStaffAllAccessPermission]
# Block list/create/update/delete but allow retrieve
permission_classes = [IsAuthenticated, DenyStaffListPermission]
```
#### Per-User Permission Overrides
Staff permissions can be overridden on a per-user basis using the `user.permissions` JSONField.
Permission keys are auto-derived from the view's basename or model name:
| Permission Class | Auto-derived Key | Example |
|-----------------|------------------|---------|
| `DenyStaffWritePermission` | `can_write_{basename}` | `can_write_resources` |
| `DenyStaffAllAccessPermission` | `can_access_{basename}` | `can_access_services` |
| `DenyStaffListPermission` | `can_list_{basename}` or `can_access_{basename}` | `can_list_customers` |
**Current ViewSet permission keys:**
| ViewSet | Permission Class | Override Key |
|---------|-----------------|--------------|
| `ResourceViewSet` | `DenyStaffAllAccessPermission` | `can_access_resources` |
| `ServiceViewSet` | `DenyStaffAllAccessPermission` | `can_access_services` |
| `CustomerViewSet` | `DenyStaffListPermission` | `can_list_customers` or `can_access_customers` |
| `ScheduledTaskViewSet` | `DenyStaffAllAccessPermission` | `can_access_scheduled-tasks` |
**Granting a specific staff member access:**
```bash
# Open Django shell
docker compose -f docker-compose.local.yml exec django python manage.py shell
```
```python
from smoothschedule.identity.users.models import User
# Find the staff member
staff = User.objects.get(email='john@example.com')
# Grant read access to resources
staff.permissions['can_access_resources'] = True
staff.save()
# Or grant list access to customers (but not full CRUD)
staff.permissions['can_list_customers'] = True
staff.save()
```
**Custom permission keys (optional):**
```python
class ResourceViewSet(ModelViewSet):
permission_classes = [IsAuthenticated, DenyStaffAllAccessPermission]
# Override the auto-derived key
staff_access_permission_key = 'can_manage_equipment'
```
Then grant via: `staff.permissions['can_manage_equipment'] = True`
### QuerySet Mixins
```python
from smoothschedule.identity.core.mixins import TenantFilteredQuerySetMixin, UserTenantFilteredMixin
# For tenant-scoped models (automatic django-tenants filtering)
class ResourceViewSet(TenantFilteredQuerySetMixin, ModelViewSet):
queryset = Resource.objects.all()
deny_staff_queryset = True # Optional: also filter staff at queryset level
def filter_queryset_for_tenant(self, queryset):
# Override for custom filtering
return queryset.filter(is_active=True)
# For User model (shared schema, needs explicit tenant filter)
class CustomerViewSet(UserTenantFilteredMixin, ModelViewSet):
queryset = User.objects.filter(role=User.Role.CUSTOMER)
```
### Feature Permission Mixins
```python
from smoothschedule.identity.core.mixins import PluginFeatureRequiredMixin, TaskFeatureRequiredMixin
# Checks can_use_plugins feature on list/retrieve/create
class PluginViewSet(PluginFeatureRequiredMixin, ModelViewSet):
pass
# Checks both can_use_plugins AND can_use_tasks
class ScheduledTaskViewSet(TaskFeatureRequiredMixin, TenantFilteredQuerySetMixin, ModelViewSet):
pass
```
### Base API Views (for non-ViewSet views)
```python
from rest_framework.views import APIView
from smoothschedule.identity.core.mixins import TenantAPIView, TenantRequiredAPIView
# Optional tenant - use self.get_tenant()
class MyView(TenantAPIView, APIView):
def get(self, request):
tenant = self.get_tenant() # May be None
return self.success_response({'data': 'value'})
# or: return self.error_response('Something went wrong', status_code=400)
# Required tenant - self.tenant always available
class MyTenantView(TenantRequiredAPIView, APIView):
def get(self, request):
# self.tenant is guaranteed to exist (returns 400 if missing)
return Response({'name': self.tenant.name})
```
### Helper Methods Available
| Method | Description |
|--------|-------------|
| `self.get_tenant()` | Get tenant from request (may be None) |
| `self.get_tenant_or_error()` | Returns (tenant, error_response) tuple |
| `self.error_response(msg, status_code)` | Standard error response |
| `self.success_response(data, status_code)` | Standard success response |
| `self.check_feature(key, name)` | Check feature permission, returns error or None |
## Common Tasks
### After modifying Django models:
```bash
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
docker compose -f docker-compose.local.yml exec django python manage.py makemigrations
docker compose -f docker-compose.local.yml exec django python manage.py migrate
```
### After modifying frontend:
Frontend hot-reloads automatically. If issues, restart:
```bash
cd /home/poduck/Desktop/smoothschedule2/frontend
npm run dev
```
### Debugging 500 errors:
```bash
cd /home/poduck/Desktop/smoothschedule2/smoothschedule
docker compose -f docker-compose.local.yml logs django --tail=100
```
### Testing API directly:
```bash
curl -s "http://lvh.me:8000/api/resources/" | jq
```
## Database Initialization & Seeding
When resetting the database or setting up a fresh environment, follow these steps in order.
### Step 1: Reset Database (if needed)
```bash
cd /home/poduck/Desktop/smoothschedule/smoothschedule
# Stop all containers and remove volumes (DESTRUCTIVE - removes all data)
docker compose -f docker-compose.local.yml down -v
# Start services fresh
docker compose -f docker-compose.local.yml up -d
```
### Step 2: Create Activepieces Database
The Activepieces service requires its own database:
```bash
docker compose -f docker-compose.local.yml exec postgres psql -U FDuVLuQjfpYzGizTmaTavMcaimqpCMRM -d postgres -c "CREATE DATABASE activepieces;"
```
### Step 3: Initialize Activepieces Platform
Create the initial Activepieces admin user (this auto-creates the platform):
```bash
curl -s -X POST http://localhost:8090/api/v1/authentication/sign-up \
-H "Content-Type: application/json" \
-d '{"email":"admin@smoothschedule.com","password":"Admin123!","firstName":"Admin","lastName":"User","newsLetter":false,"trackEvents":false}'
```
**IMPORTANT:** Update `.envs/.local/.activepieces` with the returned IDs:
- `AP_PLATFORM_ID` - from the `platformId` field in response
- `AP_DEFAULT_PROJECT_ID` - from the `projectId` field in response
Then restart containers to pick up new IDs:
```bash
docker compose -f docker-compose.local.yml restart activepieces django
```
### Step 4: Run Django Migrations
```bash
docker compose -f docker-compose.local.yml exec django python manage.py migrate
```
### Step 5: Seed Billing Catalog
Creates subscription plans (free, starter, growth, pro, enterprise):
```bash
docker compose -f docker-compose.local.yml exec django python manage.py billing_seed_catalog
```
### Step 6: Seed Demo Data
Run the reseed_demo command to create the demo tenant with sample data:
```bash
docker compose -f docker-compose.local.yml exec django python manage.py reseed_demo
```
This creates:
- **Tenant:** "Serenity Salon & Spa" (subdomain: `demo`)
- **Pro subscription** with pink theme branding
- **Staff:** 6 stylists/therapists with salon/spa themed names
- **Services:** 12 salon/spa services (haircuts, coloring, massages, facials, etc.)
- **Resources:** 4 treatment rooms
- **Customers:** 20 sample customers
- **Appointments:** 100 appointments respecting business hours (9am-5pm Mon-Fri)
### Step 7: Create Platform Test Users
The DevQuickLogin component expects these users with password `test123`:
```bash
docker compose -f docker-compose.local.yml exec django python manage.py shell -c "
from smoothschedule.identity.users.models import User
# Platform users (public schema)
users_data = [
('superuser@platform.com', 'Super User', 'superuser', True, True),
('manager@platform.com', 'Platform Manager', 'platform_manager', True, False),
('sales@platform.com', 'Sales Rep', 'platform_sales', True, False),
('support@platform.com', 'Support Agent', 'platform_support', True, False),
]
for email, name, role, is_staff, is_superuser in users_data:
user, created = User.objects.get_or_create(
email=email,
defaults={
'username': email,
'name': name,
'role': role,
'is_staff': is_staff,
'is_superuser': is_superuser,
'is_active': True,
}
)
if created:
user.set_password('test123')
user.save()
print(f'Created: {email}')
else:
print(f'Exists: {email}')
"
```
### Step 8: Seed Automation Templates
Seeds 8 automation templates to the Activepieces template gallery:
```bash
docker compose -f docker-compose.local.yml exec django python manage.py seed_automation_templates
```
Templates created:
- Appointment Confirmation Email
- SMS Appointment Reminder
- Staff Notification - New Booking
- Cancellation Confirmation Email
- Thank You + Google Review Request
- Win-back Email Campaign
- Google Calendar Sync
- Webhook Notification
### Step 9: Provision Activepieces Connections
Creates SmoothSchedule connections in Activepieces for all tenants:
```bash
docker compose -f docker-compose.local.yml exec django python manage.py provision_ap_connections --force
```
### Step 10: Provision Default Flows
Creates the default email automation flows for each tenant:
```bash
docker compose -f docker-compose.local.yml exec django python manage.py shell -c "
from smoothschedule.identity.core.models import Tenant
from smoothschedule.identity.core.signals import _provision_default_flows_for_tenant
for tenant in Tenant.objects.exclude(schema_name='public'):
print(f'Provisioning default flows for: {tenant.name}')
_provision_default_flows_for_tenant(tenant.id)
print('Done!')
"
```
Default flows created per tenant:
- `appointment_confirmation` - Confirmation email when appointment is booked
- `appointment_reminder` - Reminder based on service settings
- `thank_you` - Thank you email after final payment
- `payment_deposit` - Deposit payment confirmation
- `payment_final` - Final payment confirmation
### Quick Reference: All Seed Commands
```bash
cd /home/poduck/Desktop/smoothschedule/smoothschedule
# 1. Create Activepieces database
docker compose -f docker-compose.local.yml exec postgres psql -U FDuVLuQjfpYzGizTmaTavMcaimqpCMRM -d postgres -c "CREATE DATABASE activepieces;"
# 2. Run migrations
docker compose -f docker-compose.local.yml exec django python manage.py migrate
# 3. Seed billing plans
docker compose -f docker-compose.local.yml exec django python manage.py billing_seed_catalog
# 4. Seed demo tenant with data
docker compose -f docker-compose.local.yml exec django python manage.py reseed_demo
# 5. Seed automation templates
docker compose -f docker-compose.local.yml exec django python manage.py seed_automation_templates
# 6. Provision Activepieces connections
docker compose -f docker-compose.local.yml exec django python manage.py provision_ap_connections --force
# 7. Provision default flows (run in Django shell - see Step 10 above)
```
### Verifying Setup
After seeding, verify everything is working:
```bash
# Check all services are running
docker compose -f docker-compose.local.yml ps
# Check Activepieces health
curl -s http://localhost:8090/api/v1/health
# Test login
curl -s http://api.lvh.me:8000/auth/login/ -X POST \
-H "Content-Type: application/json" \
-d '{"email":"owner@demo.com","password":"test123"}'
# Check flows exist in Activepieces
docker compose -f docker-compose.local.yml exec postgres psql -U FDuVLuQjfpYzGizTmaTavMcaimqpCMRM -d activepieces -c "SELECT id, status FROM flow;"
```
Access the application at:
- **Demo tenant:** http://demo.lvh.me:5173
- **Platform:** http://platform.lvh.me:5173
## Git Branch
Currently on: `feature/platform-superuser-ui`
Main branch: `main`
## Production Deployment
### Quick Deploy
```bash
# From your local machine
cd /home/poduck/Desktop/smoothschedule2
./deploy.sh poduck@smoothschedule.com
```
### Initial Server Setup (one-time)
```bash
# Setup server dependencies
ssh poduck@smoothschedule.com 'bash -s' < server-setup.sh
# Setup DigitalOcean Spaces
ssh poduck@smoothschedule.com
./setup-spaces.sh
```
### Production URLs
- **Main site:** `https://smoothschedule.com`
- **Platform dashboard:** `https://platform.smoothschedule.com`
- **Tenant subdomains:** `https://*.smoothschedule.com`
- **Flower (Celery):** `https://smoothschedule.com:5555`
### Production Management
```bash
# SSH into server
ssh poduck@smoothschedule.com
# Navigate to project
cd ~/smoothschedule
# View logs
docker compose -f docker-compose.production.yml logs -f
# Run migrations
docker compose -f docker-compose.production.yml exec django python manage.py migrate
# Create superuser
docker compose -f docker-compose.production.yml exec django python manage.py createsuperuser
# Restart services
docker compose -f docker-compose.production.yml restart
# View status
docker compose -f docker-compose.production.yml ps
```
### Environment Variables
Production environment configured in:
- **Backend:** `smoothschedule/.envs/.production/.django`
- **Database:** `smoothschedule/.envs/.production/.postgres`
- **Frontend:** `frontend/.env.production`
### DigitalOcean Spaces
- **Bucket:** `smoothschedule`
- **Region:** `nyc3`
- **Endpoint:** `https://nyc3.digitaloceanspaces.com`
- **Public URL:** `https://smoothschedule.nyc3.digitaloceanspaces.com`
See [DEPLOYMENT.md](DEPLOYMENT.md) for detailed deployment guide.