Skip to content

SetTimes Production Deployment Guide

Deploying to Cloudflare Pages + Functions + D1


Table of Contents

  1. Overview
  2. Prerequisites
  3. Initial Setup
  4. Database Setup
  5. Environment Configuration
  6. First Deployment
  7. Custom Domain Setup
  8. Post-Deployment
  9. Continuous Deployment
  10. Rollback & Recovery
  11. 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 wrangler CLI
  • 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_SIGNUP is 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):

  1. 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())").

  1. Navigate to /admin/signup?code=REPLACE-WITH-SECURE-UUID and complete account creation. The password is hashed with PBKDF2 automatically.

  2. Once logged in, generate additional invite codes from the admin panel under Settings β†’ Invite Codes for any other admins.

  3. The bootstrap invite code is consumed on use and expires automatically β€” no cleanup needed.


Environment Configuration

Cloudflare Pages Environment Variables

In Cloudflare Dashboard:

  1. Go to Pages β†’ Your Project β†’ Settings β†’ Environment Variables

  2. 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)

  1. 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
  1. Export local data to a production seed file:
./scripts/export-local-data.sh
  1. Commit database/seed-production.sql, then push to origin/main.
  2. Import into production D1 (one-time):
./scripts/import-production-data.sh
  1. Keep PUBLIC_DATA_PUBLISH_ENABLED=false until you’re ready to go live.
  1. Go to Cloudflare Dashboard
  2. Navigate to Pages
  3. Click Create a project

  4. Connect GitHub Repository

  5. Select Connect to Git
  6. Authorize Cloudflare
  7. Select repository: BreakableHoodie/settimesdotca

  8. 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]
  1. Configure Functions
  2. Functions directory: functions (auto-detected)
  3. Compatibility date: 2025-01-01

  4. Deploy

  5. Click Save and Deploy
  6. Wait 2-3 minutes for build
  7. 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:

  1. Go to Pages β†’ Your Project β†’ Custom domains
  2. Click Set up a custom domain
  3. Enter: settimes.ca
  4. 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

  1. Go to https://settimes.ca/admin
  2. Log in with admin credentials
  3. Verify dashboard loads
  4. 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
  • /admin returns the admin shell HTML
  • /api/schedule?event=current returns valid JSON for the current publish state (200, 404, or 503 depending 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:

  1. Go to Analytics β†’ Web Analytics
  2. Add site: settimes.ca
  3. Copy tracking code
  4. Add to frontend/index.html (if not already present)

Configure Alerts:

  1. Go to Notifications
  2. Create alerts for:
  3. High error rate (>5%)
  4. Traffic spike (unusual patterns)
  5. SSL certificate expiration
  6. 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_ID
  • CF_PAGES_API_TOKEN
  • CRON_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-scheduled endpoint 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 to CF_D1_PRODUCTION_DATABASE_NAME when 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:

  1. Go to Pages β†’ Your Project β†’ Deployments
  2. Find previous successful deployment
  3. Click β‹― β†’ Rollback to this deployment
  4. 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.json and package-lock.json are 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.toml has correct d1_databases binding
  • Ensure DATABASE binding 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.ca to 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 (_headers file)
  • [ ] 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:


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!