# Quickstart

Your server proves who the user is by signing a short-lived token with the app's server secret. The browser only ever sees that token, never the secret.

## Before you start

- A mondochat account. Staff create it on request: tell them your company name and the email of the person who will own it. That email gets a notice titled "You have access to your company on mondochat", and the owner signs in at `https://dash.mondochat.io` with a link sent to the same email, whenever they like. See [Tenants and apps](/guides/tenants-and-apps/).
- A server that can sign an HS256 token for the user who is logged in, in Node, PHP, Python or any language with a JWT library.
- A page of your site served over https from an origin you can list in the dashboard.

New accounts start in test mode, with up to 50 users per app. Ask staff to switch yours to live when you are ready.

## Steps

1. Get the app id and a server secret. Open **Secrets** in the dashboard and create one. It starts with `mcs_`, is shown once and belongs on your server only. The app id is in the dashboard URL, `/apps/<app id>/...`, and on the same page. You send it as `Mondochat-App-Id`.

2. Allow your site's origin. Open **Origins** and add every origin that shows the chat, for example `https://www.example.com` or `https://*.example.com`. The chat loads only inside pages from listed origins. Every origin is `https://host[:port]`: an `http://` origin, `http://localhost` included, answers 422 when you save. The sample servers below listen on plain http (port 8081), so put TLS in front of them and list the https address you open. For a local try, make a self-signed certificate and a small proxy:

   ```sh
   openssl req -x509 -newkey rsa:2048 -nodes -days 30 -keyout key.pem -out cert.pem -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
   ```

   Save this as `tls-proxy.mjs` next to the two files and run `node tls-proxy.mjs` while the sample server runs:

   ```js
   import { readFileSync } from 'node:fs'
   import { request } from 'node:http'
   import { createServer } from 'node:https'

   createServer({ key: readFileSync('key.pem'), cert: readFileSync('cert.pem') }, (req, res) => {
     const up = request({ port: 8081, path: req.url, method: req.method, headers: req.headers }, (r) => {
       res.writeHead(r.statusCode, r.headers)
       r.pipe(res)
     })
     req.pipe(up)
   }).listen(8443)
   ```

   List `https://localhost:8443` as the origin, open that address, and accept the certificate warning once: the browser must trust the certificate before the page and the chat load. With mkcert installed, `mkcert localhost 127.0.0.1` makes a trusted pair instead (use its `localhost+1.pem` and `localhost+1-key.pem` files).

3. Optional: set your own roles and policy. Open **App** in the dashboard. The defaults have one role, `member`, and members may message each other but not call, which is enough for this quickstart. For fans and models, or any other roles, see [Settings overview](/settings/overview/). Omitted keys keep their defaults.

