Stéphane De Todaro — tech lead

@super-dev.app · Tech-lead
Active since 2017

Technical lead and full-stack architect, freelance since 2019. I design, industrialize and operate business platforms on Azure, and I publish open-source software engines.

Back to articles
FLUTTER
FLUTTER

A Flutter monorepo with Melos

Article 3 of 3 — Flutter in production
Offline-first, Riverpod, a Melos monorepo: serious mobile, not a prototype.

Un produit Flutter... → Let me translate this article now.

A moderately ambitious Flutter product rarely fits in a single package. There's the app, a shared design system, an API client, sometimes one module per team. Spread across separate repositories, these packages impose on every cross-cutting change a sequence of publications and version bumps to order by hand. Melos, Invertase's tool, handles the other approach: a single Dart/Flutter repository, multiple packages, commands that run across the whole graph in one pass.

One repo, multiple packages

The packages live under a folder at the root, generally packages/ , each with its own pubspec.yaml . The app and the modules reference each other via path dependency ( path: ../core_api ), and Melos resolves these links locally instead of fetching the versions published on pub.dev.

Dart
1// packages/feature_auth/lib/feature_auth.dart
2import 'package:core_api/core_api.dart';
3
4class AuthRepository {
5 AuthRepository(this._api);
6 final ApiClient _api;
7
8 Future<Session> signIn(String email, String password) =>
9 _api.post('/auth/login', {'email': email, 'password': password});
10}

This split carries an architectural constraint. feature_auth depends on core_api , the reverse doesn't compile. The dependency graph is written in black and white in the pubspec.yaml files, and a forbidden dependency shows up immediately: it doesn't resolve. A single repository keeps these boundaries intact and simply makes crossing them easier when it's legitimate.

The melos.yaml file

The configuration sits in a melos.yaml at the root (recent versions also accept a melos: key in the root pubspec.yaml ). It declares the workspace packages and named scripts, reusable both locally and in CI.

YAML
1name: my_app
2
3packages:
4 - app
5 - packages/**
6
7command:
8 version:
9 linkToCommits: true # add commit links in each CHANGELOG
10 workspaceChangelog: true # also aggregate a root CHANGELOG.md
11 bootstrap:
12 hooks:
13 post: melos run generate # run codegen once linking is done
14
15scripts:
16 analyze:
17 exec: dart analyze .
18 description: Static analysis in every package.
19
20 test:
21 exec: flutter test
22 description: Test only packages that ship a test/ directory.
23 packageFilters:
24 flutter: true
25 dirExists: test
26
27 generate:
28 exec: dart run build_runner build --delete-conflicting-outputs
29 description: Regenerate sources in packages that use build_runner.
30 packageFilters:
31 dependsOn: build_runner

A script in exec: form launches its command in each selected package, and packageFilters narrows the set before execution. Here flutter: true excludes pure Dart packages, and dirExists: test skips those without a test folder, which avoids a noisy failure on a package with no test suite. The command: section configures the built-in commands; under version , workspaceChangelog adds an aggregated changelog at the root in addition to each package's own.

A script is triggered by melos run analyze or, with no argument, by a melos run that offers the list of scripts as a keyboard prompt. The melos exec command remains the escape hatch for anything that doesn't have a dedicated script.

Bootstrap and local linking

melos bootstrap (shortened melos bs ) is the first command run after a clone. It fetches the dependencies of all packages at once and wires up the path dependencies between them, without having to chain flutter pub get package by package.

Concretely, Melos writes a pubspec_overrides.yaml in each package that points the internal dependencies to their local folder:

YAML
1# packages/feature_auth/pubspec_overrides.yaml (generated by Melos, git-ignored)
2dependency_overrides:
3 core_api:
4 path: ../core_api

This file is a native Dart mechanism, not a Melos invention: pub reads it as an overlay on top of pubspec.yaml and prefers the local version indicated. It's left out of version control, and bootstrap is rerun after each modification of a pubspec.yaml . On recent Dart versions, Melos can also rely on pub 's native workspaces ( resolution: workspace ) to get the same local resolution.

The post hook in the configuration above fires at the end of bootstrap . It's the natural place for code generation, very common in a Flutter project with freezed , json_serializable , or a provider generator: the clone is ready to compile from the first command, with no manual step forgotten. The dependsOn: build_runner filter limits generation to the packages that actually need it.

Targeting a subset: filters

The same filtering mechanism powers melos exec , which runs an arbitrary command on part of the graph. Filters combine:

Bash
1# Run tests only in packages changed since main
2melos exec --diff="origin/main" -- flutter test
3
4# Analyze core_api and everything that depends on it
5melos exec --scope="core_api" --include-dependents -- dart analyze .
6
7# List which packages would run, without executing anything
8melos list --diff="origin/main"

--diff compares the working tree to a git reference and keeps only the touched packages. --scope and --ignore filter by name with globs. --depends-on and --include-dependents follow the graph's edges to also catch packages upstream or downstream of a change. In CI, --diff="origin/main" is the lever that avoids replaying the analysis and tests of the whole repository on every push: only the modified subgraph runs, and the rest keeps its previous result.

Coordinated versioning

melos version translates conventional commits into version changes. It reads the git history since each package's last tag, deduces the bump ( fix: as patch, feat: as minor, a BREAKING CHANGE: as major), updates the version: field of the relevant pubspec.yaml , and writes its CHANGELOG.md . The tag it sets bears the package's name, for example core_api-v1.4.0 .

The point that justifies the tool is propagation. A fix(core_api): ... bumps core_api , but also the packages that depend on it: Melos adjusts their version constraint and applies an increment to them. The whole stays consistent, without a package referencing a version of a neighbor that doesn't exist yet. The scope in parentheses ( core_api ) ties the commit to the right package when a single change touches several.

Once the versions are set and tagged, melos publish pushes the publishable packages to pub.dev. The command runs dry by default and only acts for real with --no-dry-run , and it automatically skips packages marked publish_to: none , such as the Flutter app itself.

Chaining it in CI

A pipeline reuses these building blocks in order. melos bootstrap installs and links, then melos run analyze and melos run test run on the modified packages via --diff , and the release branch adds melos version then melos publish . Each step reuses the same script definitions as developers use locally, which avoids drift between CI and the workstation: what passes on a machine passes in the pipeline, with the same command.

A monorepo is judged by the cost of a cross-cutting change. With Melos, touching a shared package and its consumers fits in a single branch to review and merge, whereas separate repositories would impose a queue of publications to order by hand.
Stéphane De Todaro — super-dev.app
// More in FLUTTER
Offline-first sync in Flutter + Firebase
Feb 20, 2026 • 5 min
A testable Flutter architecture with Riverpod
Jan 22, 2026 • 5 min