Integration plan
What it would take to build, in what order, and what has to be true first.
Two pieces of foundational work must land first, and neither is gaming-specific: an OAuth token store with encryption and refresh (Sway has none), and a session data model (Sway's domain is post-shaped end to end and cannot represent a play session).
| Twitch | Roblox | |
|---|---|---|
| Approach | OAuth 2.0 API integration, narrow scope | No API integration. On-device OS signals + guided setup |
| Blocked on | Written answer from Twitch on §VI.C | Apple Family Controls entitlement |
| Unit of data | Contacts, follows, safety events | Playtime only |
| Why not more | 24-hour cache cap; no watch history exists | Prohibited by policy; data absent from Open Cloud |
Four blockers in the existing code
① Eligibility SQL requires a cookie
All three CTEs in schedule.repository.ts require
cookie IS NOT NULL AND btrim(cookie) <> ''. An OAuth account with a null cookie is
invisible to the scheduler. Change the predicate — do not stuff a token into the
cookie column.
② getConnectedAccount is cookie-typed
Returns { cookie: string } | null. Every consumer destructures .cookie.
The type must widen.
③ The post workflow rejects contentless records
if (!hasContent) return { error: 'Post has no content' }. A play session has no
text, images or audio. Gaming data must not route through
SocialPostWorkflowV2.
④ The platform switch is exhaustive
No default case — so TypeScript errors the moment you add an enum member. That is a
feature: a compiler-enforced checklist. Duplicated in v1/ and
v2/.
SocialPlatform is a table, not a DB enum — adding twitch
needs no migration. Author is already platform-scoped
(youtube:UC… → twitch:<id>). Alerts are platform-agnostic. The
orchestrator fans out over a plain string[].
Twitch ingestion — three modes, two of which ship
| Mode | Transport | Fits Workers? | Status |
|---|---|---|---|
| A — polling follows, labels, blocked users |
Existing cron orchestrator, 12h cycle | Yes | Reuses the pipeline wholesale |
| B — whispers the highest-value capability |
EventSub webhooks, HMAC-verified | Yes | Stateless HTTP — a perfect fit. Costs 0 |
| C — chat | Persistent WebSocket or IRC per child | No | Deferred. Needs a Durable Object or container |
Who consents when the user is a minor
Both, and neither alone is sufficient.
- Parent — holder of parental responsibility; provides COPPA verifiable consent via the card-transaction method, bound to the existing subscription event
- Child — must complete the OAuth flow themselves and be told, in age-appropriate terms, what is collected and that monitoring is active. Not courtesy: ICO Standard 11's operative requirement
- Twitch's own ToS §2 already requires guardian supervision for minors, which helps
Rendering Twitch's login inside a WebView Sway controls reproduces the credential-interception
pattern that creates the Apple 5.1.1(vi) exposure. Use expo-web-browser. The whole
point of OAuth is that Sway never sees the password.
New tables
| Table | Why it is new |
|---|---|
user_platform_credentials |
Encrypted OAuth tokens. UserSocialAccount.cookie has no expiry, refresh or scope |
play_sessions |
Carries source and confidence — so an Android exact reading and an iOS
threshold band never render identically |
platform_contacts |
Deliberately not Author. Mandatory expiresAt |
activity_signals |
Classifier output. contextRef holds an ID — never message text |
Author was built for public creators — public figures with
bio, followers, region. Pointing it at private children silently
converts a public-figure table into a table of minors, inheriting a retention posture designed for
public data. And Post.caption is a @db.Text field holding full content:
do not add a gaming equivalent.
Where a new abstraction is genuinely required
Sway's Trust Score cannot absorb gaming data. It counts each Social Post once at its
worst Trust Level and omits the score when there are no classified posts. Gaming contributes zero
classified posts, so a Games platform would render as noDataAnalyzed.
| (a) Synthesise post-shaped records | (b) Parallel model, separate surface | |
|---|---|---|
| Effort | Lower | Higher |
Requires fake url and postId | Yes | No |
| Trust Score covers gaming | Yes, misleadingly | No |
| Honest | No | Yes |
Recommend (b). Option (a) means inventing synthetic URLs for events that have none and feeding a trust classifier text it was never designed for. It would produce a number that looks like the existing Trust Score and means something different — the worst outcome for a product whose value is parental trust.
Phased delivery
| Phase | Est. | Notes |
|---|---|---|
| 0 — Unblock Twitch question, Apple entitlement, DPIA, counsel |
2–3w + wait | Do this regardless. The DPIA is mandatory under Art 35(3)(b) |
| 1 — Foundations token store, encryption, eligibility fix, hourly validate, retention sweep, child indicator | 3–4w | Fixes a live App Store exposure. Pays for itself independently of gaming |
| 2 — Twitch read-only OAuth, follows, labels, diff-and-signal | 3–4w | |
| 3 — Whispers EventSub, in-memory classification, alerts | 2–3w | The highest-value capability on the platform |
| 4 — Roblox on-device Expo prebuild migration, Android + iOS, checklist |
4–6w | Gated on Phase 0 |
| 5 — Games dashboard | 3–4w | See the prototype |
Total 17–24 engineer-weeks, of which 5–7 are foundations that pay for themselves independently.
Twitch chat monitoring (Mode C), spend tracking, any Roblox API integration, voice chat (does not exist on Twitch).
Start here this week
A spike an engineer can begin immediately, before any of the above is approved:
- Register a Twitch application as a Confidential client. Confirm authorization-code + PKCE end to end. ~1 day
- Answer the two Unverified questions —
own-message echo, and
channel.chat.messagecost. ~1 day - Test whether an under-13 Roblox account can complete OAuth consent. ~½ day
- Submit the Apple Family Controls entitlement request. It has a lead time and blocks dev builds. A form
Items 1–3 are ~3 days and de-risk the two largest unknowns. Item 4 costs nothing to be waiting on.