# createApp and lifecycle

> Create an app, register connect, key, resize, and close handlers, then call listen

`createApp()` returns a chainable **app** that you configure with event handlers and then start with `.listen()`. Nothing runs until you call `listen()`, which is what wires the app to the host.

```js

createApp()
  .onConnect(handleConnect)
  .onKey(handleKey)
  .onResize(handleResize)
  .onClose(handleClose)
  .listen();
```

Each `on*` method returns the app, so calls chain, and every handler may be `async`. One app instance handles every connected user (they are all multiplexed into it), so keep per-user state keyed by `conn.id`.

## A minimal app

```js title="app.js"

createApp()
  .onConnect((conn) => {
    conn.write('\x1b[2J\x1b[H'); // clear screen, home cursor
    conn.write('hello from voxal\r\n');
    conn.write('press q to quit\r\n');
  })
  .onKey((conn, data) => {
    if (data === 'q' || data === '\x03') return conn.close();
    conn.write('you pressed: ' + JSON.stringify(data) + '\r\n');
  })
  .onClose((conn) => {
    console.log('bye', conn.id); // server logs
  })
  .listen();
```

## Handlers

Pass each `on*` method a function. Passing `null` or `undefined` clears that handler, which is handy for conditional registration. Passing any other non-function throws a `TypeError` at registration, so mistakes surface immediately instead of at the first event.

### onConnect(handler)

```ts
onConnect(handler: (conn: Conn) => void | Promise<void>): App
```

Fires when a user connects. This is the first event, so use it to build and seed per-connection state and to do the first render. `conn.cols` and `conn.rows` already hold the terminal size.

```js
.onConnect((conn) => {
  conn.write('welcome, your terminal is ' + conn.cols + 'x' + conn.rows + '\r\n');
})
```

### onKey(handler)

```ts
onKey(handler: (conn: Conn, data: string) => void | Promise<void>): App
```

Fires on input. `data` is the decoded input string, usually a single character, but escape sequences and control codes arrive too. See [The connection](/sdk/connection#key-encoding) for the full encoding.

```js
.onKey((conn, data) => {
  if (data === 'q' || data === '\x03') return conn.close();
  if (data === '\x1b[A') return conn.write('up\r\n');
  conn.write('you pressed ' + JSON.stringify(data) + '\r\n');
})
```

<Callout title="Tip">
If you are matching arrow keys, control codes, and editing keys by hand, consider the [UI layer's `useInput`](/sdk/ui/hooks). It decodes all of that into a friendly key object for you.
</Callout>

### onResize(handler)

```ts
onResize(handler: (conn: Conn, size: Size) => void | Promise<void>): App
```

Fires on terminal resize. `conn.cols` and `conn.rows` are **already updated** before your handler runs, and the new size is also passed as `{ cols, rows }`.

```js
.onResize((conn, size) => {
  conn.write('\x1b[2J\x1b[H');               // clear and home
  conn.write('-'.repeat(size.cols) + '\r\n'); // redraw to fit
})
```

### onClose(handler)

```ts
onClose(handler: (conn: Conn) => void | Promise<void>): App
```

Fires when the user disconnects, whether they dropped the session or your app called `conn.close()`. Clean up per-connection state here.

```js
const sessions = new Map();

createApp()
  .onConnect((conn) => sessions.set(conn.id, { score: 0 }))
  .onClose((conn) => sessions.delete(conn.id))
  .listen();
```

## listen()

```ts
listen(): App
```

Activates the app. **Call it exactly once, after registering handlers.** Calling `listen()` on a second app within one bundle logs a warning and the last one wins, since only one app per bundle is supported.

## Error handling

The SDK does not await your handlers, so events are not ordered relative to your own async work. Guard your own `await`s with `try/catch`.

<Callout>
Thrown errors and rejected promises from your handlers are caught and logged to the **server logs** (prefixed `[voxal app error]`), not the user's terminal, so one bad handler can never crash the shared sandbox. To show the user something, write to the terminal yourself.
</Callout>

## Managing per-connection state

All users of your app share one sandbox, so keep per-session data in a `Map` keyed by `conn.id`, and free it in `onClose`.

```js
const state = new Map();

createApp()
  .onConnect((conn) => {
    state.set(conn.id, { count: 0 });
    render(conn);
  })
  .onKey((conn, data) => {
    const s = state.get(conn.id);
    if (data === '+') s.count++;
    render(conn);
  })
  .onClose((conn) => state.delete(conn.id))
  .listen();

function render(conn) {
  const { count } = state.get(conn.id);
  conn.write('\x1b[2J\x1b[Hcount: ' + count + '\r\n');
}
```

Module-level values like `state` above are shared across **all** connections and reset when the app scales to zero and re-boots. See [How voxal works](/how-it-works) for the details.

---

Source: https://docs.voxal.sh/sdk/lifecycle
