> ## Documentation Index
> Fetch the complete documentation index at: https://synthetiq.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Vanity URLs

> Serve an app on a domain you own with your own reverse proxy

By default, each deployed app is served on your organization's provisioned domain
as two hosts:

* **App (frontend):** `your-app.apps.yourcompany.com`
* **API:** `your-app.api.apps.yourcompany.com`

Vanity URLs let you serve a specific app on a **domain you own** — e.g.
`console.mycompany.com` — by placing your own reverse proxy in front of the app's
existing hosts. The deployment itself doesn't change: you tell the app which
public URLs to advertise, and your proxy routes your domain to the internal hosts.

<Note>
  Vanity URLs use a **bring-your-own reverse proxy** model. You keep control of DNS,
  TLS, and routing for your domain; Synthetiq just makes the app emit the right
  public URLs (OAuth issuer, redirects, cookies, API base). There's no change to how
  the app is built or deployed.
</Note>

## How it works

```
browser → console.mycompany.com                (your domain)
        → your reverse proxy                    (your TLS + certificate)
            • /gateway/* → your-app.api.apps...   (API + OAuth, Host rewritten)
            • else       → your-app.apps...       (frontend, Host rewritten)
        → Synthetiq edge                        (routes by the internal Host)
        → your app                              (advertises console.mycompany.com)
```

On its internal hosts the app serves two roots: the **frontend** on the app host,
and the **API + OAuth** on the API host. The API host serves your app's data API
under `/api` plus the OAuth/OIDC surface at `/oauth`, `/auth`, `/.well-known`, and
`/mcp`. Your proxy maps your public domain onto those.

## Configure the app's public URLs

Set two values on the production app:

| Field                   | What it is                                    | Example                                 |
| ----------------------- | --------------------------------------------- | --------------------------------------- |
| **App URL** (`app_url`) | Public origin for the frontend                | `https://console.mycompany.com`         |
| **API URL** (`api_url`) | Public base for the API, and the OAuth issuer | `https://console.mycompany.com/gateway` |

<Tip>
  The simplest setup is **one domain with a path for the API**:
  `app_url = https://console.mycompany.com` and
  `api_url = https://console.mycompany.com/gateway`. Your proxy then needs a single rule.
  You can also use a dedicated API subdomain
  (`api_url = https://api.console.mycompany.com`) if you prefer — see the tip at the
  end of the Cloudflare example.
</Tip>

<Note>
  `/gateway` is an **arbitrary routing prefix** — name it anything (`/backend`,
  `/edge`, …). Just avoid the app's own top-level paths (`/api`, `/oauth`, `/auth`,
  `/.well-known`, `/mcp`); `/api` would produce awkward `…/api/api/…` URLs.
</Note>

Set them from the **desktop app** — open the app → **Deployments** → **Domain** tab
→ **Custom domain**, enter the App URL and API URL, save, then redeploy — or from
the **CLI**:

```bash theme={null}
synthetiq production-app update <production-app-id> \
  --app-url https://console.mycompany.com \
  --api-url https://console.mycompany.com/gateway

# vanity URLs are baked at build time — apply them with a deploy
synthetiq deploy create <production-app-id>
```

Clear a vanity URL by passing an empty string: `--app-url ""`.

<Warning>
  A vanity URL only takes effect after a **redeploy** — the app's API base is baked
  at build time. Once it's live, the app is served **only** from your domain: the
  default `your-app.apps.yourcompany.com` URL stops working, because the app now
  advertises your domain for login, cookies, and API calls.
</Warning>

## What your reverse proxy must do

Your proxy sits in front of your domain and forwards to the app's internal hosts,
**rewriting the `Host` header** so Synthetiq's edge routes to the right app.

<Steps>
  <Step title="Terminate TLS for your domain">
    Serve `console.mycompany.com` with your own certificate.
  </Step>

  <Step title="Route the API base to the internal API host">
    Send everything under `api_url` to `your-app.api.apps.yourcompany.com`, with `Host`
    rewritten and the `api_url` path prefix (e.g. `/gateway`) stripped. The app mounts
    `/api` (data), `/oauth`, `/auth`, `/.well-known`, and `/mcp` beneath `api_url` — all
    of these must reach the API host.
  </Step>

  <Step title="Route everything else to the internal frontend host">
    Send all other paths to `your-app.apps.yourcompany.com` (the static frontend), with
    `Host` rewritten.
  </Step>
</Steps>

<Note>
  The internal hostnames are your app's default URLs — the **Production URL** shown in
  the Domain tab (`your-app.apps.yourcompany.com`) and its API counterpart
  (`your-app.api.apps.yourcompany.com`).
</Note>

## Example: Cloudflare

This example serves `console.mycompany.com`, with the API under `/gateway`, using a
Cloudflare Worker for the routing.

<Steps>
  <Step title="Add the domain to Cloudflare">
    Add a **proxied (orange-cloud)** DNS record for `console.mycompany.com`. The Worker
    (next step) intercepts every request and connects to Synthetiq's hosts itself, so
    this record's target is never used as an origin — it exists only so the hostname
    resolves through Cloudflare (which runs the Worker and terminates TLS). Use a
    placeholder target: an **`AAAA` record pointing to `100::`** (or an `A` record to
    `192.0.2.1`), with **Proxy status: Proxied**. Grey-cloud (DNS-only) will not work.
    Cloudflare provisions TLS automatically.
  </Step>

  <Step title="Deploy the Worker">
    Create a Worker with the routing below and bind it to the route
    `console.mycompany.com/*`. Replace the two origin hostnames with your app's defaults.

    ```js theme={null}
    // app_url = https://console.mycompany.com
    // api_url = https://console.mycompany.com/gateway
    const FRONTEND_ORIGIN = "your-app.apps.yourcompany.com";      // static frontend
    const API_ORIGIN      = "your-app.api.apps.yourcompany.com";  // API + OAuth
    const API_PREFIX      = "/gateway";                                 // the path in api_url

    export default {
      async fetch(request) {
        const url = new URL(request.url);
        const underApi =
          url.pathname === API_PREFIX || url.pathname.startsWith(API_PREFIX + "/");

        // Pick the origin, and strip the /gateway prefix for API traffic so the app sees
        // its own paths (/api, /oauth, /auth, /.well-known, /mcp).
        const origin = new URL(url);
        origin.hostname = underApi ? API_ORIGIN : FRONTEND_ORIGIN;
        origin.pathname = underApi ? url.pathname.slice(API_PREFIX.length) || "/" : url.pathname;

        // Dropping the incoming Host lets the request go out with Host: <internal>,
        // so Synthetiq's edge routes it to the right app. redirect: "manual" lets
        // OAuth redirects flow back to the browser instead of being followed here.
        const headers = new Headers(request.headers);
        headers.delete("Host");
        return fetch(origin.toString(), {
          method: request.method,
          headers,
          body: request.body,
          redirect: "manual",
        });
      },
    };
    ```
  </Step>

  <Step title="Set the URLs and redeploy">
    Set `app_url` / `api_url` on the app (above) and redeploy. Then sign in at
    `https://console.mycompany.com` to confirm the login round-trip works end to end.
  </Step>
</Steps>

<Tip>
  Prefer a dedicated API subdomain? Set `api_url = https://api.console.mycompany.com`,
  point that subdomain at the API origin (`Host` rewritten, no path to strip), and
  point `console.mycompany.com` at the frontend origin. Same idea, two hostnames
  instead of one Worker.
</Tip>

## CORS

With the **path layout** (`api_url = https://console.mycompany.com/gateway`) the frontend
and API share one origin, so there is **no CORS** to think about.

With the **subdomain layout** (`api_url = https://api.console.mycompany.com`) the
frontend and API are different origins, so requests are cross-origin — but this is
**handled automatically**, the same way the default two-host deployment already is
(its frontend and API also live on different hosts). The app allows browser
requests whose `Origin` matches `app_url`. So it just works, provided:

* You set **both** `app_url` and `api_url` — CORS is keyed to `app_url` (the
  frontend origin). Setting only `api_url` leaves the app allowing the default host,
  and your frontend's calls would be rejected.
* Your proxy forwards the `Origin` header unchanged and passes `OPTIONS` preflight
  requests through to the API host (standard proxy behavior).

Cross-origin API calls authenticate with Bearer tokens rather than cookies, so
there's nothing extra to configure for credentials.

## Notes and limitations

* **Redeploy required.** Setting or changing a vanity URL only takes effect on the
  next deploy.
* **Use your vanity URL once it's live.** The app then advertises only your domain
  for login and API calls, so opening the default `your-app.apps.yourcompany.com`
  URL won't work as an entry point — its API calls are rejected (CORS) and its login
  won't complete. The internal hosts do stay network-reachable behind Synthetiq's
  edge (that's how your proxy reaches them); to restrict them to proxy-only access,
  ask us about origin hardening.
