Technology

How 193.dk is built, how data flows, and how the nightly batch works.

AboutBadgesCountriesTechnology

Stack

LayerTechnology
ServerNode.js + Express
DatabaseSQLite via better-sqlite3
Map renderingD3.js + TopoJSON (client-side SVG)
EmailBrevo (transactional SMTP API)
Process managerPM2
Web server / TLSNginx + Let’s Encrypt (Certbot)
HostingVPS, Ubuntu 24.04 LTS

Database structure

All data is stored in a single SQLite file at data/193.db. The main tables are:

TableContents
profilesOne row per user: display name, email, edit key, verified status
profiles_metaExtended profile data: birth year, month, day
visitsOne row per country visit: profile ID, ISO code, status (overnight/transit), is_territory flag
badgesEarned badges per profile. Updated nightly by the batch job.
email_tokensShort-lived 4-digit verification codes for email confirmation

Key files

FilePurpose
server.jsMain Express application — all routes, page rendering, API endpoints
data/countries.jsonMaster list of 193 UN member states with ISO codes, regions, flags
data/country-stats.jsonPer-country statistics (population, GDP, coastline, etc.) — regenerated nightly
data/badges.jsAll badge definitions: criteria, emoji, check() functions, supersedesBy logic
data/badge-country-lists.jsonPre-computed country lists per badge — used on the /badges page
public/map.jsClient-side map rendering using D3 + TopoJSON
public/world-units.topojsonWorld map geometry (TopoJSON format)
scripts/fetch-stats.jsFetches live data from World Bank API, writes country-stats.json
scripts/generate-badge-lists.jsRegenerates badge-country-lists.json based on current badge definitions and stats
scripts/compute-badges.jsCalculates each user’s badges, diffs against DB, sends notification emails

Nightly batch

Every night at 02:00 UTC, three scripts run in sequence:

00:00  SQLite backup         193.db → 193.db.bak
02:00  fetch-stats.js        World Bank API → country-stats.json
02:15  generate-badge-lists  badge definitions → badge-country-lists.json
02:30  compute-badges.js     badge calculation + email notifications

fetch-stats.js

Calls the World Bank API with four indicators in parallel:

Other data (coastline, languages, UNESCO sites, climate zones, timezones) is maintained as curated static lists in the script and updated manually when needed.

The batch fetches data using mrv=1 (most recent value) from the World Bank, so it always uses the latest available year for each country — typically 1–2 years behind the current year.

compute-badges.js

For each verified profile:

  1. Load all visits from DB
  2. Run all check() functions from badges.js against the visit list and stats
  3. Apply filterSuperseded() — removes lower-tier badges when a higher tier is earned (e.g. Top 10% hides Top 25%)
  4. Diff against what’s already in the badges table
  5. INSERT newly earned badges, DELETE lost badges
  6. Send a notification email if anything changed
TEST_MODE is currently active. All badge emails are sent only to anders@blauenfeldt.dk regardless of the actual profile email. Set TEST_MODE = false in scripts/compute-badges.js to go live.

Badge supersedes logic

Progressive badges define a supersedesBy field listing higher-tier badge IDs that replace them. Examples:

Only the highest achieved tier is stored in the database and shown on the profile.

Map rendering

The world map is rendered client-side as an SVG using D3.js and a TopoJSON world geometry file. Each country feature in the TopoJSON is matched to a visit by ISO 3166-1 alpha-2 code. The map supports three modes:

Email

All transactional emails are sent via the Brevo API. There are four email types:

TriggerSubject
New profile createdEmail verification code
Email verifiedWelcome email with map links
/find requestYour 193 travel page link
Nightly batch (badge change)You have new badges / Badge update

The sender domain 193.dk has DKIM and DMARC configured for reliable delivery.

OG image

Each profile has a dynamic Open Graph image at /:id/og.png, generated server-side using the Canvas API (canvas npm package). The image shows the user’s name, country count, and a row of flag emojis for their visited countries.