Self-hosted service
coopcycle/coopcycle-web avatar
coopcycle/coopcycle-web

CoopCycle packages delivery logistics for co-ops into one Symfony platform

Logistics & marketplace platform. Only for worker-owned business.

605 stars142 forksPHPNOASSERTION

At a glance

What is it?
CoopCycle is a self-hosted platform for ordering meals in a neighbourhood and delivering them by bike, and its licence reserves the software to worker-owned co-ops. The repository carries the web API, the website front end and the dispatch algorithm, and it runs locally through Docker Compose with routing, geocoding and four separate test suites.
Who is it for?
CoopCycle fits a courier co-op that wants to run its own marketplace, own its customer data and keep the margin on delivery, and the Docker Compose path makes a first evaluation cheap to try. It is the wrong choice for a conventional restaurant delivery business or for any company that wants to run the platform commercially under its own ownership, since the licence is the obstacle rather than the code.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly PHP, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A delivery platform that only co-ops may run

CoopCycle is described as a self-hosted platform to order meals in your neighbourhood and get them delivered by bike couriers. The interesting constraint comes in the next sentence: the only difference with proprietary platforms such as Deliveroo or UberEats is that this software is reserved to co-ops.

The main idea, as the README puts it, is to decentralize this kind of service and to allow couriers to own the platform they are working for. In each city, couriers are encouraged to organize into co-ops and to run their very own version of the software.

That is a business model encoded in a licence, and it is the first filter for anyone evaluating the project. The licence is a custom one that GitHub cannot classify, with the LICENSE file as the authoritative text, so the software is free in the sense that you can read it, run it and change it, but the right to run it is reserved to the co-operative form.

The users, then, are city-level courier co-ops rather than restaurants. A restaurant that wants to sell meals with delivery has to partner with a co-op or run one, and the software is the vehicle for that relationship.

The platform is also genuinely multi-tenant in intent, since the co-op is the unit of organisation rather than a single national operator. The demo instance at demo.coopcycle.org exists so you can see it in action before building anything.

What coopcycle-web actually contains

The repository is described as the main repo, containing the web API, the front end for the website and the dispatch algorithm, with a technical overview and a comprehensive list of the other repos in the project wiki.

The stack explains the structure. The project is a Symfony application, with symfony.lock, a config directory, migrations, templates, translations and a features directory for behavioural tests. It uses API Platform, which is why apiplatform is one of the topics, and the front end is a JavaScript bundle built with webpack, with a .storybook-react directory for component work and Storybook served on port 6006 by the storybook script.

The dispatch and data side has its own directories. There is a recommender directory, a cube directory with cube.js for the analytics layer, a clickhouse directory, and a shopify-app and shopify-gateway pair, which is how storefront integration is handled. Sonic appears as a configuration file, and a docker directory holds the service images.

The observability and testing footprint is visible too: phpstan with a baseline for static analysis, rector.php for automated refactoring, eslint and prettier for the front end, phpunit.xml.dist, behat.yml, jest.config.js and cypress.config.js for the four test layers, and an .editorconfig for formatting.

For an engineer, this is the shape of a mature application rather than a prototype, and it is the reason the setup instructions are long.

Bringing the stack up with Docker Compose

The local installation assumes Docker and Docker Compose, and the README has platform notes for OSX, Windows and Linux, including a Windows note that Docker does not work natively there and that you need Linux under hypervisualization. Additional tips and configurations live in the wiki.

Images for some services are prebuilt and pushed to Docker Hub, so you can pull them instead of building locally:

sh
docker compose pull

Next, populate the environment files:

sh
cp .env.dist .env
touch .env.test.local

Only the variables you want to override need to go into.env.test.local, for example setting a geocoding key there.

The stack starts with the minimum configuration:

sh
docker compose up

Two profiles add optional pieces, Storybook for front end work and Odoo for a related service:

sh
docker compose --profile devFrontend up
sh
docker compose --profile devOdoo up

At that point the platform is up and running, but the database is still empty. The schema and demo data are created with:

sh
make install

and the local instance is then reachable in a browser with:

sh
open http://localhost

The install target in the Makefile depends on osrm and install-db, and install-db runs the Doctrine schema creation for both the dev and test environments, creates a typesense index, runs a coopcycle:setup console command, syncs the metadata storage and marks the migrations as applied.

Geocoding and autocomplete need three credentials

CoopCycle uses OpenStreetMap to geocode addresses and to provide autocomplete features, and the setup section treats this as an optional configuration step. The choice of provider decides which environment variables you set.

For address autocomplete you choose a provider, grab the credentials and configure the environment variables:

sh
LOCATIONIQ_ACCESS_TOKEN
GEOCODE_EARTH_API_KEY

The adapter variable selects which of them is in use, with COOPCYCLE_AUTOCOMPLETE_ADAPTER set to geocode-earth for Geocode Earth and to locationiq for LocationIQ.

Geocoding is a separate choice, configured with an account on OpenCage and the OPENCAGE_API_KEY environment variable. The Cypress section of the README repeats the requirement, since the end to end tests need a geocoding key in the .env file before they can drive the application.

That is a real operational detail with a real cost. A co-op that wants address autocomplete and reliable geocoding has to obtain API keys from third-party services and keep them renewed, and the platform has no bundled geocoder. The routing side is different, because the routing engine runs inside the compose stack rather than as a hosted API.

The compose file shows the split clearly: a postgres service running the postgis image for the geospatial database, a redis service, a webpack build service, an osrm service for cycling routes, and a valhalla service behind a profile.

Cycling routes are precomputed for Paris by default

