| Layer | Technology |
|---|---|
| Server | Node.js + Express |
| Database | SQLite via better-sqlite3 |
| Map rendering | D3.js + TopoJSON (client-side SVG) |
| Brevo (transactional SMTP API) | |
| Process manager | PM2 |
| Web server / TLS | Nginx + Let’s Encrypt (Certbot) |
| Hosting | VPS, Ubuntu 24.04 LTS |
All data is stored in a single SQLite file at data/193.db. The main tables are:
| Table | Contents |
|---|---|
| profiles | One row per user: display name, email, edit key, verified status |
| profiles_meta | Extended profile data: birth year, month, day |
| visits | One row per country visit: profile ID, ISO code, status (overnight/transit), is_territory flag |
| badges | Earned badges per profile. Updated nightly by the batch job. |
| email_tokens | Short-lived 4-digit verification codes for email confirmation |
| File | Purpose |
|---|---|
server.js | Main Express application — all routes, page rendering, API endpoints |
data/countries.json | Master list of 193 UN member states with ISO codes, regions, flags |
data/country-stats.json | Per-country statistics (population, GDP, coastline, etc.) — regenerated nightly |
data/badges.js | All badge definitions: criteria, emoji, check() functions, supersedesBy logic |
data/badge-country-lists.json | Pre-computed country lists per badge — used on the /badges page |
public/map.js | Client-side map rendering using D3 + TopoJSON |
public/world-units.topojson | World map geometry (TopoJSON format) |
scripts/fetch-stats.js | Fetches live data from World Bank API, writes country-stats.json |
scripts/generate-badge-lists.js | Regenerates badge-country-lists.json based on current badge definitions and stats |
scripts/compute-badges.js | Calculates each user’s badges, diffs against DB, sends notification emails |
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
Calls the World Bank API with four indicators in parallel:
SP.POP.TOTL — total populationAG.LND.TOTL.K2 — land area in km²NY.GDP.MKTP.CD — GDP in USDNY.GDP.PCAP.CD — GDP per capita in USDOther data (coastline, languages, UNESCO sites, climate zones, timezones) is maintained as curated static lists in the script and updated manually when needed.
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.For each verified profile:
check() functions from badges.js against the visit list and statsfilterSuperseded() — removes lower-tier badges when a higher tier is earned (e.g. Top 10% hides Top 25%)badges tableanders@blauenfeldt.dk regardless of the actual profile email. Set TEST_MODE = false in scripts/compute-badges.js to go live.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.
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:
#4dd9b8), transit in dark brown (#8a6a20)All transactional emails are sent via the Brevo API. There are four email types:
| Trigger | Subject |
|---|---|
| New profile created | Email verification code |
| Email verified | Welcome email with map links |
| /find request | Your 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.
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.