GoWay is Oxy's open map and geographic platform, built for web, iOS and Android with Expo.
It provides both the consumer map experience at https://goway.to and the reusable @goway.to/sdk used by FairCoin, Moovo, Mercaria, Homiio, Mention, Clarity and other Oxy or third-party apps.
Bun only — never npm, yarn or pnpm, and bun.lock is committed with the
package.json change that moved it.
bun install
# PostgreSQL 17 + PostGIS 3.5 on 127.0.0.1:5440, loopback only.
# Both versions are pinned: a floating PostGIS minor is a floating set of
# function volatilities, and a generated `geography` column needs ST_MakePoint
# to stay IMMUTABLE.
docker compose -f docker-compose.postgres.yml up -d --wait postgres
cp packages/backend/.env.example packages/backend/.env
bun run db:migrate --target-database=goway_dev
bun run dev # backend + Expo together
bun run dev:backend # backend alone, on :3000
bun run dev:frontend # Expo alonePort 5440 rather than 5432: an Oxy developer machine routinely has several backends' local databases up at once, and 5432–5439 and 5441 are already spoken for (5432 oxy-api, 5433 Mention, 5434 Homiio/Syra, 5435 Mercaria, 5436 CrowdSource, 5437 Moovo, 5438 Noted, 5439 Peable, 5441 Schedio). A port collision does not fail loudly once a container is running — it connects you to somebody else's database and migrates it.
CREATE EXTENSION postgis is privileged. On a brand-new database a role with
rds_superuser has to run it once before GoWay's migration role can migrate;
packages/backend/README.md explains why IF NOT EXISTS hides that.
A route is never a fixture. With fixtures on, places, search and geocoding are
answered locally, but POST /routes goes to the API at EXPO_PUBLIC_API_URL
exactly as it does with fixtures off (a fixture place is sent as its point,
because the API has never heard of its id). So the line on the map is always an
engine's, and where there is no engine the planner says "GoWay can't work out
this route right now" and draws nothing.
EXPO_PUBLIC_API_URL |
Who routes |
|---|---|
unset / https://api.goway.to |
GoWay's production Valhalla. Works from localhost signed out; signed in, the browser sends a bearer, which only the origins in production's CORS_APP_ORIGINS may, and the request is refused. |
| a local backend | Whatever its ROUTING_VALHALLA_URL names. Unset, every route answers service_unavailable. |
A local backend routes with GoWay's own engine run locally — the same tiles
production serves (packages/routing-engine/README.md → "Running it locally"),
then ROUTING_VALHALLA_URL=http://127.0.0.1:8002/route in
packages/backend/.env. The public community instance in .env.example is a
documented fallback for one developer within its fair-use policy, never more.
bun run check:routing with GOWAY_API_URL=http://127.0.0.1:<port> proves a
local backend routes every mode.
bun run typecheck # bun check (TypeScript 7) on contracts, backend, its tools program and the SDK; tsc on the frontend
bun run lint # Biome (lint + format check) everywhere, then the frontend's minimal Expo ESLint
bun run lint:fix # apply Biome's safe fixes and formatting
bun run format # Biome formatter only
bun run test # bun test
bun run check:migrations # deploy-phase markers + no $1 placeholders in generated SQL
bun run check:routing # api.goway.to returns REAL drive, walk and bike routes (Barcelona, Madrid), never a straight line
bun run check:lockfile # bun.lock matches the manifests it describes
bun run test:gates # proves the routing, migration, OpenAPI and JSON Schema gates can fail
bun run check:openapi # packages/contracts/openapi.json is complete and fresh
bun run check:contracts # every named contract schema converts to JSON Schema
bun run openapi # regenerate openapi.json after a contract changeCI runs all of these, and the AWS deploy is a JOB of the CI workflow with
needs: those jobs — not a workflow with its own push trigger, which would
race CI and always win. The Cloudflare frontend deploy is a second such job
(deploy-frontend), for the same reason and on the same gates.
https://goway.to is served by a Cloudflare Worker serving static assets,
published by .github/workflows/deploy-frontend.yml on every push to main
that passes CI. The build is an expo export --platform web of
packages/frontend; the Worker is configured entirely by
packages/frontend/wrangler.toml.
It runs against the live API. A production build never uses the fixture
layer (fixturesOn, packages/frontend/lib/goway/mockTransport.ts), so
goway.to talks to api.goway.to: real search, real geocoding, and real
turn-by-turn routing from GoWay's own Valhalla (packages/routing-engine). A
development build DEFAULTS fixtures on, so bun run dev:frontend works with no
backend running — for everything except directions, which are never a fixture
(below).
One thing is knowingly empty: GoWay Places has no rows in production, so
/places/bounds answers an empty page and the GoWay-owned pin layer is blank until it is
seeded. That was chosen over keeping the fixtures live, because those fixtures
are real Barcelona names carrying invented facts, and fabricated places on a
public map are the same defect as a fabricated route. The basemap still draws
every POI it has, every name on it is tappable, search is real and directions
are real.
The seed now exists and has not been run. packages/backend/src/import/osm/
imports OpenStreetMap POIs into places — 771,515 of them for Spain, measured —
and packages/backend/README.md has the one-shot aws ecs run-task that
applies it, plus the IAM grant oxy-goway-github-deploy still needs before
.github/workflows/import-osm-pois.yml can do it instead. Switching the
basemap's own poi-* layers off is the change AFTER that, not before: turning
them off over an empty table is an empty map.
A Cloudflare Pages project ALWAYS serves <project>.pages.dev and offers
no way to turn it off (wrangler pages project has only list, create,
delete). Every Oxy app that was on Pages therefore had a second, indexable
copy of itself on a hostname in no CORS allowlist and no Oxy application's
redirectUris — it rendered the shell and failed every call it made. GoWay is
born on Workers so it never has that hostname, and workers_dev = false in
wrangler.toml is the line that keeps it that way. Do not "simplify" this to
wrangler pages deploy.
The trade that comes with it: Pages adds x-content-type-options: nosniff
and referrer-policy: strict-origin-when-cross-origin to every response;
Workers Static Assets does not. Nothing in this repository asks for them, so
the difference is invisible to every build, test and deploy. They are declared
explicitly in packages/frontend/public/_headers, which expo export copies
into dist/ verbatim, and the deploy workflow asserts both are on the live
response afterwards.
Everything below is done ONCE, by a human with access to Oxy's Cloudflare account and the GitHub org. The deploy is fully automatic afterwards.
1. Actions secrets — and the shadowing trap.
The workflow reads exactly two values from secrets:
| Secret | Value |
|---|---|
CLOUDFLARE_API_TOKEN |
The Oxy org-level token — the same one every sibling frontend (Clarity, Inbox, Mention, Allo, Noted) deploys its Worker with. It must be Workers-capable: Workers Scripts: Edit on the account, and Zone: Read plus Workers Routes: Edit on the goway.to zone, which is what lets wrangler attach the custom domain and have Cloudflare write the record. |
CLOUDFLARE_ACCOUNT_ID |
The Oxy Cloudflare account id. |
If the first deploy fails with Authentication error [code: 10000], the token
is Pages-scoped rather than Workers-capable — which is the failure the
shadowing note below describes.
Do NOT add these as repository secrets if the org already provides them. A repo-level Actions secret silently SHADOWS the org-level one of the same name, and nothing reports it.
OxyHQ/Notedcarried its ownCLOUDFLARE_API_TOKENfrom 2026-07-15, scoped to Pages only; when the org token was rotated to a Workers-capable one on 2026-09-05 the repo copy kept winning and Noted's deploy failedAuthentication error [code: 10000]while every sibling repo deployed fine. Check the org secrets first. If a repo-level copy already exists, delete it rather than updating its value.
Optionally set the repository variable EXPO_PUBLIC_OXY_CLIENT_ID to
GoWay's production Oxy client id. A variable, not a secret: the id ships inside
the bundle, so it is public by construction. Unset, the map still works — only
sign-in is disabled, and silently.
2. DNS, in this order: DNS → deploy. This order is not a preference.
A Worker custom domain REFUSES a hostname that already has externally
managed DNS records (code: 100117). Cloudflare writes and manages the
record for a custom domain itself, so the apex must be empty of address records
when wrangler deploy first runs.
- In the
goway.tozone, delete any existing A / AAAA / CNAME record at the apex. Type-scoped — aTXT/SPF/DMARC record at the same name must survive; deleting those breaks mail, not the website. - If
goway.tois attached to a Cloudflare Pages project, detach it there too. - Run the workflow (merge to
main, orworkflow_dispatchonmain).wranglercreates the Worker, attachesgoway.toas a custom domain and writes the DNS record.
The downtime is that window — roughly 30 seconds when the build is ready first.
api.goway.to is a separate record in front of the AWS ALB and is not
touched by any of this.
wrangler deploy exiting 0 means an upload was accepted, not that goway.to
serves this build — and the SPA fallback makes the naive check actively
misleading, because with not_found_handling = "single-page-application" every
path that misses an asset answers 200 with index.html. A bare status
check would pass against a deployment containing nothing but a stale shell.
So the final step asserts a size floor plus positive markers: the shell is at
least 1 KB and carries the React root, the title and three or more hashed
bundle references; both _headers security headers are on the live response;
the entry bundle this run just built, addressed by the content hash read out
of the local dist/, comes back as JavaScript rather than text/html (the
one assertion that cannot pass against the previous deployment or against the
fallback); and a route that exists only inside the bundle answers with the
shell, proving deep links work.
GoWay follows the Oxy packages/* convention:
packages/
frontend/ Expo Router app for web, iOS and Android
backend/ GoWay API, Places persistence and provider adapters
sdk/ public @goway.to/sdk package
contracts/ the zod contracts: every request, response, error and value set, the route registry and openapi.json
reconstruction-worker/ Python/CUDA Street 3D worker
The frontend uses the same Oxy application foundation as the rest of the ecosystem: Expo Router, React Native/react-native-web, @oxy.so/app-preset, Bloom, Oxy Services/contracts/core and Oxy authentication. Street 3D contribution UI stays inside this same Expo app rather than becoming a separate product.
- Maps — MapLibre rendering over GoWay's own vector tiles: Planetiler builds one PMTiles archive from an OpenStreetMap extract, it lives in Cloudflare R2, and the Cloudflare Worker reads byte ranges out of it. No tile server, and no third-party tile origin. See
packages/frontend/README.md→ "The tiles". - Map performance — every 3D layer reads one quality profile (Low/Medium/High, chosen per device or in Settings → Performance), animates through one clock that pauses when the map is idle, and is held to frame budgets by a CI gate;
?perf=1shows the HUD andbun run --cwd packages/frontend bench:mapmeasures. Seedocs/MAP_PERFORMANCE.md. - Places — a first-class GoWay Places schema backed by PostgreSQL + PostGIS, with stable place IDs, source provenance, business relationships and extensible capabilities such as FairCoin acceptance.
- Search — provider-neutral place/address search, geocoding and reverse geocoding.
- Routing — provider-neutral directions and route geometry.
- SDK —
@goway.to/sdkexposes maps, Places, search and routing without making consumer apps depend directly on MapLibre or an external geographic provider.
The Places schema is part of GoWay v1, not deferred. Its public/domain contracts live in packages/contracts; its canonical database schema, spatial indexes and migrations live in packages/backend using PostgreSQL + PostGIS. We intentionally do not create a separate public database-schema package because SDK consumers should depend on stable GoWay contracts, not on GoWay's internal tables or migrations.
GoWay hosts the map dataset itself — a planet PMTiles build in R2, at roughly $1.50 a month because R2 charges nothing for egress. The map infrastructure is still replaceable independently from GoWay-owned Places and ecosystem data: lib/map/provider.ts is the one module that names where cartography comes from, and moving the tiles from a third party to GoWay's own storage touched no component, no screen and no SDK contract.
Street 3D is GoWay's community-built future street-level layer. Instead of depending on proprietary panorama coverage, users can contribute ordinary geotagged photos or videos. GoWay temporarily stores useful source media, matches overlapping views, solves camera geometry and progressively builds versioned 3D Gaussian scenes.
The design is intentionally cost-conscious:
- raw photos/videos are temporary reconstruction inputs, not a permanent archive;
- default source retention is measured in weeks/months, with aggressive deduplication and early raw-video deletion after useful keyframes are extracted;
- incomplete areas can become at risk when useful temporary captures are nearing expiry, allowing the community to contribute missing viewpoints before the opportunity is lost;
- published 3D scenes remain durable even after their raw source images are deleted;
- compact derived metadata, manifests and published splat/LOD assets are kept instead of indefinite raw media;
- AWS provides the control plane, S3/object storage and queueing during the bootstrap stage;
- expensive reconstruction can run on external workers through
packages/reconstruction-worker, initially an external GPU worker consuming durable AWS SQS jobs; - the worker can be offline without breaking GoWay and can later scale to multiple owned/cloud GPUs using the same contract;
- privacy preprocessing is required before captures become reconstruction inputs, including face/license-plate and dynamic-object handling;
- Street 3D is streamed by versioned scene/LOD manifests and integrated back into GoWay Places rather than embedding business metadata into 3D assets.
Street 3D is not required for the first 2D GoWay release, but it is an explicit GoWay roadmap track rather than an undefined future Street View add-on.
- #1 repository/packages/Oxy foundation
- #2 MapLibre + OpenFreeMap rendering
- #3
@goway.to/sdk - #4 GoWay Places + PostgreSQL/PostGIS
- #5 search/geocoding
- #6 routing
- #7 GoWay Bloom map experience
- #8 FairCoin merchant-discovery reference integration
- #16 Street 3D epic / implementation order / cost-quality gates
- #9 geotagged photo/video contribution pipeline
- #10 temporary storage, retention, deduplication and cost budgets
- #11 geospatial capture graph + automated 3D Gaussian reconstruction
- #12 distributed GPU reconstruction worker + external GPU worker/AWS SQS
- #13 privacy-safe capture/reconstruction pipeline
- #14 streamed Street 3D viewer + map integration
- #15 coverage health, expiry risk and community rescue UX
The Breathe License 1.0 (LicenseRef-Breathe-1.0) — the same licence as the
rest of the Oxy ecosystem. The full text is in LICENSE, with NOTICE for
attribution. Commercial terms:
https://github.com/OxyHQ/.github/blob/main/LICENSE-COMMERCIAL.md.
Attribution under Section 3.1 is required of everyone, including paying commercial licensees, and cannot be waived.
@goway.to/sdk ships its own copies of LICENSE and NOTICE in the
published tarball, because a consumer who installs the package never sees this
repository. Its release smoke test asserts both are present.
Geographic data served through GoWay carries its own upstream licences and is not relicensed by this file — OpenStreetMap-derived data is ODbL.