The osrm service in the compose file runs the OSRM backend image, mounts a data directory and exposes port 5000. Its command checks for a prepared data file and, if it is missing, prints a message telling you to run make osrm.

That make target is more than a wrapper. It downloads an OpenStreetMap extract of Paris from the project's asset storage, then runs the OSRM extract, partition and customize steps against it using a bicycle profile, and finally restarts the osrm service. The underlying commands pull the Paris OSM PBF into var/osrm, then run osrm-extract with /opt/bicycle.lua, osrm-partition and osrm-customize. The profile matters, since the routing is for bicycles rather than cars, which is the whole point of a bike courier network.

The alternative routing engine is Valhalla, which runs under a profile rather than by default and builds its tiles on first boot from an OSM PBF served from the project asset storage, exposing its service on port 8002. The make target for it starts the profile in the background.

The practical consequence for evaluation is that a first run downloads a city extract and spends time preprocessing it. That is a reasonable trade for a routing service that matches the vehicles in the fleet, and it is the kind of detail that separates a demo from a platform you can actually run a delivery on.

Four test suites and the commands that drive them

The testing section is thorough, which is a good signal about how the project is worked on.

Before anything runs, the test database has to exist, and the command is a Symfony console call inside the php container:

sh
docker compose run php bin/console doctrine:schema:create --env=test

PHPUnit runs through a make target, and the Makefile target first updates the test schema and then invokes the PHPUnit binary, so the schema and the tests agree:

sh
make phpunit

A single package can be run by passing the path through ARGS:

sh
make phpunit ARGS="/var/www/html/tests/AppBundle/Sylius/OrderProcessing"

Behat covers the behavioural layer and has its own targets, including a variant that runs only tests carrying the @only tag:

sh
make behat
sh
make behat-only

A single feature file or extra options are passed the same way, for example ARGS="features/authentication.feature".

The JavaScript side has Jest for unit tests and Cypress for end to end work. Cypress runs on your own machine and drives the app served by the nginx_test container in the test environment on http://localhost:9080, which is the baseUrl in cypress.config.js. The npm scripts give the same thing a JavaScript entry point, with jest wired to jest.config.js and an e2e script that runs Cypress headless in Chrome.

Four layers with different speeds and different purposes is a heavier setup than a typical application, and it is also what makes a change to the dispatch logic or the storefront safe to merge.

The licence is the real gate on adoption

Most technical decisions here are ordinary and well documented. The exception is the legal one, and it deserves more attention than a paragraph at the end.

The repository's licence is a custom one that GitHub cannot classify, with the LICENSE file at the root as the authoritative text. The README states plainly that the software is reserved to co-ops and calls that the only difference with proprietary platforms such as Deliveroo or UberEats.

That reservation rules out a specific, common adoption path. A company that wants to run a delivery service, keep the revenue, and not be a worker-owned cooperative cannot simply deploy this stack and trade on it. A restaurant chain wanting its own delivery operation is in the same position. If the reservation does not fit your organisation, no amount of technical fit will help.

It also has an operational edge. A co-op running its own instance of the platform takes on the hosting, the routing preprocessing, the API keys and the upgrades, which is exactly the decentralisation the README describes and exactly the reason the project ships Docker Compose, Make targets and prebuilt images.

The release cadence tells you how that platform is maintained. The recent tags are v6.18.1 on 2026-10-19, v6.18.2 on the same day and v6.18.3 on 2026-10-20, with the last push on 2026-10-22. Patch releases landing within hours of each other is a normal pattern for a project with continuous delivery, and it means an operator should be prepared to take releases frequently rather than on a schedule.

Editorial conclusion

CoopCycle fits a courier co-op that wants to run its own marketplace, own its customer data and keep the margin on delivery, and the Docker Compose path makes a first evaluation cheap to try. It is the wrong choice for a conventional restaurant delivery business or for any company that wants to run the platform commercially under its own ownership, since the licence is the obstacle rather than the code. Verify first that your organisation fits the co-operative model the licence reserves the software for, then read the LICENSE file itself, and only after that run docker compose up and make install to see the schema and demo data the platform seeds.

Frequently asked questions

Who is allowed to run CoopCycle?

The README states that the software is reserved to co-ops and that the point is to let couriers own the platform they work for, with couriers in each city organising into co-ops that run their own version. The licence is a custom one whose authoritative text is the LICENSE file at the repository root.

How do I run CoopCycle locally?

Install Docker and Docker Compose, run docker compose pull to fetch the prebuilt images, copy .env.dist to .env, then start the stack with docker compose up. The database is still empty at that point, so run make install to create the schema and seed demo data, then open http://localhost.

Which geocoding services does CoopCycle use and what keys are required?

CoopCycle uses OpenStreetMap for geocoding and address autocomplete. Autocomplete uses either LocationIQ or Geocode Earth, chosen with COOPCYCLE_AUTOCOMPLETE_ADAPTER, with the LOCATIONIQ_ACCESS_TOKEN and GEOCODE_EARTH_API_KEY variables, while geocoding uses OpenCage through OPENCAGE_API_KEY.

How do I run the different CoopCycle test suites?

Create the test database with docker compose run php bin/console doctrine:schema:create --env=test, then use make phpunit for the PHP tests, make behat for the behavioural suite, make jest for the JavaScript unit tests and the Cypress npm scripts for end to end runs against the nginx_test container on port 9080.

Official sources

  1. coopcycle/coopcycle-web on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/coopcycle-coopcycle-web.svg)](https://hysenlabs.com/projects/coopcycle-coopcycle-web)

Was this useful?