Skip to content

How the RideScore DC Site Works ​

For you if: you are new to the project and want to know what the site is made of, what each part does, and why the parts are split the way they are.

Not a setup guide. To make changes to the site, follow either the Front-End Developer Guide (changes to the webpages only) or the Full Stack Developer Guide (changes to the four programs below or the data).


Schematic overview ​

The four programs ​

programjobwhere it is configured
nginxReceives every request from the user/browser. Serves the pages from disk. Forwards /tiles/ and /api/ elsewhere.nginx/default.conf (local), nginx/server.conf.example (deployed)
MartinTurns rows in the database into map tiles the browser can draw.martin.yaml
the APIRecords a survey response. Reports whether the database is reachable.api/main.py
PostgreSQL / PostGIS DatabaseDatabase that stores the road data, the crash data, the scores, and the survey responses. PostGIS is the extension that lets PostgreSQL hold and query geometry.docker-compose.yml

Locally, all four run as Docker containers, started by docker compose up (npm run stack). On the staging and production servers the same four programs run directly on the machine, managed by systemd.

How a request is answered ​

you ask foranswered byreading
/, /survey/nginx, straight from diskfrontend/
/tiles/...Martinthe serving schema
/api/...the APIthe app schema
anything elsenginx, with a 404nothing

The map never touches the API. Streets, scores and crashes all reach the browser as tiles from Martin, which reads the database directly. The API is involved only at the moment a survey response is submitted.

nginx serves the pages, not the API. Nginx directly serves static files. Page requests like /survey/ are converted into survey/index.html (located in the frontend/ folder). Asking the API (/api/) for a page returns 404, because the API does not serve pages.

The three database schemas ​

A schema is a named area inside one PostgreSQL database. RideScore DC defines three schemas, divided by who is allowed to write to each.

schemaholdswritten bycan it be regenerated?
dataroads, crashes, scoresowned by the ridescoredc-models repository, written by scripts/load_data.py, which replaces the whole schema on every loadYes — it is a copy of a published package
appsurvey responsesOwned by the ridescoredc-website repository, written by the APINo. A response is the original. Nothing else can produce it.
servingviews and functions deciding what a map may showowned by the ridescoredc-models repository, written by the serving bundle, rebuilt on every loadYes

The Full Stack Developer Guide shows how to look inside each schema with psql.

The pages and the map ​

The webpages are plain HTML files with MapLibre GL JS loaded from a CDN for drawing maps, plus three shared scripts in frontend/src/shared/:

filejob
config.jsThe basemap, the starting view, and the /tiles/{name}/{z}/{x}/{y} URL builder
basemap.jsCreates the map; adds the DC aerial imagery layer; the toggle buttons
crashes.jsThe crash points and the crash heatmap

The tiles Martin serves are vector tiles (MVT): the browser receives the road lines and their attributes as data, not as a picture, and MapLibre decides the colors. That is why the map page can recolor every street the instant you move a slider.

The pages differ in what they add on top:

frontend/index.html (map)frontend/survey/index.html (survey)
road tilesupdate_scoresurvey_segments
road colorby user_score, red to greenone grey for every road
popup showsscore, LTS, bike lane, speed limit, crashesstreet name and segment_id
talks to the APInoyes, POST /api/submissions

The sliders on the map page are weights to combine score components into a custom score. Moving one and pressing Update Map calls source.setTiles(...) with new i_speedlimit, i_facility and so on in the query string. serving.update_score is a PostgreSQL function rather than a view precisely because those weights are chosen per request: it computes user_score dynamically inside the database for each request.

tile_id and segment_id ​

Every road segment has two identifiers:

tile_idsegment_id
typeintegertext
lifetimeone build of the datadurable, belongs to the street
purposeMapLibre feature-state needs an integer id, so the survey page sets promoteId: { survey_segments: 'tile_id' } and highlights selections by itthe identity a survey response is stored against
meaning after a reload of datanonethe same street

segmentIdsFor() in frontend/survey/index.html converts the tile_id values the map worked with into the segment_id values the request body carries. app.survey_submissions stores segment_ids TEXT[], never the integer. It also stores geometry_source, the published data package those ids came from, read from data.load_record by the API.

No build step ​

The HTML, CSS and JavaScript files in frontend/ are the files nginx serves. There is no bundler output, no dist/, no compile step. Editing frontend/index.html and reloading the browser is the basic front-end development workflow.

Vite is used only for convenience while developing. npm run dev starts the local Vite server, which serves the same files out of frontend/ and forwards /tiles and /api to an upstream server: the shared dev server by default, or your own stack if you set VITE_UPSTREAM. Vite is never deployed, and vite.config.js is not read by anything in production. The deploy script (scripts/gitlab-ci/deployment.sh) does a git pull --ff-only, applies the app migrations, and restarts Martin and the API. Nothing is built.

One consequence worth knowing: because the pages attach buttons with onclick attributes, the shared scripts are plain <script> tags defining globals rather than ES modules. Converting to modules means converting every onclick first.

Where the data comes from ​

Score computation lives in the ridescoredc-models repository and nowhere else. The website repository never computes a score. That separation is deliberate: a score has to be reproducible from a pipeline run.

ridescoredc-models publishes two things, versioned separately:

published artefactcontainslands in
data package (ridescoredc-data-preview.tar.gz)roads, crashes, scores as GeoParquetschema data
serving bundle (ridescoredc-bundle-preview.tar.gz)the SQL in serving/schema serving

The serving bundle is separate from the data package, because it defines what and how data is presented, which is specific to the RideScore DC website.

scripts/load_data.py (npm run data) fetches both from the models repository's GitHub Releases, runs the models repository's own loader, and restarts Martin — Martin reads the database once at startup and publishes what it finds then, so without that restart a freshly loaded map shows nothing and every tile 404s. Addresses and file names are all in scripts/data_source.py.

The app tables (for user feedback on bike routes) are separate again: they are created by scripts/migrate.py (npm run migrate) from api/migrations/, and a deploy applies those migrations but never touches data or serving. A migration history for derived data would be a fiction.

A Civic Tech DC project