How to Use Svelte and Go to Build a Video Chat App

August 28, 2026
Written by
Reviewed by
Paul Kamp
Twilion

The internet has made the world a much smaller place. Not only can you chat with your loved ones in real-time, but with video you can also see them and experience an extra level of interaction that words alone fail to capture.

In this tutorial, I will show you how simple it is to build a video chat app using Twilio Video, using Svelte for the frontend and Go for the backend.

This tutorial is based on an older tutorial by Joseph Udonsak. It has been updated for the latest framework and library versions.

Prerequisites

To follow along, you will need the following:

  • A Twilio account (a free account works fine).
  • Go 1.24 or newer.
  • Node.js 20.19 or newer (or 22+) and npm. The current Vite tooling requires one of these versions.
  • Git.
  • Basic knowledge of Go and Svelte.

How the application works

Before writing any code, it helps to understand the flow. The application has two parts, a Go REST API and a Svelte single-page app. A video call comes together in four steps:

  • The frontend presents a form where the user enters a room name.
  • The room name is sent to the backend.
  • The backend generates a Twilio Access Token with a video grant for that room.
  • The frontend uses the token to connect to the Twilio Video room and attaches the video tracks to the page.

In this tutorial, you will build the backend first, then the frontend that talks to it. Let’s get started!

Setting up the backend

Start by creating a project folder with a backend folder inside it, then initialize a Go module. To do this, run the following commands.

mkdir svelte-go-video-chat
cd svelte-go-video-chat
mkdir backend
cd backend
go mod init video_app

Next, add the three dependencies the backend relies on: a CORS handler so the browser can call the API, GoDotEnv to load environment variables, and the Twilio Go Helper Library to build the Access Token.

go get github.com/rs/cors
go get github.com/joho/godotenv
go get github.com/twilio/twilio-go

Next, create a new file named .env in the backend folder. This file is a placeholder template that holds no secrets, so it is safe to commit to version control. Paste the following code into it.

TWILIO_ACCOUNT_SID="<<TWILIO_ACCOUNT_SID>>"
TWILIO_API_KEY_SID="<<TWILIO_API_KEY_SID>>"
TWILIO_API_SECRET_KEY="<<TWILIO_API_SECRET_KEY>>"

Your real credentials will go in a separate .env.local file, which is the file the application actually reads. Before you create it, add a .gitignore in the backend folder so that .env.local is never committed.

echo ".env.local" > .gitignore

Now, create your local copy of the template with the following command. Because .env.local is git-ignored, your real credentials stay out of version control.

cp .env .env.local

You will fill in the values in a moment. Your Account SID is on the Twilio Console dashboard. Next, create a new API Key which will be used for authentication to the Twilio Video API. In the Twilio Console, go to Settings > API keys & auth tokens, create a Standard key, and copy the SID and Secret, as the secret is only shown once.

Paste those three values into backend/.env.local:

TWILIO_ACCOUNT_SID="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TWILIO_API_KEY_SID="SKxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
TWILIO_API_SECRET_KEY="your-api-key-secret"

With the credentials in place, you can write the code that mints a token. Create a new folder named helper in the backend folder. In the new helper folder, create a new file named token.go and add the following code to it.

package helper
import (
    "crypto/rand"
    "encoding/hex"
    "fmt"
    "github.com/twilio/twilio-go/client/jwt"
    "os"
)
func GenerateToken(roomName string) string {
    params := jwt.AccessTokenParams{
        AccountSid: os.Getenv("TWILIO_ACCOUNT_SID"),
        SigningKeySid: os.Getenv("TWILIO_API_KEY_SID"),
        Secret: os.Getenv("TWILIO_API_SECRET_KEY"),
        Identity: identity(),
    }
    jwtToken := jwt.CreateAccessToken(params)
    videoGrant := &jwt.VideoGrant{
        Room: roomName,
    }
    jwtToken.AddGrant(videoGrant)
    token, err := jwtToken.ToJwt()
    if err != nil {
        fmt.Println(err)
    }
    return token
}
func identity() string {
    buffer := make([]byte, 16)
    rand.Read(buffer)
    return hex.EncodeToString(buffer)
}

The GenerateToken function reads your Twilio credentials from the environment, builds an Access Token, attaches a VideoGrant scoped to the requested room, and signs it into a JWT. The identity helper gives each participant a unique, random identity using crypto/rand, so two people in the same room never collide.

Next, the backend needs to validate the room name coming from the frontend. To do this, create a new folder named model in the backend folder, and in that new folder create a file named room.go. Then, paste the following code into the new file.

package model
import "errors"
type Room struct {
    Name string `json:"roomName"`
}
func (room *Room) Validate() error {
    if len(room.Name) < 6 {
        return errors.New("room name cannot be less than 6 characters")
    }
    return nil
}

The Room struct maps the incoming JSON payload, and Validate enforces a simple rule: room names must be at least six characters long.

All that is left is a main() function to serve as the entry point for the application and handle incoming requests. In the backend folder, create a new file named main.go and paste the following code into it.

package main
import (
    "encoding/json"
    "fmt"
    "github.com/joho/godotenv"
    "github.com/rs/cors"
    "log"
    "net/http"
    "video_app/helper"
    "video_app/model"
)
func main() {
    loadEnv()
    c := cors.Default()
    handler := http.HandlerFunc(roomHandler)
    fmt.Printf("Starting server at port 8000\n")
    if err := http.ListenAndServe(":8000", c.Handler(handler)); err != nil {
        log.Fatal(err)
    }
}
func loadEnv() {
    err := godotenv.Load(".env.local")
    if err != nil {
        log.Fatal("Error loading .env file")
    }
}
func roomHandler(writer http.ResponseWriter, request *http.Request) {
    writer.Header().Set("Content-Type", "application/json")
    response := make(map[string]string)
    var room model.Room
    json.NewDecoder(request.Body).Decode(&room)
    err := room.Validate()
    if err != nil {
        writer.WriteHeader(http.StatusBadRequest)
        response["message"] = err.Error()
    } else {
        response["jwt"] = helper.GenerateToken(room.Name)
    }
    jsonResponse, err := json.Marshal(response)
    if err != nil {
        log.Fatalf("Error happened in JSON marshal. Err: %s", err)
    }
    writer.Write(jsonResponse)
}

This loads the environment variables, wraps the handler in the default CORS middleware so the browser can reach it, and starts an HTTP server on port 8000. For each POST request, roomHandler decodes the room name, validates it, and returns either a 400 with an error message or a signed access token as JSON.

Start the backend to confirm it runs.

go run main.go

You should see “Starting server at port 8000”. Leave it running and open a second terminal for the frontend.

Setting up the frontend

For the frontend, scaffold a new Svelte project with Vite, the current official tooling for Svelte apps. From the project root (the svelte-go-video-chat folder), run the following command. The --template svelte flag selects the Svelte project for you, so the command runs without any prompts.

npm create vite@latest frontend -- --template svelte

This creates a frontend folder with a ready-to-run Svelte 5 project. Move into it and install the dependencies.

cd frontend
npm install

Next, install the three packages this app needs: twilio-video to manage the video connection, axios to call the backend, and izitoast to display notifications.

npm install twilio-video axios izitoast

With the project scaffolded, create a new file named src/Helper.js and add the following code. This module handles all the communication with Twilio Video.

