Skip to main content
Glama

Bus Seychelles — SPTC bus timetables (Mahé, Seychelles)

An unofficial project built around the timetables of the Seychelles Public Transport Corporation (SPTC), which only publishes PDFs (no GTFS feed, no API). The project turns those PDFs into usable data and offers them in two ways:

For whom

Where

🌐 Web app

Everyone: natural-language questions ("next bus from Victoria to Beau Vallon after 4pm"), nearby stops, frequent trips, transfers, FR/EN, mobile-first

https://bus-seychelles.pages.dev

🤖 MCP server

AI assistants (Claude, Cursor…)

https://bus-seychelles.pages.dev/mcp — see MCP.md

⚠️ Scheduled times only, no real-time data. SPTC data valid as of 4 August 2026, extracted automatically from PDFs. Errors are possible: check sptc.sc/timetable or the stop itself. Unofficial project, not affiliated with the SPTC.

Web app — web/

Live: https://bus-seychelles.pages.dev (hosted for free on Cloudflare Pages).

A single page with a natural-language input, clickable examples, documentation and the data sources (links to the official PDFs, "Unofficial service" banner). Bilingual (/ in French, /en/ in English), optimised for mobile.

  • A free AI (Cloudflare Workers AI) is only used to understand the question; times always come from the official data, the AI never invents them. When the AI quota is used up, a rule-based parser takes over.

  • Timetables are served as static JSON (web/public/timetable.json) and queried in memory; the D1 database is only used for logs (kept 60 days; the site never stores IP addresses).

  • Personalisation lives in the browser only (localStorage): nearby stops (the user's position never leaves the device), frequent trips, a "Clear my memory" button.

Step-by-step deployment: web/DEPLOY.md · maintenance: web/MAINTENANCE.md.

Related MCP server: UK Bus Departures MCP Server

MCP server

The same timetable engine is exposed to AI assistants through a free, account-less remote MCP server: https://bus-seychelles.pages.dev/mcp. Tools, setup and limits: MCP.md.

Hosting and scaling

How it is hosted

Everything runs on Cloudflare's free plan, with no server to administer and no custom domain:

Piece

Cloudflare service

Role

Pages (web/public)

Pages (static assets)

Site, timetable.json, FR/EN pages

API and MCP (web/functions)

Pages Functions (Workers)

/api/ask, /api/trip, /api/stops, /mcp

Question understanding

Workers AI (AI binding)

Llama 3.1 8B, only to parse the question

Logs

D1 database sptc-bus (DB binding)

query_log and mcp_log, 60-day retention

Requests are served from Cloudflare's edge network, so there is nothing to keep running. The bindings are declared in web/wrangler.toml; no API key is stored anywhere. Deployment is one command (wrangler pages deploy, see web/DEPLOY.md).

Why it already holds up

  • Timetables are read from memory, not from a database. timetable.json (≈ 350 KB, compressed by Cloudflare) is loaded once per instance and queried in memory. An earlier SQL design read about 150,000 D1 rows per question and exhausted the free daily quota (5 M rows/day); it now reads none.

  • A fast path skips the AI for simple questions ("line 20", two known stops), the AI call (~3 s) being the main latency. If the AI quota or model is unavailable, the rule-based parser still answers.

  • Static content costs nothing in compute: pages, JSON and trip details (Cache-Control: public, max-age=3600) are cached at the edge.

  • Abuse protection: per-IP rate limits and a 300-character question cap (best-effort, per instance).

Where the limits are (free plan)

The free-plan figures change over time; check the Cloudflare dashboard. Approximately:

Resource

Free plan

What happens when exceeded

Pages Functions / Workers requests

~100,000 per day

Dynamic requests fail until the daily reset (static files keep being served)

Workers AI

~10,000 "neurons" per day

The site falls back to the rule-based parser (via: "rules")

D1

~5 M rows read, ~100 k rows written per day

Only logging is affected; answers do not depend on D1

For a bus app on an island of ~100,000 people, the realistic bottleneck is the AI quota, and its failure mode is graceful (simpler parsing, never a wrong timetable).

Scaling up

  1. Stay free, get more headroom: add a Cloudflare Rate limiting rule (Security → WAF) to block abusive IPs before they hit the Functions; cache more API responses at the edge; extend the fast path in web/functions/_lib/parse.js so fewer questions need the AI.

  2. Workers Paid plan (about US$5/month): lifts the request cap to millions per month with cheap per-million pricing beyond that, raises D1 limits by orders of magnitude, and bills Workers AI by usage instead of a hard daily quota. No code change is needed: the same project, bindings and wrangler pages deploy.

  3. Custom domain (about €10/year): better branding and SEO; Cloudflare Pages supports it directly.

  4. Beyond that: because the timetable is a static file and the logic is stateless, load spreads across Cloudflare's edge automatically. The remaining costs would be AI usage and logs; both can be capped (smaller model, shorter log retention, sampling).

How the data was built

The SPTC publishes no GTFS feed, no API and no stop map, only timetable PDFs. The database data/sptc.sqlite was therefore built in three reproducible steps (pipeline/run_all.sh).

1. Timetables: extraction from the official PDFs

  • Source: the two PDFs at sptc.sc/timetable (weekday: 85 pages; weekend: 87 pages), valid as of 4 August 2026. The PDFs are not redistributed here: pipeline/fetch.sh downloads them.

  • Extraction (pipeline/extract.py): each page holds "one table per route and direction" tables. The first row is the title (Route 20, Route 1 from Victoria…), the second the header (stops, in trip order), the rest are trips. pdfplumber detects the tables; the footer is cropped so it does not mix into the last rows.

  • Normalisation (pipeline/build.py):

    • a table can hold several glued routes; it is split at each Route … title;

    • duplicates (tables repeated by the PDF) are removed;

    • the "How to read this timetable" guide page is dropped;

    • - or an empty cell = stop not served on that trip;

    • one manual fix (page 11 of the weekend PDF: Saturday routes 2C/2D are glued into a single table).

  • Result: ~287 services on weekdays (136 routes), ~276 at weekends (64 routes), ~2,870 trips, ~8,070 stop times.

Schema: services(day_type, route, title, source_page), service_stops(service_id, seq, stop), trips(id, service_id, trip_no), stop_times(trip_id, seq, stop, time), stops_geo(stop, lat, lon, osm_name, quality, score).

2. Stop positions (partial)

The PDFs give stop names ("Beau Vallon", "Anse Etoile"…), not coordinates, and there is no official map. Positions come from open sources:

  1. OpenStreetMap (pipeline/geo.py): ~1,000 Seychelles bus stops via the Overpass API, matched by name similarity (difflib, threshold 0.88) → quality arret_osm; otherwise to an OSM place (place=*) → lieu_approx.

  2. Nominatim (pipeline/geo2.py): geocoding restricted to Mahé (1 request/s, per their usage policy) → geocode_approx. Results outside Mahé are discarded.

  3. Otherwise inconnu (no position).

Actual coverage: ~45 stops out of 155 (about 60% of departures). SPTC names designate villages while OSM names designate physical stops (shop, locality), so matching is weak. An "approximate" position is that of a village or a neighbouring stop, not the exact stop. Improvements welcome.

3. Publication

web/schema.sql (a copy of data/sptc.sqlite) is used to generate web/public/timetable.json (pipeline/make_schema.py), which the site and the MCP server (Cloudflare Pages Functions, web/functions/) query in memory. See web/DEPLOY.md.

Known limitations

  • No real time (delays, cancellations); no multi-leg journey planning beyond simple transfers.

  • Notes in the PDFs (e.g. "every Tuesday the 1D leaves at 1:20pm") are not imported.

  • Weekend = Saturday and Sunday merged (some titles specify "Saturday").

  • Irregular columns in a few tables: some secondary routes may be shifted.

  • The PDFs change (new timetables in July 2025, May 2026, August 2026…): re-run run_all.sh.

Contact and reporting errors

To report a wrong time, a misunderstood question, or for any request (including from the SPTC): open an issue on this repository (a GitHub account is required). This service is an unofficial integration, not affiliated with the SPTC.

Credits and licences

  • Code: MIT (see LICENSE).

  • Timetables: © SPTC — Seychelles Public Transport Corporation, https://sptc.sc. Factual data reorganised for personal and informational use; the original PDFs are not redistributed. If the SPTC asks, the database will be removed or updated.

  • Positions: © OpenStreetMap contributors, licence ODbL; geocoding by Nominatim. The stops_geo table is a derived database and remains under ODbL.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers