Install EUDI Wallet age verification for Shopify
Application package: 0.1.8 · Guide updated: 30 October 2026
Add a verification button to your store and block checkout for selected 18+ products until the signed-in customer has presented a valid age proof. Follow these steps to deploy the application for your own store. You do not need access to our source repository or an Application API key.
Back to Wallet Connectors · Download the application source ZIP · Bridge integration guide · Contact
1 Before you start
You need:
- Administrator access to a Shopify store and permission to create/deploy an app in your Shopify developer organization.
- A compatible Shopify plan/distribution. For an initial test, create a Dev store with the Plus test plan. For a custom app containing Functions on a production store, Shopify Plus is required. Shopify permits public apps containing Functions on other supported plans when distributed through the Shopify App Store. This package does not constitute an App Store listing. Check Shopify Functions availability before choosing or paying for a plan.
- A development computer with Node.js 22.12 or later, npm and unzip.
- A server with Python 3.12, venv support, a process manager, persistent storage, and outbound HTTPS to Shopify and the OIDC bridge.
- Your own public HTTPS hostname, such as
age.example.com, with a valid TLS certificate and a reverse proxy. Replace that hostname in every example.
A development store remains password protected and uses test payments only. The customer must be signed in to a Shopify customer account. An administrator session does not replace that account. Guest checkout for restricted products is not supported by this application.
| Component | Runs where | Installation step |
|---|---|---|
| Merchant settings, OIDC callback and wallet redirect | Your Python server | Steps 4–5 |
| EUDI Wallet button | Shopify theme extension | Steps 3, 6 and 9 |
| Cart and Checkout Validation Function | Shopify | Steps 6 and 8 |
| Wallet verification | Our OIDC bridge | Step 7 |
Deploying the Function alone is not sufficient. shopify app deploy publishes
Shopify configuration and extensions; it does not host the Python application.
2 Download the application — development computer
Download the ZIP above. In the directory containing the downloaded file:
unzip eudi-wallet-age-shopify-0.1.8.zip
cd eudi-wallet-age-shopify
node --version
npm ci
The archive includes the backend, theme block, Function source and official GraphQL input schema, locked npm dependencies, deployment templates and this guide. It excludes private settings, installed dependencies, databases, compiled Wasm and our Shopify extension UIDs. Your app will receive its own UIDs.
3 Create and configure your Shopify application — development computer
- In the Shopify Dev Dashboard, select your own organization, then create an app for this integration. Record its Client ID and Client secret from the app settings. These are Shopify credentials, separate from the bridge credentials created in step 7. Keep the secret private.
- Link the downloaded project to this application:
bash
npm run shopify -- app config link --file-name shopify.app.toml
Sign in when prompted. Select No, connect it to an existing app, then the
application you just created in your organization. Do not select the Talao
demonstration app. Linking does not deploy the extensions.
3. Open the generated shopify.app.toml. Keep the real Client ID obtained
from Shopify and configure the values below. Replace YOUR_SHOPIFY_CLIENT_ID
and age.example.com. The source ZIP also includes this configuration in
shopify.app.example.toml.
client_id = "YOUR_SHOPIFY_CLIENT_ID"
name = "EUDI Wallet Age Verification"
application_url = "https://age.example.com"
embedded = false
[access_scopes]
scopes = "write_products,write_customers,write_validations,write_app_proxy"
[auth]
redirect_urls = ["https://age.example.com/oauth/callback"]
[app_proxy]
url = "https://age.example.com/proxy"
prefix = "apps"
subpath = "eudi-age"
[webhooks]
api_version = "2026-07"
[[webhooks.subscriptions]]
topics = ["app/uninstalled"]
uri = "/webhooks/uninstalled"
[[webhooks.subscriptions]]
compliance_topics = ["customers/data_request"]
uri = "/webhooks/data-request"
[[webhooks.subscriptions]]
compliance_topics = ["customers/redact"]
uri = "/webhooks/customer-redact"
[[webhooks.subscriptions]]
compliance_topics = ["shop/redact"]
uri = "/webhooks/shop-redact"
[build]
automatically_update_urls_on_dev = false
Do not add use_legacy_install_flow=true: it conflicts with the app-specific
webhooks above. Keep the proxy path /apps/eudi-age; the backend uses this path
to obtain the signed-in customer's identity from Shopify. Do not replace it
with a URL containing an unsigned customer ID.
Keep handle = "eudi-age-validation" and target
cart.validations.generate.run in the Function manifest. The activation code
finds this Function by its handle. Do not copy extension UIDs from another app.
4 Install the Python backend — server
The following is a first installation on Debian/Ubuntu with systemd.
It creates /opt/eudi-shopify-age; adapt paths and the process manager if your
hosting uses a different deployment layout. Existing installations must be
backed up and updated in place, preserving the private JSON and state database.
Transfer the original source ZIP to /tmp/eudi-wallet-age-shopify-0.1.8.zip
on your server. No secrets are included in this transfer. Install Python 3.12,
its venv module and unzip using your operating system's package manager, then run:
sudo bash <<'SH'
set -euo pipefail
umask 022
useradd --system --user-group --home-dir /opt/eudi-shopify-age --shell /usr/sbin/nologin eudi-shopify
install -d -m 0755 /opt/eudi-shopify-age
test ! -e /opt/eudi-shopify-age/app
unzip /tmp/eudi-wallet-age-shopify-0.1.8.zip -d /opt/eudi-shopify-age
mv /opt/eudi-shopify-age/eudi-wallet-age-shopify /opt/eudi-shopify-age/app
install -d -o eudi-shopify -g eudi-shopify -m 0700 /opt/eudi-shopify-age/private /opt/eudi-shopify-age/state
python3.12 -m venv /opt/eudi-shopify-age/.venv
/opt/eudi-shopify-age/.venv/bin/python -m pip install -r /opt/eudi-shopify-age/app/requirements.txt
SH
Run the configuration helper on the server, replacing the origin and shop with your actual values. Enter your Shopify Client ID and Client secret at its hidden prompts; do not put secrets in command arguments or commit them to Git.
sudo -u eudi-shopify /opt/eudi-shopify-age/.venv/bin/python \
/opt/eudi-shopify-age/app/scripts/configure.py \
--config /opt/eudi-shopify-age/private/shopify-secrets.json \
--origin https://age.example.com \
--shop YOUR-SHOP.myshopify.com
The helper creates a private 0600 JSON, generates the session/encryption keys and selects a private SQLite state path. It refuses to overwrite an existing configuration. Back up the JSON and database together securely: losing the encryption key prevents recovery of encrypted installation credentials.
Use the permanent *.myshopify.com hostname from Shopify, in lowercase, not a
custom storefront domain. Do not use our demonstration hostname or credentials.
5 Start the backend and expose HTTPS — server
A complete systemd template is included in deploy/eudi-shopify-age.service.example.
For the paths above:
sudo install -m 0644 /opt/eudi-shopify-age/app/deploy/eudi-shopify-age.service.example /etc/systemd/system/eudi-shopify-age.service
sudo systemctl daemon-reload
sudo systemctl enable --now eudi-shopify-age.service
sudo systemctl is-active eudi-shopify-age.service
The service must report active. It listens only on 127.0.0.1:5095.
The runtime user needs read/execute access to the code and venv and write access
only to its private state directory. A root-created venv must remain readable
and executable by that user; do not run the service as root or use chmod 0777.
Configure your HTTPS reverse proxy to forward all paths, preserving the
public Host header, to http://127.0.0.1:5095. The package includes
deploy/nginx-https.conf.example: replace its hostname and certificate paths
with yours, install it using your normal Nginx procedure, run nginx -t, then
reload Nginx. This template requires an existing valid certificate. Obtain
and configure it with your hosting provider or ACME client first.
Disable access logs for this application at every proxy/CDN and at Gunicorn:
callback URLs, signed customer parameters and POST bodies must not be recorded.
Do not cache responses. Permit HTTPS callbacks, /proxy, /webhooks/* and
static assets. If you use a WAF, make sure these requests reach the application.
Open https://age.example.com/: the welcome page should load over verified HTTPS.
A 502 means the reverse proxy cannot reach the running Python service. A 404
can indicate that the proxy forwarded a different Host than the configured
origin. An HTTP 200 does not yet validate the Shopify or wallet integration.
6 Compile and deploy the extensions — development computer
From the extracted application directory:
npm run shopify -- app function schema --path extensions/age-validation --config shopify.app.toml
npm run shopify -- app build --config shopify.app.toml
npm run shopify -- app deploy --config shopify.app.toml
The build must succeed, including theme checks, GraphQL types and the Wasm Function. Review the app name and organization before confirming deployment. Shopify releases a new version containing both the theme extension and the validation Function. This does not activate the validation on a store or add the button to a theme automatically.
This deploy command does not publish an App Store listing and does not deploy or restart your Python backend. If you change Python files later, deploy those files on your server and restart its service separately.
7 Install the app, copy the callback and register the bridge client
- Open
https://age.example.com/install?shop=YOUR-SHOP.myshopify.comin your browser. Sign in to Shopify and authorize the app for your store. Follow Shopify's Dev Dashboard installation/distribution procedure if required for your selected app distribution. The app opens EUDI Wallet configuration. - Copy your return URL shown in that screen. It must be exactly
https://age.example.com/oidc/callback, using your real origin. - Select Create a free client. Choose Other OIDC application, Age verification (18+) and HTTP Basic. Paste your exact return URL, enter the developer/company contact, acknowledge the linked policies and verify the developer email with the six-digit code.
- Select Download configuration (JSON) on the confirmation screen. Keep it private: this confidential client JSON contains a secret.
- Return to the Shopify app, choose that JSON file and click Save. Confirm Client configured is displayed. Importing a file without saving it does not configure the client.
| Setting | Which registration? |
|---|---|
| Shopify Client ID / secret | Shopify Dev Dashboard → your app settings; entered in the server helper |
/oauth/callback |
Shopify merchant authorization; registered in the app TOML |
| Bridge Client ID / secret | Bridge registration JSON; imported in the app's web settings |
/oidc/callback |
Wallet age-verification return; registered with the bridge |
Do not interchange these credentials or callbacks. No API key is required for the OIDC age check. Free registration quotas apply; multiple stores sharing one callback hostname share the Free daily budget. Obtain changes to quotas through your existing client rather than recreating its credentials.
8 Activate the checkout rule and select products
- In the application's configuration screen, select Activate 18+ validation.
This creates the app-owned metafield definitions and the checkout rule using
the deployed Function handle
eudi-age-validation. - In Shopify Settings → Checkout → Checkout rules, confirm the EUDI Wallet age verification rule is active. Its failure policy must block checkout; do not disable it to hide a verification error.
- Back in the app, tick each product that requires proof of majority and click its Save button. Leave unrestricted products unticked. The initial selection screen shows the first 50 products.
A deployed Function does not enforce anything until its rule is active. The rule allows unrestricted products without an age proof and blocks restricted checkout without a valid authorization on the signed-in customer account.
9 Add the button to the theme
- Open Online Store → Themes → Edit theme / Customize for the active theme.
- On the home page or another suitable page, select Add section → Apps → EUDI Wallet 18+.
- Set Application HTTPS origin to
https://age.example.comusing your real origin, without a trailing slash, then save the theme. - Preview the storefront. Signed-out customers see Sign in; signed-in customers see Verify I am 18 or over. The block follows the storefront's French/English locale. Publish/configure your storefront languages in Shopify separately; the administrator interface language does not select storefront language or currency.
The button starts a wallet journey; the Function enforces checkout on the server. A green success page, browser parameter or cart attribute alone cannot grant access.
10 Test before enabling customer access
Use fictional products and a Shopify test payment method on a development store.
- Add an unrestricted product: the age rule does not block checkout.
- Add a restricted product without a proof: checkout is blocked.
- Sign in with a customer account distinct from the administrator account.
- Select the EUDI Wallet button and present an age attestation. On success, return to the cart: checkout is allowed for that account until the end of the store's calendar day. Do not enter a real payment card for a demo.
- Test cancellation, an expired request and a different customer account: no new 18+ authorization must be granted.
- Test again after the store's day changes: the old authorization is expired.
- Test your actual theme, checkout blocks, mobile flow and accelerated payment options individually. Do not assume every sales channel uses this rule.
The age flow stores no wallet name, birth date or identity credential in Shopify. It associates the daily authorization with the Shopify customer account. The wallet holder is not separately identified as the owner of that account.
Changing your application domain
Keep the HTTPS origin stable once merchants have installed the app. If you must change it:
- Configure DNS and a valid HTTPS certificate for the new origin, then update
the backend's private
originsetting. - Update
application_url, the/oauth/callbackredirect and the app proxy URL in your Shopify app configuration. Deploy that configuration with Shopify CLI. - Create a new bridge client for the new
/oidc/callbackURL and import its private JSON in the app settings. The bridge does not allow changing an existing client's callback hostname. Keep the previous client until you have validated the replacement. - Set the new origin in the theme's EUDI Wallet block and save the theme.
- Restart your backend, authorize the app through a fresh
/installflow and repeat the restricted/unrestricted checkout tests above.
Changing a domain does not transfer an existing browser session. Shopify app authorization and the bridge client registration are separate steps.
Troubleshooting and maintenance
| Symptom | Check |
|---|---|
| Function build fails | Node version, npm ci, correct app configuration and current 2026-07 schema |
| Shopify rejects app-specific webhooks | Remove use_legacy_install_flow=true and deploy the TOML again |
| App installation error | Exact Shopify callback, app ID/secret, declared write scopes and a fresh /install flow in the same browser |
| JSON rejected | Exact /oidc/callback, age purpose, openid age_verification, correct issuer and token authentication method |
| No products listed | Products exist in the installed store; Shopify token has write_products |
| Rule cannot be activated | Correct app/store, Function deployed, compatible plan/distribution and write_validations permission |
| Button absent | Add the app block to the active theme and save it |
| Verification unavailable in the block | Set the app origin in the block settings |
| Customer asked to sign in | A Shopify customer session is required; an administrator login is insufficient |
| Age proof fails on return | Same browser, unexpired transaction, exact bridge callback and unchanged registered client |
| Wallet succeeds but Shopify authorization fails | Check protected customer-data access for customer identifiers and the age metafield, in addition to write_customers. A granted scope alone is insufficient. Do not request name, email, phone or address for this age flow |
The backend requests expiring offline access tokens and stores their refresh
tokens encrypted on the server. It renews access before API calls when needed;
no user or bridge token is used for this operation. After updating an older
installation that stored a permanent token, authorize once again through
/install to obtain an expiring grant. Expired/revoked refresh access also
requires merchant reauthorization; do not regenerate unrelated bridge
credentials or delete shop settings. Back up configuration and state before
updates. Keep existing extension UIDs after first linking your own app.
Local retention and manual rights requests
Version 0.1.3 includes scripts/privacy.py. Run it as the backend runtime user,
with the backend's own private configuration. It never prints webhook bodies,
customer identifiers, API tokens or client secrets to the terminal.
# Inspect pending request references, without their personal data.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json list
# Simulate local cleanup first. No Shopify API request is made.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json purge
# Apply after checking the simulation and the backup/retention procedure.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json purge --apply
# Export one request to a NEW file, readable only by its owner (0600).
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json export REFERENCE \
--output /PRIVATE/request.json
# AFTER fulfilling the person's rights, simulate then record closure.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json fulfilled REFERENCE
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json fulfilled REFERENCE --apply
An export contains personal data. Keep it private, provide it only to the
verified recipient through the agreed channel, and remove the working copy
when the request is completed. The closure command does not export data to
the person, delete their data, remove a Shopify metafield or revoke their
credentials. Perform and verify those applicable actions before recording
closure. Reference values come from list; never paste private request exports
into support logs or review videos.
purge deletes expired local browser transactions and anti-abuse counters.
It deletes a rights-request payload only 30 days after an operator records
fulfillment; pending requests are never removed by age alone. It does not
reset an active quota, alter installation records or delete Shopify grants.
To schedule this local cleanup on the documented AWS layout, inspect and copy
deploy/eudi-shopify-age-maintenance.service.example and the matching timer
into /etc/systemd/system/, removing .example from each destination name.
Adjust paths/user first if your installation differs. Run the dry-run command
as eudi-shopify, then systemctl daemon-reload and
systemctl enable --now eudi-shopify-age-maintenance.timer. Start the oneshot
service once and check its Result and ExecMainStatus. These files do nothing
until installed and enabled by your operator.
Expiry is not erasure. The Shopify age metafield becomes unusable at the end of the store day, but this command does not remove it. Define and implement its removal, backup rotation, handling of restored data and the rights-request workflow before promising a complete retention policy. Privacy webhooks queue requests for manual fulfillment; an HTTP success only confirms receipt.
Shopify authorization content retention
Version 0.1.4 adds a separate per-shop operation. It reads the existing customer IDs and only the app-owned proof metafield, so authorizations written by older releases are included. It does not request names, email addresses, phone numbers or addresses, nor create a local customer registry.
# Simulation: no age authorization is modified.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json shopify-expired \
--shop YOUR_STORE.myshopify.com
# Apply after checking the simulation.
python scripts/privacy.py --config /PRIVATE/shopify-secrets.json shopify-expired \
--shop YOUR_STORE.myshopify.com --apply
Both modes make authenticated Shopify API calls and may renew an expired API
access token using the existing refresh mechanism. Required customer-data access
must already be granted. The shop must be installed in this backend; its Shopify
ID is checked before processing. The operation uses Shopify's current shop
timezone and touches only the known version-1 proof format whose valid_on is
strictly earlier than today's shop date. Current, future, empty and customized
values are preserved.
Content erasure, not field deletion. Expired content is replaced with {}
using the read value's compareDigest. A renewal or deletion between reading
and writing prevents the old update; there is no unguarded retry. This avoids
clearing a fresh authorization. The empty metafield remains associated with the
customer but contains neither the former age boolean nor its date. It grants no
checkout access. Full removal of the metafield and removal of Shopify's own
history/backups are not implemented or promised by this task.
The command reports only counts, never customer IDs, proof values or API
response bodies. renewed counts values skipped after a concurrent change.
complete: false with exit code 2 means the page limit was reached. Increase
--max-pages if required (default 100 pages of 100 customers; maximum 10,000
pages). An API error stops the run with failure; previous successful updates
remain applied, and retrying is safe. Do not claim full cleanup from an
incomplete or failed run. Very large shops need a separately assessed schedule
and request budget.
To automate on the documented AWS checkout layout, inspect/copy the
deploy/eudi-shopify-age-retention@.service.example and matching timer to
/etc/systemd/system/, removing .example. Run the simulation, reload systemd,
then enable the timer for the exact permanent shop hostname, for example
eudi-shopify-age-retention@talao-eudi-wallet-demo.myshopify.com.timer.
Run the matching service once and verify its result and counts before relying
on the hourly schedule. Enable one instance per installed shop, and disable
it when removing a shop. Monitor API errors and incomplete scans. The existing
local maintenance timer is unchanged and makes no Shopify API calls.
Before production, establish the merchant data-protection agreement and notice, actual retention/backup procedures and required protected customer-data access. The operator's draft checklist is not an adopted merchant agreement.
Official references: Shopify app configuration, linking a project, deploying versions and Functions availability.
Human review alternative
When the operator explicitly enables human_review_enabled: true, customers
can request a genuine merchant review from the app's age-confirmation
page before opening their wallet, or after a failed return with a valid browser
context. Merchants handle the queue under Pending human reviews, provide a
reasoned response and explicitly approve only after checking age using appropriate
information. A request alone grants nothing. See human review setup and checks.
This feature requires backend deployment and real merchant follow-up; it is not
automatically enabled on AWS by downloading this guide.
In 0.1.8, each merchant must opt in under app settings → Allow requests for
review by the merchant, and acknowledge the commitment to real checks and
reasoned responses. The operator must also enable the module using the private
boolean human_review_enabled=true. Existing shops do not opt in automatically
on upgrade. Disabling new requests preserves received cases and follow-up. Do
not declare the optional alternative as offered when it is disabled.