Skip to content

RFC-001 — Router design: hash vs history API, param handling

Status · Closed · superseded by ADR-013 TA anchor · §components/router · §constraints Closes into · ADR-013 (History API routing via SvelteKit) Why this was an RFC · Hash routing was locked for v1 (ADR-004) at the time this RFC was opened, but the router's internal design — how it handles params, how screens register routes, whether to use a library or write from scratch — had genuine alternatives with real trade-offs.

Closure note (April 2026)

This RFC is closed by pre-emption. ADR-012 (SvelteKit) and ADR-013 (History API routing via SvelteKit's built-in router) together obsolete the question this RFC asked: there is no hand-written router to design because SvelteKit's router is now the chosen tool. Routes are file-based (src/routes/[screen]/+page.svelte), URLs are clean (/explore, /fly?mission=id), and navigation is via <a href> and goto(). The original deliberation below is preserved as historical context.

The question

ADR-004 locks hash-based routing for v1. This RFC addresses the implementation design of the router itself: how routes are registered, how params are parsed, how navigation events are dispatched to screens, and what the upgrade path to History API looks like for v2.

Use cases

  • /fly?mission=curiosity — loads mission arc with Curiosity pre-loaded
  • /missions?dest=MARS — opens library filtered to Mars
  • Browser back/forward between screens
  • Bookmarkable URLs for specific missions

Goals

  • Parse #/route?key=value into { route, params } cleanly
  • Dispatch route changes to the active screen without tight coupling
  • Support all six named routes plus query params
  • Simple enough to read and modify without a framework
  • Not make History API migration harder than it needs to be

Constraints

From TA §constraints: no server-side routing logic. nginx serves index.html for all paths.

Proposed approach

A minimal hand-written router (~60 lines). Reads window.location.hash on load and on hashchange. Parses into { route, params }. Calls the registered handler for the route.

js
const routes = {};
export const router = {
  on(route, handler) { routes[route] = handler; },
  navigate(route, params = {}) {
    const qs = new URLSearchParams(params).toString();
    window.location.hash = qs ? `/${route}?${qs}` : `/${route}`;
  },
  _handle() {
    const hash = window.location.hash.slice(1);
    const [path, qs] = hash.split('?');
    const route = path.slice(1);
    const params = Object.fromEntries(new URLSearchParams(qs || ''));
    (routes[route] || routes['explore'])?.(params);
  }
};
window.addEventListener('hashchange', () => router._handle());

Alternatives considered

Client-side routing library (navaid, page.js) — adds a dependency for ~60 lines of code. The hand-written router is more legible and has no upgrade risk. Rejected unless the router grows substantially more complex.

History API from the start — cleaner URLs; requires nginx try_files $uri /index.html. The redirect is one nginx config line. Deferred to v2 — the hand-written router is designed to make this a one-file swap.

Framework router — requires ADR-002 to be reversed. Rejected.

Trade-offs

Hand-written means edge cases (concurrent navigation, special characters in params, back-button timing) may surface bugs. A library handles these out of the box. The hand-written version is simpler to understand and modify.

Open questions

  1. Should router.navigate() push to history or replace the current entry?
  2. How does the active screen clean up on route change — event-based teardown or explicit destroy()?
  3. Should screen modules be lazily imported (import()) on first navigation?

Closing evidence

Router handles all six routes plus ?mission=id and ?dest=MARS|MOON params, tested end-to-end in the production build. Available at Slice 1 gate.

How this closes

One ADR: router implementation (library vs hand-written, specific API surface).

Orrery — architecture documentation · MIT · No tracking