Skip to main content

WhatsApp MVP Operator Runbook

Use this runbook to validate and operate the WhatsApp Event Submission MVP in hosted dev. It is written for the founder/operator path: run the smoke, inspect the result, know what is safe, and know where to look when something fails.

Operating Boundaries

  • Supabase is the source of truth for submissions, candidates, moderation, audit, and connector jobs.
  • Hosted-dev smoke writes only to a disposable hosted-dev Supabase project.
  • Calendar connector output is dry-run only in this phase. It creates package/job rows; it must not write to GoLatinDance, MiGente, WordPress, EventON, social platforms, or production calendars.
  • Human moderation is required before real publishing. WhatsApp can notify and collect context, but approval actions must be recorded as system state.
  • Phone numbers, Twilio tokens, Supabase service-role keys, database URLs, and phone hash peppers are secrets. Do not paste values in issues, PRs, docs, screenshots, or workflow logs.

Required Secrets And Variables

Configure GitHub Environment secrets in gld-hosted-dev, not broad repository secrets:

  • GLD_HOSTED_DEV_SUPABASE_URL
  • GLD_HOSTED_DEV_SUPABASE_SERVICE_ROLE_KEY
  • GLD_HOSTED_DEV_DATABASE_URL
  • GLD_HOSTED_DEV_WHATSAPP_INGEST_URL
  • GLD_HOSTED_DEV_TWILIO_AUTH_TOKEN
  • GLD_HOSTED_DEV_WHATSAPP_PHONE_HASH_PEPPER
  • GLD_SOURCE_TRACKER_WORKBOOK_URL, only for write_smoke

Configure the deployed whatsapp-ingest Supabase Edge Function with:

  • SUPABASE_URL
  • SUPABASE_SERVICE_ROLE_KEY
  • TWILIO_AUTH_TOKEN
  • WHATSAPP_PHONE_HASH_PEPPER, or MOBILIS_WHATSAPP_PHONE_HASH_PEPPER
  • TWILIO_WEBHOOK_PUBLIC_URL, when the exact public URL used for Twilio signature validation needs to be pinned
  • MOBILIS_TENANT_ID, optional for future tenant-scoped rows

Optional GitHub variables:

  • GLD_SMOKE_MARKET, default Washington DC
  • GLD_WHATSAPP_SMOKE_MARKET, defaults to GLD_SMOKE_MARKET
  • GLD_SMOKE_BLOCKED_PROJECT_REFS, comma-separated extra Supabase project refs that the smoke must refuse

Twilio Webhook Setup Notes

For hosted dev, configure the Twilio WhatsApp sender or sandbox inbound webhook to call the hosted-dev Supabase Edge Function URL:

POST https://<hosted-dev-ref>.supabase.co/functions/v1/whatsapp-ingest

The request must be Twilio's normal form-encoded inbound message webhook. The function validates X-Twilio-Signature using TWILIO_AUTH_TOKEN, the public URL, and the submitted form fields. The URL in Twilio, GLD_HOSTED_DEV_WHATSAPP_INGEST_URL, and the Edge Function TWILIO_WEBHOOK_PUBLIC_URL must match exactly for signature validation.

Do not point production Twilio senders, production auth tokens, or shared Supabase projects at the hosted-dev workflow.

Run Hosted-Dev Smoke

Start with read-only validation:

gh workflow run gld-hosted-dev-smoke.yml -f mode=verify

Run the WhatsApp end-to-end smoke only after confirming the target is disposable hosted dev:

gh workflow run gld-hosted-dev-smoke.yml \
-f mode=whatsapp_smoke \
-f write_confirmation=I_UNDERSTAND_THIS_WRITES_TO_HOSTED_DEV \
-f approval_issue_number=179 \
-f approval_comment_url="https://github.com/vitalychernobyl/00-MobilisArchitectureInfo/issues/179#issuecomment-APPROVAL_COMMENT_ID"

The approval comment must be authored by vitalychernobyl and contain APPROVE_GLD_HOSTED_DEV_WRITE, the repository, workflow, mode, and an unexpired UTC date. Do not put secrets in that comment.

Find the latest run:

gh run list --workflow gld-hosted-dev-smoke.yml --limit 5

Inspect logs and artifacts:

gh run view <run-id> --log
gh run download <run-id> -D tmp/whatsapp-smoke-artifacts

Read Smoke Results

A passing whatsapp_smoke run proves:

  • the workflow refused blocked/shared/prod-looking targets before writing
  • migrations applied to the disposable hosted-dev target
  • a signed Twilio-style inbound payload reached whatsapp-ingest
  • Supabase contains the submitter, thread, message, evidence, and audit rows
  • the ready submission mapped to a GLD event candidate
  • moderation readback shows the candidate was approved by the smoke actor
  • The Events Calendar and EventON connector jobs were created as dry-run only
  • smoke artifacts passed redaction checks for phone numbers and secret names

