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=valueinto{ 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.
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
- Should
router.navigate()push to history or replace the current entry? - How does the active screen clean up on route change — event-based teardown or explicit
destroy()? - 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).