|
| 1 | +# Onboarding, users, and going live |
| 2 | + |
| 3 | +This document explains how people become users today, what they need to text, |
| 4 | +how Photon Spectrum fits in, and when you need persistent storage. It is aimed |
| 5 | +at hosting the product and pairing it with a marketing landing page in another |
| 6 | +repo. |
| 7 | + |
| 8 | +For what users can say after they are set up, see [`MESSAGING.md`](MESSAGING.md). |
| 9 | +For runtime internals, see [`TECHNICAL.md`](TECHNICAL.md). |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## The two different questions (this is the common confusion) |
| 14 | + |
| 15 | +People often mix up **“can many users use it at the same time?”** and **“do we |
| 16 | +need a database?”** They are separate: |
| 17 | + |
| 18 | +| Question | Answer today | Needs a database? | |
| 19 | +| --- | --- | --- | |
| 20 | +| Can Alice and Bob both text the agent and get their own alerts? | **Yes** — each iMessage thread is a separate user in memory. | **No** (while the agent process is running) | |
| 21 | +| Do Alice’s settings survive if we restart or redeploy the agent? | **No** — preferences live only in RAM until she texts again. | **Yes**, if you want settings (and “registered” state) to survive restarts | |
| 22 | +| Can we run two agent servers behind a load balancer? | **Not cleanly** — each instance has its own memory; users would be split and confused. | **Yes**, for shared state | |
| 23 | +| Does the landing page need to write users into a DB before they can text? | **No** — texting the number is enough to start using the product as built today. | Only if *you* want web signup, billing, or invite-only access | |
| 24 | + |
| 25 | +**Short version:** persistent storage is **not** required for *concurrent* |
| 26 | +multi-user chat. It **is** required for anything that must **outlive a single |
| 27 | +process** (restarts, deploys, accounts, billing, invite lists, analytics tied |
| 28 | +to identity). |
| 29 | + |
| 30 | +--- |
| 31 | + |
| 32 | +## How Photon Spectrum works for this project |
| 33 | + |
| 34 | +The agent (`ts_agent`) runs a **Spectrum server** that listens for inbound |
| 35 | +iMessage events. When someone texts your line, Spectrum delivers a |
| 36 | +`[space, message]` tuple to the agent: |
| 37 | + |
| 38 | +- **`space`** — the conversation (thread). One-to-one chats get one `space.id` |
| 39 | + per person; that id is how we store preferences today. |
| 40 | +- **`message.sender`** — who sent the text (Spectrum’s sender id for that |
| 41 | + person). |
| 42 | + |
| 43 | +The agent does **not** implement a separate “sign up” API and does **not** send |
| 44 | +“you need to register” replies. Spectrum is the front door: **only if Photon |
| 45 | +delivers the text into `app.messages`** does your code run (preferences, Grok, |
| 46 | +alerts). |
| 47 | + |
| 48 | +If an unknown tester sees a registration message on iMessage, that is almost |
| 49 | +certainly **Photon’s cloud gate** (sender not linked to your project in shared |
| 50 | +mode), not `ts_agent`. Your console will show **no** `[iMessage] inbound event` |
| 51 | +for those texts. |
| 52 | + |
| 53 | +Photon’s [Spaces and Users](https://photon.codes/docs/spectrum-ts/spaces-and-users) |
| 54 | +doc describes the **SDK model** (`space`, `user`, `space.send`, `im.user(phone)`) |
| 55 | +for conversations your **code** creates or replies in. It does **not** define |
| 56 | +end-user “registration” on the platform — that lives in Photon’s **iMessage line / |
| 57 | +plan** setup; see [iMessage provider → Line model](https://photon.codes/docs/spectrum-ts/providers/imessage#line-model). |
| 58 | + |
| 59 | +### Shared pool vs dedicated line (Photon plan) |
| 60 | + |
| 61 | +From Photon’s [iMessage provider docs](https://photon.codes/docs/spectrum-ts/providers/imessage#line-model): |
| 62 | + |
| 63 | +| Photon plan | Line model | What end users see | |
| 64 | +| --- | --- | --- | |
| 65 | +| Free / Pro | **Shared pool** — routing may use different pool numbers per recipient | iMessage from a number that can differ; senders often must be **linked** to your project in the dashboard | |
| 66 | +| Business | **Dedicated** — one number for your project | Everyone texts the **same** agent number | |
| 67 | + |
| 68 | +Run `cd ts_agent && npm run build && npm run info` to see what **your** project |
| 69 | +token reports (`dedicated` vs shared). Startup also prints a hint when shared. |
| 70 | + |
| 71 | +| Token / mode | What users text | What you configure | |
| 72 | +| --- | --- | --- | |
| 73 | +| **Dedicated** | Your project’s phone number(s) from `npm run info` | Provision the line in the Photon dashboard; strangers can DM that number without per-phone linking in our repo | |
| 74 | +| **Shared** | The shared Photon number (dashboard) | **Link each tester’s sending phone** to this project, or Photon blocks them (auto-reply / no agent logs) | |
| 75 | + |
| 76 | +Put the number users should text on your landing page. On **Business / |
| 77 | +dedicated**, that is your stable agent line. On **Free/Pro / shared**, also |
| 78 | +document that testers must complete Photon’s sender linking (or upgrade plan). |
| 79 | + |
| 80 | +### Blue bubble only |
| 81 | + |
| 82 | +The agent only processes **`platform === "iMessage"`** and **text** messages. |
| 83 | +Green-bubble SMS or non-text events are logged and skipped. Users need an Apple |
| 84 | +device (or Mac) with iMessage to talk to the agent. |
| 85 | + |
| 86 | +--- |
| 87 | + |
| 88 | +## Can anyone register by texting? What do they say? |
| 89 | + |
| 90 | +### Two layers: Photon gate vs your agent |
| 91 | + |
| 92 | +| Layer | Who controls it | Unknown phone texts your line | |
| 93 | +| --- | --- | --- | |
| 94 | +| **Photon (iMessage cloud)** | Dashboard linking (shared) or dedicated line (Business) | May get “need to register” / no delivery to agent | |
| 95 | +| **`ts_agent` (this repo)** | No allowlist in code | Once `[iMessage]` logs appear, first text = user in memory | |
| 96 | + |
| 97 | +There is **no allowlist**, **no invite code**, and **no required magic word** |
| 98 | +in **this codebase**. Anyone whose message **reaches** `app.messages` can: |
| 99 | + |
| 100 | +1. Become a “user” on first contact (their thread is stored in memory). |
| 101 | +2. Have every subsequent text interpreted as a **preference update** (unless it |
| 102 | + looks like a follow-up to a recent alert — see [`MESSAGING.md`](MESSAGING.md)). |
| 103 | + |
| 104 | +That is **self-service onboarding in the agent** — but only **after** Photon |
| 105 | +routes the iMessage to you. |
| 106 | + |
| 107 | +### What they need to say (nothing special) |
| 108 | + |
| 109 | +They do **not** need to text `REGISTER` or `START`. Any normal preference |
| 110 | +phrase works on the first message, for example: |
| 111 | + |
| 112 | +- `Alert me on CPI and FOMC, threshold 0.5` |
| 113 | +- `Watch TSLA and NVDA` |
| 114 | +- `Only big alerts, threshold 0.8` |
| 115 | + |
| 116 | +The agent replies with something like: |
| 117 | + |
| 118 | +```text |
| 119 | +Got it — saved your macro preferences for this chat. |
| 120 | +Tracked keywords: CPI, FOMC |
| 121 | +Watchlist: (none) |
| 122 | +Sentiment threshold: 0.5 |
| 123 | +Source trust: any source |
| 124 | +``` |
| 125 | + |
| 126 | +From then on they are a user for that chat: proactive macro alerts can fire when |
| 127 | +headlines match their settings (see below). |
| 128 | + |
| 129 | +**Empty keywords + empty watchlist** means “match every headline” (subject to |
| 130 | +thresholds). So even a vague first message like `hello` may still parse into |
| 131 | +defaults and start matching broadly — worth knowing before you publish a public |
| 132 | +number. |
| 133 | + |
| 134 | +### What “registered” means in code today |
| 135 | + |
| 136 | +On each inbound text the agent: |
| 137 | + |
| 138 | +1. Caches the Spectrum `space` handle (`spacesById`) so it can **send** alerts |
| 139 | + later. |
| 140 | +2. Merges extracted preferences into `userPreferences` keyed by **`space.id`**. |
| 141 | + |
| 142 | +There is no separate `users` table and no check against a landing-page signup. |
| 143 | + |
| 144 | +### Proactive alerts: one extra requirement |
| 145 | + |
| 146 | +Headlines come from `cpp_engine` over ZeroMQ. For a user to **receive** proactive |
| 147 | +alerts: |
| 148 | + |
| 149 | +1. **`cpp_engine` must be running** and connected to the agent. |
| 150 | +2. The user must have **messaged at least once since the agent last started** — |
| 151 | + so the agent has cached their `space` for outbound iMessage. |
| 152 | +3. Their preferences must match an incoming headline (keywords/watchlist + |
| 153 | + severity + source trust). |
| 154 | + |
| 155 | +If the agent restarts, preferences in memory are **gone** until they text again |
| 156 | +(and the space cache is empty until they text again). After they text once, |
| 157 | +settings are rebuilt from that message onward. |
| 158 | + |
| 159 | +--- |
| 160 | + |
| 161 | +## End-to-end flow (landing page + hosted agent) |
| 162 | + |
| 163 | +```text |
| 164 | + Landing page (other repo) |
| 165 | + │ |
| 166 | + │ "Text +1 …… on iMessage to get macro alerts" |
| 167 | + ▼ |
| 168 | + User's iPhone (iMessage, blue bubble) |
| 169 | + │ |
| 170 | + ▼ |
| 171 | + Photon Spectrum ──► ts_agent (hosted) |
| 172 | + │ │ |
| 173 | + │ ├── saves prefs per space.id (RAM) |
| 174 | + │ └── pushes filter union to cpp_engine (ZMQ) |
| 175 | + ▼ |
| 176 | + cpp_engine (hosted) ── headlines ──► ts_agent ── alerts ──► user |
| 177 | +``` |
| 178 | + |
| 179 | +Recommended copy for the landing page: |
| 180 | + |
| 181 | +1. **CTA** — Text our iMessage number: `+1 …` (from `npm run info` or Photon |
| 182 | + dashboard). |
| 183 | +2. **First message examples** — 2–3 lines from the table in |
| 184 | + [`MESSAGING.md`](MESSAGING.md) (e.g. CPI/FOMC + threshold, or a watchlist). |
| 185 | +3. **Requirements** — iMessage (Apple device); not SMS/green bubble. |
| 186 | +4. **What happens next** — They get a confirmation reply; macro alerts arrive |
| 187 | + when news matches their settings (may take a few seconds for watchlist sync — |
| 188 | + see README → Dynamic filter sync). |
| 189 | + |
| 190 | +The landing page does **not** need to call this repo unless you add web signup |
| 191 | +later. |
| 192 | + |
| 193 | +--- |
| 194 | + |
| 195 | +## Hosting checklist |
| 196 | + |
| 197 | +When you deploy (VPS, container, etc.), run **both** processes and keep **one** |
| 198 | +agent instance (the agent uses a single-instance lock): |
| 199 | + |
| 200 | +| Component | Role | |
| 201 | +| --- | --- | |
| 202 | +| `cpp_engine` | Polls/simulates headlines, filters, publishes to `ZMQ_ENDPOINT` | |
| 203 | +| `ts_agent` | Spectrum + Grok + per-user routing + `FILTER_ENDPOINT` to engine | |
| 204 | + |
| 205 | +Environment (minimum): |
| 206 | + |
| 207 | +- **Spectrum:** `PROJECT_ID`, `PROJECT_SECRET` |
| 208 | +- **Grok:** `XAI_API_KEY` |
| 209 | +- **Live news (optional):** `FINNHUB_API_KEY` on `cpp_engine` for `--live` |
| 210 | +- **ZMQ:** `ZMQ_ENDPOINT` / `FILTER_ENDPOINT` aligned between processes |
| 211 | + |
| 212 | +Operational notes: |
| 213 | + |
| 214 | +- **Restarts wipe in-memory users** — expect users to text again after deploy, or |
| 215 | + add persistence (below). |
| 216 | +- **Public number = public product** — budget Grok/Finnhub for unsolicited texts |
| 217 | + unless you add gating. |
| 218 | +- **One agent process** — do not run duplicate `npm start` (duplicate replies). |
| 219 | + |
| 220 | +--- |
| 221 | + |
| 222 | +## When to add persistent storage |
| 223 | + |
| 224 | +Add a database (or similar) when you need any of the following: |
| 225 | + |
| 226 | +| Need | Why memory is not enough | |
| 227 | +| --- | --- | |
| 228 | +| Preferences survive deploys/restarts | Maps are empty after restart | |
| 229 | +| “Registered” / opted-in flag | No durable record of who joined | |
| 230 | +| Invite-only or paid access | Must check identity before accepting prefs | |
| 231 | +| Landing page creates account first | Web stores phone/email; agent must match inbound sender | |
| 232 | +| Support / billing / GDPR delete | Need stable `user_id` and audit trail | |
| 233 | +| Horizontal scale (multiple agents) | Shared prefs + space routing | |
| 234 | + |
| 235 | +A minimal schema later might look like: |
| 236 | + |
| 237 | +- `users`: `id`, `sender_id` (Spectrum), `phone` (if available), `created_at`, |
| 238 | + `status` (`active` / `blocked`) |
| 239 | +- `preferences`: `user_id`, JSON matching `UserPreferences` in `ts_agent` |
| 240 | +- `spaces`: `user_id`, `space_id` (last known Spectrum thread) |
| 241 | + |
| 242 | +Load on startup; upsert on each inbound message; still cache `spacesById` in |
| 243 | +memory for outbound (Spectrum needs the live `Space` object). |
| 244 | + |
| 245 | +**Phase 1 without a DB:** landing page + public number + examples in |
| 246 | +[`MESSAGING.md`](MESSAGING.md) is valid for a demo or small beta if you accept |
| 247 | +reset-on-restart and open registration. |
| 248 | + |
| 249 | +--- |
| 250 | + |
| 251 | +## Product options you may want later (not implemented yet) |
| 252 | + |
| 253 | +These are design choices for when open registration is too risky or UX needs a |
| 254 | +clearer “join” step: |
| 255 | + |
| 256 | +| Approach | User experience | Implementation sketch | |
| 257 | +| --- | --- | --- | |
| 258 | +| **Open (today)** | Text anything preference-like | No change | |
| 259 | +| **Explicit opt-in** | First reply: “Text START to enable alerts” | Ignore preference parsing until `START` or store `opted_in` in DB | |
| 260 | +| **Invite-only** | “You need an invite” | Allowlist `sender.id` in env or DB | |
| 261 | +| **Web-first signup** | Enter phone on site, then text | DB pending row; first text links `sender.id` | |
| 262 | +| **Welcome on first text** | Short onboarding before prefs | Detect new `space.id`; send welcome; optional second message for prefs | |
| 263 | + |
| 264 | +Document any choice you ship in this file and in the landing page repo. |
| 265 | + |
| 266 | +--- |
| 267 | + |
| 268 | +## Quick FAQ |
| 269 | + |
| 270 | +**Do users need to say “register”?** |
| 271 | +No. First iMessage that reaches the line starts a user session for that thread. |
| 272 | + |
| 273 | +**Is storage required for multiple users?** |
| 274 | +No for simultaneous use; yes if settings and identity must survive restarts or |
| 275 | +you want signup/billing/invite control. |
| 276 | + |
| 277 | +**Why did you say storage wasn’t needed before?** |
| 278 | +Meant: the *architecture already supports many chats at once* without adding a |
| 279 | +database first. That is not the same as “never use a database for production.” |
| 280 | + |
| 281 | +**Someone got “you need to register” — is that us?** |
| 282 | +Almost certainly **Photon**, not this repo (grep finds no such copy). Check |
| 283 | +the agent terminal: no `[iMessage] inbound event` = Photon never delivered it. |
| 284 | +Fix: link their sending number (shared) or use a **dedicated Business line**. |
| 285 | + |
| 286 | +**What if someone texts but never sets keywords?** |
| 287 | +Defaults apply; empty keyword + empty watchlist can match all headlines (see |
| 288 | +[`MESSAGING.md`](MESSAGING.md) → Defaults). |
| 289 | + |
| 290 | +**Can the landing page register users without texting?** |
| 291 | +Not with the current agent. The landing page is marketing + instructions unless |
| 292 | +you build a separate signup service and later teach the agent to honor it. |
| 293 | + |
| 294 | +--- |
| 295 | + |
| 296 | +## Related docs |
| 297 | + |
| 298 | +- [`MESSAGING.md`](MESSAGING.md) — what to text after onboarding |
| 299 | +- [`README.md`](README.md) — build, run, test, dynamic filter sync |
| 300 | +- [`TECHNICAL.md`](TECHNICAL.md) — Spectrum loop, in-memory maps, ZMQ channels |
0 commit comments