Guia do BikeKit

Getting started

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:

  1. System. Raspberry Pi OS Lite, 64-bit. Put /var/lib on the SSD (or boot from it) and turn on TRIM: sudo systemctl enable --now fstrim.timer.
  2. Database. Install PostgreSQL from apt, include the tuned settings from deploy/pi/postgresql.conf, and create a bikekit user and database.
  3. Server. Build it on the Pi (about 20 minutes) with SQLX_OFFLINE=true cargo build --release -p server, and install target/release/server as /opt/bikekit/server.
  4. 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. Copy build/ to /opt/bikekit/web/ with package.json beside it, and run npm ci --omit=dev there.
  5. Settings. Copy deploy/pi/server.env to /etc/bikekit/server.env and fill in every CHANGE-ME (see Settings below).
  6. Services. Create the user (sudo useradd -r bikekit), copy bikekit-server.service and bikekit-web.service to /etc/systemd/system/, set ORIGIN in the web unit to the backoffice's address, then sudo 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:8080 by 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 need RATE_LIMIT_EXEMPT set 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.5 or 127.0.0.1 is 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_URL to 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=stripe with your Stripe keys (see Payment providers), or connect another provider through PAYMENT_GATEWAY_URL, which speaks a small HTTP contract. With Stripe, before the first real rider:
    • Run it in test mode first: sk_test_… and pk_test_… keys, and STRIPE_TEST_SECRET_KEY=sk_test_… cargo test -p server --test it stripe_mock::live to 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, the payment_intent.* events, and its signing secret as STRIPE_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.
  • 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=smtp and SMTP_URL to 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. Set EMAIL_FROM, APP_URL, and API_URL so 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_URL at your own or a commercial one.
  • Backups. Back up the database and FILE_STORE_DIR together, 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_URL to 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-For and is in TRUSTED_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 /health says the server is up and when it last reached the database; GET /ready checks the database now, for a load balancer.
  • GET /metrics on METRICS_PORT serves 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.