Open source at the core. All of it.

The gtfs.media platform is a suite of GPL‑2.0+ modules for Drupal 10 and 11. The code that runs our managed deployments is the code you can install yourself; there is no enterprise edition. All eight are published on drupal.org, each with its own documentation.

In plain language

There are eight modules. Each one does a separate job.

One reads the timetable file. One reads the live feeds. One draws screens for stops. One draws maps. One makes timetables. One adds the extra files used in the Bay Area.

You install only the ones you need.

All of them are free to use and free to change.

Eight Drupal modules, five layers

Each layer depends only on the ones beneath it. Use just the data core, add realtime, or run the full display stack.

Module stack diagram: gtfs at the base; gtfs_rt above it; gtfs_display above that; then gtfs_display_map and gtfs_display_map_rt; provisioning and add-ons alongside.

  • gtfs_display_map_rt — live vehicles & embeds
  • gtfs_display_map — vector map utilities
  • gtfs_schedule — timetables
  • gtfs_display — display engine
  • gtfs_511 — feed provisioning
  • gtfs_plus — GTFS+ support
  • gtfs_rt — realtime ingestion & API
  • gtfs — data core: entities, importer, API

drupal/gtfs

Import an agency's published GTFS feed and get its routes, stops, trips and calendars as real content — the foundation every other module in the suite reads from.

Every file in the specification becomes its own content entity. The importer reads a remote zip and updates rows in place, so a service change is a re-import rather than a wipe-and-reload, and anything a team added by hand survives it.

Storage is one gtfs_row content entity bundled by gtfs_filename, with a GTFSFile plugin per file mapping columns to fields and resolving references through a gtfs_row_reference field type. Because bundles are plugins, a feed carrying files outside the specification is a new plugin, not a patched module.

It behaves like content, so the rest of Drupal works on it: Views data for listings, tokens for feed values, Pathauto for aliases, and Pathauto aliases survive, with translation for rider-facing strings. Feeds are gtfs_feed entities addressed by a URL-safe alias, so one install can serve several agencies or several versions of the same feed side by side.

Drush covers the scripted paths — gtfs:feed:import, gtfs:feed:refresh, gtfs:feed:audit for a diff between releases, plus gtfs:feed:delete and gtfs:cleanup:orphans. An optional gtfs_fares submodule covers fare attributes, rules, products, media, rider categories, and the leg and transfer rules.

Provides 29 of the 33 documented endpoints — everything under /feeds/, plus the discovery root in the API reference.

drupal.org/project/gtfs

drupal/gtfs_rt

Turn your GTFS-Realtime Protobuf feeds into JSON your own front end, apps and screens can read, without any of them linking a Protobuf library.

Trip updates, vehicle positions and service alerts are fetched, decoded and cached server-side, then served through REST resources: predicted arrivals at a stop, vehicles on a route, and alerts affecting an entity. The decoding runs on gtfs-realtime-php, the PHP binding library we maintain for the GTFS-Realtime protocol — gtfs-media/gtfs-realtime-php on Packagist, and useful on its own if you want the decoding without the rest of the suite.

Each feed is a gtfs_rt_feed config entity with its own refresh_interval — 60 seconds by default — so feeds deploy with your configuration and move between environments like everything else, instead of being clicked in per environment. Cron drives the refresh; last-fetch state lives in the State API and surfaces as meta.last_fetched on every response.

Two details matter in production. Responses set a max-age of zero, so a CDN in front of the site cannot serve yesterday's predictions. And an out-of-band refresh endpoint lets a display asking for stale data trigger its own update rather than waiting for the next cron run, which is what keeps busy screens warm.

Where a realtime feed identifies stops differently from the schedule — common, since many agencies publish stop_code in realtime and stop_id in the static feed — a hook reconciles the two centrally instead of in every consumer.

Provides the three /rt/ endpoints in the API reference.

drupal.org/project/gtfs_rt

drupal/gtfs_display

Drive public information screens — a shelter, a platform, a transit centre, an operations wall — from the feeds you already import, on hardware that is old, unattended, and on a network that comes and goes.

A screen is a display_context config entity: which entity it shows, which variant, how often it refreshes. Contexts deploy through configuration sync, and a permission deriver generates a per-context permission so a kiosk gets anonymous access to exactly the display it renders and nothing else.

Variants are skins rather than themes — lcd, eink, mobile — registered through hook_gtfs_display_variants_alter(), with the shell attaching the matching library automatically. Eight hooks in total cover variant resolution, shell libraries and attachments, realtime ID mapping, annotation filtering and device events, which is how an agency module adds its own hardware skin without forking anything.

Staff notes are display_annotation content entities: attach a message to a stop or substitute one entity for another, scoped to particular displays and to particular hours through office_hours.

The front end is dependency-free vanilla JavaScript — a pub-sub store, a polling layer with backoff and clock-skew correction for lockstep multi-screen paging, a declarative render layer, and a service worker that keeps the last good data on screen when the network drops. Displays render anonymously and bypass site theming, so a kiosk carries no session and none of your front-end weight.

