---
title: Getting Started
description: Get started with NoLag in 5 minutes. Learn how to connect, subscribe to topics, and publish messages.
---

# Getting Started

Get up and running with NoLag in under 5 minutes.

## Prerequisites

- A NoLag account ([sign up free](https://portal.nolag.app))
- Node.js 18+ (for the JavaScript SDK)

You create your app and access tokens in Step 2, so nothing else is needed up front.

## Step 1: Install the SDK

Install the NoLag SDK for your preferred language:

```bash [npm]
npm install @nolag/js-sdk
```
```bash [pip]
pip install nolag
```
```bash [go get]
go get github.com/NoLagApp/go-sdk
```

## Step 2: Create an App, a Room, and Two Access Tokens

You need three kinds of thing:

- an **app**, a container for your rooms, carrying the list of topics it allows
- a **room** inside that app, the namespace your topics live in
- two **actors**, client identities whose access tokens authenticate the connections

Why two actors? A publishing actor never receives its own message, so one token
that subscribes and then publishes hears nothing. This guide uses one actor to
publish and another to subscribe.

**In the dashboard:**

1. Log in to the [NoLag Dashboard](https://portal.nolag.app) and create a project.
2. Create an app inside the project, with `messages` in its topic list.
3. Add a room to the app, and note its slug.
4. Open the **Actors** section, create two actors, and copy both access tokens.

**Or over the REST API.** This is the path to use from a script or an AI agent
that sets everything up on its own. The only bootstrap secret is a project-scoped
[API key](/docs/authentication#api-keys), created once in the dashboard;
everything else is an API call, through the SDK's `NoLagApi` client or plain
`curl`:

```typescript [Setup (server side, once)]
import { NoLagApi } from '@nolag/js-sdk'

const api = new NoLagApi(process.env.NOLAG_API_KEY) // nlg_live_...

// 1. Create an app and declare the topics it allows. The slug that comes back
//    is not the one you sent (see the note below), so read it from the response.
const app = await api.apps.create({
  name: 'My App',
  slug: 'my-app',
  topics: ['messages'],
})
console.log(app.slug) // e.g. "my-app-a3f9": this is what you pass to setApp()

// 2. Create a room in that app. Clients cannot subscribe to a room that does not exist.
await api.rooms.create(app.appId, { name: 'General', slug: 'general' })

// 3. Create two actors. Each access token is shown once; keep both.
const publisher = await api.actors.create({ name: 'Publisher', actorType: 'service' })
const subscriber = await api.actors.create({ name: 'Subscriber', actorType: 'user' })
console.log(publisher.accessToken, subscriber.accessToken)
```
```bash [Terminal]
# 1. Create an app with the topics it allows. Read appId and slug back out of
#    the response: the returned slug is not the one you sent (see the note below).
curl -X POST https://api.nolag.app/v1/apps \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"name":"My App","slug":"my-app","topics":["messages"]}'
# => {"appId":"019fd987-9dee-75cb-97a7-39d71957ca23","slug":"my-app-a3f9",...}

# 2. Create a room in that app, using the appId from step 1.
#    Clients cannot subscribe to a room that does not exist yet.
curl -X POST https://api.nolag.app/v1/apps/019fd987-9dee-75cb-97a7-39d71957ca23/rooms \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"name":"General","slug":"general"}'

# 3. Create two actors, and copy accessToken from each response
curl -X POST https://api.nolag.app/v1/actors \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"name":"Publisher","actorType":"service"}'
curl -X POST https://api.nolag.app/v1/actors \
  -H "Authorization: Bearer nlg_live_xxx.secret" \
  -H "Content-Type: application/json" \
  -d '{"name":"Subscriber","actorType":"user"}'
```

**Save the tokens now.** An actor's access token is only returned when the actor is
created. It is never shown again.

**App slugs always get a random suffix.** NoLag appends four random characters to
every app slug to keep it unique within the project, so a requested slug of
`my-app` comes back as something like `my-app-a3f9`. Read the `slug` field from the
create response and pass that value to `setApp()`. Room slugs are different: they
are stored exactly as you supply them.

**Rooms must exist before you subscribe.** Rooms are never created implicitly.
Subscribing to a room that has not been created returns an `unknown_topic` error
(code 42940) instead of delivering messages. If you skip the error handler in
Step 3, this looks like total silence: `connect()`, `subscribe()`, and `emit()`
all appear to succeed and no message ever arrives.

See the [REST API Reference](/docs/api-reference) for the full app, room, and actor endpoints.

## Step 3: Connect to NoLag

Attach an error handler before you connect. Subscribe and publish calls do not
throw: the broker reports problems such as an unknown topic or a permission
refusal on the `error` event, so without a handler a misconfigured app simply
goes quiet. Broker errors arrive as `NoLagServerError`, with `code`, `topic` and
`hint`; transport failures arrive as a plain error.

This first client is the subscriber:

```typescript [TypeScript]
import { NoLag, NoLagServerError } from '@nolag/js-sdk'

// Create client and connect
const client = NoLag('subscriber_access_token')

// Attach this first: server-side errors arrive here, not as thrown exceptions
client.on('error', (err) => {
  if (err instanceof NoLagServerError) {
    console.error(`NoLag ${err.error} (${err.code}) on ${err.topic}: ${err.hint}`)
  } else {
    console.error('NoLag error:', err)
  }
})

await client.connect()

console.log('Connected to NoLag!')
```
```python [Python]
from nolag import NoLag

# Create client and connect
client = NoLag('subscriber_access_token')

# Attach this first: server-side errors arrive here, not as raised exceptions.
# Broker errors are NoLagServerError; str(err) includes code, topic and hint.
def handle_error(err):
    print(f'NoLag error: {err}')

client.on('error', handle_error)

await client.connect()

print('Connected to NoLag!')
```
```go [Go]
package main

import (
    "fmt"
    "log"

    nolag "github.com/NoLagApp/go-sdk"
)

func main() {
    // Create client and connect
    client := nolag.New("subscriber_access_token")

    // Attach this first: server-side errors arrive here, not as returned errors
    client.OnError(func(err *nolag.ServerError) {
        log.Printf("NoLag %s (%d) on %s: %s", err.Name, err.Code, err.Topic, err.Hint)
    })

    if err := client.Connect(); err != nil {
        log.Fatal(err)
    }

    fmt.Println("Connected to NoLag!")
}
```

## Step 4: Subscribe to a Topic

Topics are channels for messages. Subscribe to receive messages published to a topic.

Pass the app slug exactly as the create response returned it, suffix included, and
the slug of a room that already exists:

```typescript [TypeScript]
// Set up app and room, then subscribe.
// 'my-app-a3f9' is the slug returned when the app was created.
const room = client.setApp('my-app-a3f9').setRoom('general')
room.subscribe('messages')

// Listen for messages
room.on('messages', (data) => {
  console.log('Received:', data)
})
```
```python [Python]
# Set up app and room, then subscribe.
# 'my-app-a3f9' is the slug returned when the app was created.
room = client.set_app('my-app-a3f9').set_room('general')
await room.subscribe('messages')

# Listen for messages
def handle_message(data, meta):
    print('Received:', data)

room.on('messages', handle_message)
```
```go [Go]
// Set up app and room, then subscribe with handler.
// "my-app-a3f9" is the slug returned when the app was created.
room := client.SetApp("my-app-a3f9").SetRoom("general")
room.Subscribe("messages", func(data any, meta nolag.MessageMeta) {
    fmt.Println("Received:", data)
})
```

## Step 5: Publish a Message

Publish from a second connection, authenticated with the publisher's token.
Publishers never receive their own messages, so an `emit()` on the connection
that subscribed in Step 4 would deliver nothing:

```typescript [TypeScript]
const publisher = NoLag('publisher_access_token')
await publisher.connect()

// Publish a message
publisher.setApp('my-app-a3f9').setRoom('general').emit('messages', {
  text: 'Hello, World!',
  sender: 'user-123',
  timestamp: Date.now()
})
```
```python [Python]
import time

publisher = NoLag('publisher_access_token')
await publisher.connect()

# Publish a message
await publisher.set_app('my-app-a3f9').set_room('general').emit('messages', {
    'text': 'Hello, World!',
    'sender': 'user-123',
    'timestamp': time.time()
})
```
```go [Go]
publisher := nolag.New("publisher_access_token")
if err := publisher.Connect(); err != nil {
    log.Fatal(err)
}

// Publish a message
publisher.SetApp("my-app-a3f9").SetRoom("general").Emit("messages", map[string]any{
    "text":      "Hello, World!",
    "sender":    "user-123",
    "timestamp": time.Now().Unix(),
})
```

## Complete Example

Here's a complete example in your preferred language. It assumes the app, room, and
two actors from Step 2 already exist. One actor subscribes and a different actor
publishes, because publishers never receive their own messages.

```typescript [TypeScript]
import { NoLag, NoLagServerError } from '@nolag/js-sdk'

const APP_SLUG = 'my-app-a3f9' // the slug returned when you created the app

// Surface broker errors: subscribe and emit never throw
const logErrors = (err: Error) => {
  if (err instanceof NoLagServerError) {
    console.error(`NoLag ${err.error} (${err.code}) on ${err.topic}: ${err.hint}`)
  } else {
    console.error('NoLag error:', err)
  }
}

// One actor subscribes...
const subscriber = NoLag('subscriber_access_token')
subscriber.on('error', logErrors)
await subscriber.connect()

const inbox = subscriber.setApp(APP_SLUG).setRoom('general')
inbox.subscribe('messages')
inbox.on('messages', (data) => {
  console.log('Received:', data)
})

// ...and a different actor publishes
const publisher = NoLag('publisher_access_token')
publisher.on('error', logErrors)
await publisher.connect()

publisher.setApp(APP_SLUG).setRoom('general').emit('messages', { text: 'Hello, World!' })
```
```python [Python]
import asyncio

from nolag import NoLag

APP_SLUG = 'my-app-a3f9'  # the slug returned when you created the app


# Surface broker errors: subscribe and emit never raise.
# Broker errors are NoLagServerError; str(err) includes code, topic and hint.
def handle_error(err):
    print(f'NoLag error: {err}')


def handle_message(data, meta):
    print('Received:', data)


async def main():
    # One actor subscribes...
    subscriber = NoLag('subscriber_access_token')
    subscriber.on('error', handle_error)
    await subscriber.connect()

    inbox = subscriber.set_app(APP_SLUG).set_room('general')
    await inbox.subscribe('messages')
    inbox.on('messages', handle_message)

    # ...and a different actor publishes
    publisher = NoLag('publisher_access_token')
    publisher.on('error', handle_error)
    await publisher.connect()

    await publisher.set_app(APP_SLUG).set_room('general').emit('messages', {'text': 'Hello, World!'})

    await asyncio.sleep(1)  # give the message time to arrive
    publisher.disconnect()
    subscriber.disconnect()


asyncio.run(main())
```
```go [Go]
package main

import (
    "fmt"
    "log"
    "time"

    nolag "github.com/NoLagApp/go-sdk"
)

const appSlug = "my-app-a3f9" // the slug returned when you created the app

func main() {
    // Surface broker errors: Subscribe and Emit do not return them
    logErrors := func(err *nolag.ServerError) {
        log.Printf("NoLag %s (%d) on %s: %s", err.Name, err.Code, err.Topic, err.Hint)
    }

    // One actor subscribes...
    subscriber := nolag.New("subscriber_access_token")
    subscriber.OnError(logErrors)
    if err := subscriber.Connect(); err != nil {
        log.Fatal(err)
    }
    defer subscriber.Close()

    inbox := subscriber.SetApp(appSlug).SetRoom("general")
    inbox.Subscribe("messages", func(data any, meta nolag.MessageMeta) {
        fmt.Println("Received:", data)
    })

    // ...and a different actor publishes
    publisher := nolag.New("publisher_access_token")
    publisher.OnError(logErrors)
    if err := publisher.Connect(); err != nil {
        log.Fatal(err)
    }
    defer publisher.Close()

    publisher.SetApp(appSlug).SetRoom("general").Emit("messages", map[string]string{"text": "Hello, World!"})

    time.Sleep(time.Second) // give the message time to arrive
}
```

## Troubleshooting

**Nothing arrives, and nothing errors.** Almost always one of three things: the
room does not exist, the app slug is missing its random suffix, or the same actor
is publishing and subscribing (publishers never receive their own messages).
Attach the `error` handler from Step 3 and look for `unknown_topic`, then check
the slug against `GET /apps` and the room against `GET /apps/{appId}/rooms`.

**`unknown_topic` on a topic you did create.** Topics resolve as
`app-slug/room-slug/topic-name`, and the topic name must be in the app's topic
list. A room's own topic list is not consulted for access, so confirm the topic
is listed on the app.

See the [Error Reference](/docs/api-reference/errors) for the full list of codes.

## Next Steps

- [Learn about Topics & Pub/Sub](/docs/concepts/topics)
- [Understand Presence Tracking](/docs/concepts/presence)
- [Configure Quality of Service](/docs/concepts/qos)
- [Full JavaScript SDK Reference](/docs/sdks/javascript)
