6. Paivana Operator Manual#
6.1. Introduction#
Paivana is an approach to implement paywalls using GNU Taler.
For the seller, the Paivana design offers a scalable, cachable, static paywall with flexible payment options to sell access to digital assets. Aside from selling digital articles, Paivana can be used to protect servers against DDoS-attacks (say from botnets ignoring rate-limits), providing an alternative to Anubis.
For buyers, Paivana offers all of the conveniences, privacy and security features of the GNU Taler system. Paivana paywalls are also suitable for agentic payments, as agents can easily be provisioned with access to a GNU Taler wallet enabling them to purchase digital goods and services while limiting the financial risk to the amount of money placed into the agent’s wallet.
6.1.1. About this manual#
This manual targets system administrators who want to install, operate or integrate Paivana-based paywalls. To report issues or learn about known limitations, please check our bug tracker.
6.1.2. Overview#
At this point, there are several integration options for Paivana-style paywalls:
Using the Paivana reverse proxy: This is a generic solution suitable for any HTTP-based service. By placing the Paivana reverse proxy before the existing HTTP service, clients can be required to pay before accessing the service, without requiring any changes to the service itself. A disadvantage is that it can be difficult to make the Paivana reverse proxy fit into the look-and-feel of the site, and that the configuration of payment rules must be done separate from the actual content of the site.
Using the Drupal Paivana extension: This extension works basically for any Drupal site, making it easy to keep the overall look-and-feel of the site. Prices can be configured per article within the site itself. The extension only works for Drupal.
Using the Wordpress Paivana extension: This extension works basically for any Wordpress site, making it easy to keep the overall look-and-feel of the site. Prices can be configured per article within the site itself. The extension only works for Wordpress.
In all cases, Paivana additionally requires access to a GNU Taler merchant backend. It is assumed that such a backend is available and configured to process orders. When setting up the respective Paivana service, you must have the base URL, username and password of the selected merchant backend at hand.
6.2. Paivana-httpd#
This chapter documents the installation and operation of the Paivana
reverse proxy paivana-httpd. The reverse proxy sits between the
public Internet and an upstream Web service, intercepting requests
that have not yet been paid for and presenting the client with a
GNU Taler paywall. Once a payment has been confirmed by the
configured GNU Taler merchant backend, paivana-httpd forwards
subsequent requests of that client to the upstream service.
The full list of command-line options is documented in paivana-httpd(1); the configuration file is documented in paivana.conf(5).
6.2.1. Architecture overview#
paivana-httpd does not implement any payment logic of its own.
Instead, every Paivana deployment combines three components:
The upstream web service. This is the existing HTTP service whose content should be sold (a static website, a cgit service, a REST API, …). It does not need to be modified to work with Paivana.
A GNU Taler merchant backend (
taler-merchant-httpd). The merchant backend manages templates, creates orders, talks to one or more Taler exchanges, and ultimately reports back whether a given order has been paid. See the Taler Merchant Backend Operator Manual for full details.``paivana-httpd`` itself. This is the reverse proxy that gates the upstream service. It reads a single paivana.conf configuration file that points at both the merchant backend and the upstream service.
Typically a TLS-terminating reverse proxy (Nginx or Apache) is
deployed in front of paivana-httpd to handle HTTPS and to route
multiple virtual hosts; see Reverse proxy configuration below.
In normal operation the request flow is:
client ──▶ Nginx/Apache (TLS) ──▶ paivana-httpd ──▶ upstream
│
▼
taler-merchant-httpd
│
▼
Taler exchange
6.2.2. Installation#
6.2.2.1. Installing from source#
The package sources can be found in our download directory.
GNU Taler components follow the MAJOR.MINOR.MICRO version
scheme. The general rule for compatibility is that MAJOR and
MINOR must match across components; exceptions are noted in the
release notes. For example, paivana-httpd 1.6.x is expected to
work with taler-merchant-httpd 1.6.x. A MAJOR version of 0
indicates experimental development; in that case you should always
run the latest releases of every component together.
The following packages must be installed before compiling
paivana-httpd:
GNUnet (
libgnunetutil) matching the Taler releaseGNU Taler exchange libraries (
libtalerexchange,libtalerutil)GNU Taler merchant client library (
libtalermerchant)GNU Taler HTTP daemon helpers (
libtalermhd,libtalertemplating)libmicrohttpd, libcurl, libjansson, libgcrypt, zlib
Build and install with:
$ ./bootstrap
$ ./configure --prefix=$PREFIX
$ make
$ sudo make install
6.2.2.2. Installing the binary packages on Debian#
To install the GNU Taler Debian packages, first ensure that you have the right Debian distribution. At this time, the packages are built for Debian trixie.
You need to add a file to import the GNU Taler packages. Typically,
this is done by adding a file /etc/apt/sources.list.d/taler.list that
looks like this:
deb [signed-by=/etc/apt/keyrings/taler-systems.gpg] https://deb.taler.net/apt/debian trixie main
Next, you must import the Taler Systems SA public package signing key into your keyring and update the package lists:
# wget -O /etc/apt/keyrings/taler-systems.gpg \
https://taler.net/taler-systems.gpg
# apt update
Note
You may want to verify the correctness of the Taler Systems SA key out-of-band.
Now your system is ready to install the official GNU Taler binary packages using apt.
To install paivana-httpd you can now simply run:
# apt install paivana-httpd
The package does not perform any deployment-specific configuration
work; it only sets up the paivana-httpd system user, the systemd
service and socket units, and installs example configuration
snippets for Nginx and Apache under /etc/nginx/sites-available/
and /etc/apache2/sites-available/. You still must configure the
HTTP request routing and the Paivana templates as described below.
6.2.2.3. Installing the binary packages on Ubuntu#
To install the GNU Taler Ubuntu packages, first ensure that you have
the right Ubuntu distribution. At this time, the packages are built for
Ubuntu Lunar and Ubuntu Jammy. Make sure to have universe in your
/etc/apt/sources.list.d/ubuntu.sources (after main)
as we depend on some packages from Ubuntu universe.
A typical /etc/apt/sources.list.d/taler.list file for this setup
would look like this for Ubuntu Noble:
deb [signed-by=/etc/apt/keyrings/taler-systems.gpg] https://deb.taler.net/apt/ubuntu/ noble main
Next, you must import the Taler Systems SA public package signing key into your keyring and update the package lists:
# wget -O /etc/apt/keyrings/taler-systems.gpg \
https://taler.net/taler-systems.gpg
# apt update
Note
You may want to verify the correctness of the Taler Systems key out-of-band.
Now your system is ready to install the official GNU Taler binary packages using apt.
To install paivana-httpd, run:
# apt install paivana-httpd
As on Debian, the package does not perform any deployment-specific configuration work.
6.2.3. Configuring paivana-httpd#
The main configuration file is /etc/paivana/paivana.conf. Its
syntax follows the standard GNUnet configuration file format and is
documented in full in paivana.conf(5). Default values
shipped with the package live under
/usr/share/paivana/config.d/; values in paivana.conf
override those defaults.
All Paivana-specific keys live in the [paivana] section. At a
minimum, the file must specify three things:
where
paivana-httpdshould listen for incoming requests (SERVE,UNIXPATH/PORT);where it should forward paid requests to (
DESTINATION_BASE_URL);how it should reach the merchant backend (
MERCHANT_BACKEND_URLandMERCHANT_ACCESS_TOKEN).
A typical configuration that listens on a UNIX domain socket managed by systemd and forwards to a local upstream server looks like this:
[paivana]
# Listen on the socket provided by paivana-httpd.socket.
SERVE = unix
UNIXPATH = /run/paivana/httpd/paivana-http.sock
UNIXPATH_MODE = 660
# Public base URL of this paywall as seen by clients.
# Used when the Host/X-Forwarded-Host headers are unavailable.
BASE_URL = https://paywall.example.com/
# Upstream service that gets proxied after payment.
DESTINATION_BASE_URL = http://127.0.0.1:8080/
# Merchant backend used to create and verify orders.
MERCHANT_BACKEND_URL = http://localhost:9966/
MERCHANT_ACCESS_TOKEN = secret-token:CHANGE-ME
# Stable secret used to MAC the access cookie.
# If unset, a random value is generated at every startup,
# invalidating all previously issued cookies.
SECRET = please-change-this-to-a-long-random-value
# Resources that should never trigger the paywall, e.g.
# logos, stylesheets or favicons.
WHITELIST = ^/(favicon\.ico|assets/.*|robots\.txt)$
The exhaustive list of supported keys (SERVE, PORT,
BIND_TO, UNIXPATH, UNIXPATH_MODE, BASE_URL,
DESTINATION_BASE_URL, MERCHANT_BACKEND_URL,
MERCHANT_BACKEND_UNIX_PATH, MERCHANT_ACCESS_TOKEN,
SECRET, WHITELIST) is documented in
paivana.conf(5).
If you reach the merchant backend over a UNIX domain socket on the
same host (recommended for a single-machine deployment), replace
the MERCHANT_BACKEND_URL block with:
MERCHANT_BACKEND_URL = http://localhost/
MERCHANT_BACKEND_UNIX_PATH = /run/taler-merchant/merchant.sock
Note
MERCHANT_ACCESS_TOKEN and SECRET are sensitive values.
Make sure paivana.conf is only readable by the
paivana-httpd user. The Debian package installs the file
accordingly.
When paivana-httpd runs behind a trusted reverse proxy
(Nginx/Apache), pass -f / --respect-forwarded-headers in the
systemd unit’s ExecStart= so the real client address is taken
from X-Forwarded-For. See paivana-httpd(1) for the
remaining command-line flags (in particular -g to require only
a single payment per site and -n to disable the paywall for
debugging).
6.2.3.1. Starting and stopping the service#
The Debian/Ubuntu package ships a socket-activated systemd unit.
After editing /etc/paivana/paivana.conf enable and start it:
# systemctl enable --now paivana-httpd.socket
# systemctl status paivana-httpd
The socket listens on /run/paivana/httpd/paivana-http.sock with
group www-data, which lets a co-located Nginx or Apache talk to
the daemon without granting it broader filesystem access. Logs are
sent to the journal:
# journalctl -u paivana-httpd -f
6.2.4. Configuring Paivana templates#
paivana-httpd does not store any per-site pricing or URL-matching rules
itself. Instead, all rules are expressed as merchant templates of type paivana in the merchant backend. When
paivana-httpd starts up it asks the merchant backend for every template
configured for the instance identified by MERCHANT_BACKEND_URL and uses
the website_regex field of each template to decide which template (and
therefore which payment options) applies to an incoming request URL.
The corresponding REST API is documented in detail in the Merchant Backend HTTP API; see in particular the POST /private/templates endpoint and the TemplateContractPaivana definition.
6.2.4.1. Prerequisites#
Before creating a template you need:
a running
taler-merchant-httpd(see the Launching the backend section of the merchant manual);a merchant instance with at least one configured bank account;
the access token of that instance (used as
MERCHANT_ACCESS_TOKENinpaivana.conf).
In the examples below we assume the merchant backend is reachable
at http://localhost:9966/, the default instance is default,
its access token is secret-token:sandbox and the currency is
KUDOS. Adjust the URLs, tokens and amounts to match your
deployment. The
src/backend/test.sh
script that ships with Paivana sets up exactly this minimal
configuration and is a good starting point for experimentation.
6.2.4.2. Creating a single global template#
The simplest Paivana setup uses one template that matches every
URL on the site and charges a fixed price. This is the
configuration created by src/backend/test.sh:
$ curl -X POST http://localhost:9966/private/templates \
-H 'Authorization: Bearer secret-token:sandbox' \
-H 'Content-Type: application/json' \
-d '{
"template_id": "paivana",
"template_description": "A Paivana template",
"template_contract": {
"template_type": "paivana",
"summary": "Access to example.com",
"website_regex": ".*",
"choices": [ { "amount": "KUDOS:1" } ]
}
}'
The template_type must be "paivana": this allows
paivana-httpd to pick the template up at startup and
also enables some required logic in the merchant backend. The
website_regex is a POSIX extended regular expression that is
matched against the request URL; .* covers everything. Each
entry in choices describes one way the client may pay and is an
OrderChoice object (so the paywall can also support
the use of subscription tokens, discount coupons, etc.).
A successful create returns HTTP 204 No Content. After
creating the template, (re)start paivana-httpd so that it
re-reads the template list:
# systemctl restart paivana-httpd
6.2.4.3. Multiple templates with URL-specific pricing#
When a single site contains content with different prices, define one template
per price bucket and use website_regex to scope each template to the
matching URLs. When several templates match the same URL paivana-httpd
picks the first one if finds that matches. Be careful: if multiple templates
match a URL, the result is non-deterministic!
For example, a news site might charge 2 KUDOS for premium articles
and 50 cents (KUDOS:0.5) for standard articles:
$ curl -X POST http://localhost:9966/private/templates \
-H 'Authorization: Bearer secret-token:sandbox' \
-H 'Content-Type: application/json' \
-d '{
"template_id": "premium",
"template_description": "Premium long-form articles",
"template_contract": {
"template_type": "paivana",
"summary": "Premium article on example.com",
"website_regex": "^/premium/.*",
"choices": [ { "amount": "KUDOS:2" } ]
}
}'
$ curl -X POST http://localhost:9966/private/templates \
-H 'Authorization: Bearer secret-token:sandbox' \
-H 'Content-Type: application/json' \
-d '{
"template_id": "default",
"template_description": "Standard articles",
"template_contract": {
"template_type": "paivana",
"summary": "Standard article on example.com",
"website_regex": "^/standard/.*",
"choices": [ { "amount": "KUDOS:0.5" } ]
}
}'
6.2.4.4. Offering multiple payment options#
The choices array lets a single template offer several mutually exclusive
ways to pay. A common pattern is to accept either a cash payment or to sell a
subscription; the wallet shows both options and the customer picks one. The
third option, where the customer already has a subscription, will be used
automatically by the wallet for subscribers and the customer will not even
have to click to bypass the paywall as a subscriber. See the merchant manual
for the details of OrderChoice objects.
$ curl -X POST http://localhost:9966/private/templates \
-H 'Authorization: Bearer secret-token:sandbox' \
-H 'Content-Type: application/json' \
-d '{
"template_id": "article",
"template_description": "Single article, paid or via subscription",
"template_contract": {
"template_type": "paivana",
"summary": "Article on example.com",
"website_regex": ".*",
"choices": [
{ "amount": "KUDOS:1",
"description": "Pay per article" },
{ "amount": "KUDOS:100",
"description": "Buy subscription",
"outputs": [ { "token": "monthly-subscription" } ] },
{ "amount": "KUDOS:0",
"description": "Use my subscription",
"inputs": [ { "token": "monthly-subscription" } ],
"outputs": [ { "token": "monthly-subscription" } ] }
]
}
}'
6.2.4.5. Managing templates#
Templates can be listed, updated and deleted through the merchant
backend’s REST API or through the merchant backend SPA at
$MERCHANT_BACKEND_URL/. See the merchant manual section on
templates for details, and the API reference for
the relevant endpoints:
GET /private/templates — list all templates of the instance;
PATCH /private/templates/$TEMPLATE_ID — update a template;
DELETE /private/templates/$TEMPLATE_ID — remove a template.
After any change, restart paivana-httpd so the new template
list takes effect.
6.2.5. Reverse proxy configuration#
paivana-httpd itself speaks plain HTTP on a UNIX socket (or a
local TCP port). In production it is often run behind an Internet-facing
reverse proxy that terminates TLS and forwards requests to the
Paivana socket. This section gives minimal working examples for
both Nginx and Apache. The same approach is used for the merchant
backend; see the merchant manual’s
Reverse proxy configuration section for additional
discussion.
The examples assume the public domain is example.com,
that paivana-httpd is socket-activated by the shipped
paivana-httpd.socket unit (so its listening socket lives at
/run/paivana/httpd/paivana-http.sock) and that TLS termination
happens at the reverse proxy.
Place the snippet below in
/etc/nginx/sites-available/example.com (the
Debian package installs a starter template under
/etc/nginx/sites-available/paivana), then enable it via
ln -s ../sites-available/example.com
/etc/nginx/sites-enabled/ and reload Nginx
(systemctl reload nginx).
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://unix:/run/paivana/httpd/paivana-http.sock;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://$host$request_uri;
}
Make sure paivana-httpd is started with
--respect-forwarded-headers (see
paivana-httpd(1)) so the X-Forwarded-For
header set above is honoured.
Enable the required modules once:
# a2enmod proxy proxy_http headers ssl
# systemctl reload apache2
Then drop the following into
/etc/apache2/sites-available/example.com.conf
(the Debian package installs a starter template at
/etc/apache2/sites-available/paivana.conf), enable it
with a2ensite example.com and reload Apache.
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
<Location "/">
ProxyPass "unix:/run/paivana/httpd/paivana-http.sock|http://example.com/"
ProxyPassReverse "unix:/run/paivana/httpd/paivana-http.sock|http://example.com/"
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Host "example.com"
</Location>
</VirtualHost>
As with Nginx, run paivana-httpd with
--respect-forwarded-headers so that the client IP is
taken from X-Forwarded-For.
If you operate both Paivana and the merchant backend on the same
host, you typically expose them under two different hostnames (e.g.
example.com and backend.example.com); the merchant
backend must never be proxied through paivana-httpd, only
the upstream content service should be.
6.2.6. Verifying the setup#
After completing the steps above, a quick smoke test is to request
a paywalled URL with curl:
$ curl -i https://example.com/some-article
An unpaid request should return HTTP/1.1 402 Payment Required together
with a Taler-formatted paywall body containing the taler://pay/... URI of
the freshly created order. Paying that order with any GNU Taler wallet (see
the Wallet documentation) and
re-requesting the URL from the same client should then yield the upstream
content unchanged. If the page is run in a browser, the client-side
JavaScript should automatically trigger the required reload of the page after
the wallet made the payment.
For interactive debugging, paivana-httpd -n disables the
paywall and turns the daemon into a transparent reverse proxy;
this is useful to confirm that the network plumbing to the
upstream service works before involving the merchant backend.
See paivana-httpd(1) for the other runtime flags.
6.3. Drupal integration#
This chapter documents the installation and operation of the
taler_turnstile Drupal module. The module gates individual
nodes (articles, pages, …) of a Drupal 9 or 10 site behind a
GNU Taler paywall: visitors who have not yet paid are shown a
teaser plus a payment button, and once their wallet has paid the
associated merchant template the full content is unlocked.
The module is designed around the same Paivana-style flow as paivana-httpd: a static, cacheable paywall page references a payment template in the merchant backend, no order is created when a visitor merely loads a paywalled article, and no PHP session is started for unauthenticated traffic. This keeps bot traffic from polluting the merchant backend and lets Drupal’s page caches serve the paywall page to anonymous visitors without per-request work.
6.3.1. Architecture overview#
A Drupal site using Turnstile combines three independent components:
The Drupal site. An ordinary Drupal 9 or 10 installation serving content of one or more content types (
article,page, …). Thetaler_turnstilemodule adds an entity-reference field called Price category to the content types you select, and useshook_entity_view_alter()to intercept rendering of nodes that carry a non-empty value.A GNU Taler merchant backend (
taler-merchant-httpd). The backend stores one payment template per price category defined in Drupal, manages orders, talks to one or more Taler exchanges, and ultimately reports back whether a given order has been paid. See the Taler Merchant Backend Operator Manual for full details.The visitor’s GNU Taler wallet. The wallet scans the QR code shown on the paywall page (or follows the
taler://pay-template/…link), pays the merchant template and triggers the unlock.
Turnstile itself stores no per-visitor state in the Drupal
database; once the merchant backend confirms a payment, Turnstile
mints a short-lived HMAC cookie (taler_turnstile_paivana)
that the browser presents on subsequent requests to the same
fulfillment URL. Clearing cookies forgets paid access, but
GNU Taler wallet’s repurchase detection will restore the cookie
if the user attempts to pay for the same article again.
Note
This feature likely does not interact well with expiration periods for article purchases. This is a bug that will require further work to fully address. See #11443.
6.3.2. Installation#
6.3.2.1. Requirements#
Drupal 9 or 10
PHP 8.1 or later (the module uses native enums)
The Drupal core modules
node,field,userandpath_alias(the latter is used by the confirmation endpoint to map fulfillment URLs back to nodes)A reachable GNU Taler merchant backend supporting the v29 (or newer)
paivanatemplate type and the public/sessions/$SESSION_IDendpointAn access token for the merchant instance with the
templates-writeandorders-readpermissions
6.3.2.2. Obtaining the module#
The module sources are available from the GNU Taler Git repository as well as
Drupal’s own module repository. Download the latest release and extract it
into your Drupal installation’s modules/custom/ directory so that the
resulting layout is:
$ ls modules/custom/taler_turnstile/
config/ js/ taler_turnstile.info.yml
src/ templates/ taler_turnstile.module
...
There is no build step and no Composer package: the module is plain PHP and a couple of JavaScript files (the QR code library is vendored from davidshimjs/qrcodejs).
6.3.2.3. Enabling the module#
The module can be enabled via Drush or via the Drupal admin interface:
$ drush en taler_turnstile
$ drush cr
Navigate to /admin/modules, locate GNU Taler Turnstile
in the System package, tick its checkbox and press
Install.
After installation, clear the cache so that Drupal picks
up the new routes, services and theme template
(drush cr or Configuration → Performance →
Clear all caches).
Enabling the module triggers hook_install(), which attaches the
field_taler_turnstile_prcat entity-reference field to every content type
listed in enabled_content_types (default: article).
Uninstalling with drush pmu taler_turnstile removes the
price-category field from every configured content type and
deletes the module’s configuration. It does not delete
templates from the merchant backend; remove those manually if
desired (see Templates).
6.3.3. Configuring the basics#
The main settings page lives at
/admin/config/system/taler-turnstile and is also reachable
through Configuration → System → GNU Taler Turnstile basics.
It collects the four pieces of information Turnstile needs to
talk to the merchant backend and to know which content to gate:
The fields are:
Enabled content types — a list of checkboxes derived from the site’s configured node bundles. Each ticked content type gains the Price category field (and gets it removed again when unticked). The change takes effect immediately on save: the field is created or destroyed as a side-effect of saving this form, not via
drush cim.Payment backend URL — the HTTP(S) base URL of the merchant backend. Instance-specific URLs ending in
/instances/$ID/are accepted; the URL must end with a trailing slash. When you save the form with a non-empty value, Turnstile contacts the backend’s/configendpoint to verify reachability before accepting the value.Access token — the Bearer token used for every call to the backend. It must begin with
secret-token:(following RFC 8959, see Access control in the merchant manual). When both URL and token are present, the form performs a live authentication check against the backend and an error is shown if the access token is incorrect.Disable Turnstile when payment backend is unavailable (fail-open toggle, on by default) — when the merchant backend cannot be reached, this controls whether visitors see the unpaywalled article (toggle on) or an error message in place of the payment button (toggle off). The settings form refuses to save with the toggle off and an empty backend URL or token unless you confirm the warning, so a misconfiguration cannot silently break the live site.
The first time you save a working backend URL and access token,
Turnstile pushes a paivana-style template to the merchant
backend for every price category that already exists. Each
template carries the ID turnstile-{category_id}. You do not
need to create those templates by hand, and you do not need to
visit the merchant backend’s SPA to manage them — Turnstile keeps
them in sync as you edit the categories (see
Defining price categories below).
Before continuing, make sure that the merchant backend instance itself is ready to take payments:
The instance has at least one configured bank account.
Any legitimization required by the chosen payment service provider has been completed.
6.3.4. Configuring subscription prices#
Note
The term subscription is slightly misleading, as it is not auto-renewing. We are in the process of renaming this to passes which is clearer. However, the current module uses subscription and thus so does this manual.
Turnstile supports the GNU Taler subscription model, in which a
visitor buys a subscription token once and the token is then
consumed to unlock individual articles for some validity period.
The catalogue of available token families is configured on the
merchant backend (see the merchant manual section on
templates and the wider Taler concept of
subscription tokens); Turnstile reads the catalogue live from
private/tokenfamilies and asks you to set the
purchase price per token family per currency.
Navigate to
/admin/config/system/taler_turnstile/subscription-prices
(also reachable as Configuration → System → GNU Taler Turnstile
subscription prices). The form lists one collapsible Details
block per token family advertised by the backend, with one
numeric input per supported currency:
Leaving a field empty disables the Buy subscription in {currency} payment choice for that combination — the subscription will then not be offered for sale in that currency.
If the merchant backend has no token families configured, the form shows a warning and offers nothing to edit. In that case you can still operate Turnstile in pure pay-per-article mode; simply skip this configuration step.
Whenever this form is saved, Turnstile republishes every price-category template against the backend so the new subscription prices appear in the wallet’s Buy subscription choices immediately.
Note
GNU Taler token families offer more flexible payment options, not just subscriptions. However, only subscriptions are supported at this time.
6.3.5. Defining price categories#
A price category is a named bucket of payment options that can
be attached to nodes. Categories are managed at
/admin/structure/taler-turnstile-price-categories (also
reachable as Structure → GNU Taler Turnstile price
categories). The collection page lists the existing categories
together with their machine names and descriptions and offers
Edit, Delete and Add price category actions:
6.3.5.1. Creating or editing a price category#
Click Add price category (or Edit on an existing row) to reach the price-category form. The form has three sections:
Name — human-readable label for the category (e.g. Standard article, Long-form feature). Shown to editors when they pick a category on a node form.
Machine name — the slug used to build the merchant template ID (
turnstile-{machine_name}). Derived automatically from the initial Name; fixed once the category exists.Description — free text shown to editors in the node form to help them pick the right category.
Prices — a fieldset with one collapsible Details block per known token family plus a special block labelled
No reductionthat holds the price for non-subscribers. Each block contains one numeric input per supported currency.
Pricing semantics:
A value in the
No reductionblock sets the per-article price for visitors without a subscription in that currency. Leave it empty to make the article available only to subscribers in that currency.A value in a subscription block sets the discounted price for holders of that subscription token in that currency. A value of exactly
0marks the article as fully covered by that subscription — subscribers see it for free. An empty field means “this subscription does not unlock articles of this category in this currency”, forcing subscribers (with this type of subscription) to pay the full per-article price.
At least one price (across all blocks and currencies) must be set; otherwise the form refuses to save as there would be no way to purchase articles in that price category otherwise.
Saving the form publishes the category as a
paivana-style template to the merchant backend (the v1
choices array is built from the price grid; see
TemplateContractPaivana in the
Merchant Backend HTTP API). If the local
save succeeds but the backend push fails (network error, 401,
…), the form surfaces an error message.
Note
In this case, the local price in Drupal and the merchant template may have drifted out of sync. Visitors would still pay the price in the merchant backend, so this error should always be investigated.
Editing a category, renaming it, or changing prices automatically refreshes the matching merchant template. Deleting a category removes its template; if the delete on the backend fails, you are warned to clean it up manually.
6.3.6. Gating individual nodes#
Once a content type is enabled in the basic settings (see above) and at least one price category exists, every node of that content type gains a new Price category field in its editing form. By default the field is placed in the meta group on the right-hand side of the node form.
To paywall a node:
Edit or create a node of an enabled content type.
Pick a price category in the Price category field.
Save the node.
Leaving the field empty leaves the node freely accessible — the paywall logic only fires for nodes that reference a category.
If you want to remove the paywall from a node, edit it and clear the field. If you want to lift the paywall from an entire content type, untick it in the Enabled content types list on the settings form; the field and its values are then removed. (The field’s contents are dropped, so re-enabling the content type later starts from a clean slate, forcing you to set the price categories again for all nodes in that category.)
6.3.7. The visitor experience#
6.3.7.2. Paying#
The visitor scans the QR code with a GNU Taler wallet (or clicks the Open GNU Taler payment Web page button when running a browser with a wallet extension). The wallet shows the available payment choices — for a typical category these are:
Pay in {currency} — the per-article price for non-subscribers.
Pay in {currency} with subscription — only offered to visitors whose wallet holds a matching, unspent subscription token (in which case it usually costs
{currency}:0).Buy subscription in {currency} — pays the per-article price plus the subscription’s purchase price (from Configuring subscription prices above) and yields a fresh subscription token in the visitor’s wallet.
The visitor picks an option, the wallet pays, and Drupal’s JavaScript proceeds to the confirmation step.
6.3.7.3. Confirmation and unlock#
Once the merchant backend reports the order as paid, the
JavaScript POSTs {order_id, nonce, cur_time, website} to
/taler-turnstile/paivana. The Drupal-side controller
re-derives the session ID, fetches the order from the merchant
backend and verifies that the paid contract’s
``fulfillment_url`` matches the requesting page and that the
paid amount is one of the amounts the price category accepts.
Only then does it mint the taler_turnstile_paivana cookie
and 303-redirect the browser back to the article.
The cookie is keyed on Drupal’s per-site private_key, bound
to the visitor’s client IP and to the specific fulfillment URL,
marked HttpOnly, SameSite=Lax, and Secure when the
page was served over HTTPS. It is valid for 24 hours (the
ORDER_VALIDITY_SECONDS constant in the API service; this
will follow the merchant-supplied
pay_deadline/max_pickup_time once backend v1.6 lands).
The cookie covers exactly one fulfillment URL — clearing
cookies forgets paid access, and access to a different
paywalled article requires a fresh payment (or a valid
subscription).
Note
ORDER_VALIDITY_SECONDS should probably be made configurable in the future, as should the restriction of the client to a particular IP address, as that may or may not be desired. See #11444.
6.3.7.4. Subscriptions in action#
When a paid contract bought a subscription token, Turnstile also stores the token family slug and the token’s expiration time in the visitor’s PHP session. Subsequent requests to other articles whose price category is fully covered by the same subscription short-circuit the paywall entirely: the visitor never sees the Payment required block again until the token expires.
Visitors without a session cookie still see the paywall page (so anonymous, cache-friendly traffic stays cacheable).
6.3.8. Operational notes#
6.3.8.1. Logging#
The module logs to the dedicated taler-turnstile channel.
To watch the live event stream while debugging:
$ drush watchdog:tail --type=taler-turnstile
Levels are used meaningfully: debug for flow tracing,
info for expected skips (for example, a subscription slug
the backend no longer advertises), warning for user-fixable
configuration issues, and error for protocol violations or
unexpected HTTP statuses from the backend.
6.3.8.2. Cache management#
The module declares cookies:taler_turnstile_paivana as a
cache context on every paywalled response, so Drupal’s internal
page cache and dynamic_page_cache separate visitors with a
cookie from those without. A reverse-proxy cache in front of
Drupal must therefore honour the Vary: Cookie header to
avoid handing one paying visitor’s full-content response to
another visitor.
6.3.8.3. Configuration import/export#
Both taler_turnstile.settings and each
taler_turnstile_price_category.* config entity participate in
the normal Drupal drush cex / drush cim workflow.
Importing a configuration change that adds or removes content
types from enabled_content_types triggers field
add/removal; importing a price-category change republishes the
matching merchant template.
6.3.8.4. Permissions#
Two permissions are exposed:
Administer GNU Taler Turnstile — grants access to the basics and subscription prices settings forms.
Administer price categories — grants access to the price-category CRUD pages.
Both default to restrict access and should normally only be granted to trusted administrators.
6.4. Wordpress integration#
This chapter documents the installation and operation of the Wordpress Paivana module.
FIXME!