A Copier template for generating Flutter/Dart packages that wrap native Rust libraries using Flutter Rust Bridge (FRB) with full CI/CD support.
- Cross-platform native library support (Android, iOS, macOS, Linux, Windows, Web/WASM)
- Flutter Rust Bridge integration for type-safe bindings
- Automatic native library download via build hooks
- SHA256 checksum verification for supply chain security
- GitHub Actions workflows for CI/CD
- Python 3.8+
- Copier 9.0+
- jinja2-strcase (for case conversion filters)
All platforms (pip):
pip install copier jinja2-strcasemacOS (Homebrew + pip):
brew install copier && pip install jinja2-strcaseLinux (pipx - recommended for isolation):
pipx install copier
pipx inject copier jinja2-strcaseWindows (pip or pipx):
pip install copier jinja2-strcase
# or with pipx
pipx install copier
pipx inject copier jinja2-strcasemkdir my_package && cd my_package
copier copy https://github.com/djx-y-z/copier-dart-frb-wrapper .If you use Claude Code, clone this template — it
bundles a create-project skill that drives Copier for you:
git clone https://github.com/djx-y-z/copier-dart-frb-wrapperThen open the cloned directory in Claude Code and run /create-project. Describe
your package in plain language and the skill runs the Copier commands above.
cd my_package
copier updatemkdir my_signal_lib && cd my_signal_lib
copier copy https://github.com/djx-y-z/copier-dart-frb-wrapper . \
--data package_name=my_signal_lib \
--data description="Dart bindings for Signal Protocol" \
--data native_library_name=signal \
--data github_repo=user/my_signal_lib \
--data native_repo=signalapp/libsignal \
--data crate_name=my_signal_lib_frb \
--data rust_edition=2024 \
--data rust_version=1.88 \
--data frb_version="2.13.0" \
--data upstream_crates=libsignal \
--data upstream_version=v0.86.0 \
--data enable_web=true \
--data flutter_version=3.38.4 \
--data dart_sdk_version="^3.10.0" \
--data flutter_sdk_version=">=3.38.0" \
--data android_min_sdk=21 \
--data android_compile_sdk=34 \
--data topics="cryptography,ffi,native" \
--data license=MITmkdir my_lib && cd my_lib
copier copy https://github.com/djx-y-z/copier-dart-frb-wrapper . \
--trust \
--defaults \
--data package_name=my_lib \
--data description="My Rust library wrapper" \
--data native_library_name=mylib \
--data github_repo=user/my_lib \
--data native_repo=original/mylib| Variable | Description | Example |
|---|---|---|
package_name |
Dart package name (lowercase, underscores) | my_signal_lib |
description |
Package description for pubspec.yaml | Dart bindings for Signal Protocol |
native_library_name |
Name of the native library | signal |
github_repo |
GitHub repository of your wrapper | user/my_signal_lib |
native_repo |
GitHub repository of native library | signalapp/libsignal |
| Variable | Description | Default | Example |
|---|---|---|---|
crate_name |
Rust crate name for FRB wrapper | <package_name>_frb |
my_signal_lib_frb |
rust_edition |
Rust edition (2021 or 2024) | 2024 |
2021 |
rust_version |
Minimum Rust version (MSRV) | 1.88 |
1.75 |
frb_version |
Flutter Rust Bridge version | 2.13.0 |
2.10.0 |
| Variable | Description | Default | Example |
|---|---|---|---|
upstream_crates |
Upstream Rust crates (comma-separated) | `` | libsignal-protocol or libsignal-protocol,libsignal-core |
upstream_version |
Version/tag of upstream crate | `` | v0.86.0 |
version_tag_prefix |
Tag prefix for upstream version tags (used for version normalization) | v |
release-v |
| Variable | Description | Default | Example |
|---|---|---|---|
flutter_version |
Flutter version for FVM | 3.38.4 |
3.35.0 |
dart_sdk_version |
Dart SDK version constraint | ^3.10.0 |
^3.8.0 |
flutter_sdk_version |
Flutter SDK version constraint | >=3.38.0 |
>=3.35.0 |
enable_web |
Enable Web/WASM support | true |
false |
enable_protoc |
Enable Protocol Buffers support (install protoc in setup/CI) | false |
true |
enable_fuzzing |
Include a cargo-fuzz harness (rust/fuzz/, Fuzz CI, Makefile targets) | true |
false |
enable_claude |
Include Claude Code files (CLAUDE.md, .claude/skills/) | true |
false |
| Variable | Description | Default | Example |
|---|---|---|---|
ios_min_version |
iOS minimum deployment target | 13.0 |
15.0 |
macos_min_version |
macOS minimum deployment target | 10.15 |
11.0 |
| Variable | Description | Default | Example |
|---|---|---|---|
android_min_sdk |
Android minimum SDK version | 24 |
21 |
android_compile_sdk |
Android compile SDK version | 36 |
35 |
android_ndk_version |
Android NDK version for Rust (r28+) | 28.2.13676358 |
28.1.13356709 |
android_gradle_version |
Android Gradle plugin version | 8.11.1 |
8.7.0 |
android_java_version |
Java version for Android compilation (11, 17 or 21) | 17 |
21 |
| Variable | Description | Default | Example |
|---|---|---|---|
license |
Package license | MIT |
Apache-2.0 |
copyright_year |
Year of first publication for the LICENSE copyright line (rendered as <year>-present; a stored answer, so it does not move on re-render) |
2026 |
2025 |
topics |
pub.dev topics (comma-separated) | ffi,native,rust |
cryptography,ffi,native |
After generating the project, follow these steps:
cd <package_name>
# Install dependencies and set up the project
make setup
# Generate Flutter Rust Bridge bindings
make codegen
# Build native libraries for current platform
make build
# Generate the third-party notice inventory (needs the Cargo.lock from `make build`)
# CI verifies the committed file against the dependency graph, so the first CI
# run fails until this is generated and committed.
make third-party-notices
# Verify everything works
make analyze # Dart static analysis (should be clean)
make rust-check # Rust type check
make test # Run tests (should pass)- Create a new repository on GitHub matching
github_repo - Push the generated code:
git remote add origin https://github.com/<github_repo>.git git push -u origin main
Configure secrets and variables in your repository settings (Settings → Secrets and variables → Actions).
| Secret | Description | Required For |
|---|---|---|
APP_PRIVATE_KEY |
GitHub App private key (PEM format) | Update checker workflow |
ANTHROPIC_API_KEY |
Anthropic API key | AI changelog generation (anthropic/… entries) |
GEMINI_API_KEY |
Google AI Studio API key | AI changelog generation (google/… entries) |
OPENROUTER_API_KEY |
OpenRouter API key | AI changelog generation (openrouter/… entries) |
GIST_TOKEN |
Personal Access Token with gist scope |
Coverage badge |
| Variable | Description | Required For |
|---|---|---|
APP_CLIENT_ID |
GitHub App Client ID (not the numeric App ID) | Update checker workflow |
AI_MODELS |
Ordered provider/model list, e.g. anthropic/claude-opus-5,google/gemini-3.5-flash-lite. No default — unset means no model is called |
AI changelog generation |
AI_EFFORT |
low | medium (default) | high | xhigh | max |
AI changelog generation |
COVERAGE_GIST_ID |
Gist ID for coverage badge JSON | Coverage badge |
Both check-*-updates.yml (new upstream release) and check-template-updates.yml (new template release) use the App to open Pull Requests with signed commits. This ensures commits are verified and associated with a bot account rather than a personal account.
- Go to Settings → Developer settings → GitHub Apps → New GitHub App
- Fill in:
- Name:
my-package-bot(must be unique on GitHub) - Homepage URL: Your repository URL
- Webhook: Uncheck "Active" (not needed)
- Name:
- Set Repository permissions:
- Contents: Read & Write
- Pull requests: Read & Write
- Workflows: Read & Write — see below
- Metadata: Read-only
- Click Create GitHub App
- Copy the App ID (shown at the top) → add as
APP_IDvariable - Scroll down to Private keys → Generate a private key
- A
.pemfile will download → copy its contents asAPP_PRIVATE_KEYsecret - Go to Install App (left sidebar) → Install on your repository
Why
Workflows. A template update is an ordinary commit over whatever the template owns, and it owns.github/workflows/**. GitHub refuses to let an App write a workflow file without this permission, and the refusal is easy to misread:create-pull-requestcommits through the Git Data API, so it arrives asResource not accessible by integrationonPOST /git/trees— after every blob has been created, which is itself aContents: writeoperation and therefore proves that permission is fine. It also stays invisible until the first update that happens to touch a workflow; every PR the bot opened before that one goes through untouched.Two traps when adding it to an App that already exists.
Actionsis notWorkflows:Actionsgoverns workflow runs,Workflowsgoverns the workflow files, and only the second unblocks this. And a permission added to an App does not reach the tokens it issues until every installation accepts the change — Settings → Applications → Installed GitHub Apps → your app → Review request. Until that is accepted the App's own settings page shows the new permission while the workflow keeps failing exactly as before.
The check-*-updates.yml and check-template-updates.yml workflows write the
CHANGELOG entry with an AI model. Which model is configuration rather than
code: AI_MODELS holds an ordered, comma-separated list of provider/model
entries and the first one that has a key and answers wins, so changing provider
is a repository-variable edit instead of a template release rolled out across
every generated project. (That cost is why it works this way: the previous
provider, GitHub Models, was retired on 2026-07-30 and had been hard-coded.)
There is no default list. With AI_MODELS unset nothing is called: the
workflows still open the pull request, record the entry as not written, and
label it changelog-needed for a human. That is also how a project says "no AI
here" without a provider sitting there waiting for a key to appear.
- Get an API key from the provider you want — Anthropic, Google AI Studio or OpenRouter.
- Add it as a repository secret:
ANTHROPIC_API_KEY,GEMINI_API_KEYorOPENROUTER_API_KEY. - Add the repository variable
AI_MODELS, naming the models to try in order — e.g.anthropic/claude-opus-5,google/gemini-3.5-flash-lite. An entry whose key is unset is skipped; a malformed one is warned about loudly, because it is a typo rather than a choice. - Optionally add the variable
AI_EFFORT(low|medium|high|xhigh|max, defaultmedium).
openrouter is an aggregator, so its model half carries its own slash:
openrouter/anthropic/claude-opus-5. The pull-request body names the model that
actually wrote the entry, so a first entry that has quietly started failing is
visible immediately rather than months later as a drift in house style.
Each generated project also gets .github/agent-prompts/changelog-scope.md —
its own statement of what it binds and exposes, which every entry is classified
against. It is written once and never overwritten by a template update, so edit
it to match the project.
The publish.yml workflow uses OIDC authentication to publish to pub.dev without tokens. This requires a one-time setup on both pub.dev and GitHub.
On pub.dev:
- Go to https://pub.dev and sign in
- Navigate to your publisher page (or create a publisher)
- Go to Admin → Automated publishing
- Click Enable automated publishing
- Add your GitHub repository (e.g.,
user/my_package) - Set Publishing from: GitHub Actions with tag → tag pattern:
v*
See dart.dev/tools/pub/automated-publishing for details.
On GitHub (create environment):
- Go to your repository → Settings → Environments
- Click New environment → name it exactly
pub.dev - Under Deployment protection rules:
- Check Required reviewers → add yourself (and/or your team) as reviewer
- Uncheck Allow administrators to bypass configured protection rules
- Click Save protection rules
Why? The
pub.devenvironment is required by the publish workflow. Protection rules ensure that every publish to pub.dev requires manual approval, preventing accidental releases.
- Go to https://gist.github.com and create a new secret gist
- Name the file
coverage.jsonwith content:{"schemaVersion":1,"label":"coverage","message":"0%","color":"red"} - Click Create secret gist
- Copy the Gist ID from URL (e.g.,
https://gist.github.com/user/abc123def456→abc123def456) - Add the Gist ID as
COVERAGE_GIST_IDvariable (not secret) - Create a Personal Access Token:
- Go to Settings → Developer settings → Personal access tokens → Tokens (classic)
- Click Generate new token (classic)
- Name:
gist-coverage - Scopes: check only
gist - Click Generate token → copy and add as
GIST_TOKENsecret
Rulesets and environments live on GitHub, not in the repository, so nothing in
the generated tree applies them for you. Once the repository exists and the first
push has landed, run — as a repo admin, with gh
authenticated:
make setup-repo-protectionsIt creates every ruleset committed under .github/rulesets/ (protected main,
required commit signing, protected release tags, no branch deletion) and the
native-build environment with you as its required reviewer. Until it runs, the
native binary that every consumer downloads at build time can be published by
anyone with write and no review, and the release tags that trigger that build
are unprotected. See .github/rulesets/README.md for what each ruleset does,
how to adjust bypass actors, and how to re-apply after editing one.
Add these topics to your GitHub repository for discoverability:
dartflutterrustffiflutter-rust-bridge- Your native library name
Review and update SECURITY.md with your security policy and contact information.
The generated project includes a Makefile. Run make help to see all available commands:
# Setup
make setup # Full setup (FVM + Rust tools + dependencies)
make setup-fvm # Install FVM and project Flutter version only
make setup-rust-tools # Install Rust tools (cargo-audit, frb codegen)
make setup-android # Install Android build tools (cargo-ndk)
make setup-web # Install web build tools (wasm-pack) - if enable_web=true
# Build & Codegen
make codegen # Generate Dart bindings from Rust code
make build # Build Rust library for current platform
make build-android # Build for Android (all ABIs)
make build-web # Build WASM for web platform - if enable_web=true
# Development
make check-upstream # Check for native library updates
make version # Show current crate version
# Rust Quality
make rust-check # Check Rust code compiles
make rust-doc # Rustdoc GATE: intra-doc links, -D warnings
make rust-geiger # Unsafe-expression census (diagnostic, not gated)
make rust-audit # Audit Rust dependencies for vulnerabilities
make test-web # Browser tests in headless Chrome - if enable_web=true
# Dart Quality
make test # Run tests
make coverage # Run tests with coverage report
make analyze # Run static analysis
make format # Format Dart code
make format-check # Check Dart code formatting
make doc # Generate API docs (GATE: fails on unresolved refs)
# Utilities
make get # Get dependencies
make clean # Clean build artifacts
make publish-dry-run # Validate package before publishingEnsure you have the correct Rust toolchain and cross-compilation targets installed.
Note: These are target architectures (what you build for), not your development machine's architecture. You can add targets on any OS, but some targets have platform requirements.
rustup update
# Desktop - Linux (any OS, but native builds require Linux)
rustup target add x86_64-unknown-linux-gnu # Linux x86_64
rustup target add aarch64-unknown-linux-gnu # Linux ARM64
# Desktop - macOS (macOS only - requires Xcode)
rustup target add aarch64-apple-darwin # macOS Apple Silicon
rustup target add x86_64-apple-darwin # macOS Intel
# Desktop - Windows (any OS, but native builds require Windows + MSVC)
rustup target add x86_64-pc-windows-msvc # Windows x86_64
# Mobile - iOS (macOS only - requires Xcode)
rustup target add aarch64-apple-ios # iOS devices (ARM64)
rustup target add aarch64-apple-ios-sim # iOS Simulator on Apple Silicon
rustup target add x86_64-apple-ios # iOS Simulator on Intel Mac
# Mobile - Android (any OS with Android NDK)
rustup target add aarch64-linux-android # ARM64 devices (most modern phones)
rustup target add armv7-linux-androideabi # ARMv7 devices (older phones)
rustup target add x86_64-linux-android # x86_64 emulator
# Web/WASM (any OS)
rustup target add wasm32-unknown-unknown- Check that
native_repois correct and the repository has releases - Verify the release assets follow the expected naming convention
- Check your network connection and GitHub API rate limits
Ensure you're using Dart 3.10+ with native assets support:
dart --version
flutter --versionFor Web/WASM support:
# Install wasm-pack
cargo install wasm-pack
# Build WASM
make build-webContributions are welcome! Please see CONTRIBUTING.md for guidelines.
This template is licensed under the MIT License. See LICENSE for details.
Generated projects inherit this license by default, but you can choose a different license during generation.