The Rust web ecosystem already has good answers to most problems, and Topcoat is solving a specific one that the existing options handle awkwardly.
Axum is what you reach for when you are building HTTP APIs. It is lower-level, composable, sits cleanly on Tower, and the Tokio team themselves are explicit about this: Topcoat and Axum cover different ground, and many Topcoat apps will use both. No conflict there.
Leptos and Dioxus are the two leading full-stack Rust frameworks, and both work by compiling Rust to WebAssembly and running it in the browser. That is genuinely the right approach for highly interactive applications where you want shared types across the client/server boundary and fine-grained reactive updates. For a lot of apps, though, it is more than you need. WASM bundles, separate build targets, serializing data across the client/server split. The overhead compounds quickly when most of your UI is server-rendered HTML with a button that occasionally does something. Dioxus also offers a single codebase that targets web, desktop, mobile, and TUI, which is a meaningful advantage for teams building cross-platform tools. For a web-only app, that flexibility comes with surface area you are not using.
Topcoat's position is closer to Rails or Phoenix than to either of those. Everything renders on the server. Components can be async, query the database directly, and check permissions without any API layer. For interactivity, rather than compiling to WASM, Topcoat cross-compiles a subset of type-checked Rust expressions to JavaScript at build time. A small JS runtime walks the DOM on load, wires up the reactive graph, and handles updates from there. No WASM bundle, no separate client build step, no manual JSON serialization between server and browser.

Ridgeline has no database. Everything lives in an in-memory Board struct behind a couple of Mutexes, seeded on startup and reset on restart. That was a deliberate choice. The point of the project was the framework, not the persistence layer, and Topcoat's app_context handles exactly this shape of state: something shared across every request, wired up once at startup.
pub fn router() -> topcoat::router::RouterBuilder {
topcoat::router::module_router!()
}
That one line is most of the routing story. module_router!() walks the module tree and registers every #[page], #[layout], #[layer], and #[route] it finds, so the file structure is the route table:
src/app.rs -> / root layout + front page
src/app/items.rs -> nested layout for /items/*
src/app/items/id.rs -> /items/{item_id} detail, comment, upvote
src/app/submit.rs -> /submit form + create
src/app/about.rs -> /about
src/app/api/health.rs -> /api/health
src/app/rss.rs -> /rss.xml
A folder named id under items/ becomes the {item_id} path parameter automatically. There is no route table to keep in sync with the actual handlers by hand, which feels like a small thing until you have spent an afternoon debugging a route registered twice under two different paths somewhere else.
The thing I actually came to test was Topcoat's answer to client interactivity without a SPA. The model has three pieces.
signal declares a piece of reactive state inside a view! block, with an id and a value serialized into the HTML as it renders. #[procedure] is an async server function exposed at an HTTP endpoint, callable from a browser event handler like a normal async function call. #[shard] is a server-rendered fragment that re-fetches itself over the network whenever a tracked signal it depends on changes, then replaces its own DOM region.
None of this touches WebAssembly. A small JavaScript runtime walks the DOM on load, finds the comment markers Topcoat left behind during server rendering, and wires up signals and event handlers to a client-side reactive graph (Maverick Signals underneath). The Rust side of an event handler gets transpiled to an equivalent JavaScript expression at compile time. It is not shipped as Rust or run through WASM.

Here is the live upvote button from the front page, after converting it from a full-page-reload form post to an in-place update:
signal item_id = item.id as f64;
signal score = f64::from(item.score);
signal voted = already_voted;
<button
aria-label="Upvote"
:disabled=$(voted.get())
:class=$(if voted.get() {
"... text-primary animate-vote-pop"
} else {
"... text-muted-foreground"
})
@click=$(async |_e| {
score.set(upvote_item(item_id.get()).await);
voted.set(true);
})
>
icon(data: feather::CHEVRON_UP, size: 16)
</button>
<span class="text-xs tabular-nums text-muted-foreground">$(score.get())</span>
upvote_item is a #[procedure] defined once, elsewhere, as a plain async function taking &Cx and an f64. Calling it from the click handler compiles to a fetch() to a generated endpoint. Awaiting it resolves to the typed return value, no manual JSON wrangling, no separate client SDK to keep in sync with the server API. When it works, it is a genuinely pleasant way to write interactivity. Server-side data access and validation come free, and the signal model is close enough to what a frontend developer already knows that it does not feel foreign.