import {connect, createLocalVideoTrack} from "twilio-video";
import axios from "axios";
import iziToast from "izitoast";
const notify = message => {
    iziToast.success({
        message,
        position: 'topRight'
    });
}
const axiosInstance = axios.create({
    baseURL: "http://localhost:8000",
});
const getAccessToken = async (roomName) => {
    const response = await axiosInstance.post("", {roomName});
    const {jwt} = response.data;
    return jwt;
};
export const connectToRoom = async (roomName, videoContainer) => {
    const token = await getAccessToken(roomName);
    const room = await connect(token, {name: roomName});
    const videoTrack = await createLocalVideoTrack();
    notify(`Successfully joined a Room: ${room.name}`);
    videoContainer.appendChild(videoTrack.attach());
    room.on('participantConnected', participant => {
        notify(`A remote participant connected: ${participant.identity}`);
        participant.tracks.forEach(publication => {
            if (publication.isSubscribed) {
                const track = publication.track;
                videoContainer.appendChild(track.attach());
            }
        });
        participant.on('trackSubscribed', track => {
            videoContainer.appendChild(track.attach());
        });
    });
    room.participants.forEach(participant => {
        participant.tracks.forEach(publication => {
            if (publication.track) {
                videoContainer.appendChild(publication.track.attach());
            }
        });
        participant.on('trackSubscribed', track => {
            videoContainer.appendChild(track.attach());
        });
    });
}

connectToRoom first requests an access token from the backend, then calls connect() to join the room and createLocalVideoTrack() to capture the user's camera. It attaches the local track to the page, then wires up the participantConnected and trackSubscribed events so that when someone else joins, their video track is attached too. The final loop over room.participants handles anyone who was already in the room when you joined. These are the core Twilio Video client calls, and they are unchanged from earlier versions of the SDK.

Now, update frontend/src/App.svelte to match the following code.

<script>
    import 'izitoast/dist/css/iziToast.min.css'
    import {connectToRoom} from "./Helper";
    let hasJoinedRoom = false;
    const handleSubmit = async e => {
        const formData = new FormData(e.target);
        const roomName = formData.get('roomName');
        hasJoinedRoom = true
        const videoContainer = document.getElementById('remote-media');
        await connectToRoom(roomName, videoContainer)
    }
</script>
<main>
    <h1> Svelte Go Twilio Video Chat App </h1>
    <div id="remote-media"></div>
    {#if !hasJoinedRoom}
        <form on:submit|preventDefault={handleSubmit}>
            <div>
                <label for="roomName">Room name</label>
                <input
                        type="text"
                        id="roomName"
                        name="roomName"
                        value=""
                />
            </div>
            <button type="submit">Submit</button>
        </form>
    {/if}
</main>
<style>
    main {
        text-align: center;
        padding: 1em;
        max-width: 240px;
        margin: 0 auto;
    }
    @media (min-width: 640px) {
        main {
            max-width: none;
        }
    }
</style>

The component shows a form that accepts a room name. When it is submitted, handleSubmit reads the room name, hides the form by setting hasJoinedRoom, and calls connectToRoom, passing in the #remote-media container where the video tracks are attached. The import at the top pulls in the iziToast styles so the notifications are styled correctly.

Two small housekeeping steps finish the setup. The Vite scaffold generates src/main.js to bootstrap the app, and it already uses Svelte 5's mount() API, so there is nothing to change.

import { mount } from 'svelte'
import './app.css'
import App from './App.svelte'
const app = mount(App, {
  target: document.getElementById('app'),
})
export default app

Finally, open index.html and set the <title> to something meaningful such as “Svelte Go Twilio Video Chat App”, and clear out the demo markup in src/app.css if you would like a blank slate. The scaffold also ships a src/lib/Counter.svelte and some assets from the starter template that this app does not use, so you can safely delete them.

Testing the application

With the Go backend still running on port 8000, start the frontend dev server.

npm run dev

Vite serves the app at http://localhost:5173 by default (if that port is taken, it prints the one it chose). Open the app in two browser tabs, enter the same room name (at least six characters) in both, and submit. Your browser will ask for camera and microphone permissions the first time, allow them, and each tab will show both the local and remote video streams.

Conclusion

There you have it, without writing any code for permissions, RTC (Real Time Communication) or data streaming, you have an application capable of handling all those and even more.

In addition to video streaming, the Twilio Video SDK provides functionality for muting audio, recording video chats and displaying camera previews before joining a room. You can view the documentation here.

I'm excited to see you build the next big thing in video communication.

You can review the final codebase for this article on GitHub. Until next time!

Additional resources