Provides the annotations endpoint in the API reference.

drupal.org/project/gtfs_display

drupal/gtfs_display_map

Draw route shapes and stop geometry from your imported feed, anywhere Drupal renders a field.

A computed Geodata field on GTFS rows carries the geometry, and a field formatter renders it with MapLibre GL. Because it is a field formatter, a route or stop can show its own map through the normal display settings — no template override, and it works in any view mode you already have.

What appears when someone taps a stop is a map tooltip view mode you lay out per bundle, so the popup is configuration rather than markup. One site-wide basemap style URL means changing the cartography is a settings change, and every map follows.

drupal.org/project/gtfs_display_map

drupal/gtfs_display_map_rt

Put live vehicles on those maps, and hand your main website an embeddable one — whatever that website is built with.

Vehicle markers carry heading arrows and route labels and refresh on every poll tick, reading from your own realtime ingest rather than a third-party prediction service. Stop dots open arrival predictions on click, and the route shape draws in the route's own colour. Icons default to Maki glyphs selected by GTFS route_type; an agency module supplies per-vehicle liveries through a hook, which keeps fleet artwork out of the shared module.

The embed is the reason this module is separate. It adds a map display variant at a predictable URL, sets CSP frame-ancestors from configuration rather than leaving framing wide open, and posts a postMessage handshake so the host page can reveal the embed or collapse it when there is nothing to show. URL parameters cover host-page integration — open a stop's popup on load, hide labels, or link stop names back to pages on the embedding site.

drupal.org/project/gtfs_display_map_rt

drupal/gtfs_schedule

Publish readable timetables — stops down the side, trips across, times in the cells — from the same feed, on the same site or a different one.

A timetable page per route at a URL pattern you control, assembled server-side so the grid arrives ready to render rather than as raw stop times to pivot in the browser.

It runs against a GTFS Utilities install on the same site, or against a remote one through its REST API via a small API client. That second mode matters when the public website and the data importer are separate builds on separate release cycles — the marketing site can render timetables without hosting the importer.

Timepoints only by default, with the full stop list available, so a long route does not produce an unreadable wall of numbers. A route_stop_order config entity handles routes where the feed's own sequence does not match how riders read the line. Stop headings link through to stop pages, so a timetable is a way into the rest of the site rather than a dead end.

drupal.org/project/gtfs_schedule

drupal/gtfs_plus

Stop your importer skipping the extension files your region actually publishes.

GTFS+ is the extension set used by the Metropolitan Transportation Commission and the Bay Area agencies reporting to it. It adds files the base specification does not define, and this module gives each one an entity type alongside the standard ones — same storage, same Views data, same API treatment.

Covered: directions for human-readable direction names per route, timepoints for published timing points, stop_attributes, farezone_attributes, calendar_attributes for service descriptions, and rider_categories with fare_rider_categories joined to fare attributes.

The three that earn their keep fastest are realtime_routes, realtime_stops and realtime_trips — the mapping tables that line a realtime feed up with the static schedule when the two use different identifiers.

drupal.org/project/gtfs_plus

drupal/gtfs_511

If you are a Bay Area operator, skip the feed bookkeeping entirely: tick your agency in a list and the feeds configure themselves.

A thin provisioning layer over the 511 SF Bay Open Data API. Storage and import are still GTFS Utilities and GTFS Realtime; what this removes is the URLs, the operator codes, and the three separate feed entities each agency needs.

The settings form reads the operator list live from 511 and renders it as checkboxes, so you choose from what the API actually offers rather than transcribing codes out of a PDF. Selecting an operator creates its static GTFS feed plus trip-update and vehicle-position realtime feeds. The sync is idempotent — run it again to pick up operators added or renamed upstream.

The API token lives in a Key entity rather than in exported configuration, and settings can be saved without syncing, for lining up operators before importing them.

drupal.org/project/gtfs_511 · More for Bay Area agencies →

Why Drupal

Transit data is content, and agencies have content teams.

An admin UI ops staff can use

Feeds, displays, annotations, and overrides are managed in forms — not YAML files and redeploys. Service change on Saturday? Your ops team handles it Friday afternoon.

Views, fields, view modes

GTFS records behave like any Drupal content — query them with Views, attach fields, change how they render. Your developers extend transit data with tools they already use.

Real permissions

Per-display access, role-based API grants, restricted admin — the permission system is Drupal's own.

Config as code

Display contexts and feed definitions are config entities: export them, review them in version control, deploy them through your normal pipeline.

Translation built in

Imported GTFS entities are translatable content — serve rider information in every language your community speaks.

Works with

The specifications the suite implements, the feed sources it has already run against, and the software underneath. Any publisher of GTFS and GTFS‑Realtime works; these are the ones we have worked with.

Put your feed to work.

Start with the modules on your own Drupal site — and when you'd rather ship than host, we're one email away.