Skip to content

ekggg/getting-started

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

30 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

EKG.gg getting started

This repository is documentation and a help center for all folks that want to get started writing widgets for the EKG.gg platform. This README.md will cover the high level details, but feel free to read the docs and examples for more in-depth information.

Overall Widget Architecture

EKG.gg uses a familiar but modern architecture for our widgets.

Already done some web dev?

Widgets are comprised of three parts: JS, HTML templating, and CSS styling.

New to web development?

Widgets are comprised of three parts: functionality, markup, and styling.

  • Functionality: Given the previous state and new input, what should the widget's next state be?
  • Markup: Given a state, what should the widget's structure be?
  • Styling: Given a structure, how should it be presented to users?

Note

If this sounds obvious / familiar that is because EKG.gg widgets are very much modeled after the architecture of the web and use standard web technologies.

Let's now dive further into these three parts to see how they work in concert with one another. We'll start with the most complicated part, functionality.

Functionality - How state changes

Language: JavaScript

Widgets are often not very useful unless they have the ability to change over time and react to outside information. The way EKG.gg enables developers to safely get access to this information and change over time is via our state + event system. Widget functionality is expected to be written in JavaScript. Let's take a look at a simple example below:

// widget.js
EKG.widget("Counter")
  .initialState(() => ({ count: 0 }))
  .register((event, state, _ctx) => {
    switch (event.type) {
      case "ekg.chat.sent":
        return { ...state, count: state.count + 1 };
      default:
        return state;
    }
  });

This is a simple widget that just counts up whenever a new chat message is sent. This state will later be sent to the markup renderer to be rendered which we'll cover later in this document. Let's quickly break down this script:

EKG.widget(name?) - EKG is a variable in the global namespace that your widget has access to. The EKG global provides a few helpers, but the most important one is the widget function. Every widget with functionality is expected to call this function once which registers your widget into the EKG event bus. If you do not call this function your widget will never receive events. The optional name argument is used for debugging purposes and will appear in logs and error reporting in the browser.

Warning

Make sure you only call EKG.widget()...register() once. Calling it multiple times in your widget's script will overwrite the previous registration, which can be confusing.

.initialState(fn): This is a chainable method that takes a function (ctx, initialData) => state and returns the first state of your widget. This initial state will be used for the first render of the widget and for the subsequent .register() function call.

The initialData argument lets you bootstrap from the latest events EKG.gg has already seen before your widget mounted. When available, it may contain latestFollower, latestSubscriber, latestStreamStart, and latestTip. It also always includes an activeGoals array holding the channel's current goals, which is useful for goal widgets.

.persist(fn) and .restore(fn): These two handlers let you save part of your widget's state across multiple scenes or streams. The persist function ((state) => persistedState) lets you choose what data needs to be saved. The restore function ((state, persistedState) => newState) controls how that saved data gets merged in to the existing state. Persisted state can be rather complicated, for more details please read our persisted state guide.

.register(fn): This is the most important part of any widget. This method takes a function (event, state, ctx) => newState that will be invoked any time something happens in the outside world that EKG.gg currently tracks. This could be when a chat is sent, a monetary tip is given to the streamer, or even something as neat as something being bought from the streamer's Shopify store. The register function is a "pure" function that follows the pattern of state + event = newState. This pattern keeps the logic of your widget very simple, makes the function extremely easy to test, and enables very neat tricks like "time-travel debugging".

ctx: This is an object where one can get context about the current widget and runtime. Read more about it here.

Tip

If the return value of .register() is a falsy value, then the old state will be retained and a rerender of the widget will not be scheduled.

Note

As you'll see in the security section below, our widgets actually run in a VM called QuickJS. Many JavaScript APIs and globals you may expect in a normal browser environment aren't actually available. Instead your widget should only use core non-async ECMAScript 2023 APIs.

Further reading

Markup - How state is rendered

Language: HTML + Handlebars

Once you have some state, you'll probably want to render it to something that can be shown on screen. To do that EKG.gg expects a Handlebars template that renders HTML to be provided. Let's take a look at one now!

<div class="main">
  <div class="num">{{count}}</div>
  <div class="subtext">Chats have been sent</div>
</div>

Continuing with our chat counter from above this is an extremely simple template that will show an incrementing integer every time a new chat is sent. With handlebars anything between {{ and }} is considered a dynamic value and will attempt to read that expression from the state object. Additionally EKG.gg provides a series of view/block helpers to the Handlebars execution context to make writing more complex renderings easier. Let's take a look at a more complicated example that uses some of these view helpers to build a live chat renderer.

