Runbook: Space Profile Intake (v1)

LIVE (M3-authoritative) as of 2026-07-04

Audience: operators deploying or troubleshooting this feature
Related: docs/superpowers/specs/2026-07-04-space-profile-intake-design.md (design), docs/codex/2026-07-04-progressive-web-intake-design.md (parent design), riff-m1-failover-runbook.html (the two-machine phone setup this feature runs on top of)

What this feature is

A caller on the Space Channel line (321-559-MARS) presses/says option 0 and, in one short back-and-forth, gives their name, agrees (or doesn't) to be texted a link, reads back a verification code so we know the phone number is really theirs, and says the space topics they want updates on. If they say yes to the text, they get a link to a live web page. While the call is still going, that page fills in with what they just said, word for word. After the call, they can revisit the same page and edit anything — their name, their topic list — and it saves.

The point: voice is fast but bad at exact text (spellings, long lists, corrections). The page is the "write it down properly" surface for whatever the call already collected.

Caller story, start to finish:

  1. Caller dials in, picks option 0.
  2. "What's your name?" → caller answers.
  3. "Can we text you links...say yes to allow text messages or say no to deny." → caller says yes or no.
  4. If yes: we text a link plus a 6-digit code, in two lines (link first, code second). Caller reads the code back on the call. Two wrong tries and we give up on verifying but keep going voice-only.
  5. "Tell me the most interesting things about space you'd like updates on — just speak freely." Caller talks; we ask "anything else?" until they're done (or after 4 rounds, or two rounds of silence).
  6. We save what we heard, then read it back: "I have [name], texting [allowed/not allowed], interests: [what they said] — did I get that right?"
  7. Call loops back to the main Space Channel menu. No dead end.
  8. If they opened the texted link, they now see (or, once verified, can edit) exactly what was saved, plus a running transcript of what they said during the call.
  9. Next time they call from the same verified number, both the phone and the page greet them by name.

The voice sequence (what actually runs)

Compiled by riff/business_pack/importers/space_channel_widgets.py into flows/space_channel_widgets.yaml option 0 (space_profile_intake). Do not hand-edit that YAML — it is regenerated output and gets overwritten.

name → consent (yes/no) → [yes: send link+code → verify code] → interests
     → save → confirm-back → loop back to the main menu

If the caller says no to texting: name and interests are still collected and saved, nothing is texted, and there is no web page for that call.

The page (/intake.html)

Required deploy environment

These three environment variables gate the feature. Get any of them wrong and either the feature silently doesn't run, or M1 starts writing to a database it must never own.

VariableValueWhereWhy
RIFF_INTAKE_STORE_AUTHORITY 1 M3 .env ONLY. Never set on M1. Turns on the SQLite-backed /api/intake/* routes and makes this machine the single writer of data/profiles/profiles.db. Both machines load the same riff/phone/server.py code (active-active failover), so without this gate M1 would silently create its own separate profile database and fork the data away from M3.
RIFF_INTAKE_INTERNAL_TOKEN <shared secret, same string on both machines> M3 .env AND M1 .env, must match exactly. The intake API's internal endpoints (session create, mark-verified, transcript append) require this token in an X-RIFF-Intake-Internal header. Voice-side code on M1 needs it to authenticate its writes to M3's API. If the two machines' tokens don't match, M1's voice saves are silently rejected (403) and fall back to the local spool — everything looks fine on the call but nothing reaches the M3 database until the spool is manually flushed.
RIFF_INTAKE_API_BASE http://100.127.252.53:8767 (the M3's Tailscale address) M1 .env only. Tells M1's voice-side intake client where to send its writes. M3 doesn't need this — it talks to itself over 127.0.0.1:8767.

A misconfigured deploy (e.g., RIFF_INTAKE_STORE_AUTHORITY=1 accidentally set on M1 too) is the single most dangerous failure mode here: it splits the profile data across two independent databases with no reconciliation. Check this first if data looks like it's disappearing or two different names show up for the same caller.

Where the data lives

How to check it's working

# 1. Schema is being served (confirms the API routes registered — i.e.
#    RIFF_INTAKE_STORE_AUTHORITY=1 took effect on M3):
curl -s "http://127.0.0.1:8767/api/intake/schema?flow_id=space_channel_widgets&capability=space_profile_intake"

# 2. Pull a real session by its token (from a texted link, or one you
#    created for testing) — internal endpoints need the shared token:
curl -s "http://127.0.0.1:8767/api/intake/session?token=<token>"

# 3. Internal-only endpoints (session create, mark-verified, transcript)
#    require the header:
curl -s -X POST "http://127.0.0.1:8767/api/intake/session" \
  -H "X-RIFF-Intake-Internal: $RIFF_INTAKE_INTERNAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"call_id":"test","phone":"+15551234567","flow_id":"space_channel_widgets","capability_id":"space_profile_intake"}'

# 4. Logs — same location as the rest of the phone-node setup:
tail -f /tmp/riff-phone-node/phone-server.log | grep -i intake

A healthy M3 boot log line looks like registering the intake routes; a node that is intentionally NOT the authority (M1) logs "intake API not registered: this node is not the intake store authority" — that line on M1 is expected and correct, not a bug.

Troubleshooting

SymptomLikely causeCheck
Page stays locked forever, never unlocks The code check on the call never passed (wrong code read back, or caller gave up after two tries and only got voice-only intake) Pull the session (GET /api/intake/session?token=...) — if locked: true persists, the call's verify step didn't complete. Re-verify by calling back.
"Hello, ⟨name⟩" greeting missing on a repeat call Caller identity wasn't populated (first call never got a verified name saved), or their caller ID is blocked/unavailable so the lookup can't run Check caller_identity for the phone number; blocked caller ID has no fix — it's a silent skip by design, not an error.
Saves from the call aren't showing up on M3 / page never updates Either RIFF_INTAKE_STORE_AUTHORITY isn't 1 on M3 (routes never registered), or the write is sitting in the spool because M3 was briefly unreachable Curl the schema endpoint (above) to confirm routes exist; check data/profiles/spool/*.jsonl for unreplayed lines; check the internal-token match between M1 and M3.
M1 writes are silently rejected (403) RIFF_INTAKE_INTERNAL_TOKEN differs between M1 and M3 .env Compare the two files directly; this fails closed (writes fall back to spool) rather than loud, so it's easy to miss.
Data looks split / two names for one caller RIFF_INTAKE_STORE_AUTHORITY=1 was accidentally set on M1 too — now two independent databases exist Check M1's .env; it must NOT have this flag. Unset it and restart the M1 node.

Known v0 limits