Primary artifact files:

  • gld-whatsapp-smoke.json: full redacted smoke summary
  • whatsapp-ingest-response-redacted.json: redacted webhook response
  • whatsapp-smoke-summary.md: concise operator summary from the smoke runner
  • gld-whatsapp-smoke-summary.md: concise shell-runner summary

Expected summary fields:

  • rows.evidenceCount is at least 2
  • rows.auditCount is at least 1
  • candidate.moderationStatusAfterDecision is approved
  • every connectors[] row has mode: dry_run
  • every connectors[] row has submitted: false
  • every connectors[] row has noExternalWrite: true
  • safeguards.phoneNumbersRedacted and safeguards.secretsRedacted are true

Dry-Run Versus Live Writes

whatsapp_smoke does write test data to hosted-dev Supabase. That is expected.

It must not write to production or external calendars. The connector phase creates gl_calendar_connector_jobs rows with dry-run payloads. The workflow intentionally leaves external calendar credentials empty and asserts dry-run connector behavior.

Live calendar writes remain out of scope until a separate issue/PR explicitly verifies staging endpoints, credentials, idempotency, rollback behavior, operator documentation, and production approval gates.

Moderation Handoff

For real candidate review, use the moderation CLI against an approved dev/internal environment:

cd workers/playwright-source-scanner
npm run dev -- gld-moderation-list --market "Washington DC" --status needs_review
npm run dev -- gld-moderation-show --candidate-id <candidate-id>

Record a decision:

npm run dev -- gld-moderation-decide \
--candidate-id <candidate-id> \
--decision approve \
--actor-id <operator-id> \
--reason "approved by operator review"

Use reject, needs_info, mark_duplicate, or hold when the evidence does not support approval. If the event lacks required fields, keep the candidate in review or needs-info state and use the WhatsApp follow-up path rather than publishing from incomplete data.

Troubleshooting

Invalid Twilio Signature

Symptoms:

  • webhook response has invalid_twilio_signature
  • workflow fails after the signed fixture POST
  • Twilio console shows webhook delivery but the function returns 403

Check:

  • GLD_HOSTED_DEV_TWILIO_AUTH_TOKEN matches the Edge Function TWILIO_AUTH_TOKEN
  • the webhook URL used to sign the fixture is exactly the URL received by the function
  • TWILIO_WEBHOOK_PUBLIC_URL is set on the Edge Function when URL rewriting or alternate function hostnames are involved
  • Twilio is sending POST form-encoded webhook fields, not JSON

Duplicate Messages

Symptoms:

  • webhook response status is duplicate
  • no new candidate appears from the expected message

Meaning:

  • idempotency is working by MessageSid
  • Twilio retries or reused fixture payloads should not create duplicate message rows

Check:

  • gl_submission_messages.provider_message_id
  • workflow logs for the fixture providerMessageId
  • whether the smoke reused an old artifact or manually replayed a prior payload

Missing Fields Or Thread Not Ready

Symptoms:

  • candidate mapping fails because the thread is not ready_for_candidate
  • candidate remains collecting_fields or needs_submitter_info
  • moderator sees missing title, date, start time, venue, city, market, organizer, or relationship

Check:

  • gl_submission_threads.submission_state
  • gl_submission_evidence rows for message text, links, and media
  • gl_followup_requests for requested clarification
  • extraction output and missing_fields

Operator action:

  • ask for the missing fields through the follow-up path
  • do not approve or publish incomplete candidates
  • re-run candidate mapping only after the thread state is ready and required fields are present

Connector Dry-Run Failures

Symptoms:

  • connector job is missing
  • connector job status is blocked or failed
  • smoke summary does not show noExternalWrite: true

Check:

  • candidate has candidate_status: approved
  • approved_by and approved_at are set
  • gl_calendar_connector_jobs exists in hosted dev
  • connector job dry_run is true
  • connector job metadata has hosted-dev environment and no live-write flag
  • workflow target was not blocked as shared/prod/local

Do not fix this by adding production WordPress/EventON credentials to the smoke workflow. The correct fix is usually candidate approval state, missing migration, or connector dry-run package validation.

Missing Tables Or Supabase API Errors

Symptoms:

  • workflow fails during table checks
  • Supabase JS reads fail for new gl_* tables
  • hosted-dev smoke passes migrations but cannot read rows

Check:

  • supabase db push output in the workflow artifact/log
  • table existence in gld-tables.txt
  • hosted-dev project ref matches the database URL and function URL
  • Data API exposure/schema cache for new public tables in the hosted dev project

Redaction Failure

Symptoms:

  • workflow fails with Sensitive value found in smoke artifact

Action:

  • treat the artifact as sensitive
  • inspect locally or in restricted GitHub Actions access only
  • remove raw phone numbers, secret variable names, or secret values from the smoke output before retrying