Por enquanto, este guia está só em inglês.
Start by trying BikeKit on a laptop with a simulated city. You'll see a working scheme with real station names, riders on the move and technicians at work, and you can click through everything in this guide. When you're ready, install it for real on a server, a Raspberry Pi 4 included.
Try it with a simulated city
For whoever sets up the laptop. It takes a few commands and about fifteen minutes, most of it building.
You need a Linux or macOS machine with:
- Rust (stable), to build the server and the simulator;
- Node.js 20 or later, with npm, for the backoffice;
- Docker or Podman with Compose, for the database;
psql, the PostgreSQL client;curl, which the simulator uses once to fetch the city's streets from OpenStreetMap.
Then, from a copy of the repository:
scripts/demo.sh
The script starts PostgreSQL in a container, builds and starts the server, starts the simulator with the sandbox city, and serves the backoffice. When the city is ready it prints:
Backoffice: http://localhost:3000
Login: [email protected]
Password: demo-admin-password
Open the address and sign in with those details. Press Ctrl-C in the terminal to stop everything; the demo's database is deleted when it stops.
What you get. The sandbox city is Lisbon-sized: 140 dock stations with real Lisbon names and positions, 20 virtual stations, 2,600 bikes and 50,000 riders who commute in the morning and evening, sightsee in the afternoon and ride downhill more than up. Bikes wear out and break, and riders report faults. Eight technicians in two teams work shifts, four vans rebalance stations, and a workshop repairs what can't be fixed at the dock. Three stations are reserved for a company's staff. The city runs at real time from a Tuesday at 07:30, so leave it running for an hour and the reports and charts fill in.
The ports are 8080 (the server), 8081 (the simulator's clock control) and 3000 (the backoffice). Set DEMO_HTTP_PORT, DEMO_CONTROL_PORT or DEMO_WEB_PORT to change them.
The demo's database lives in memory, without safe writes, so nothing it does wears your disk. Never use that container for real data.
Ride a bike yourself
The rider app can ride in the simulated city: when you unlock a bike, the simulator lifts it from the dock for you, and when you pick a station it rides the bike there along real streets and docks it. Start the demo with the simulator's play interface turned on:
SIM_PLAY_BIND=127.0.0.1:18190 scripts/demo.sh
Then run the app against it, from its own repository (it needs Bun):
bun install
BIKEKIT_API=http://127.0.0.1:8080 SIM_PLAY=http://127.0.0.1:18190 bunx vite dev --port 5200
Open http://localhost:5200, create an account, top up with the test card and buy a pass. See The rider app for what it does.
Other simulated cities
The simulator takes any scenario in scenarios/. Most are tests; three are useful to look at:
| Scenario | What it is |
|---|---|
sandbox.toml |
The demo city: Lisbon's week, at real time, running until you stop it |
metro.toml |
A city bigger than Lisbon: 550 stations, 10,000 bikes, 60,000 riders and 36 technicians |
zones.toml |
A short run with a zone closed for a quarter of an hour and two stations reserved for a company |
scripts/sim.sh scenarios/zones.toml --seed 42 --rate 120 runs one on a fast clock and checks the system's rules as it goes. To drive a server you installed elsewhere, such as a Pi, see Load-test a real server below.
Install it for real
For whoever runs the servers.
BikeKit is three programs and a database:
| Part | What it is | Listens on |
|---|---|---|
| PostgreSQL 17 or 18 | Holds everything that matters | 5432, local only |
The server (server) |
One Rust program: the rider and staff API, the station and bike protocol, the open data feeds and every background job | 8080; 9100 for metrics, kept private |
| The backoffice | A Node.js program serving the staff website | 3000 |
| Riders' files | Fault photos and identity documents, on disk (FILE_STORE_DIR) |
The server migrates the database itself when it starts. It needs no other services: no cache, queue or search engine.
On a Raspberry Pi 4
A Raspberry Pi 4 with 4 GB of memory and an SSD runs the server, PostgreSQL and the backoffice for a large city. The full steps, with the files they use, are in deploy/pi/README.md. In short:
- System. Raspberry Pi OS Lite, 64-bit. Put
/var/libon the SSD (or boot from it) and turn on TRIM:sudo systemctl enable --now fstrim.timer. - Database. Install PostgreSQL from apt, include the tuned settings from
deploy/pi/postgresql.conf, and create abikekituser and database. - Server. Build it on the Pi (about 20 minutes) with
SQLX_OFFLINE=true cargo build --release -p server, and installtarget/release/serveras/opt/bikekit/server. - Backoffice. On any machine with Node.js:
cd backoffice && npm ci && BACKOFFICE_ORIGIN=https://backoffice.example.org npm run build, using the address staff will open. Copybuild/to/opt/bikekit/web/withpackage.jsonbeside it, and runnpm ci --omit=devthere. - Settings. Copy
deploy/pi/server.envto/etc/bikekit/server.envand fill in everyCHANGE-ME(see Settings below). - Services. Create the user (
sudo useradd -r bikekit), copybikekit-server.serviceandbikekit-web.serviceto/etc/systemd/system/, setORIGINin the web unit to the backoffice's address, thensudo systemctl enable --now bikekit-server bikekit-web.
On four cores, keep the settings server.env already has: four runtime threads (TOKIO_WORKER_THREADS=4) and smaller connection pools (DB_POOL_SIZE=16, DB_JOB_POOL_SIZE=4, DB_LIST_POOL_SIZE=4).
How much it carries. Measured on a laptop held to a Pi 4's processor and memory, with the database on disk: a city of 550 stations and 10,000 bikes at 7,000 rides an hour used about a fifth of the processor and 1 GB of memory, and three times that load about a third. Real Pi storage is slower, so keep that headroom. Measure your own Pi with scripts/pi-bench.sh, or drive it from another machine (below) and watch the system page.
On any Linux server
The same steps work on any 64-bit Linux machine: build the server for it, use the same systemd units and settings, and size the pools to its cores (the defaults suit eight or more).
In front of it
Put both programs behind a web server that handles HTTPS, such as Caddy or nginx:
- the server at an address riders' phones, stations, bikes and other systems can reach, for example
https://api.example.org. The open data feeds are on it too, at/gbfs/and/mds/; - the backoffice at the address you built it for, for example
https://backoffice.example.org. It talks to the server itself (BIKEKIT_API,http://127.0.0.1:8080by default), so staff never reach the server directly; - keep the metrics port (
METRICS_PORT, 9100) private, for your monitoring only.
The web server in front must pass on who is asking, in the X-Forwarded-For header (Caddy, nginx's proxy_add_x_forwarded_for and Tailscale Serve all do), so the server's rate limits count each person rather than the web server. The server believes that header only from addresses in TRUSTED_PROXIES, which by default is the machine itself; name your web server there if it runs on another machine. The backoffice passes on each person's address the same way, so it belongs in TRUSTED_PROXIES too if it runs elsewhere.
Security
BikeKit is built to face the internet. What it does for you:
- Rate limits. The server counts requests from each address. Signing in, signing up, password resets and token renewals get 10 a minute (
RATE_LIMIT_AUTH), which stops password guessing; everything else gets 1,200 a minute (RATE_LIMIT_API). Over the limit, the answer is "too many requests" with how long to wait, and the backoffice's login page says to wait a minute. Stations and bikes are never limited, nor are the health checks. An office where many staff share one public address may needRATE_LIMIT_EXEMPTset to it, or a higher limit. - Webhooks reach only the public internet. An admin chooses where webhooks go. So that an admin account can't be used to reach your own network (the server itself, your LAN, a cloud provider's metadata service), a webhook may only go to a public internet address. An address such as
10.0.0.5or127.0.0.1is refused when the webhook is saved, and a name is looked up each time a delivery is sent: if it points somewhere private, the delivery fails with the reason and nothing is sent. Redirects aren't followed. - Addresses you configure are trusted. Stations' and e-locks' addresses, the payment provider, the identity provider, the email server and the trace collector are set by you or by your hardware, may well be on your own network, and are not restricted.
- The source code. BikeKit is licensed under the GNU Affero General Public License, version 3, which asks anyone running it as a service to offer its source code to the people using it. The backoffice links to it from the login page and from Your account: set
SOURCE_URLto where your copy is published, especially if you have changed it.
Settings
The server reads its settings from the environment (or a TOML file). It refuses to start with a setting it doesn't know, so a typo can't go unnoticed. The ones you must set:
| Setting | What |
|---|---|
DATABASE_URL |
The database, such as postgres://bikekit:…@127.0.0.1:5432/bikekit |
JWT_SIGNING_KEY |
A long random secret that signs sign-ins |
BOOTSTRAP_ADMIN_EMAIL, BOOTSTRAP_ADMIN_PASSWORD |
The first admin, created when the server starts if that email has no account. Use a long random password and keep it as the break-glass account; give everyone else their own account on Staff |
STATION_COMMAND_TOKEN |
The secret the server sends with every command to a station or bike, which the hardware checks |
FILE_STORE_DIR |
Where riders' photos and documents go: /var/lib/bikekit/files on the Pi |
And the ones worth knowing:
| Setting | What |
|---|---|
OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_NAME |
Staff sign in with the city's accounts, such as Microsoft Entra ID. Each person's role comes from the provider's roles claim (OIDC_ROLE_MAP translates your names, such as BikeKit.Operator=operator) |
STAFF_PASSWORD_LOGIN=admins |
With single sign-on, only admins may still use a password |
OIDC_ACCOUNT_URL |
Where staff signed in through the provider change their password, linked from Your account: https://myaccount.microsoft.com for Entra ID, {OIDC_ISSUER}/account for Keycloak |
PAYMENT_PROVIDER |
Who takes payments: mock (the built-in test gateway, the default), stripe, or http for a provider speaking BikeKit's own contract at PAYMENT_GATEWAY_URL. See Payment providers |
STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, STRIPE_WEBHOOK_SECRET |
Your Stripe account's keys, with PAYMENT_PROVIDER=stripe |
EMAIL_PROVIDER=smtp, SMTP_URL |
Send riders' email through your mail provider's SMTP relay, such as smtps://user:[email protected]:465 or smtp://user:[email protected]:587?tls=required. Without it, email goes nowhere |
EMAIL_FROM, EMAIL_REPLY_TO |
Who email comes from, [email protected] until set, and where replies go. Without a name, the sender is named after the scheme (FEED_SYSTEM_NAME) |
APP_URL |
The rider app's address, linked from every email |
API_URL |
The server's public address, such as https://api.example.org, for the unsubscribe link in marketing email |
METRICS_PORT |
Prometheus metrics, /health and /ready on a port of their own |
OTEL_EXPORTER_OTLP_ENDPOINT |
Send traces to an OpenTelemetry collector or a vendor such as Dynatrace |
HTTP_PORT |
8080 by default |
RATE_LIMIT_AUTH, RATE_LIMIT_API |
Requests a minute from one address: 10 for signing in and account changes, 1,200 for the rest; 0 turns a limit off (see Security) |
TRUSTED_PROXIES |
The web servers in front, and the backoffice, whose X-Forwarded-For the server believes, as addresses or blocks such as 10.0.0.0/8; the machine itself by default |
RATE_LIMIT_EXEMPT |
Addresses never limited, such as an office whose staff share one, or a machine running the simulator against this server |
Your scheme's own names, which BikeKit fills with placeholders until you set them:
| Setting | Default | What |
|---|---|---|
FEED_SYSTEM_ID |
bikekit |
The open data feeds' system ID: short, lower case, never changed once published |
FEED_SYSTEM_NAME |
BikeKit |
The scheme's name in the GBFS feed, as apps such as Google Maps show it |
FEED_CONTACT_EMAIL |
[email protected] |
Who answers questions about the feeds |
FEED_LICENSE |
CC-BY-4.0 |
The feeds' licence, as an SPDX identifier |
MDS_PROVIDER_ID |
a placeholder UUID | The ID the MDS registry gives you as a provider |
INVOICE_ISSUER_NAME, INVOICE_ISSUER_NIF, INVOICE_ISSUER_ADDRESS |
placeholders | The seller printed on every invoice and in its QR code. A document keeps the seller it was issued by |
INVOICE_ISSUER_COUNTRY |
PT |
The seller's country, as two letters. The server checks INVOICE_ISSUER_NIF by that country's rule and won't start with a number that fails it; a rider's tax number given without a country is checked as that country's, and is printed with it |
VAT_STANDARD_PCT |
23 |
The VAT rate on rides, passes and fees, in percent, prices including it. Changing it changes documents issued from then on; issued ones keep their rate, and a refund of one gives back the VAT it took |
TIME_ZONE |
Europe/Lisbon |
Your city's time zone, by its IANA name, such as Europe/Madrid or America/New_York. Reports count days and hours in it, closures' daily hours follow its clock, a document is valid to its midnight, and the backoffice, notifications and invoices print times in it, whatever the zone of the computer reading them. Set it before the first start: BikeKit keeps the daily totals by its days, so once the server has started it refuses to start with another zone and says which one it keeps |
The backoffice reads its own settings from its environment:
| Setting | Default | What |
|---|---|---|
BIKEKIT_API |
http://127.0.0.1:8080 |
Where it reaches the server |
MAP_STYLE_URL |
the demonstration tile server's style | A MapLibre style for the maps, from a host of OpenMapTiles vector tiles, such as your own TileServer GL or a commercial host. The backoffice repaints it in its own quiet colours |
MAP_ATTRIBUTION |
empty | Credit to show on the maps beside the style's own, such as © OpenStreetMap contributors when the style carries none |
MAP_CENTER |
-9.14,38.72 |
Where a map with no stations on it yet opens, as longitude,latitude; it also fills the example in New station |
SOURCE_URL |
a placeholder | Where your copy of BikeKit's source code is published, linked as Source code from the login page and Your account, as its licence asks |
The full list is in plan.md §14.
What stays fixed. Some things are written for Portugal and have no setting yet:
- Invoices follow Portuguese law: series, ATCUD, the signature chain and the tax authority's QR code.
Tax numbers are checked by the rider's country: Portugal (NIF), Spain (DNI, NIE and companies' NIF), the Netherlands (BSN and RSIN) and Belgium (national and enterprise numbers). A number may be typed with spaces, dots or dashes and in either case; it is kept in upper case without them. A number given without a country is taken to be from INVOICE_ISSUER_COUNTRY, and a country BikeKit has no rule for keeps the number exactly as typed.
To add a country's rule, write its check in crates/domain/src/tax_id.rs from the tax authority's specification, add it to the RULES list under the country's two-letter code, and test it with the authority's published examples. Riders' profiles, sign-up, integrations and the seller's own number all use the list; numbers already stored are not checked again.
Adding stations and bikes
An admin adds each station on Fleet → Overview → New station (see Stations). Its device token is shown once: give it to the station, which sends it with every report. Bikes are added one at a time on Fleet → Overview → New bike (see Add a bike), or many at once through the API (POST /v1/admin/bikes) by a script fed from the manufacturer's list. Each bike's device token is shown once, for its e-lock; its QR sticker is printed from its page. A station becomes Active by itself when it first reports the right number of docks, and a bike shows up in its dock as soon as a station reports it.
Before going live
BikeKit runs a complete scheme against stand-ins for the outside services. Replace each one before real riders pay:
- Payments. The built-in gateway accepts test tokens only. Set
PAYMENT_PROVIDER=stripewith your Stripe keys (see Payment providers), or connect another provider throughPAYMENT_GATEWAY_URL, which speaks a small HTTP contract. With Stripe, before the first real rider:- Run it in test mode first:
sk_test_…andpk_test_…keys, andSTRIPE_TEST_SECRET_KEY=sk_test_… cargo test -p server --test it stripe_mock::liveto check BikeKit's calls against your account. - Add the webhook in Stripe's dashboard (Developers → Webhooks): the URL
https://your-server/v1/payments/stripe/webhook, thepayment_intent.*events, and its signing secret asSTRIPE_WEBHOOK_SECRET. Without it, 3-D Secure and SEPA payments still settle, but minutes late. - Put BikeKit behind a proxy that sets
X-Forwarded-For: a SEPA mandate records the address the rider signed from, and Stripe refuses one without it. - Turn on the payment methods you sell in Stripe (cards, Apple Pay, Google Pay, Multibanco, SEPA Direct Debit). PayPal through Stripe isn't supported.
- Keep the day pass's deposit within a week: Stripe lets a card hold lapse after seven days, and the day pass's lasts a day plus the five-day release.
- Use a restricted key if you like: BikeKit needs write access to PaymentIntents, SetupIntents, Refunds, Customers and PaymentMethods, and read access to Mandates.
- Run it in test mode first:
- Invoices. Invoices are formatted to Portuguese law, but the built-in invoicing provider signs them with a test key and marks them as not valid tax documents. Connect a certified invoicing provider.
- Email. Email goes nowhere until you set
EMAIL_PROVIDER=smtpandSMTP_URLto your mail provider's relay (see Settings). Send from a domain you own, and publish its SPF, DKIM and DMARC records as the provider tells you, or mailboxes will file it as spam. SetEMAIL_FROM,APP_URL, andAPI_URLso marketing email carries its unsubscribe link. Send yourself a password reset to check. - Push. Push notifications go to a test provider that accepts everything. Connect a real push provider.
- Map tiles. The backoffice's map loads its tiles from a demonstration tile server. Point
MAP_STYLE_URLat your own or a commercial one. - Backups. Back up the database and
FILE_STORE_DIRtogether, on a schedule, and test a restore. BikeKit doesn't do this for you. - Stations and bikes. Your hardware must speak the station and bike protocol (
plan.md§8.2 to §8.5). Try one station against a test server first. - Your names. The open data feeds' name, contact, licence and MDS provider ID, and the seller on invoices, are placeholders until you set them (see Settings).
- Your source code. Set the backoffice's
SOURCE_URLto where your copy of BikeKit is published; until then the Source code link goes to a placeholder (see Security). - The address of whoever is asking. Check that the web server in front passes
X-Forwarded-Forand is inTRUSTED_PROXIES, or every person shares one rate limit and a few failed sign-ins lock everyone out for a minute (see In front of it).
Load-test a real server
To see how a server copes before opening, drive it from another machine with a simulated city at real time. Start the server normally, on a fresh database, then from the laptop:
SIM_API_URL=http://pi.local:8080 SIM_PUBLIC_HOST=<this laptop's address> \
BOOTSTRAP_ADMIN_EMAIL=… BOOTSTRAP_ADMIN_PASSWORD=… STATION_COMMAND_TOKEN=… \
scripts/sim-remote.sh scenarios/metro.toml
The simulated stations listen on port 18200 on the laptop, which the server must be able to reach. Start the server with RATE_LIMIT_EXEMPT set to the laptop's address, or its thousands of simulated riders hit the rate limits. Add --duration-minutes 60 to stop after an hour and print a report. Use a test server, never the live one: the simulator creates its own stations, bikes and riders.
Monitoring
GET /healthsays the server is up and when it last reached the database;GET /readychecks the database now, for a load balancer.GET /metricsonMETRICS_PORTserves Prometheus metrics: requests by route, job timings, database pools, station heartbeats, webhook delivery and open reconciliations.- The backoffice's system page shows the same figures live, without any monitoring tools.