Under the Working Agenda Value Driven Digitization, the Dutch government is preparing for the introduction of European digital identity wallets (in short ID-wallets) through the revision of the eIDAS-regulation. One of the ways in which they are doing this, is by developing a public reference wallet called the NL Wallet. These ID-wallets will be mobile apps that citizens can use to identify (or ‘log in’) to public and private online services, share data about themselves, and sign electronically.
The first version of the NL Wallet will focus on online identification and data sharing and will be piloted at small scale in 2024. In the future, it may be possible to use such an ID-wallet in a lot of different situations, for example to share your diploma’s when applying for a job, to show your driver’s license, or to prove that you are 18+ to buy a beer.
The NL Wallet is being developed in an open and transparent way. We offer the following channels to allow you to contribute:
- The user interface of the app is available on Figma.
- The source code is published in this GitHub repository.
- More information, events and discussions can be found on Pleio.
- Project documentation is available on Github pages.
Feel free to look around and share your feedback and ideas.
[[TOC]]
As this project is a work in progress, you will find that the different components are at different levels of maturity. Most notably, the user interface is always a few steps ahead of the software under the hood. To put it simply, we incrementally add functionality to the wallet in three steps:
- We design the user interface of a piece of functionality in Figma. This is purely graphical, ideal for quick iterations.
- We then build the user interface in the app displaying dummy data and using mocked logic. This makes it fast and easy to explore, demonstrate and discuss different scenarios and possibilities.
- We then replace the mocked logic with actual working software, still using dummy data. This allows us to prove the app works and is secure.
Once the first version of the app is complete, thoroughly tested and considered secure, we can fill it with real data and pilot it in real life scenarios.
See the releases page for the latest release. You can follow the latest work by subscribing to the releases of this GitHub repository at the top of this page.
We have a dedicated documentation site. In more general terms, with the NL reference wallet we want to achieve the following things:
- We want to validate the feasibility of the framework as proposed in the EU.
- We want to explore how we can set the bar in terms of privacy protection, security, usability and inclusion.
- We want to learn what this development means for citizens, businesses, other governments and public service providers.
- We want to help citizens, especially those with special needs, in the best way possible.
- We want to offer a testing ground for a variety of use cases.
- We want to share the lessons we learn with the public and share them with the EU community.
If you want to learn more about the NL Wallet development, please read the background information on the Pleio hub. The development of the user flows and screens can be followed through Figma.
The source code of the NL Wallet is released under the EUPL license. The documentation is released under the CC0 license. Please see the .reuse/dep5 file for more details, which follows the Reuse specfication.
We’re releasing the source code with the explicit intention of allowing contributions. The coordination of the project lies with the development team of the European Digital Identity Progam, but we’re open to all contributions. You can directly create a new Pull Request via Github, or contact the community manager via [email protected].
The development team works on the repository in a private fork (for reasons of compliance with existing processes) and shares its work as often as possible. If you watch the repository on GitHub, you will be notified of a new release. We will also send a notification through Pleio.
Although we are open to contributions, please consider the nature of this project as outlined in this Readme. At this stage the most useful way to contribute to the project is to participate on our community site edi.pleio.nl, and visit our EDI Meet-ups and/or Heartbeats.
If you plan to make non-trivial changes, we recommend that you open an issue beforehand where we can discuss your planned changes. This increases the chance that we might be able to use your contribution (or it avoids doing work if there are reasons why we wouldn't be able to use it).
Note that all commits should be signed using a GPG key.
This section contains the general setup requirements of the project. For more information on how to configure specific components like wallet app, wallet core, wallet_web, and wallet_provider, please see the corresponding README files.
The app's UI is build using Flutter, but to avoid tying the app to Flutter &
Dart, all core business logic is build using Rust. This gives us the more
flexibility to migrate to completely native iOS/Android app's if the need
arises. This does mean building the app is slightly more complex than a simple
flutter run. This section describes how to set up your environment.
The various components of NL Wallet have different requirements. To make sure things run correctly, you need to take the following system requirements into account.
Our mobile apps require at least the following operating system versions:
- Android 7.0 (API-level 24)
- iOS 14.0
The app does not put a particulary heavy load on the device, so CPU and memory requirements are low to average. Note that this is subject to change.
The wallet_web frontend helper library effectively runs in the browser of a person that wants to interact with a relying party that integrates with the NL Wallet platform. As such, wallet_web has requirements on the minimum browser version supported:
- Firefox 60.9+
- Chrome 109+
- Edge 109+
- Safari 13+
Note that the above are not recommendations, but simply a statement about the minimum version we have some confidence in running correctly. We always recommend that you run the latest stable browser your platform offers. Also note that the above is subject to change.
We have various backend services, mostly built in Rust that make up the wallet
platform. In general, we build binaries for glibc and musl Linux-based
distributions. We've found that usually the musl binary will work on almost
anything, but the glibc binary really requires a glibc-based distribution.
- Alpine 3.x
- Arch Linux (any current)
- Debian 12+
- RHEL 8+ (and derivatives)
- PostgreSQL 10+ (first version with
jsonbsupport) - RDO-MAX v2.13.x+ (see their repository for details)
- BrpProxy v2.1.x (see their repository for details)
Specifically for PostgreSQL you need to consider storage requirements. Our
database-backed services are wallet_provider, verification_server,
issuance_server, demo_issuer (usually not built for production environments)
and pid_issuer. They have a very simple database layout. A good ballpark
figure is to allocate 100GiB for a wallet_provider instance and 10GiB for
instances of the verification_server, issuance_server or pid_issuer. Of course,
these requirements will change with time and duration of usage, and are subject
to change. Also note that these size requirements assume somewhat serious usage;
for development purposes you can make do with a lot less.
- wallet_provider
- verification_server
- issuance_server
- pid_issuer
- demo_relying_party
- demo_issuer
- gba_hc_converter
The above Rust-based services require a regular Linux machine or container based on one of the aforementioned operating systems. Memory requirements of these services are very low (we're seeing 20 to 50 megabytes of usage on our Kubernetes clusters, but of course it depends on usage too). The storage requirements are effectively non-existent due to usage of PostgreSQL for state.
The Wallet app needs several supporting services to run, and also requires the user to log in using DigiD in order to create the Wallet. The services are the following:
- digid-connector (rdo-max)
- configuration_server
- gba_hc_converter
- brpproxy
- postgresql
All these applications will need to be configured correctly. A local development
environment with automatic configuration of all the above services can be set up
using the scripts/setup-devenv.sh and scripts/start-devenv.sh scripts, which
we'll document later in this Readme.
For end-users, an internet connection is required to use the disclosure and issuance features of the wallet app. For relying parties and issuers, who want to obtain disclosed attributes and issue attributes respectively, the same requirement holds.
To do development work on the NL reference wallet, you need to following tools:
- Rust (including additional targets and utilities)
- Android SDK + NDK (for Android builds)
- Xcode (for iOS builds)
- Flutter
- Docker (to run supporting services)
- PKCS #11 library
In the following sub-sections we will document how to install and configure these.
To install Rust and Cargo using rustup, follow the installation guide.
After installation, make sure to add the following targets:
- For iOS:
rustup target add aarch64-apple-ios x86_64-apple-ios - For iOS simulator:
rustup target add aarch64-apple-ios-sim - For Android:
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
Make sure rustc, and cargo are on your path and run the following commands
to install a few additional utilities we use:
cargo install --locked cargo-edit cargo-expand 'cargo-ndk@^4' cargo-nextest
cargo install --locked --version 2.11.1 flutter_rust_bridge_codegen
cargo install --locked --version 1.1.16 sea-orm-cli
cargo install --listTo build for Android you need to have the Android SDK and NDK installed on your
system. You can download Android Studio here. Note the install location
and set the ANDROID_HOME environment variable to point to that installation
location. The following is an example that assumes you installed Android Studio
in /opt/android, sets the necessary variables, and adds certain
Android-specific tools and utilities to your path:
export ANDROID_HOME="/opt/android"
export ANDROID_NDK_HOME="$(find "$ANDROID_HOME/ndk" -maxdepth 1 -type d | sort -V | tail -n1)"
export PATH="$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_NDK_HOME"After having done the above, you will have tools like adb, sdkmanager, and
ndk-build on your path.
With sdkmanager --list_installed you can list the installed Android SDK
components. Now let's install some extra components we'll need:
# Install all needed packages. On intel/amd, x86_64 is
# fine, on arm64, use arm64-v8a for the system-image.
sdkmanager --install \
'build-tools;36.0.0' \
'cmdline-tools;latest' \
'emulator' \
'ndk;28.2.13676358' \
'platform-tools' \
'platforms;android-36' \
'sources;android-36' \
"system-images;android-36;google_apis_playstore;$(uname -m|sed s/arm64/arm64-v8a/)"Note: The ndk version was the latest non-release-candidate version as of this
writing (2025-08-28); If there is a newer stable version, feel free to use that.
Same for the android version - it's 36 as of this writing, but newer might be
available when you read this - feel free to use that too.
You will need to create an Android virtual device (AVD) so you can run the emulator. To do that, do the following:
- Start Android Studio, open or create any project;
- Click "File" pulldown menu, then "Tool Windows" -> "Device Manager";
- Click on "+", then "Create Virtual Device", select "Pixel 3"
- For "Name", choose something descriptive like "pixel3-x86_64-android16-api36"
- For "API", select latest available, like "API 36.0"
- For "Services", select "Google Play Store"
- For "System Image", choose whichever latest Google Play enabled image is available. For example: "Google Play System Image", "API 36.0"
- Click on the "Additional Settings" tab
- Set "Expanded Storage" to "Custom", "12", "GB"
- Set "RAM" to "4", "GB"
- Set "VM heap size" to "512", "MB"
- Click on the blue "Finish" button
When you run $ANDROID_HOME/emulator/emulator -list-avds on the command-line,
you should see your just-created virtual device show up.
- Install Xcode
- Follow the steps to install iOS simulators
- Start the simulator using
open -a Simulator
To install Flutter follow this installation guide. You can validate your
initial setup by making sure flutter is on your path, and then running
flutter doctor which will tell you if all is well with its dependencies. Make
sure flutter doctor has no complaints or warnings; specifically, it needs to
find the Android and/or Xcode related components (in the case of Android, it
needs to find the SDK toolchain and Android Studio itself).
FVM is a simple CLI to manage Flutter SDK versions per project. It enables fast
switching between Flutter versions and pin them to your Flutter project. When
using FVM; all Flutter related commands need to be prefixed with fvm, e.g.
fvm flutter run.
To install FVM follow this installation guide. You can validate your
initial setup by running fvm flutter doctor after installation. Select [Y]es
when asked to install the pinned Flutter version defined in
fvm_config.json.
Note that FVM only pins the Flutter version for local development, not for the CI pipelines.
Make sure you can run Docker on your development system. You can follow the
getting started guide to do so. Make sure docker is on your path.
The wallet_provider is designed to use a HSM with the PKCS #11 API. For
local development we use the SoftHSMv2 library. As of this writing
(2025-08-28), the latest actually released version of this library is more than
five years old and does not work in combination with the latest openssl.
Therefore, you either need to install a git version of SoftHSMv2 using your
package manager, or you need to compile the library from source, specifically,
from the develop branch, and set the correct HSM_LIBRARY_PATH (if the setup
script does not detect the library). There is an
install script available, which you can use.
Assuming you've followed the steps in the setting up Android Studio section, you can now run the emulator. You can do that from within Android Studio, or, as shown below, from the command-line:
# Optional, but required on Linux (QT_QPA_PLATFORM=wayland segfaults emulator).
export QT_QPA_PLATFORM=xcb
# List configured Android virtual devices you can run.
# You can create these in Android Studio -> Device Manager.
"$ANDROID_HOME/emulator/emulator" -list-avds
# Start an avd named pixel3-x86_64-android16-api36 (can be named anything,
# should have appeared in previous listing). We use the -wipe-data flag to
# start with a clean data partition. Issues have been observed with default
# snapshotting behavior, which is why we start with no snap storage and no
# automatic snapshotting. Audio is also not strictly needed. The -no-window
# flag is optional and good enough for running connected android tests. If
# you do want to see the phone gui, you need to remove the -no-window option.
"$ANDROID_HOME/emulator/emulator" -avd pixel3-x86_64-android16-api36 -wipe-data -no-snapstorage -no-snapshot -no-audio -no-window
# Optional, disables bluetooth on emulator to avoid observed interference
# with bluetooth on host.
adb shell cmd bluetooth_manager disableIn order to connect to our locally running services from within the running
Android emulator, some port mappings have to be made (note that this must
be done every time the Android emulator is restarted). This is automated in
our map-android-ports.sh script, which our setup-devenv.sh script will
call automatically when it detects adb on the path.
The start-devenv.sh script can set up a dockerized PostgreSQL database for you
or you could opt to configure it yourself. If you want to set it up yourself,
make sure to have it running on localhost, port 5432. Here's an example docker
command to bring up a latest version PostgreSQL container:
docker run --name postgres --volume postgres:/var/lib/postgresql/data \
--rm --publish 5432:5432 --env POSTGRES_PASSWORD=verysecret postgresThe above docker command will start a Docker container running the latest
postgres image, bound to localhost on port 5432, with verysecret as a
password for the postgres system user. There are no other users or
databases yet, just the defaults.
If you want, you can create a dedicated wallet user for the various databases
we need to run, instead of the postgres system user:
export PGPASSWORD=verysecret
psql -h localhost -U postgres -c "create user wallet with password 'verysecret';"
psql -h localhost -U postgres -c "create database pid_issuer owner wallet;"
psql -h localhost -U postgres -c "create database issuance_server owner wallet;"
psql -h localhost -U postgres -c "create database verification_server owner wallet;"
psql -h localhost -U postgres -c "create database wallet_provider owner wallet;"
psql -h localhost -U postgres -d wallet_provider -c "create extension \"uuid-ossp\" schema public;"We need to set DB_USERNAME and DB_PASSWORD, which are used by the
setup-devenv.sh script to initialize the schemas of the above databases:
cat > scripts/.env <<EOD
DB_USERNAME=wallet
DB_PASSWORD=verysecret
EODAfter having done all of the above (i.e., you have Rust and Flutter installed,
you have Docker up and running, configured a PostgreSQL database and installed
Android Studio and/or Xcode and you're running an Android Emulator or the iOS
simulator), you are almost ready to configure the local development environment
with the help of our setup-devenv.sh script. There are two more optional
environment variables to consider setting:
# Where the rdo-max git repository is cloned to.
export DIGID_CONNECTOR_PATH="$HOME/Desktop/rdo-max"
# For android, by default, we build x86_64, arm64-v8a and armeabi-v7a,
# This is quite time consuming. You can opt to only build for one of
# these. The below would usually choose either x86_64 or arm64-v8a.
export ANDROID_NDK_TARGETS="$(uname -m|sed s/arm64/arm64-v8a/)"To run the setup script, enter the git repository directory and go:
cd nl-wallet
scripts/setup-devenv.shThe setup-devenv.sh script will configure the digid-connector to listen on
https://localhost:8006/ . Note the https in the URL, which is provided using
self-signed certificates.
Besides that, the development setup runs without using TLS. Therefore, the
feature allow_insecure_url enables the possibility to use a return URL with
the scheme http (while normally only https is allowed).
The local wallet can be connected to Sentry for crash and error reporting by
setting the SENTRY_DSN environment variable.
Additionally, the wallet crate offers the config_env feature to aid during
local development, which does the following:
- Any constant defined in the file
data.rscan be overridden by an environment variable of the same name at compile time. - Additional environment variables are read from a file named
.envin thewalletcrate directory, if present.
The individual services of the development environment can be started using
start-devenv.sh. Tip: use the --help commandline option to show the help
output.
For example, after having run setup-devenv.sh, you could run start-devenv.sh
with the --all, which starts everything, including a dockerized PostgreSQL
database and the NL Wallet flutter app itself (which means that you need to have
an Android emulator or iOS simulator running already, or you have otherwise
connected a mobile target for flutter to connect and deploy to).
Conveniently, you could run with the --default flag, which starts everything
except PostgreSQL and the NL Wallet app. We use this mode of running a lot when
developing locally: simply start your own PostgreSQL, dockerized or locally and
then scripts/start-devenv.sh --default to bring up all backend services. You
can then work on the code and simply flutter run in the wallet_app directory
once you're ready to see the mobile app running. So:
cd nl-wallet
scripts/start-devenv.sh --defaultTo run our integration tests you need to have a working database, hsm and applied migrations. All needed migrations can be ran via:
cd nl-wallet
scripts/migrate-db.shTo run both our unit and integration tests, we can run the following (note: we
use cargo nextest here, but you can use regular cargo test too):
cd nl-wallet
cargo nextest run --manifest-path wallet_core/Cargo.toml --features integration_testNote that the above runs both unit and integration tests. If you only want to
run the unit tests, simply don't specify --features integration_test.
Make sure your Android emulator is up and running, and then run the following:
cd nl-wallet/wallet_core/wallet/platform_support/android
./gradlew testDebugUnitTest connectedDebugAndroidTestYou should see gradle build running. This will create a special kind of APK that is uploaded to the connecte (emulated) Android device, which the tests interact with.
Make sure you have an Android Emulator or iOS Simulator running and start the app:
cd nl-wallet/wallet_app
flutter pub get
flutter run # 🎉After a few moments, you should see the NL Wallet app appear on your (emulated
or simulated) device. The app will interact with the backend services started
by start-devenv.sh.
Communication between the Flutter and Rust layers relies on the
flutter_rust_bridge package. The bridge code is generated. The definition of
this bridge is located at /wallet_core/flutter_api/src/api and generation is
done with the following command:
# Enter the nl-wallet git directory
cd nl-wallet
# Generate bridge code (note: take a look at git diff afterwards. If flutter
# analyze later in this guide fails, revert with git checkout -- wallet_app).
scripts/generate-flutter-rust-bridge.shA note for Linux users, specifically on non-Debian systems: You need to set
CPATH to workaround issues when using flutter_rust_bridge_codegen. Note that
these issues have been observed under both flutter_rust_bridge_codegen 1.x and
2.x. For more background on this, see here, and here. To set CPATH
for flutter_rust_bridge_codegen, you can run as follows:
CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include" \
scripts/generate-flutter-rust-bridge.shNote that the generated code is checked into our git repository, so generation only has to be performed when the API code changes.
You can format Rust code as follows:
cd nl-wallet
cargo clippy --manifest-path wallet_core/Cargo.toml --all-features --tests -- -Dwarnings
find wallet_core -mindepth 2 -type f -name Cargo.toml -print0 | xargs -0 -n1 cargo fmt --manifest-pathYou can format Dart code as follows (note that formatting output is different
when flutter pub get never ran before):
cd nl-wallet/wallet_app
flutter pub get
dart format . --line-length 120All Rust code goes in the wallet_core/ directory and their appropriate
sub-directories.
All Dart/Flutter code goes in the wallet_app/lib/ directory and their
appropriate sub-directories.
- Capitalize the subject line
- Use the imperative mood in the subject line
- Do not end the subject line with a period
- Wrap lines at 72 characters
- Prefix the branch name with the Jira code for the story or subtask the branch relates to. If there is no story or subtask, strongly consider making one or forego the prefix.
- The rest of the branch name should be a short description of the purpose of
the branch, in lowercase and separated by hyphens. The description should be
clear enough that any reader should understand it without having to look up
the Jira ticket. Consider starting the description with the component that is
being worked on, e.g.
ci-orcore-.
Example of a branch name: PVW-123-wp-teapot-status-code
See commit message.
- Default to squash merge (combined with PR title conventions)
Follow these steps to (force) distribute internal alpha & beta builds that
target the Android platform":
Use
Alphadistribution at any time during development cycle.
- Push commit of your choosing to: alpha/v{X.Y.Z}
- After the GitHub Action has completed successfully; install the release via F-Droid repo
Use
Betadistribution at the end of a sprint cycle; to represent the sprint demo version.
$ git fetch && git pull- Push
mainbranch to: beta/v{X.Y.Z} - After the GitHub Action has completed successfully; install the release via F-Droid repo
The app build includes the configuration for the connection to the configuration-server (config-server-config.json).
Additionally it includes an initial configuration from the configuration-server (wallet-server.json).
Next to these configuration files the build can be configured with:
| Name | Type | Components | Description |
|---|---|---|---|
| APPLE_ATTESTATION_ENVIRONMENT | Env | Apple Entitlement, Cargo option_env | Attestation environment for iOS (development / production). Only used in Cargo if fake_attestation is set. Default is development, which is ignored by Testflight and App Store. See wallet_app/README.md for more info. |
| UL_HOSTNAME | Env | Apple Entitlement | Universal Link hostname (iOS only). |
| universal_link_base | Option | Android | Universal Link hostname (Android only), passed via Dart define as UL_HOSTNAME. |
| UNIVERSAL_LINK_BASE | Env | Cargo option_env | Universal Link base URL used in Wallet Core. |
| ALLOW_INSECURE_URL | Dart only | Cargo feature | Whether to allow http urls in Wallet Core (passed via Dart define as ALLOW_INSECURE_URL via Xcode / build.gradle as wallet/allow_insecure_url). Defaults to false. |
| CONFIG_ENV | Env | Cargo build | The configuration environment name (should match the environment in config-server-config.json and wallet-config.json). Defaults to dev. |
| SENTRY_AUTH_TOKEN | Env | Flutter | Sentry Auth Token, empty if not enabled (read by Dart plugin via environment). |
| SENTRY_PROJECT | Env | Flutter | Sentry Project, empty if not enabled (read by Dart plugin via environment). |
| SENTRY_ORG | Env | Flutter | Sentry Organization slug, empty if not enabled (read by Dart plugin via environment). |
| SENTRY_URL | Env | Flutter | Sentry URL, empty if not enabled (read by Dart plugin via environment). |
| SENTRY_DSN | Env | Flutter | Sentry Data Source Name, empty if not enabled (passed via Dart define as SENTRY_DSN). |
| SENTRY_ENVIRONMENT | Env | Flutter | Sentry Environment (passed via Dart define as SENTRY_ENVIRONMENT). |
| SENTRY_RELEASE | Env | Flutter | Sentry Release (passed via Dart define as SENTRY_RELEASE). |
| build | Option | iOS / Android build | The build number of the build (should be strictly increasing when submitting to App or Play Store). Defaults to 0. |
| version | Option | iOS / Android build | The version of the build (should be semver). Defaults to the version in the pubspec.yaml. |
| app_name | Option | iOS / Android build | The app name (passed via environment as APP_NAME). Defaults to the NL Wallet. |
| application_id | Option | Android build | The Android application id (passed via environment as APPLICATION_ID). Defaults to nl.ictu.edi.wallet.latest. |
| bundle_id | Option | iOS | The iOS bundle id (changed via update_code_signing_settings lane). Defaults to nl.ictu.edi.wallet.latest. |
| build_mode | Option | Flutter | The build mode of Flutter (debug / profile / release). Defaults to release. |
| file_format | Option | Android build | File format (aab / apk) for Android build. Defaults to aab. |
| fake_attestation | Option | Cargo feature | Whether to use a fake Apple attestation (passed via Dart define as FAKE_ATTESTATION, via Xcode as wallet/fake_attestation). Defaults to true if built for Simulator otherwise false. |
| mock | Option | Flutter | Whether or not to use mock mode in Flutter (passed via Dart define as MOCK_REPOSITORIES). Defaults to false. |
| demo_index_url | Option | Flutter | The URL to launch the demo index page in Browser for tests (passed via Dart define as DEMO_INDEX_URL). |
Generate/update localisation files (to compile/run the project successfully):
$ flutter gen-l10n
Below you'll find a collection of links which we reference to through the entire text. Note that they don't display when rendered within a website, you need to read the text in a regular text editor or pager to see them.