4. Add a token endpoint to your server. The chat calls `getToken` whenever it needs to sign in, on load and again after a session ends. Your endpoint answers with a token for the logged-in user, or an empty body when nobody is logged in.

   The token is an HS256 JWT signed with the server secret:

   | Claim | Required | Value |
   |---|---|---|
   | `iss` | yes | your app id |
   | `sub` | yes | the user's id on your site, 1 to 128 chars of `A-Z a-z 0-9 . _ : @ -`; do not start it with `guest:` |
   | `name` | yes | display name |
   | `iat`, `exp` | yes | seconds; `exp` at most 600 after `iat` (30s clock skew allowed) |
   | `role` | no | a role key from your settings; default `default_role` |
   | `avatar` | no | https URL |
   | `lang` | no | `it`, `en`, `es` or `fr` |
   | `perms` | no | per-user overrides of policy flags and limits, for example `{"call": false}` |
   | `peer` | no | the external id this user was sent to talk to; counts as a vouch, see [Embed](/guides/embed/) |
   | `jti` | no | when present, the token is single use |

   Each example below is a whole small site: the token endpoint, the page with the embed snippet, and a stand-in login (`/login?as=alice`) that you replace with your own sessions. Any JWT library works too, as long as the algorithm is HS256.

   Save it as `server.mjs`. It has no dependencies.

      ```js
      import { createHmac, randomUUID } from 'node:crypto'
      import { createServer } from 'node:http'

      const APP_ID = process.env.MONDOCHAT_APP_ID
      const SECRET = process.env.MONDOCHAT_SERVER_SECRET
      const EMBED = process.env.MONDOCHAT_EMBED ?? 'https://app.mondochat.io/embed.js'
      const sessions = new Map()

      const b64url = (s) => Buffer.from(s).toString('base64url')

      function mondochatToken(user) {
        const iat = Math.floor(Date.now() / 1000)
        const claims = { iss: APP_ID, sub: user.id, name: user.name, iat, exp: iat + 300 }
        const body = `${b64url(JSON.stringify({ alg: 'HS256', typ: 'JWT' }))}.${b64url(JSON.stringify(claims))}`
        return `${body}.${createHmac('sha256', SECRET).update(body).digest('base64url')}`
      }

      const page = `<!doctype html>
      <meta charset="utf-8"><title>My site</title>
      <form onsubmit="MondoChat.open({ with: this.peer.value }); return false">
        <input name="peer" placeholder="user id"> <button>Chat</button>
      </form>
      <script>
        window.MondoChat = window.MondoChat || function () { (MondoChat.q = MondoChat.q || []).push(arguments) };
        MondoChat('init', { appId: '${APP_ID}', getToken: async () => (await fetch('/mondochat-token')).text() || null });
      </script>
      <script async src="${EMBED}"></script>`

      createServer((req, res) => {
        const url = new URL(req.url, 'http://localhost')
        const user = sessions.get(/(?:^|; )sid=([\w-]+)/.exec(req.headers.cookie ?? '')?.[1])
        if (url.pathname === '/login') {
          const id = (url.searchParams.get('as') ?? '').toLowerCase().replace(/[^a-z0-9]/g, '')
          const sid = randomUUID()
          if (id) sessions.set(sid, { id, name: id[0].toUpperCase() + id.slice(1) })
          res.writeHead(303, { location: '/', 'set-cookie': `sid=${sid}; Path=/; HttpOnly; SameSite=Lax` }).end()
        } else if (url.pathname === '/mondochat-token') {
          res.writeHead(200, { 'content-type': 'text/plain', 'cache-control': 'no-store' })
          res.end(user ? mondochatToken(user) : '')
        } else {
          res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }).end(page)
        }
      }).listen(8081, '0.0.0.0')
      ```
     Save it as `index.php`. Serve it with PHP's built-in server as the router script, on port 8081: `php -S 0.0.0.0:8081 index.php`.

      ```php
      <?php
      $appId = getenv('MONDOCHAT_APP_ID');
      $secret = getenv('MONDOCHAT_SERVER_SECRET');
      $embed = getenv('MONDOCHAT_EMBED') ?: 'https://app.mondochat.io/embed.js';
      session_start();

      function b64url(string $s): string
      {
          return rtrim(strtr(base64_encode($s), '+/', '-_'), '=');
      }

      function mondochat_token(array $user, string $appId, string $secret): string
      {
          $iat = time();
          $claims = ['iss' => $appId, 'sub' => $user['id'], 'name' => $user['name'], 'iat' => $iat, 'exp' => $iat + 300];
          $body = b64url(json_encode(['alg' => 'HS256', 'typ' => 'JWT'])) . '.' . b64url(json_encode($claims));
          return $body . '.' . b64url(hash_hmac('sha256', $body, $secret, true));
      }

      $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
      if ($path === '/login') {
          $id = preg_replace('/[^a-z0-9]/', '', strtolower($_GET['as'] ?? ''));
          $_SESSION['user'] = $id === '' ? null : ['id' => $id, 'name' => ucfirst($id)];
          header('Location: /', true, 303);
          exit;
      }
      if ($path === '/mondochat-token') {
          header('Content-Type: text/plain');
          header('Cache-Control: no-store');
          $user = $_SESSION['user'] ?? null;
          echo $user ? mondochat_token($user, $appId, $secret) : '';
          exit;
      }
      ?><!doctype html>
      <meta charset="utf-8"><title>My site</title>
      <form onsubmit="MondoChat.open({ with: this.peer.value }); return false">
        <input name="peer" placeholder="user id"> <button>Chat</button>
      </form>
      <script>
        window.MondoChat = window.MondoChat || function () { (MondoChat.q = MondoChat.q || []).push(arguments) };
        MondoChat('init', { appId: <?= json_encode($appId) ?>, getToken: async () => (await fetch('/mondochat-token')).text() || null });
      </script>
      <script async src="<?= htmlspecialchars($embed) ?>"></script>
      ```
     Save it as `server.py`. It uses the standard library only.

      ```python
      import base64
      import hashlib
      import hmac
      import json
      import os
      import re
      import secrets
      import time
      from http.cookies import SimpleCookie
      from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
      from urllib.parse import parse_qs, urlsplit

      APP_ID = os.environ["MONDOCHAT_APP_ID"]
      SECRET = os.environ["MONDOCHAT_SERVER_SECRET"].encode()
      EMBED = os.environ.get("MONDOCHAT_EMBED", "https://app.mondochat.io/embed.js")
      sessions: dict[str, dict] = {}

      def b64url(b: bytes) -> str:
          return base64.urlsafe_b64encode(b).rstrip(b"=").decode()

      def mondochat_token(user: dict) -> str:
          iat = int(time.time())
          claims = {"iss": APP_ID, "sub": user["id"], "name": user["name"], "iat": iat, "exp": iat + 300}
          body = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}).encode()) + "." + b64url(json.dumps(claims).encode())
          return body + "." + b64url(hmac.new(SECRET, body.encode(), hashlib.sha256).digest())

      PAGE = f"""<!doctype html>
      <meta charset="utf-8"><title>My site</title>
      <form onsubmit="MondoChat.open({{ with: this.peer.value }}); return false">
        <input name="peer" placeholder="user id"> <button>Chat</button>
      </form>
      <script>
        window.MondoChat = window.MondoChat || function () {{ (MondoChat.q = MondoChat.q || []).push(arguments) }};
        MondoChat('init', {{ appId: {json.dumps(APP_ID)}, getToken: async () => (await fetch('/mondochat-token')).text() || null }});
      </script>
      <script async src="{EMBED}"></script>"""

      class Handler(BaseHTTPRequestHandler):
          def send(self, code: int, body: str = "", **headers: str) -> None:
              self.send_response(code)
              for k, v in headers.items():
                  self.send_header(k.replace("_", "-"), v)
              self.end_headers()
              self.wfile.write(body.encode())

          def do_GET(self) -> None:
              url = urlsplit(self.path)
              sid = SimpleCookie(self.headers.get("Cookie")).get("sid")
              user = sessions.get(sid.value) if sid else None
              if url.path == "/login":
                  uid = re.sub(r"[^a-z0-9]", "", parse_qs(url.query).get("as", [""])[0].lower())
                  new = secrets.token_urlsafe()
                  if uid:
                      sessions[new] = {"id": uid, "name": uid.capitalize()}
                  self.send(303, Location="/", Set_Cookie=f"sid={new}; Path=/; HttpOnly; SameSite=Lax")
              elif url.path == "/mondochat-token":
                  token = mondochat_token(user) if user else ""
                  self.send(200, token, Content_Type="text/plain", Cache_Control="no-store")
              else:
                  self.send(200, PAGE, Content_Type="text/html; charset=utf-8")

      ThreadingHTTPServer(("0.0.0.0", 8081), Handler).serve_forever()
      ```
5. Run your site. Set `MONDOCHAT_APP_ID` and `MONDOCHAT_SERVER_SECRET` in its environment and serve it over https on an origin you listed in step 2. The examples read the script address from `MONDOCHAT_EMBED` and default to `https://app.mondochat.io/embed.js`.

6. Add the embed snippet to every page that shows the chat. The examples above already hold it:

   ```html
   <script>
     window.MondoChat = window.MondoChat || function () { (MondoChat.q = MondoChat.q || []).push(arguments) };
     MondoChat('init', { appId: 'APP_ID', getToken: async () => (await fetch('/mondochat-token')).text() || null });
   </script>
   <script async src="https://app.mondochat.io/embed.js"></script>
   ```

   A launcher bubble appears in the corner once `getToken` returns a token. Call `MondoChat('logout')` when the user logs out of your site. Every option, method and event is on [Embed](/guides/embed/).

7. Open a chat between two users. Open `https://localhost:8443/login?as=alice` in one browser profile and `https://localhost:8443/login?as=bob` in another, or in a private window. The sample session cookie is shared within a profile, so two normal windows of one profile stay the same user. As Alice, type `bob` and press Chat. The button calls `MondoChat('open', { with: 'bob' })`, where `with` is the other user's id on your site.

   A user must exist before others can open a conversation with them. A user exists after their first session, or you create them from your server right away:

   ```sh
   curl -X PUT https://app.mondochat.io/v1/server/users/bob \
     -H "Authorization: Bearer $MONDOCHAT_SERVER_SECRET" -H "Mondochat-App-Id: $MONDOCHAT_APP_ID" \
     -H 'content-type: application/json' -d '{"name": "Bob", "role": "member"}'
   ```

   `MondoChat('open')` without `with` opens the inbox. The request is [Upsert a user](/api/operations/upsert-user/).

## Check it works

- Alice sends a message and Bob sees it arrive.
- To check a token by hand, exchange it. A refused token answers 401 with the reason in `detail`:

  ```sh
  curl -X POST https://app.mondochat.io/v1/session -H 'content-type: application/json' \
    -d '{"app_id": "<app id>", "token": "<token>"}'
  ```

- The **Test console** in the dashboard opens the chat as any user and role without a token endpoint. Two browser windows with two users are the quickest way to try messages and calls.

## When it fails

| Symptom | Cause |
|---|---|
| Nothing appears, the console shows a `frame-ancestors` error | The page is not on a listed origin. |
| `POST /v1/session` answers 401 with `detail` | The token was refused. `detail` says why: `signature`, `exp must be at most 600s after iat`, `bad header` and so on. Check that `iss` is the app id and that the secret is active. |
| 401 `guests are disabled` | `getToken` returned nothing and `guests.enabled` is off: nobody is logged in. |
| 403 `test_user_limit` | Tenants in `test` status accept 50 users per app. |
| Opening a chat with `bob` fails, `POST /v1/conversations` answers 404 | Bob has never signed in and was not created with `PUT /v1/server/users/bob`. |

## Limits

- A client token lives at most 600 seconds.
- An app has at most 2 active server secrets and 20 origins, and the origins together take at most 1024 bytes.
- A tenant in test status accepts at most 50 users per app.
- The session exchange is limited to 300 per IP per minute.

## Related topics

- [Sessions and tokens](/guides/sessions-and-tokens/)
- [Embed](/guides/embed/)
- [Exchange a token for a session](/api/operations/exchange-session/)
- [Errors](/errors/)
- [Settings reference](/reference/settings/)