SetTimes Production Deployment Guide¶
Deploying to Cloudflare Pages + Functions + D1
Table of Contents¶
- Overview
- Prerequisites
- Initial Setup
- Database Setup
- Environment Configuration
- First Deployment
- Custom Domain Setup
- Post-Deployment
- Continuous Deployment
- Rollback & Recovery
- Monitoring & Maintenance
Overview¶
SetTimes is deployed as a full-stack application on Cloudflare's edge network:
Architecture:
- Frontend: React SPA built with Vite, hosted on Cloudflare Pages
- Backend API: Cloudflare Pages Functions (serverless)
- Database: Cloudflare D1 (SQLite at the edge)
- CDN: Cloudflare CDN (automatic)
- SSL: Automatic HTTPS with Cloudflare
Deployment Targets:
- Production:
settimes.ca(main branch) - Development:
dev.settimes.ca(dev branch) - Preview: Automatic preview deployments for PRs
Deployment Method:
- Continuous Deployment: Automatic on git push
- Manual Deployment: Via
wranglerCLI - Rollback: One-click in Cloudflare Dashboard
Prerequisites¶
Required Accounts & Access¶
- [ ] Cloudflare Account (with Pages enabled)
- [ ] GitHub Account (with repo access)
- [ ] Domain (e.g., settimes.ca) managed by Cloudflare DNS
- [ ] Node.js 20+ installed locally
- [ ] Wrangler CLI installed (
npm install -g wrangler)
Install Wrangler¶
# Install globally
npm install -g wrangler
# Verify installation
wrangler --version
# Login to Cloudflare
wrangler login
Initial Setup¶
1. Clone Repository¶
git clone https://github.com/BreakableHoodie/settimesdotca.git
cd settimesdotca
2. Install Dependencies¶
# Install frontend dependencies
cd frontend
npm install
# Return to root
cd ..
3. Verify Local Build¶
cd frontend
npm run build
Expected output:
β 1234 modules transformed.
dist/index.html 1.23 kB
dist/assets/index-abc123.js 456.78 kB
β built in 12.34s
Database Setup¶
1. Create Production Database¶
Run the setup script which guides you through the process:
./scripts/setup-prod-db.sh
Or manually:
# Create production D1 database
wrangler d1 create settimes-production-db
Output:
β
Successfully created DB 'settimes-production-db'
[[d1_databases]]
binding = "DATABASE"
database_name = "settimes-production-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Important: Copy the database_id - you'll need it for wrangler.toml.
2. Create Development Database (Optional)¶
wrangler d1 create settimes-dev-db
3. Update wrangler.toml¶
Edit wrangler.toml in project root:
name = "settimes"
compatibility_date = "2025-01-01"
# Production environment
[env.production]
name = "settimes-production"
[env.production.vars]
ENVIRONMENT = "production"
[[env.production.d1_databases]]
binding = "DATABASE"
database_name = "settimes-production-db"
database_id = "YOUR_PRODUCTION_DB_ID_HERE" # From step 1
# Development environment
[env.development]
name = "settimes-development"
[env.development.vars]
ENVIRONMENT = "development"
[[env.development.d1_databases]]
binding = "DATABASE"
database_name = "settimes-dev-db"
database_id = "YOUR_DEV_DB_ID_HERE" # From step 2 (optional)
4. Run Database Migrations & Seed Data¶
If you didn't use the setup script, run:
# Apply migrations to production
wrangler d1 migrations apply settimes-production-db --env production
# Seed with Long Weekend Band Crawl seasonal data
wrangler d1 execute settimes-production-db --env production --file=database/seed-production.sql
Expected output:
π Applying migration 001_initial_schema.sql
π Applying migration 002_add_audit_logs.sql
π Applying migration 003_add_sessions.sql
β
Successfully applied 3 migration(s)
5. Create Initial Admin User¶
The system uses invite codes for admin account creation β do not insert password hashes directly into the database. The hash algorithm is PBKDF2 (not bcrypt), so any manually inserted bcrypt hash will fail authentication.
Note:
ALLOW_ADMIN_SIGNUPis a test-only env var that bypasses invite codes. It does not exist in the production functions and must never be set in a production environment.
Production bootstrap procedure (one-time):
- After deploying, open the Cloudflare D1 dashboard (or use
wrangler d1 execute) and insert a bootstrap invite code:
INSERT INTO invite_codes (code, email, role, created_by_user_id, expires_at)
VALUES (
'REPLACE-WITH-SECURE-UUID',
'admin@yourdomain.com',
'admin',
NULL,
datetime('now', '+7 days')
);
Generate a secure UUID locally (e.g. node -e "console.log(crypto.randomUUID())").
-
Navigate to
/admin/signup?code=REPLACE-WITH-SECURE-UUIDand complete account creation. The password is hashed with PBKDF2 automatically. -
Once logged in, generate additional invite codes from the admin panel under Settings β Invite Codes for any other admins.
-
The bootstrap invite code is consumed on use and expires automatically β no cleanup needed.
Environment Configuration¶
Cloudflare Pages Environment Variables¶
In Cloudflare Dashboard:
-
Go to Pages β Your Project β Settings β Environment Variables
-
Add the following variables:
Production:
DATABASE_ID = your-production-db-id
ENVIRONMENT = production
SESSION_SECRET = random-64-char-string # Generate securely
ADMIN_EMAIL = admin@settimes.ca
PUBLIC_DATA_PUBLISH_ENABLED = false
CSP_ENFORCE = true # optional override (defaults to true in production)
EMAIL_PROVIDER = postmark
EMAIL_FROM = no-reply@settimes.ca
POSTMARK_API_TOKEN = <secret>
PUBLIC_URL = https://settimes.ca
Preview (optional):
DATABASE_ID = your-dev-db-id
ENVIRONMENT = development
SESSION_SECRET = different-random-string
Generate SESSION_SECRET:
# Generate random 64-character string
openssl rand -base64 48
First Deployment¶
Promote Local Data to Production (one-time)¶
- Build + run local Pages dev so the local D1 contains your latest data:
npm --prefix frontend run build
npx wrangler pages dev frontend/dist --port 8788
- Export local data to a production seed file:
./scripts/export-local-data.sh
- Commit
database/seed-production.sql, then push toorigin/main. - Import into production D1 (one-time):
./scripts/import-production-data.sh
- Keep
PUBLIC_DATA_PUBLISH_ENABLED=falseuntil youβre ready to go live.
Option 1: Deploy via Cloudflare Dashboard (Recommended for first deploy)¶
- Go to Cloudflare Dashboard
- Navigate to Pages
-
Click Create a project
-
Connect GitHub Repository
- Select Connect to Git
- Authorize Cloudflare
-
Select repository:
BreakableHoodie/settimesdotca -
Configure Build Settings
Production branch: main
Preview branches: dev, develop
Build command: cd frontend && npm install && npm run build
Build output directory: frontend/dist
Root directory: / (leave blank)
Environment variables:
- DATABASE_ID = [your-production-db-id]
- ENVIRONMENT = production
- SESSION_SECRET = [your-generated-secret]
- Configure Functions
- Functions directory:
functions(auto-detected) -
Compatibility date:
2025-01-01 -
Deploy
- Click Save and Deploy
- Wait 2-3 minutes for build
- Note the deployment URL:
settimes-xxx.pages.dev
Option 2: Deploy via Wrangler CLI¶
# Build frontend
cd frontend
npm run build
cd ..
# Deploy to production
wrangler pages deploy frontend/dist --project-name=settimes --branch=main
# Deploy to development
wrangler pages deploy frontend/dist --project-name=settimes --branch=dev
Custom Domain Setup¶
1. Add Custom Domain¶
In Cloudflare Dashboard:
- Go to Pages β Your Project β Custom domains
- Click Set up a custom domain
- Enter:
settimes.ca - Click Continue
2. Configure DNS¶
Cloudflare will automatically configure DNS if domain is managed by Cloudflare:
Automatic DNS Record:
Type: CNAME
Name: settimes.ca
Target: settimes-xxx.pages.dev
Proxy: Enabled (orange cloud)
If using external DNS:
Type: CNAME
Name: settimes.ca (or www)
Target: settimes-xxx.pages.dev
3. Add www Subdomain (Optional)¶
Type: CNAME
Name: www
Target: settimes.ca
Proxy: Enabled
4. Configure SPA Asset Routing¶
SPA navigation fallback is handled in wrangler.toml via:
[assets]
directory = "./frontend/dist"
not_found_handling = "single-page-application"
Do not add /* /index.html 200 to frontend/public/_redirects; Wrangler rejects that pattern because HTML handling already rewrites /index.html and /index, which creates a redirect loop during local Pages runs.
If you need host-level redirects such as www to apex, configure them in Cloudflare dashboard rules or your DNS/proxy layer instead of using a blanket SPA fallback rule.
5. Verify SSL Certificate¶
- SSL certificate auto-provisioned by Cloudflare
- Usually takes 5-15 minutes
- Check:
https://settimes.ca(should show padlock)
Post-Deployment¶
1. Verify Deployment¶
Check Frontend:
curl https://settimes.ca
# Should return HTML
Check API:
curl https://settimes.ca/api/schedule?event=current
# Should return JSON with either:
# - HTTP 200 and { event, bands } when a current/upcoming published event exists
# - HTTP 404 and { error: "Event not found", message: "No published events available" } when none exists
Check Database:
wrangler d1 execute settimes-production-db --env production --command="SELECT COUNT(*) FROM users"
# Should return count of users
2. Test Admin Login¶
- Go to
https://settimes.ca/admin - Log in with admin credentials
- Verify dashboard loads
- Create test event
3. Run Smoke Tests¶
The GitHub Actions release workflow now runs an automated smoke suite after every successful main and dev deployment. It verifies:
/returns the public app HTML/adminreturns the admin shell HTML/api/schedule?event=currentreturns valid JSON for the current publish state (200,404, or503depending on environment and available events)
Manual post-release testing is still recommended for login, publishing, and other data-changing admin flows.
Checklist:
- [ ] Homepage loads (/)
- [ ] Admin panel loads (/admin)
- [ ] Can log in successfully
- [ ] Can create event
- [ ] Can create venue
- [ ] Can create performer
- [ ] Can publish event
- [ ] Public schedule shows event (/api/schedule)
- [ ] Band profile pages work (/bands/[event]/[band])
- [ ] Mobile responsive (test on phone)
- [ ] PWA installable (mobile)
- [ ] HTTPS works (green padlock)
- [ ] www redirects to non-www
4. Configure Headers¶
Verify frontend/public/_headers is deployed correctly:
curl -I https://settimes.ca
# Check for:
# - Strict-Transport-Security
# - X-Frame-Options
# - Content-Security-Policy
5. Set Up Monitoring¶
Enable Cloudflare Web Analytics:
- Go to Analytics β Web Analytics
- Add site:
settimes.ca - Copy tracking code
- Add to
frontend/index.html(if not already present)
Configure Alerts:
- Go to Notifications
- Create alerts for:
- High error rate (>5%)
- Traffic spike (unusual patterns)
- SSL certificate expiration
- Low performance score
Continuous Deployment¶
Automatic Deployments¶
On Push to main:
- Runs CI, applies remote D1 migrations, verifies the live D1 schema, deploys to Pages, and runs smoke checks against production
- Deploys to
settimes.ca - Takes ~2-3 minutes
On Push to dev:
- Runs CI, applies remote D1 migrations, verifies the live D1 schema, deploys to Pages, and runs smoke checks against development
- Deploys to
dev.settimes.ca - Takes ~2-3 minutes
On Pull Request:
- Runs the same build and test pipeline, but does not perform a direct Pages deployment from GitHub Actions
- Any preview deployment behavior is controlled by Cloudflare dashboard Git integration, not by the checked-in workflow
GitHub Actions Release Inputs¶
The checked-in release workflow expects these repository secrets:
CF_ACCOUNT_IDCF_PAGES_API_TOKENCRON_SECRETβ required by.github/workflows/scheduled-jobs.yml(daily scheduled tasks). Generate a strong random value (e.g.openssl rand -base64 32) and set it both here as a GitHub Actions repository secret and in Cloudflare Pages β Settings β Environment Variables (Production) with the same value. The/api/internal/run-scheduledendpoint fails closed (503) if this variable is missing from the Pages environment.
The workflow also supports these repository variables:
CF_PAGES_PROJECT_NAME(default:settimesdotca)CF_D1_PRODUCTION_DATABASE_NAME(default:settimes-production-db)CF_D1_DEVELOPMENT_DATABASE_NAME(optional; defaults toCF_D1_PRODUCTION_DATABASE_NAMEwhen unset)CF_PRODUCTION_SMOKE_URL(default:https://settimes.ca)CF_DEVELOPMENT_SMOKE_URL(default:https://dev.settimes.ca)
If development uses a separate D1 database, set CF_D1_DEVELOPMENT_DATABASE_NAME explicitly. If development intentionally shares the production database, leave it unset or set it to the same value as production.
Manual Deployment¶
Via Wrangler:
npm --prefix frontend ci
npm --prefix frontend run build
./frontend/node_modules/.bin/wrangler pages deploy frontend/dist --project-name=settimesdotca --branch=main
Via Git:
git push origin main
# Wait for CI, D1 migration, schema verification, deploy, and smoke checks to finish
Deployment Status¶
Check via Dashboard:
- Go to Pages β Your Project β Deployments
- View build logs, deployment history
Check via CLI:
wrangler pages deployment list --project-name=settimes
Rollback & Recovery¶
Rollback to Previous Deployment¶
Via Dashboard:
- Go to Pages β Your Project β Deployments
- Find previous successful deployment
- Click β― β Rollback to this deployment
- Confirm rollback
Takes effect immediately (no rebuild required).
Rollback via CLI¶
# List deployments
wrangler pages deployment list --project-name=settimes
# Rollback to specific deployment
wrangler pages deployment rollback <deployment-id> --project-name=settimes
Database Recovery¶
Restore from Backup:
# Export current database (backup)
wrangler d1 export settimes-production-db --output=backup-$(date +%Y%m%d).sql
# Restore from backup
wrangler d1 execute settimes-production-db --file=backup-20251119.sql
Point-in-Time Recovery:
Contact Cloudflare support for point-in-time recovery (Enterprise feature).
Monitoring & Maintenance¶
Daily Checks¶
# Check error logs
wrangler pages deployment tail --project-name=settimes --status error
# Check database size
wrangler d1 info settimes-production-db
# Check active sessions
wrangler d1 execute settimes-production-db --command="
SELECT COUNT(*) FROM sessions WHERE expires_at > datetime('now')
"
Weekly Maintenance¶
# Clean up expired sessions
wrangler d1 execute settimes-production-db --command="
DELETE FROM sessions WHERE expires_at < datetime('now', '-7 days')
"
# Export database backup
wrangler d1 export settimes-production-db --output=backups/weekly-$(date +%Y%m%d).sql
# Review audit logs
wrangler d1 execute settimes-production-db --command="
SELECT * FROM audit_logs
WHERE created_at > datetime('now', '-7 days')
ORDER BY created_at DESC LIMIT 100
" > logs/audit-$(date +%Y%m%d).log
Monthly Maintenance¶
- [ ] Update npm dependencies (
npm update) - [ ] Run security audit (
npm audit) - [ ] Review Cloudflare analytics
- [ ] Test disaster recovery plan
- [ ] Archive old audit logs (>90 days)
- [ ] Review and optimize database queries
- [ ] Check SSL certificate status
- [ ] Review rate limiting effectiveness
Performance Monitoring¶
Core Web Vitals:
# Run Lighthouse audit
cd frontend
npm run lighthouse
API Performance:
# Check API response times
curl -w "@curl-format.txt" -o /dev/null -s https://settimes.ca/api/schedule
# curl-format.txt contents:
# time_total: %{time_total}s
# time_namelookup: %{time_namelookup}s
# time_connect: %{time_connect}s
Troubleshooting Deployment Issues¶
Build Fails¶
Error: "npm install failed"
Solution:
- Check Node version in Cloudflare settings (set to 20+)
- Verify
package.jsonandpackage-lock.jsonare committed - Check build logs for specific npm errors
Error: "Build command exited with code 1"
Solution:
- Run build locally:
cd frontend && npm run build - Fix any TypeScript/lint errors
- Check for missing dependencies
Database Connection Issues¶
Error: "DATABASE binding not found"
Solution:
- Verify
wrangler.tomlhas correctd1_databasesbinding - Ensure
DATABASEbinding name matches code - Redeploy after fixing
wrangler.toml
Error: "no such table: users"
Solution:
- Run migrations:
wrangler d1 migrations apply settimes-production-db - Verify migrations ran successfully
- Check migration files in
migrations/directory
Domain & SSL Issues¶
Error: "This site can't provide a secure connection"
Solution:
- Wait 15 minutes for SSL provisioning
- Verify DNS is correctly configured
- Check Cloudflare SSL/TLS setting (should be "Full" or "Full (strict)")
Error: "DNS_PROBE_FINISHED_NXDOMAIN"
Solution:
- Verify CNAME record points to
settimes-xxx.pages.dev - Wait for DNS propagation (up to 48 hours, usually minutes)
- Use
dig settimes.cato check DNS resolution
Function Errors¶
Error: "500 Internal Server Error" on API endpoints
Solution:
- Check function logs:
wrangler pages deployment tail - Verify environment variables are set
- Check database connection
- Review function code for errors
Security Checklist¶
Before Production:
- [ ] SESSION_SECRET is strong and unique
- [ ] Admin password is strong (12+ characters)
- [ ] HTTPS is enabled (Cloudflare SSL)
- [ ] HSTS header is set (
_headersfile) - [ ] CSP header is configured
- [ ] CSRF protection is enabled
- [ ] Rate limiting is configured
- [ ] Audit logging is enabled
- [ ] Database backups are automated
- [ ] No secrets in git repository
Additional Resources¶
Documentation:
SetTimes Docs:
- Admin Handbook - System administration
- User Guide - For event organizers
- API Documentation - API reference
- Troubleshooting - Common issues
Version: 2.1 Last Updated: January 2026 Target Event: Long Weekend Band Crawl (Feb 15, 2026)
Ready to deploy? Follow this guide step-by-step for a successful production deployment!