Complete all steps in order. Migrations are additive but the webhook will error until 0162 exists and CAL_WEBHOOK_SECRET is set.
Step 1 — Deploy
Ensure the branch containing migrations 0162–0166 is deployed to your server before running migrations.
Step 2 — Apply migrations
DATABASE_URL="mysql://user:pass@host:port/db" pnpm db:migrate
Preview first with pnpm db:migrate:dry — it lists pending migrations without applying them.
| Migration | Adds |
|---|---|
0162 |
demo_bookings table (core booking data) |
0163 |
follow_up_sent_at column |
0164 |
meeting_url, recording_url, disposition, notes, disposition_updated_at |
0165 |
end_time, duration_minutes |
0166 |
confirmation_status, confirmation_token, confirmed_at, reminder_count, last_reminder_at, attended_at, recap_sent_at |
Verify:
SHOW COLUMNS FROM demo_bookings;
-- expect 20+ columns including attended_at, disposition, duration_minutes
Step 3 — Set environment variables
Required
| Variable | Example | Purpose |
|---|---|---|
CAL_WEBHOOK_SECRET |
whsec_abc123 |
Validates Cal webhook signatures (HMAC-SHA256) — webhook returns 401 without it |
PUBLIC_APP_URL |
https://www.thesmartpro.io |
Base URL for RSVP confirm/decline links in reminder emails — without it the links are relative and broken |
RESEND_API_KEY |
re_abc123 |
Transactional email delivery |
Optional / tuning
| Variable | Default | Purpose |
|---|---|---|
SALES_NOTIFICATION_EMAIL |
info@thesmartpro.io |
Where new booking notifications go |
DEMO_REMINDER_LEAD_HOURS |
24 |
Only remind for demos starting within this many hours |
DEMO_REMINDER_MAX |
2 |
Max reminder + chaser emails per booking |
DEMO_REMINDER_GAP_HOURS |
6 |
Minimum hours between reminder sends |
DISABLE_DEMO_LIFECYCLE_JOBS |
(unset) | Set to 1 to halt all lifecycle emails |
Step 4 — Configure Cal.com
- On the SmartPRO Hub team → Webhooks → Add webhook
- Subscriber URL:
https://www.thesmartpro.io/api/webhooks/cal - Secret: paste your
CAL_WEBHOOK_SECRETvalue - Enable triggers:
BOOKING_CREATEDBOOKING_CANCELLEDBOOKING_RESCHEDULEDMEETING_STARTED← required for attendance captureMEETING_ENDED← required for no-show detectionRECORDING_READY← required for recording URL capture- Click Ping → expect HTTP 200
Step 5 — Smoke test
Book a test demo
Go to your Cal.com booking page and book a slot using a test email.
Check admin console
`/admin/demo-bookings` → new row should appear within seconds.
Check sales notification
`SALES_NOTIFICATION_EMAIL` inbox should have a "New demo booked" email.
Trigger reminder manually
Wait for the 30-min job to run, or temporarily set `DEMO_REMINDER_LEAD_HOURS=9999` and restart.
Click RSVP link
Open the reminder email → click "Yes, I'll attend" → check `confirmation_status = confirmed` in DB.
Simulate attendance
Trigger a `MEETING_STARTED` ping from Cal (or set `attended_at` directly in DB) → check recap job sends.
Test cancellation
Cancel the Cal booking → check re-engage email arrives.
Rollback / pause
| Action | Command |
|---|---|
| Pause all lifecycle emails | DISABLE_DEMO_LIFECYCLE_JOBS=true + restart |
| Stop ingesting new bookings | Remove the Cal webhook in Cal dashboard |
| Revert migrations | Migrations are additive — no destructive rollback needed; old code just won't use new columns |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Webhook returns 401 | Wrong or missing CAL_WEBHOOK_SECRET |
Match secret in Cal and env |
| Webhook returns 503 | DB unavailable (migrations not applied) | Check DB + run pnpm db:migrate |
| Webhook returns 500 | Migration 0162+ not applied (missing columns) |
Run pnpm db:migrate |
| Reminder emails not sending | RESEND_API_KEY missing |
Set env var |
| RSVP link 404 / relative | PUBLIC_APP_URL unset/wrong |
Set to https://www.thesmartpro.io |
| Everyone shows No-show | MEETING_STARTED trigger off |
Enable in Cal dashboard |
| Recap not resending | recap_sent_at set (idempotent guard) |
Expected — fires once per booking |