twitch landing page

Twitch solves a couple unique problems, namely, live video streaming and large group chat.

Before we jump into examining how their streaming and chat work, we will cover some terminology and the basics of their site.


  • stream – can either be a session of live streaming or refer to a channel
  • channel – page where a user can stream their content
  • subscribe – users can pay ~$5, although there are multiple buy in levels, to unlock perks such as emotes, chat channels, discord servers, and more. Note: These perks can vary greatly between channels.
  • vod – video of a past stream
  • rerun – rebroadcast of a past stream, similar to a vod, but lacks a video scrubber and takes the place of the stream.

Note: values in text and URLs will sometimes be substituted with variables in the form $(VAR_NAME), this helps with describing generalized formats, and helps minimize the JSON examples.

General Architecture

For transferring data between their React App and backend, Twitch uses GraphQL.

When logged in, Twitch authenticates requests using an Authorization header.

Don’t worry, all those secrets are fake.

POST /gql HTTP/1.1
Content-Type: text/plain;charset=UTF-8
Pragma: no-cache
Accept: */*
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept-Language: en-US
DNT: 1
Authorization: OAuth w8oa8myl28ttbx9r6hms9ayk8n
Content-Length: 195
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15
Cache-Control: no-cache
Client-Id: 4qzwg0zmxv2xqddqoxnydzxbbyoezf
X-Device-Id: ptt47yco0ta1njsz

Storage & Resources

First off, there is a lot of stuff stored in sessionStorage, cookies, and especially localStorage.

Some notiable tid bits from localStorage include Agolia search info, tracking and error reporting related data, ad-block detection properties (blockDetector.detected: true), CSRF token, video player settings, theme settings (the CSS class is stored as the theme value).

In cookies, Twitch stores last login date, name, login, some unique identifiers (probably for tracking), along with a lot of settings referencing twilight, which I think is the name of their React app.

Twitch also uses a service worker. It has some connection to the URL If we dig into the minified source, we find the m() function which URL encodes and POSTs to "", which is called twice: m(d.SpadeEventType.NotificationImpression, t), m(d.SpadeEventType.NotificationInteraction, t). Judging by those names, this service working is probably used for tracking.

In the m() function we see the service work does some encoding before sending the following object:

  browser: navigator.userAgent,
  platform: d.SpadePlatform.Web,
  time: (new Date).getTime() / 1e3,
  ui_context: "browser"

Twitch will JSON.stringify(), .replace(), btoa, Blobify, and then send as the body of a POST request to

The rest of the service worker is pretty similar, but there is an unused object, serviceWorkerOption, which a quick google will give us

Continuing our search through the resources of Twitch, we enter the webpack:// section. We find references to bootstrap, Redux, along with some misc items.

In the section, we find a config/settings.js which is a ~1,500 line long object assigned to window.__twilightSettings and contains, you guessed it, config settings. Convering things from ad URLs and promotions, to AB experiments. 1,300 lines of it are experiments config. serves all the game box art and user profile pictures. serves a javascript file which is used for ads. Looking at the minified source we can see that this file is using Redux, as we find store.dispatch, store.getState(), and __REDUX_DEVTOOLS_EXTENSION__. Anyway, it’s ~3,000 lines of ads software, not much more to say.


Twitch connects to a websocket via


Where it sends page views, navigation events as well as establishes listeners for whispers, sub gifts, raids, commerce events, and more. It seems to be the centralized pub-sub connection for any sort of Twitch event, except chat, which is separated and gets its own websocket.


Now we are going to dig into the frontend, specifically the React portion of the site. Earlier we found that Twitch uses Bootstrap, which we can further confirm by looking at the CSS class names. One thing to note is that Twitch prefixes all of the bootstrap classes with tw-.

We also see they use

Another thing we can see from the React dev tools are the Apollo, React Router, and a custom withLatencyTracking higher order components.

react dev tools high order components

Twitch implements code splitting for its React components to reduce bundle size. This means that Twitch will only fetch components that are relevant to a give page.

If we navigate to a channel, Twitch fetches the following JavaScript. Note the$(HASH).js, the current page we are on.

URL size 9,947 bytes 346,737 bytes,Array.prototype.findIndex,Array.prototype.includes,default,fetch,Intl.~locale.en,Object.entries,Object.values,URL 74 bytes 9,331 bytes 277,546 bytes 58,051 bytes 81,029 bytes 44,384 bytes 750 bytes 26,765 bytes 12,843 bytes 3,736 bytes 6,909 bytes 1,489 bytes 5,123 bytes 5,685 bytes 2,239 bytes 1,709 bytes 2,261 bytes 4,582 bytes 9,424 bytes 28,435 bytes 1,495 bytes 109 bytes

Then if we navigate to, we fetch

URL size 36,628 bytes 4,804 bytes

So by chunking, Twitch prevents having a huge bundle, stuffed with components that aren’t relevant to our current page.

But what about CSS? Twitch also chunks CSS. For the following page, Twitch fetches:

URL size 1,187 bytes 398 bytes

In terms of server side rendering, if we curl -o index.html && open index.html, we can see that Twitch returns HTML for the top navbar, and the loading spinner. The necessary CSS is included in a style tag, while the CSS used in the rest of the site is not loaded. What this means is that the default HTML in the page is entirely separate from the React site. However, Twitch does ensure that the og:url meta tags are the same as page URL, but besides that, the entire page is static.

In the <head> are prefetch link tags,<link rel="dns-prefetch" href="//example.url"/>, which, you gussed it, prefetch the dns lookup for a resource you’re going to use. We also find some <link rel="preload" href="//example.url"/> for ad related resources.

Twitch also includes relevant <meta/> tags such as their Open Graph data.

Sidenote: Twitch appears to have an event listener for page focus, so that when you tab back to the page, the title updates. Not sure why this is – maybe a quark of their react routing library necessitates it.

Twitch directory page

An interesting part of Twitch’s frontend app is their persisting video player, which will popout and float in the bottom left hand corner of the browser when you navigate away from a channel.

Essentially, this works by keeping the React component present in the DOM when a user navigates, while using CSS for position. So if you were to nav from a stream to the settings page, then video player component will still be present.

Seems pretty simple, except that when you navigate to the settings page directly, there is no video player present.

twitch full screen video player

And then we navigate to a different page.

twitch mini video player

Some interesting note about logging out of Twitch, is that a full page refresh will occur. This is a simple way to clear session data, but localStorage will still persist. On login, a full page reload also occurs. I am guessing this is required by way of their authentication, but could likely be avoided since they only need to set/remove their auth-token cookie.

Twitch’s search box uses Agolia, which is a hosted search solution similar to Elasticsearch. On the frontend, the search is composed of a few React components that take the search results as props.

In terms of fetching the data, Twitch contacts*/queries?x-algolia-agent=Algolia%20for%20vanilla%20JavaScript%203.24.11&x-algolia-application-id=XLUO134HOR&x-algolia-api-key=d157112f6fc2cab93ce4b01227c80a6d which returns a large chunk of JSON in ~60ms. Pretty fast. Although the intitial Agolia query returns most data, including thumbnail urls and user data, Twitch makes a GraphQL query to fetch the current thumbnail for live channels matching the query. This takes about 150ms.