{{! An inline partial that can be later used }}
{{#*inline "renderMessage"}}
  {{! Inside this partial all expressions are now scoped to the current message }}
  <div id="{{id}}" class="message-container">
    <div class="username">{{username}}</div>
    <div class="badge">
      {{! Provided block helper for repeating a block X times }}
      {{#repeat subTier}}❀️{{/repeat}}
      {{! Provided block helper rendering a block only if something equals something else }}
      {{#eq role "broadcaster"}}πŸ’»{{/eq}}
    </div>
    <div class="message">
      {{! Provided partial for rendering chat message objects }}
      {{> renderChat message }}
    </div>
  </div>
{{/inline}}

<div class="main-container">
  {{! Our state has a messages property which is an array of objects }}
  {{#each messages}}
    {{! Call the `renderMessage` partial for each message in the array }}
    {{! Inside this each block, `this` now refers to each object in the array }}
    {{> renderMessage this}}
  {{/each}}
</div>

With this technique you can see you can build sophisticated, powerful, but very maintainable UIs with very little code.

Warning

While EKG.gg mostly uses a vanilla version of Handlebars, we have removed the functionality to disable HTML escaping using the {{{}}} expression. Please do not attempt to construct HTML as a string, attach it to the state, then try and use the {{{}}} expression in Handlebars. You will get an error. Instead please rely on our view helpers that are linked below.

Further reading

Styling - How the markup looks to users

Language: CSS + Handlebars

EKG.gg gives you nearly the full power of modern CSS to style your widgets. Let's take a look at a simple example.

/* styles.css */
/* Able to style :root pseudo-class */
:root {
  /* CSS custom properties */
  --primary-color: oklab(40.1% 0.1143 0.045);
  /* oklab() color space */
  --primary-bg: oklab(from var(--primary-color) calc(l - 0.15) a b);
}

/* Whatever CSS resets you like */
*,
*::before,
*::after {
  box-sizing: border-box;
}
* {
  min-width: 0;
  min-height: 0;
}

.main {
  max-width: 500px;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  background-color: var(--primary-bg);
  color: var(--primary-color);

  /* CSS Nesting */
  .num {
    font-size: 2em;
    font-weight: bold;
  }

  .subtext {
    font-size: 0.8em;
    /* color-mix() */
    color: color-mix(in oklab, var(--primary-color), black 30%);
  }
}

Warning

Most likely the final browser that will be rendering your widget will be OBS. Most of the time widgets are added to a scene that will then be added as a browser source of OBS. While OBS uses Chromium underneath the hood, they currently use a pretty old version of Chromium. While EKG.gg does not limit what CSS you use, please ensure the CSS you end up writing does not use any features after Chrome 127. If you do they will most likely not work when your users go to use your widget in OBS. Feel free to use a site like CanIUse to check what is supported where.

Further Reading

Settings - Allowing your users to customize things

Oftentimes your users will want the ability to tweak your Widget in some way. Maybe they want to change some colors around? Maybe they want to customize how long an alert appears on screen until it fades? Maybe they want to change the thank you message donos over a certain size get? In any and all of these cases you don't want your users to modify the source code of your widget to make these changes for themselves. You as the widget creator should be able to choose how and what users are able to customize. That's where EKG.gg settings come in.

When you create or update your widget you can optionally add a list of settings and their schemas. This list will be given to EKG.gg as JSON. Let's look at some example settings.

{
  "settings": {
    "hideAfter": {
      "type": "boolean",
      "description": "Hide messages after time",
      "default": false
    },
    "hideAfterAmount": {
      "type": "integer",
      "description": "Hide messages after x seconds:",
      "default": 30,
      "min": 0
    },
    "messageLimit": {
      "type": "integer",
      "description": "Limit message amount",
      "default": 50
    },
    "alignment": {
      "type": "string",
      "description": "Align Messages:",
      "default": "column",
      "choices": {
        "column-reverse": "Top",
        "column": "Bottom"
      }
    }
  }
}

As you can see each setting key is the setting name, and the value is a schema of how that setting can be configured. Each setting as a basic "type" field and an optional "default" field. Then based on the "type" field additional options can be applied.

Settings can also include an optional "group" field. Settings with the same group label are shown together in a collapsible section when a streamer configures your widget.

This list will then be presented to users when they go to add your widget to a scene of theirs for customization. The settings schema will limit the ways a user can input a setting, so you can rest assured your widget will never be given junk data.

You may now be asking yourself, "this is well and good, but how do I then use these settings?". Good question! EKG.gg ensures that all three parts of your widget have access to the user's settings. Let's look at all three parts.

// In your scripts
EKG.widget("MyWidget")
  .initialState(() => ({
    /* ... */
  }))
  .register((event, state, ctx) => {
    // Settings can be found on the `ctx` object
    const settings = ctx.settings;
    return state;
  });
{{! In your templates }}
<div>{{settings.thank_you_message}}</div>
/* In your styles, you can use handlebars to get the settings as well! */
:root {
  --primary-color: {{settings.primaryColor}}
}

Tip

Settings can be a blessing and a curse. The more settings you add the more your users can customize your widget to fit their unique needs. But if you add too many settings it can sometimes feel overwhelming for the streamer to configure. Try to see if you can strike a nice middle ground.

Assets

For image, video, audio or font files that don't need to be customized, you can declare them as assets. These function similarly to settings in that you can access them in your script, template or css. However it'll be under ctx.assets.AssetName or {{assets.AssetName}} rather than settings.

Further reading

Development - How to not build in a vacuum

At this point hopefully you're feeling good and excited to build the next great widget for EKG.gg. The docs are making sense and everything seems so simple to the point of being easy. But how do you test things along the way? How can you simulate an event being fired and seeing how your widget reacts without having to build the whole thing, upload it, and testing it live? That's where the EKG.gg devkit comes in!

Our devkit will enable you to build your widget from the comfort of your own PC using whatever code editor you like. Because our SDK uses NodeJS + NPM it should work on any OS and any evergreen browser.

Tip

While you can develop in any browser you like, as mentioned before, ultimately your widget will often be running in the embed browser inside OBS. Currently that is Chromium version 127. For maximum compatibility you may want to consider using Chrome as the browser you choose to develop with.

Here's how you install the devkit and start a new project:

npm create ekg@latest [folder name]

You can also use pnpm or bun instead of npm. Once you've created a project cd into the new folder, and run npm run dev

Learn more about devkit from its GitHub page: https://github.com/ekggg/devkit

Packaging - Getting ready to deploy to EKG.gg

Okay, you've now been able to build your widget with the EKG.gg SDK and it works great. We're so excited for this next step for you, having your widget go live! For us to list your widget on the EKG.gg widget marketplace we'll need you to upload said widget to us. This includes all of the widget's source files, its manifest file, and all related other assets your widget needs (images, audio, etc).

To do this, EKG.gg requires two things. A manifest.json file fully filled out and a folder containing everything, including said manifest file.

manifest.json

The manifest file contains all of the information about your widget that EKG.gg needs to know about. This includes the names of all of your source files, the list of additional assets, your widget's settings, etc. Let's look at an example.

{
  "template": "template.hbs",
  "css": "styles.css",
  "js": "script.js",
  "settings": {
    "hideAfterAmount": {
      "type": "integer",
      "description": "Hide messages after x seconds:",
      "default": 30,
      "min": 0
    }
  },
  "assets": {
    "background-img": {
      "type": "image",
      "file": "bg.png"
    },
    "large-dono-alarm": {
      "type": "audio",
      "file": "kaboom.wav"
    }
  }
}

The manifest lists all of the files and settings the widget ultimately needs. Make sure you include any and all files your widget references.

The optional top-level "size" field declares a fixed widget size in pixels. When present, streamers can move the widget in a scene but cannot resize it. We recommend avoiding this setting for most widgets.

Warning

If you don't list an asset in your manifest file even if it's included in the final folder, it will not be uploaded to EKG.gg's servers. So make sure to do a full audit!

Building your assets

Running npm run build with the devkit will ensure all of your assets are built into a folder called dist and ready to be uploaded to EKG.gg.

The final folder

Once you feel good about your source files, settings, assets, and manifest file; it's time to wrap them all up into one. Just ensure all files referenced in the manifest are located in the folder in the referenced locations. If you use the devkit, it will validate this for you.

Once you have this final folder head on over the EKG.gg itself, go to the developer portal, and either update an existing widget of yours or create a brand new one.

Note

For safety reasons EKG.gg staff may not immediately publicly publish your new version. Instead it will be put into the review queue and a staff member will ensure that your widget meets the EKG.gg safety standards. Be assured, EKG.gg always attempts to review things as fast as possible and will keep you as the developer in the loop at all times.

That said, you can always use your own widgets in your own streams without waiting for review.

Widget security

For those that made it to the bottom of this very large README file, we thank you for your dedication and admire your impressive attention span. As your reward, you now get to learn about some of the coolest parts of EKG.gg (in our opinion); security!

At EKG.gg we take security extremely seriously. If our platform cannot be trusted then we have failed as an organization. But those that are possibly security minded may already see some of the challenges EKG.gg faces. Namely, for the platform to work we need to run third-party code not written by us. And unfortunately while web technologies are super prevalent and easy to use, they're not known for their security. Here is a short list of things that could go wrong:

  1. A rogue widget developer could exfiltrate all chat messages of a particularly sensitive private stream
  2. Some badly written CSS (i.e. * { size: 100em !important; }) could screw up the look of all widgets on the page
  3. A bad actor could try and mess with all widgets in the scene (i.e. window.EKG.widget = myEvilFunction)
  4. A troll could hot link to an image that during the review looks harmless and nice and later during a major stream updates the image to be a bannable offense.

While none of these would have been EKG.gg's fault per se, we don't think our job is done until none of these are possible. Streamers and widget developers should both feel extremely safe and taken care of when they give us the privilege of their time, attention, and trust of their communities.

Let's start with an overall diagram of the EKG.gg security architecture.

graph TB
      %% External Components
      EKG["🌐 EKG.gg servers<br/>Event stream"]

      %% Security Boundaries
      subgraph "πŸ›‘οΈ  Host environment"
          Browser["🌍 OBS browser source"]
          EventSystem["πŸ“‘ Event distribution<br/>Centralized event routing"]
          WidgetManager["βš™οΈ  Widget manager<br/>Orchestrates widget lifecycles"]

          subgraph "πŸ”’ ChatWidget iframe"
              Display1["πŸ“‹ ChatWidget display<br/>HTML & CSS rendering<br/>Style isolation"]
          end

          subgraph "πŸ”’ AlertBox iframe"
              Display2["πŸ“‹ AlertBox display<br/>HTML & CSS rendering<br/>Style isolation"]
          end
      end

      subgraph "πŸ” Secure Execution Environment"
          subgraph "πŸ’Ό ChatWidget Web Worker"
              VM1["πŸ–₯️  QuickJS virtual machine<br/>Sandboxed runtime"]
              State1["πŸ’Ύ Widget state<br/>Isolated data storage"]
              Security1["πŸ›‘οΈ  Security layer<br/>Data validation & control"]
          end

          subgraph "πŸ’Ό AlertBox Web Worker"
              VM2["πŸ–₯️  QuickJS virtual machine<br/>Sandboxed runtime"]
              State2["πŸ’Ύ Widget state<br/>Isolated data storage"]
              Security2["πŸ›‘οΈ  Security layer<br/>Data validation & control"]
          end
      end

      %% Data Flow
      EKG -->|"πŸ”— Secure connection<br/>Real-time events"| Browser
      Browser -->|"πŸ“‘ Event broadcasting"| EventSystem
      EventSystem -->|"πŸ“¨ Secure messaging<br/>Validated events"| VM1
      EventSystem -->|"πŸ“¨ Secure messaging<br/>Validated events"| VM2

      VM1 -->|"πŸ”„ State changes"| State1
      VM2 -->|"πŸ”„ State changes"| State2

      State1 -->|"πŸ“€ Safe data transfer<br/>Validated output"| WidgetManager
      State2 -->|"πŸ“€ Safe data transfer<br/>Validated output"| WidgetManager

      WidgetManager -->|"🎨 Safe DOM changes"| Display1
      WidgetManager -->|"🎨 Safe DOM changes"| Display2

      %% Security Annotations
      VM1 -.->|"πŸ›‘οΈ  Sandboxed & monitored"| Security1
      VM2 -.->|"πŸ›‘οΈ  Sandboxed & monitored"| Security2

      %% Styling
      classDef security fill:#ffebee,stroke:#d32f2f,stroke-width:2px
      classDef isolation fill:#e8f5e8,stroke:#4caf50,stroke-width:2px
      classDef vm fill:#fff3e0,stroke:#ff9800,stroke-width:2px

      class EKG,Browser security
      class Display1,Display2 isolation
      class VM1,VM2,State1,State2 vm
Loading

How EKG.gg addresses these security challenges

Complete Isolation

To prevent widgets from accessing sensitive data or interfering with each other, EKG.gg implements complete isolation. Widgets cannot make external requests or communicate with outside services, ensuring that chat messages and private stream data stay secure. Additionally, widgets cannot access the streamer's computer, files, or system resources, and they cannot interfere with or access other widgets' data or functionality.

Secure Execution Environment

Widget JavaScript executes in a sandboxed QuickJS virtual machine with restricted capabilities, similar to how the rest of the platform handles untrusted code. Each widget runs in a separate web worker thread, preventing any single widget from blocking the main thread. Only essential globals are available to widgets, blocking access to potentially dangerous browser APIs.

Visual Protection with iframes

Each widget's HTML and CSS renders within its own iframe container, which solves the CSS interference problem we mentioned earlier. Widget CSS cannot affect the main interface or other widgets due to these iframe boundaries. The iframes also prevent widgets from breaking out of their designated display areas, and all visual changes are validated and applied through controlled iframe updates.

Data Security

All data sent to widgets is validated and sanitized before it reaches the widget's execution environment. Widget responses are also checked before being applied to the interface, ensuring that malicious or malformed data cannot affect other parts of the system. Each widget's data is completely separated from others, preventing any cross-contamination of state or information.

About

Documentation for the EKG.gg widget platform

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors