# Routing

> The shell owns the address bar and hands each module a view and its params, which is why a module never touches window.location.

Page: https://sdk.chatfuel.com/docs/concepts/routing
Markdown: https://sdk.chatfuel.com/docs/concepts/routing.md

A module never touches `window.location`. The shell owns the address bar, and each module receives
its own slice of it through props — which is what lets the same module tree run at a domain root, at
a sub-path, or embedded in somebody else's app without a line of it changing.

The router is hand-rolled and about 160 lines. There is no `react-router`, because the vendored
dependency set stays frozen: everything you would have to install to run this app is already in it.

## The shape of an address [#the-shape-of-an-address]

```
/<moduleId>[/<view>]?<params>
```

The first segment is the module id — the same string as the directory name and the manifest `id`.
The second, when there is one, is the module's own view. Everything after `?` is that module's
deep-link state.

`/contacts` is the contacts module at its root. `/contacts/fields?density=compact` is the same
module, a different screen, with one parameter it understands.

## What a module gets [#what-a-module-gets]

Five props, and no way around them.

| Prop        | What it is                                                               |
| ----------- | ------------------------------------------------------------------------ |
| `view`      | the path segment after the module id, `''` at the module's root          |
| `setView`   | move within this module — a view change is a place, so it pushes         |
| `params`    | this module's deep-link params, the part after `?`                       |
| `setParams` | replace them, with `replaceState`, adding no history entry               |
| `navigate`  | somewhere else in the app, as an app-relative path like `/livechat?c=42` |

That split is the whole navigation policy. A view is a destination and belongs in the back stack; a
filter, a density or an open row is a refinement of where you already are and does not. Get it
backwards and the Back button either does nothing or takes eleven presses to leave a screen.

When the address points at some other module, the one being rendered gets `view: ''` — it is not the
page that was asked for, so it opens at its root rather than at a screen it half-recognises.

## Every write comes back through one listener [#every-write-comes-back-through-one-listener]

`pushState` fires no event of its own, so the shell's `navigate` dispatches the `popstate` that
everything already listens for. One code path, whether the shell moved, a module linked into another
module, the assistant undid a navigation, or somebody pressed Back.

Going nowhere is not a navigation: an identical address is dropped. That is also what stops a module
which writes its params on every render from looping.

## A plain link inside the app is a navigation [#a-plain-link-inside-the-app-is-a-navigation]

Modules write ordinary anchors — "Open in Live Chat", "set this up in Automations" — and a module
may not touch the router, so the shell catches the click instead. Same-origin, unmodified, not a new
tab, not a download, no `rel="external"`: it is turned into a navigation. Everything else is left to
the browser.

An app-relative `href` is resolved against the mount point at that moment, because the mount point is
the one thing a module is not allowed to know.

## Where the app is mounted is the shell's business [#where-the-app-is-mounted-is-the-shells-business]

The app can be served from a sub-path, and `BASE` comes from Vite's `base` option — the same value
that rewrote the asset URLs in `index.html`, so a build served from anywhere else is broken before
routing gets involved.

The rule that follows holds everywhere: inside the app, an address is written app-relative, and
exactly one file knows where the app really is.

## Old links still work [#old-links-still-work]

Addresses minted while this app routed in the fragment — `#/livechat?c=1` — are still in invite mail,
in password-reset mail and in bookmarks. One function rewrites one of those, once, before the first
render.

Supabase's own bare `key=value` fragment is left exactly where it is, and resolves to the auth
callback whatever the path says, so the gate can explain what happened.

## The rule every host has to obey [#the-rule-every-host-has-to-obey]

Real paths mean `/deals/board` is a real address, and a host that answers it with a 404 produces a
failure that looks like a routing bug and is not one. Each host needs its own SPA fallback — see
[picking a host](/docs/deploy).

## What it costs [#what-it-costs]

Address parsing is per module and it fails silently by design: a value the parser does not recognise
falls back to the default rather than throwing, because a stale link has to render something. The
practical consequence is that a view you forgot to add to the module's list of accepted values looks
like a tab that does nothing. [Your first change](/docs/first-change) walks through exactly that.

And `/` is nobody's screen. Until there is a landing page the app opens on the first module in the
menu and rewrites the address to say which one — never on a hidden surface.