dota2 search

Twitch also uses Agolia for user search in its whisper system.

whisper chat box whisper chat box

The chat uses GraphQL for sending the whispers.

  "operationName": "SendWhisper",
  "variables": {
    "input": {
      "message": "hello",
      "nonce": "9a9632f6a0fcda0f5a0df7a6e5f19339",
      "recipientUserID": "1234"
  "extensions": {
    "persistedQuery": {
      "version": 1,
      "sha256Hash": "8a5eb4b94c03ef9fe84ab8775885bad00d02ca68a2568af6e1e0a707d971a3f6"


It turns out that Messages have been deprecated, and that Messages are not the same as Whispers.

twitch messages inbox


Chat can be viewed by anyone, but requires a login to send messages. There are the concepts of separate chat rooms, which can be restricted to subscribers. Users can send predefined emotes. These emotes can be available to everyone or they can be limited to people who subscribe.

Underneath, Twitch chat uses websockets and talks to an IRC backend. The usage of IRC is abstracted away from the user, but we can see its traces in the url,, and also with the initial websocket connection.

When we connect without being logged in, we register with a NICK justinfan41197 (the last bit is just a unique id), and PASS SCHMOOPIIE. These are typically for an IRC server. Then we connect to the IRC channel for the stream, so if we are watching, we will JOIN #dota2ti.

Messages are sent in the following format:

@badges=;color=;display-name=;emotes=;id=;mod=;room-id=;subscriber=;tmi-sent-ts=;turbo=;user-id=;user-type= :$(USER_NAME)!$(USER_NAME)@$(USER_NAME) PRIVMSG #$(CHANNEL_NAME) :$(MSG)

// e.g.

@badges=premium/1;color=#8A2BE2;display-name=example_user;emotes=;id=66a3335a-524b-4940-976f-16b2e58a7d4e;mod=0;room-id=35630634;subscriber=0;tmi-sent-ts=1535211960034;turbo=0;user-id=17461;user-type= :example_user! PRIVMSG #dota2ti :Wow that was cool!

On disconnect, Twitch sends a PART message:

PART #dota2ti

Chat is largely siloed from the frontend app. It forgoes GraphQL, and uses plain HTTP requests.

For instance, Twitch calls GET$(CHANNEL_NAME)/chatters to get the current chat members of a channel. Twitch does end up making a GraphQL request when you start watching a stream (see below).

  "operationName": "ChannelPage_SetSessionStatus",
  "variables": {
    "input": {
      "sessionID": "ade0ad79ef2ae75a",
      "availability": "ONLINE",
      "activity": {
        "type": "WATCHING",
        "userID": "121437143",
        "gameID": null
  "extensions": {
    "persistedQuery": {
      "version": 1,
      "sha256Hash": "8521e08af74c8cb5128e4bb99fa53b591391cb19492e65fb0489aeee2f96947f"

You may notice that this GraphQL query is missing the query field. This is because Twitch uses persisted queries, hence the persistedQuery field in extensions, which allows for using unique IDs in place of a query field.

Something interesting to note is that the link generation, rich embeds (for links like ), as well as highlighting for user mentions via `@username`, are all handled on the client side. The client parses the plain text messages and decides what to do.

Video Streaming

Twitch uses HLS as its streaming protocol along with CloudFront, Fastly, and S3.


Twitch initially fetches:$(CHANNEL_NAME).m3u8

// which has some interesting query params
allow_source: true
fast_bread: true
p: 6240561
player_backend: mediaplayer
playlist_include_framerate: true
reassignments_supported: false
rtqos: control
sig: e9fbe26616daf5bc600701e544407bf49c1d9bb0
token: {
  "adblock": true,
  "authorization": {
    "forbidden": false,
    "reason": ""
  "channel": "dota2ti",
  "channel_id": 35630634,
  "chansub": {
    "restricted_bitrates": [],
    "view_until": 1924905600
  "ci_gb": false,
  "geoblock_reason": "",
  "device_id": "13a3960af9ecdf2e",
  "expires": 1535307382,
  "game": "",
  "hide_ads": false,
  "https_required": true,
  "mature": false,
  "partner": false,
  "platform": "web",
  "player_type": "site",
  "private": {
    "allowed_to_view": true
  "privileged": false,
  "server_ads": false,
  "show_ads": true,
  "subscriber": false,
  "turbo": false,
  "user_id": null,
  "user_ip": "",
  "version": 2
cdm: wv

and returns an .m3u8 file (see below), a plain text utf-8 format known as M3U for declaring a playlist of media.

Notice how Twitch sends its adblock detection on stream start?

-- snip --

Now we fetch the playlists listed in the playlist file, which will give us the URLs for the HLS video segments.

With returns the following video segments:

-- snip --

Then we just fetch those video segments, and repeatedly call the same .m3u8 URL to get more segment URLs.


On initial connect to a channel showing a rerun, Twitch POSTs a blob of data to$(SEGMENT_ID).ts, and gets a 204 back.

Then, like live broadcasts, we contact the usher URL$(CHANNEL_NAME).m3u8

and then fetch the subsequent playlists from:$(UNIQUE_ID)/index-live.m3u8

and fetch the segments$(UNIQUE_ID)/index-$(SEGMENT_ID).ts

Vods (Videos)

Vods are usually past streams that have been kept and stored for replay, although it is also possible to upload videos directly to Twitch.

On viewing a video page, Twitch will send GET requests to:$(VIDEO_ID).m3u8

// and$(CHANNEL_ID)_$(VIDEO_ID)/$(VIDEO_QUALITY)/index-dvr.m3u8
// or sometimes via a CDN (seems that there is some AB testing going on)$(CHANNEL_ID)_$(VIDEO_ID)/$(VIDEO_QUALITY)/index-dvr.m3u8

Which give us the segment URLs to fetch.

Twitch fetches the videos in chunks that can range from a couple hundred KBs to around ~9MB, but this is only a small sample. The size of the chunks depends on your video setting. The 160p option returns ~370KB chunks.$(CHANNEL_ID)_$(VIDEO_ID)/$(VIDEO_QUALITY)/$(CHUNK_SERIES_ID).ts`

// VIDEO_QUALITY will be set to 'chunked' if you are viewing at source resolution

// some examples:

Unlike streams, vods have popover images in the video scrubber, for some reason reruns don’t have these. Twitch fetches both a high quality and low quality versions of these images. When you scrubb further into the video, Twitch will fetch the higher indexed storyboards.$(CHANNEL_ID)_$(VIDEO_ID)/storyboards/$(STORYBOARD_ID)-$(STORYBOARD_QUALITY)-$(STORYBOARD_INDEX).jpg
// example

Twitch will periodically send a POST request to$(SEGMENT_ID).ts

Which I assume is to sync up the timing of chunks.

If you are logged in, Twitch also keeps track of what you are watching by periodically sending a PUT request to$(USER_ID) with the following data:

  position: 123127,
  type: 'vod',
  video_id: 'v594876595',

The responses from have the s3 and cloudfront related headers

Server: AmazonS3
Via: 1.1 (CloudFront)
X-Cache: Hit from cloudfront

Something to note is that the URL for fetching the index-live.m3u8 is the same for a given channel. The only difference between each response are the Date:, Expires:, and Tenfoot-Context: headers. Not sure what the Tenfoot-Context: header is for, it contains a varying hex number e.g., 0x275af65e09234ee8.


Clips are segments of a stream that are saved. Similar to videos, but limited to <1.5 mins in length (this seems to vary slightly).

To create a clip, a user must be signed in and they hit the create clip button in the video player (video player must be an unpaused stream).

create clip button

On click, Twitch sends two requests:

1. GET request to

with headers:

Access-Control-Request-Headers: authorization
Pragma: no-cache
Accept: */*
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15
Access-Control-Request-Method: POST

getting a response of:

Content-Type: application/json; charset=utf-8
Date: Tue, 28 Aug 2018 01:50:41 GMT
Connection: keep-alive
Access-Control-Allow-Methods: DELETE
Access-Control-Allow-Origin: *
Content-Length: 0
Accept-Ranges: bytes, bytes
Vary: Accept-Encoding, X-ENV, X-PLAYER, X-TWILIGHT
Access-Control-Allow-Headers: X-Requested-With,Content-Type,Authorization,Distinct-Id
X-Timer: S1535421041.406038,VS0,VE81
X-Served-By: cache-sea1043-SEA, cache-iad2128-IAD
X-Cache-Hits: 0, 0

2. POST request to

with a payload of

MIME Type: application/x-www-form-urlencoded
broadcast_id: 41110642525
channel: dota2ti
offset: 1970
play_session_id: ydTOJak9Cnk8vKqh1Cu0zl43szJrrxWo
player_backend_type: mediaplayer

with headers

Content-Type: application/x-www-form-urlencoded
Pragma: no-cache
Accept: */*
Connection: keep-alive
Accept-Encoding: gzip, deflate
Accept-Language: en-us
DNT: 1
Authorization: OAuth w8oa8myl28ttbx9r6hms9ayk8n
Content-Length: 141
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15
Cache-Control: no-cache

receiving a response with headers

HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Tue, 28 Aug 2018 01:50:41 GMT
Cache-Control: no-cache, max-age=0, s-maxage=0, no-store
Access-Control-Allow-Origin: *
Connection: keep-alive
Content-Length: 1939
Accept-Ranges: bytes, bytes
X-Timer: S1535421042.588825,VS0,VE305
X-Served-By: cache-sea1025-SEA, cache-iad2128-IAD
X-Cache-Hits: 0, 0

and data

  "broadcaster_channel_url": "",
  "broadcaster_display_name": "dota2ti",
  "broadcaster_id": "41463454",
  "broadcaster_login": "dota2ti",
  "broadcaster_logo": "",
  "broadcast_id": "30110642464",
  "curator_channel_url": "",
  "curator_display_name": "example user",
  "curator_id": "200525315",
  "curator_login": "example-user",
  "curator_logo": "",
  "preview_image": "$(BROADCAST_ID)-offset-2948-preview.jpg",
  "thumbnails": {
    "medium": "$(BROADCAST_ID)-offset-2948-preview-480x272.jpg",
    "small": "$(BROADCAST_ID)-offset-2948-preview-260x147.jpg",
    "tiny": "$(BROADCAST_ID)-offset-2948-preview-86x45.jpg"
  "game": "Dota 2",
  "communities": [],
  "created_at": "2018-08-28T02:06:54Z",
  "title": "[EN] The International 2018 Main Event",
  "language": "en",
  "url": "$(CLIP_SLUG_NAME)",
  "info_url": "$(CLIP_SLUG_NAME)",
  "status_url": "$(CLIP_SLUG_NAME)/status",
  "edit_url": "$(CLIP_SLUG_NAME)/edit",
  "embed_url": "$(CLIP_SLUG_NAME)",
  "embed_html": "\u003ciframe src='$(CLIP_SLUG_NAME)' width='640' height='360' frameborder='0' scrolling='no' allowfullscreen='true'\u003e\u003c/iframe\u003e",
  "view_url": "$(CLIP_SLUG_NAME)/view",
  "id": "498203828",
  "slug": "MagnificentCalmStapleTerrier",
  "duration": 0,
  "views": 0

Then Twitch opens a new window for you with the URL$(CLIP_SLUG_NAME)/edit, which then redirects to

In a surprising turn, Twitch fetches the video data for clip creation as a plain .mp4.

Above we can also see the status_url, which is used for polling to see if a clip has processed.

After selecting your length and adding an appropriate title, Twitch will send a GraphQL query to create the clip:

  "operationName": "PublishClip",
  "variables": {
    "input": {
      "segments": [{
        "offsetSeconds": 48,
        "durationSeconds": 30,
        "speed": 1
      "slug": "$(CLIP_SLUG_NAME)",
      "title": "Test"
  "extensions": {
    "persistedQuery": {
      "version": 1,
      "sha256Hash": "f04b575262faae00ee7566fc6a510f5adef93c8d114b666cf549fa5a870021fd"

After some processing (usually instantaneous), share options will pop up. Note, the initial diceware slug name is what is used from initial creation to sharing.

Users can manage their created clips via:$(USERNAME)/manager/clips

Which is a pretty staightforward React page that uses GraphQL to fetch the clip data.

A request made to peaked my interest.

I think it might be related to adblock detection. Perhaps, if the Ad network requests fail, but this request doesn’t, then Twitch knows you are running adblock software. Turns out, this network request is made on all pages – only noticed it now.

In terms of fetching video data, Clips fetch their video in the same way as a live stream, they also use GraphQL to fetch the chat messages (this is a recent addition) along with some other, less essential data.

2 Factor


Configuring 2 Factor falls under a sudo required operation. When trying to complete a request that requires sudo, you will be redirected to:

Which sets a cookie

set-cookie: passport_requested=uwr1m5psv3bztdhz45lfxq50cwint83r; Max-Age=0

Then by filling out the form at

and hitting send. We have our sudo cookie along with several others set.

As expected, we send a CSRF token to the backend, but this token is label _goji_csrf created via Goji, specifically goji/crsf.

Also, you may have noticed that the code url param looks base64’d.

>>> import urllib
>>> import base64
>>> raw_data = "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3Bhc3Nwb3J0LnR3aXRjaC50diIsInVpZCI6IjIwMDUyNTMxNSIsImxvZ2luIjoiZXhhbXBsZV91c2VyIiwiZXhwIjoxNTM1NTA4MzE4LCJpYXQiOjE1MzU1MDc1OTgsImF1ZCI6InNldHRpbmdzX3BhZ2UiLCJzY29wZSI6InRva2VuX2Zsb3ciLCJhdXRoeV9pZCI6MjI2NzYyNn0%3D.JD6j18EjfuGO-fK6Az7eZcjvHJSx5lQte11-N2P6OCGNJHQ6YSS3OoWrrR4CfWrZyYE_I0fHkWXBfJ7TV1LT3Q%3D%3D"
>>> data = urllib.parse.unquote(raw_data)
>>> base64.b64decode(data)

we can make it a little more readable

  "iss": "",
  "uid": "200525315",
  "login": "example_user",
  "exp": 1535508318,
  "iat": 1535507598,
  "aud": "settings_page",
  "scope": "token_flow",
  "authy_id": 2267626

JWT, plain and simple.

Note: is a seperate, plain HTTP, HTML, non-SPA site. Based on some CSS classes, I believe passport is called Kraken, compared to the main Twitch site which is called Twilight.


  1. send phone number via

  2. enter verification code at

Note: Twitch has some connection directly with Authy, so if you have Authy connected to your phone, Twitch will automatically use it.

  1. end up at the success page



  2. success page


When authenticating Twitch POSTs your username, password, and client_id to With 2 factor enabled, this results in a 400 and we get the option to submit our Authy / SMS code.

The response of the initial 400 request, after base64 decode:

  "error_code": 3011,
  "error_description": "missing authy token",
  "captcha_proof": '{"alg":"HS512","typ":"JWT"}{"iss":"","iat":1535593181,"aud":["captcha_proof"],"sub":"example_user","exp":1535593481}',
  "sms_proof": '{"alg":"HS512","typ":"JWT"}{"iss":"","uid":"200525315","login":"example_user","exp":1535593901,"iat":1535593181,"aud":"kimne78kx3ncx6brgo4mv6wki5h1ko","scope":"token_flow","authy_id":2568189}'

Sidenote: Twitch’s JWTs expire after 5 minutes.

On submitting our 2-factor code, Twitch POSTS the authy_token, username, password, client_id, and captcha.

On success, we get sudo, login, name, api_token, and a few other auth related cookies set.

After authenticating, Twitch makes a CoreAuthCurrentUser GraphQL request, which doesn’t get a response or sent any headers – not sure of its purpose.

If you forget your 2-factor you have to contact Twitch support since there aren’t any recovery codes like other services.

OAuth Connections

Twitch has a the ability to link your Steam, Blizzard, and a few other accounts to your Twitch account.

If we go to the connections page and hit one of the connect buttons, a new window will open for the other service’s login, allowing us to link our account on the other service.

So if we want to link Steam, we hit the Steam connect button and

opens in a new window.

If we decode the url and break it onto new lines it becomes more clear what is going on:

Generally a connection works like follows:

  1. open new window with OAuth login screen and an event listener for a message on the current window.
  2. login in the popup window
  3. OAuth provider redirects to the return_to url, which will send a request to Twitch backend and the window will also send a message to Twitch saying connection sucessfull.

After connecting, Twitch sends a GraphQL request fetching the connection info.

To delete the connection, Twitch sends a DELETE request to:$(USER_ID)

And then sends another GraphQL request to refetch the connection data.


On Twitch users can purchase Bits, currency that can be given to Streamers, subscriptions, monthly payments to streamers, as well as Twitch Prime, which comes with a Amazon Prime subscription.

Bits are purchased by clicking the bits tab.

twitch bits dropdown

If we decide that we can’t settle for anything less than 25,0000 bits, then a new window will open at

where the asin corresponds to the quantity of bits you are buying.

There is a similar message event listener setup to trigger a GraphQL to update bits related info after a purchase, or an attempt at a purchase is made.

Note: this buying window is also a React app.

twitch purchase bits page

The services use a similar OAuth set up as discusses previously in the connections section.

Purchasing subscriptions is more integrated into the site, and has a lot more options, but all the purchase methods work in a similar way to previously discussed.

purchasing a subscription to Bob Ross

Settings Page

The settings page is pretty plain. In a departure, settings primarily uses a REST api and makes a couple calls via GraphQL.

Its consists of pretty basic CRUD operations.

Video Producer

Although video content on Twitch is primarily created through streaming, users can also upload videos directly through

A frontend thing to note is that Twitch creates their file dropzone by using CSS to stretch an input element as well as using the HTML Drag and Drop API.

file uploader

The upload process works likes follows:

  1. select file to upload
  2. Twitch sends api request to with the following data, initialting a multipart upload to S3

      "channel_id": $(USER_ID),
      "title": "A pretty cool video",
      "viewable": "private",
      "create_premiere": true

    getting in response:

      "upload": {
        "token": "kb2OTgG8OIYZ_K6KLWIvC8R.FmHakt.UFmA2hRNoCPV3E.ALo.60yoBEBFedRjgyT0G9DzsCi.WIZt9JLOjk1KqglapSdWYJG8ozDgyWnbp_QNtevoEc0DtuB1WFp27_BTBSRebCWiIpWr40F4DdVqEhBLoA2oKCKwXDuZLiE3A-",
        "url": "$(VIDEO_ID)"
      "video": {
        "title": "A pretty cool video",
        -- snip --
        "status": "created",
        "url": "$(VIDEO_ID)",
        "viewable": "private",
        -- snip ---
        "premiere": {
          "id": "",
          "status": "unscheduled",
          "event": null
  3. Twitch sends chunk(s) to s3 via a PUT request using the upload_token and video_id created via part 2.$(VIDEO_ID)?part=1&upload_token=$(UPLOAD_TOKEN)

    In my case, the 1.5MB file was small enough to fit in one chunk.

    Another note is that while I am closest to us-east-1, Twitch chose us-west.

  4. Complete multipart upload

    Twitch completes the multipart upload with a POST request to:$(VIDEO_ID)/complete?upload_token=$(UPLOAD_TOKEN)
  5. Poll for Updates

    While Twitch doesn’t provide a progress bar, they do poll for updates to see if the transcoding is complete. Their endpoint to fetch video data is used to poll video transcode status. When polling, Twitch checks if the status property in the response is set to 'transcoding'. Eventually, the status changes to 'recorded', the polling stops, and Twitch updates the page with its fetched info.$(VIDEO_ID)

In total, Twitch took about 2 minutes to upload and transcode a 1.5 MB, H.264 encoded video.

After uploading, the video is private until you ‘premiere’ it, which is event that you can schedule with a cover image and everything.

The cover image is base64 encoded and uploaded in a request body.

schedule video event

WASM Service Worker

There as an additional service worker to the one we already discussed. This one involves both WASM and js.

The .wasm is ~233kB, and the .js is ~23kB.

By looking at the js, we can see that this service worker was compiled down via Empscripten, and that the main job of the javascript is to fetch and load the wasm worker.

The job of the webworker isn’t entirely clear. There are some references to analytics, the video player settings, media codecs and more.

At 233kB, you would expect the worker to be quite capable, so I am guessing it’s a multitasker.


The dashboard is used when streaming video content.

To fetch content the dashboard uses primarly GraphQL with some additional REST endpoints thrown in.

There is an ability to rearrange the cards, although the configuration is only stored locally.

The dashboard has its own video player of the stream, along with a chat, which use the same setup as previously discussed. The rest for the most part is a CRUD.

twitch dashboard


React.js based SPA frontend, using GraphQL for data transfer, websockets for its pub-sub and IRC based chat. Agolia for search. HLS for video streaming – with a sizable pinch of user tracking throughout.