The easy way: open the Meridian folder in Finder and double-click
Start Meridian.command
It sets everything up on first run, starts the server and opens your browser. Leave the Terminal window it opens running — closing that window stops the site.
From the terminal, if you prefer:
cd ~/Desktop/Meridian
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python3 run.py
Either way the site is at http://127.0.0.1:5001
That is the whole setup. There is no build step, no bundler and no database server — Flask serves the site and SQLite stores the data in a single file.
| Page | Route | What it does |
|---|---|---|
| Home | / |
Hero with a live rotating globe, the deadlines closing soonest, why Meridian exists, the four-step journey, newest programmes, student outcomes |
| Programs | /programs |
Interactive globe with a pin per programme. Filter by family, region and funding; click a pin and the globe flies to it and opens the full brief |
| Teachers | /teachers |
Tutor directory with photos, short intros, filters, prices shown with the fee included |
| Tutor profile | /teacher?id=… |
Photo, intro, experience, services, and a calendar of the exact times the tutor opened |
| Pricing | /pricing |
Explorer / Compass / Navigator memberships, the tutoring fee explained, comparison, FAQ |
| Checkout | /checkout?order=… |
Card, Apple Pay, Google Pay, Bank of Georgia, TBC Pay |
| Become a tutor | /tutors/apply |
Tutor application with photo upload |
| Tutor dashboard | /tutor |
Weekly availability calendar, booked lessons, earnings, profile editing |
| About us | /about |
Mission and vision, plus four sub-pages below |
| — Leadership | /about/leadership |
The co-founders, and the roles we are hiring for |
| — Programs & universities | /about/offerings |
The six programme families in the database |
| — Teachers around the world | /about/global-teachers |
Where our tutors are, grouped by country |
| — FAQ | /about/faq |
Straight answers on cost, eligibility, data and counselling |
| Engage | /engage |
Partnership enquiry form — sends straight to the team inbox |
| Log in / Sign up | /login |
Registration (stage from grade 7 to PhD, country, interests), email verification, forgot password |
| My account | /account |
Saved programmes, lesson requests, Compass membership |
| Admin | /admin |
Everything stored, behind an admin token |
Every page has the dark/light switch, the English/ქართული switch and the full footer.
This is the part worth understanding properly.
Everything a visitor submits goes into one file:
backend/meridian.db
It is a standard SQLite database. No server to install, nothing running in the background — just a file you can copy, back up or email to yourself. It is created automatically the first time you start the site.
| Table | Holds |
|---|---|
users |
First name, last name, email, optional phone, hashed password, verified flag, plan, stage of study, created/last-login timestamps |
verification_codes |
Hashed 6-digit codes, attempt counter, expiry |
sessions |
Login tokens and their expiry |
tutors |
Tutor profiles: photo, headline, intro, bio, subjects, price, status (pending / approved) |
tutor_slots |
Every time a tutor opened, and whether it is open, held during checkout, or booked |
bookings |
Lessons: student, tutor, time, service, note, price paid, status |
orders |
Every payment: lesson or membership, tutor price, fee, total, method (brand + last 4 only) |
ledger |
Where each payment’s money goes: the tutor’s share and Meridian’s fee, plus refunds |
saved_programs |
Which programmes each student saved |
partnership_inquiries |
Everything submitted through the Engage page |
Programme and tutor content is not in the database — it lives in editable JSON files (see below), so the research team can update content without touching SQL.
1. The admin page — open http://127.0.0.1:5001/admin and enter your admin token
(MERIDIAN_ADMIN_TOKEN, default meridian-admin). You get counts and tables for
accounts, lesson requests and partnership enquiries.
2. The command line
python3 tools/admin.py stats # summary counts
python3 tools/admin.py users # every account
python3 tools/admin.py bookings # lesson requests with the student's email
python3 tools/admin.py inquiries # partnership enquiries
python3 tools/admin.py saved # saved programmes per student
python3 tools/admin.py export students.csv --table users
python3 tools/admin.py sql "SELECT email, plan FROM users WHERE verified = 1"
python3 tools/admin.py reset --yes # wipe all visitor records, keep the content
The database currently contains two test accounts created while building the site (
demo@meridian.geandana.test@meridian.ge) so the admin page has something to show. Runpython3 tools/admin.py reset --yesto clear them.
3. Any SQLite browser — open backend/meridian.db in
DB Browser for SQLite, TablePlus, or the sqlite3 CLI.
Passwords are never stored as text. Each one is hashed with PBKDF2-HMAC-SHA256, 240,000 iterations, with a unique random salt per user. Nobody can read them back — not an attacker with the file, and not you. If a student forgets their password the only option is to reset it, which is correct.
Verification codes are hashed the same way, expire after 15 minutes, allow five attempts and are limited to five per email per hour.
Until a mailbox is connected, Meridian cannot reach anyone’s inbox: messages are
saved to backend/outbox/ and the code is shown on screen. That is why
verification emails were not arriving.
Connect Gmail in one step — double-click Set Up Email.command in the
Meridian folder (or run python3 tools/setup_email.py). It asks for:
hello@meridian.ge on Google Workspace, or any Gmail),It tests the login, saves everything to .env (readable only by you, never
committed), and sends you a test email. Restart Meridian and from then on every
verification code, password reset, receipt and booking notice goes to real inboxes.
The /admin page shows whether email is live or failing.
ledger table; the
admin page totals revenue and what is owed to tutors.MERIDIAN_PLATFORM_FEE in .env.Checkout takes Visa and Mastercard only. It runs in test mode until you
connect a provider, so nothing is charged. Test cards: 4242 4242 4242 4242 (Visa)
or 5555 5555 5555 4444 (Mastercard) succeed, and 4000 0000 0000 0002 is
declined. The full card number never leaves the browser: only the brand and the
last four digits are sent. Any other brand or method is refused by the server.
Going live with real money: Stripe does not accept companies registered in
Georgia or Qatar (checked on stripe.com/global), and Paddle and Lemon Squeezy
forbid tutoring and counselling. The plan, from the research in
backend/data/research/investment.json:
All three are hosted checkouts: the card is typed on the provider’s page, which
keeps Meridian outside PCI-DSS scope. charge() in backend/payments.py is the
single function to fill in.
Memberships (/pricing): Explorer (free), Compass (29 ₾/month) and Navigator
(79 ₾/month); yearly billing gives two months free.
/tutors/apply): photo, headline, short intro, full bio,
subjects, admissions systems, languages, education, and price, with a live
“students will see” preview. Photos are cropped to a 640px square, and location
and device metadata are stripped.POST /api/tutor/apply, which saves a row in the tutors
table of backend/meridian.db with status pending. The profile is not
visible to students.backend/uploads/tutors/.MERIDIAN_TEAM_INBOX), linking to /admin./admin, the Application button shows everything the tutor submitted.
Approve makes the profile live and emails the tutor; Reject keeps it
hidden.teachers.json that have no real account behind them
are labelled Sample profile and show no ratings or student counts. Only
approved, real accounts get the “verified” mark./tutor): a weekly calendar. Click or drag across times to
open them for booking, and “Repeat this week ×4” copies a week forward. Booked
lessons show the student’s name, stage, note and contact details, and what the
tutor receives. No Google Calendar needed.python3 tools/smoke_test.py
This runs 31 end-to-end checks against the running site: sign-up, verification, password reset, the 10% fee, slot holds, a declined card, payment, the ledger, a tutor application with photo, approval, the tutor’s calendar, a refund, memberships, and that only Visa and Mastercard are accepted. It deletes its test accounts afterwards.
No code changes needed — these are plain JSON files.
backend/data/programs.json{
"id": "unique-slug",
"family": "uwc",
"type": "school",
"org": "Name of the organisation",
"name": { "en": "English title", "ka": "ქართული სათაური" },
"country": { "en": "United Kingdom", "ka": "გაერთიანებული სამეფო" },
"city": "City, region",
"iso": "GB", "flag": "🇬🇧", "region": "europe",
"lat": 51.4, "lon": -3.54,
"cost": "scholarship",
"duration": { "en": "2 years", "ka": "2 წელი" },
"grades": "10-11",
"opens": "2026-09-08", "deadline": "2026-11-16",
"summary": { "en": "…", "ka": "…" },
"highlights": { "en": ["…"], "ka": ["…"] },
"eligibility": { "en": "…", "ka": "…" },
"url": "https://official-site.org",
"premium": false
}
lat / lon place the pin on the globe. Look them up on any map.family — one of uwc, erasmus, flex, summer, university, youth, startup, competition.region — europe, americas, asia, oceania, africa, middle-east, global.cost — fully-funded, scholarship, tuition-free, funded, paid.premium: true hides the detail from signed-out visitors.deadline_note (shown under the eligibility), sources (official pages
the record was checked against, shown as links), verified_at, and from the
agents score (1–10, drives the Highlights row) and score_reason.tools/agents/discover.py runs nine agents on Claude Opus 5.5 with web search
and web fetch. Eight each cover one kind of opportunity: schools/UWC, exchanges,
Erasmus+, summer, undergraduate, postgraduate, startups and competitions. The
ninth re-checks published records, because a stale deadline does more harm than a
missing programme. More agents mostly re-find the same programmes.
pip install -r tools/agents/requirements.txt
export ANTHROPIC_API_KEY=...
python3 tools/agents/discover.py --minutes 10 # quick run
python3 tools/agents/discover.py # all nine, 60-minute budget
backend/data/discoveries/<timestamp>.json, and the records appear on /admin
under Discoveries to approve or reject. Approved records are appended to
programs.json.backend/data/research/retired-programs.json./about/researchbackend/data/research.json drives the Research page, the facts on the home page
and the live counts on /about. It holds cited facts, a solution-fit table, a
competitor table, the business model, the funding calendar, payments and the open
questions. The raw agent reports, with every source, are in
backend/data/research/. GET /api/research serves it, together with live counts
of programmes, countries and checked records.
Tutors now join through /tutors/apply and are approved on /admin; they
manage their own profile, photo, price and calendar from /tutor.
backend/data/teachers.json is only the starting set: it is copied into the
database the first time the site runs.
New subject names go in frontend/js/taxonomy.js. Add the slug with its English
and Georgian labels and it appears in every filter and form automatically.
All interface text lives in frontend/js/i18n.js as two dictionaries, en and ka.
'nav.programs': 'Programs', // en
'nav.programs': 'პროგრამები', // ka
Use a key in markup with data-i18n="nav.programs", or in JavaScript with
M.t('nav.programs'). Content coming from JSON uses { "en": …, "ka": … } objects
and is picked up automatically by M.pick().
Meridian/
├── run.py start here
├── requirements.txt one dependency: Flask
├── .env.example copy to .env
│
├── backend/
│ ├── app.py routes and the JSON API
│ ├── db.py SQLite schema, password hashing, sessions
│ ├── mailer.py SMTP, with a file-based fallback
│ ├── config.py settings, read from the environment
│ ├── meridian.db ← created on first run
│ ├── outbox/ ← emails land here when SMTP is off
│ └── data/
│ ├── programs.json the programme database
│ └── teachers.json the tutor directory
│
├── frontend/
│ ├── *.html one file per page
│ ├── css/
│ │ ├── tokens.css colour, type, spacing, motion — the whole system
│ │ ├── base.css reset, typography, layout primitives
│ │ ├── components.css buttons, cards, nav, footer, forms, calendar
│ │ └── pages.css page-level composition
│ ├── js/
│ │ ├── app.js theme, language, nav, footer, reveal, toasts, API
│ │ ├── i18n.js English + Georgian dictionaries
│ │ ├── taxonomy.js subjects, regions, languages
│ │ ├── globe.js the globe engine
│ │ ├── home.js programs.js teachers.js teacher.js
│ │ ├── about.js engage.js auth.js account.js admin.js
│ └── assets/
│ ├── globe-dots.json land data for the globe
│ └── favicon.svg
│
└── tools/
├── admin.py read the database from the terminal
├── build_globe.py regenerate the globe land data
└── countries-110m.json source map data (Natural Earth, public domain)
The globe uses real NASA satellite imagery on WebGL (globe.gl,
stored locally in frontend/vendor/, so it works offline). The textures are in
frontend/assets/earth/.
Country outlines with English and Georgian names are generated by
python3 tools/build_countries.py. Borders follow United Nations practice:
Crimea is drawn as part of Ukraine, Northern Cyprus as part of Cyprus, Somaliland
as part of Somalia, Kosovo is shown within Serbia, Taiwan as a province of China,
and Western Sahara as a non-self-governing territory. Clicking a country shows its
official UN name (UNTERM).
The intro (logo assembles, curtain lifts, headline types in) plays once per visit, can be skipped with a click or any key, and is skipped entirely for people whose system asks for reduced motion. It follows the animate skill’s rules: transform, opacity and clip-path only, strong ease-out curves, CSS keyframes so it stays smooth while the page loads, and a CSS-only fallback that never traps the page.
Your logo is split into exact layers in frontend/assets/brand/: the star facets,
book pages and wordmark as masks. It recolours for dark and light mode, and the
star turns on its own (a compass quarter-turn on hover). meridian-logo.png is
the full-colour version, used in emails.
| Dark | Light | |
|---|---|---|
| Background | deep navy | dodger blue |
| Search bar | navy blue | royal blue |
| Accents | cobalt blue | royal blue |
| Surfaces | translucent navy glass | white paper |
Both themes are defined entirely as CSS custom properties in tokens.css. Nothing
else in the codebase hardcodes a colour, so changing the palette is a one-file job.
Body text always sits on a surface with at least 4.5:1 contrast — which is why light
mode puts content on white cards over the blue field rather than directly on it.
Typography is Sora for headings, Inter for body, Newsreader italic for editorial accents, and Noto Sans Georgian whenever the language is set to ქართული.
| Signed out | Explorer (free) | Compass 29 ₾/mo | Navigator 79 ₾/mo | |
|---|---|---|---|---|
| Programme list, countries, deadlines | ✅ | ✅ | ✅ | ✅ |
| Full programme briefs and official links | partial | ✅ | ✅ | ✅ |
| Tutor profiles and booking | first 4 | ✅ | ✅ | ✅ |
| Monthly shortlist by email, deadline alerts, tracker | — | — | ✅ | ✅ |
| Priority booking with counsellors | — | — | ✅ | ✅ |
| Monthly counsellor check-in + one essay review | — | — | — | ✅ |
| Meridian AI | — | — | — | coming soon |
Lessons are paid per lesson at the tutor’s price plus 10%.
MERIDIAN_SECRET_KEY and MERIDIAN_ADMIN_TOKENMERIDIAN_DEV_CODES=falseMERIDIAN_DEBUG=false and MERIDIAN_COOKIE_SECURE=truegunicorn run:application)backend/meridian.db on a schedule/about/faq links toSet Up Email.commandMERIDIAN_PAYMENT_PROVIDERtools/agents/discover.py daily and review Discoveries on /adminledger table already tracks what each tutor is owed)Meridian AI — built into the globe. Describe your grades, budget and ambition, and it proposes the programmes worth your time and the order to apply in.