Installation guide
This guide covers the full path from purchasing an on-premise license on the Veilio SaaS to running a self-hosted instance with Docker and a license.jwt file.
Two separate planes
- Control plane = the Veilio SaaS (app.veilio.fr). You create an account; a Veilio admin attributes the on-premise contract, then you download one JWT per instance and the deployment kit from Dashboard → Integration. Veilio never sees your tokenized data.
- Your on-premise instance = what you deploy on your own servers. It contains only the public key and validates the license offline. No business data leaves your infrastructure.
Attribution: a Veilio admin creates the contract and instances on the control plane (no self-serve checkout). Entry pack = license + 1st site. Extra sites as add-ons. 1 buying org → N instances, each with its own vault and
license.jwt.
1. Prerequisites
Before you touch the server
- An account on app.veilio.fr. A cloud credit subscription is not required.
- A signed quote. After acceptance, a Veilio admin attributes the contract and issues one
license.jwtper instance. - The deployment kit, from Dashboard → Integration. The zip does not contain the license or the Docker image.
On the server
- Linux host or VM, 2 CPUs, 2 GB RAM
- Docker ≥ 24 and Docker Compose ≥ 2.20
openssl, to generate the secrets below- No PostgreSQL to install. The kit starts two Postgres 16 databases (
veilioandclients) - No Docker Hub login. The image is public. The kit pins
veilio/app:3.0.3.latesttracksmaster. - The app listens on
127.0.0.1only, portAPP_PORT(default3000). Users reach it through your reverse proxy. - Outbound HTTPS is optional. Tokenize, detokenize and the vault work with no outbound network. You need outbound HTTPS only for automatic license renewal, the weekly usage report, telemetry (last-seen), and the in-app update banner.
2. License attribution (Veilio SaaS)
Everything in this section happens on the control plane (the Veilio web app), not on your on-premise server.
2.1 Create your account
- Open app.veilio.fr and click Sign up.
- Register with your work email and set a password.
- Confirm your email if prompted, then sign in.
You do not need a cloud (SaaS credits) subscription for an on-premise contract. On-premise is a separate product: a Veilio admin attributes the contract to your organization.
2.2 Get an on-premise contract
- Contact Veilio for a quote: entry pack + additional instances if needed.
- After acceptance, a Veilio admin creates and activates your contract on the control plane (Standard / Pro instance seats) and issues one JWT per seat. No self-serve payment: attribution is manual.
- Sign in at app.veilio.fr → Dashboard → Integration.
Catalog (excl. tax / year)
| Offer | Price | Quotas per instance / year |
|---|---|---|
| Standard entry (license + 1st instance) | €5,990 | 50,000 tokens · 150,000 requests |
| Pro entry (license + 1st instance) | €8,990 | 200,000 tokens · 500,000 requests |
| Additional Standard instance | €2,990 | same as Standard |
| Additional Pro instance | €4,990 | same as Pro |
- Features: the same API on Standard and Pro (tokenize, detokenize, bulk, formats, datasets, webhooks, compliance export, sovereign backup). Licenses issued from this version list
formats,webhooksandsovereign_backupin the JWT. An older JWT still runs those routes. - Unlimited: users, API keys, datasets (they consume tokens + requests)
- Quotas are annual over the license period (
iat→exp), not monthly - Mix Standard + Pro on the same contract: yes
- Bulk / format / dataset = N request units (same as SaaS)
- Do not share one JWT across deployments
2.3 What you receive after activation
| Source | Content |
|---|---|
license-<site>.jwt (one license per instance) | |
| Email (optional) | vlr_... refresh token for automatic renewal, usage report and telemetry (shown once — save it) |
| Integration page | Per-instance JWT download + shared deployment kit |
Renewals are handled via the annual contract invoice; JWTs are re-issued for the new period.
2.4 Download the deployment kit and licenses
On Dashboard → Integration, in Your on-premise instances:
- Deployment kit →
veilio-onpremise-kit.zip(docker-compose.yml,.env.onpremise.example,README.mdwith install steps and required secrets) - Download JWT per instance → one
license-….jwtfile per site
license.jwtis not inside the zip. Place one file per site in that site’sveilio/folder next todocker-compose.yml.The Docker image is not in the zip either. It is pulled from Docker Hub on the first
docker compose up.Downloads appear after the contract is activated. If a button fails just after activation, wait a few seconds and try again. The license is generated asynchronously.
2.5 Prepare files on your server
unzip veilio-onpremise-kit.zip && cd veilio-onpremise
mkdir -p veilio
cp ~/Downloads/license-mysite.jwt veilio/license.jwtRepeat with a separate kit copy + JWT for each additional instance.
3. Configure the environment
cp .env.onpremise.example .envEdit .env and set at least:
| Variable | Description |
|---|---|
DB_PASSWORD | Strong PostgreSQL password |
ENCRYPTION_KEY | openssl rand -base64 32 — back it up: without it, tokens cannot be recovered |
NEXTAUTH_SECRET | openssl rand -base64 32 |
APP_URL | Exact URL users type in the browser. https://veilio.acme.internal, or http://10.0.0.5:3000 on a preprod without TLS. NEXTAUTH_URL must be the same. |
APP_PORT | Host port number only (default 3000). Compose publishes 127.0.0.1:${APP_PORT}:3000. Do not set 127.0.0.1:3000 (breaks upgrades from older kits). |
VEILIO_IMAGE | Docker image (kit default: veilio/app:3.0.3). latest tracks master. A restart does not change the version. |
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM | Outbound email. SMTP_SECURE=false on port 587. |
BOOTSTRAP_EMAIL / BOOTSTRAP_PASSWORD / BOOTSTRAP_COMPANY | First admin, created only when the database has no user yet. |
VEILIO_LICENSE_FILE | Default: /veilio/license.jwt (file mode) |
VEILIO_LICENSE_KEY | Full JWT inline (Coolify / secret managers). Takes priority over the file. |
VEILIO_CRON_SECRET | Bearer for auto-shred. Required if you use retention. openssl rand -base64 32 |
METRICS_SECRET | Unlocks version and license details on /api/health. openssl rand -base64 32 |
APP_URL starting with http:// turns the session cookie Secure flag off. If login loops back to /login on a preprod without TLS, set VEILIO_SESSION_COOKIE_SECURE=false and recreate the app container.
Signup on the instance is invite_only by default. There is no public registration and no Google or GitHub button. Extra users are invited by an admin.
Automatic renewal, telemetry and weekly usage report (optional, outbound HTTPS):
Renewal, heartbeat (last-seen) and usage reporting share the same control-plane URL and vlr_... token. Without them the instance stays air-gapped (no upload).
No new required environment variable for telemetry. If VEILIO_LICENSE_CONTROL_PLANE_URL and VEILIO_LICENSE_REFRESH_TOKEN are already set, last-seen heartbeats start automatically. Set VEILIO_TELEMETRY=false only when you want to refuse them.
| Variable | Description |
|---|---|
VEILIO_LICENSE_CONTROL_PLANE_URL | https://app.veilio.fr — required for refresh, telemetry and usage report |
VEILIO_LICENSE_REFRESH_TOKEN | vlr_... from delivery email — required for refresh, telemetry and usage report |
VEILIO_LICENSE_AUTO_REFRESH | true to enable automatic license renewal |
VEILIO_LICENSE_REFRESH_THRESHOLD_DAYS | Default 45 |
VEILIO_LICENSE_REFRESH_TOKEN_FILE | Optional path to persist a rotated refresh token (read-write mount) |
VEILIO_TELEMETRY | Optional. Last-seen heartbeat (~90 s after start, then every 3 days). On automatically when URL + token are set. Set false only to disable |
VEILIO_USAGE_REPORT | Optional. Weekly consumption report (tokens + requests). On when URL + token are set. Set false to disable while keeping refresh |
Minimal example to upload presence + consumption (without auto-renewal):
VEILIO_LICENSE_CONTROL_PLANE_URL=https://app.veilio.fr
VEILIO_LICENSE_REFRESH_TOKEN=vlr_...
# VEILIO_TELEMETRY=false # only if you refuse last-seen
# VEILIO_USAGE_REPORT=false # only if you refuse weekly countersHeartbeat sends only app version and a timestamp (no PII). Usage reports about one minute after start, then once a week. On the Veilio side: Admin → On-Premise (last-seen + last consumption). No business data is sent.
4. Configure the license
One mode is enough.
Option A — File (default)
VEILIO_LICENSE_FILE=/veilio/license.jwt
mkdir -p veilio && cp ~/Downloads/license-mysite.jwt veilio/license.jwtThe kit mounts ./veilio read-only. To write a renewed license to disk, remove :ro in docker-compose.yml:
- ./veilio:/veilioOption B — Environment variable (Coolify, PaaS)
VEILIO_LICENSE_KEY=eyJ...full_jwt_content...
# do not set VEILIO_LICENSE_FILEVEILIO_LICENSE_KEY wins. The file is not required, and ./veilio can stay empty. In this mode a renewed license stays in memory until the next restart. It is not written to disk.
5. Start
docker compose up -dCompose pulls the pinned image veilio/app:3.0.3. Access is enforced by license.jwt, not by the registry.
On startup the instance:
- Applies the versioned database migrations shipped in the image. It refuses to start if a database does not match the version's schema afterwards.
- Checks the license signature. It refuses to start if the license is missing, invalid, or expired past the grace period.
- Creates the first admin and an organization when the database is empty and
BOOTSTRAP_*is set. The log line isOn-premise bootstrap: admin user created. - If
VEILIO_LICENSE_AUTO_REFRESH=true, refreshes the license immediately, then once a day.
Sign in at APP_URL. The first login forces a password change. If you already started without BOOTSTRAP_*, add the variables and run docker compose up -d --force-recreate veilio-app. Bootstrap does not run again once a user exists. An instance that already had an admin and no organization receives one on the next start.
6. Check status
The kit publishes the app on 127.0.0.1 only. Put your reverse proxy in front of that port. It is not exposed on the host network.
curl -s http://127.0.0.1:3000/api/health?db=1 | jqWithout X-Metrics-Secret, the body is status, deployment: "onpremise" and license.health: "protected". Version and license details (customer, expiry) need that header, set to METRICS_SECRET.
A missing ENCRYPTION_KEY, or one that cannot decrypt the API keys already stored, makes the process exit. There is no 503 health response then: the container restarts in a loop and docker compose logs app shows On-premise startup refused: …. A missing or invalid license refuses to start the same way.
Dashboard quotas count request units (bulk = N). Admin global stats count HTTP calls. The numbers differ on purpose.
SDK / API base URL for your instance: https://your-domain.com/api
The calls are the same as SaaS. Full reference: HTTP Integration.
API modes
Two tokenize modes. Omit mode, or send standard, and the value becomes an opaque tok_…. Send mode: "fpt" when the destination column checks the shape (email, phone, IBAN) and would reject tok_…. A mask needs entityId on the field or on the request. Detokenize the mask with the same entityId.
curl -s -X POST "https://your-domain.com/api/tokenize" \
-H "Authorization: Bearer $VEILIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": "ada@example.com",
"type": "email",
"mode": "fpt",
"entityId": "BP10042"
}'type is at most 64 characters. retention.retentionUntil must be in the future. A bad body is 400, not 500.
type | Example in | Mask out |
|---|---|---|
email | ada@example.com | 00000000@example.com |
phone | 0612345678 | 0600000000 |
phone | +447911123456 | +440000000000 |
iban | a French IBAN | one valid zero IBAN for FR |
bic | AAAAFRPP | XXXXFRX0 |
birthdate | 1990-05-12 | 1900-01-01 |
document_date | 2020-01-15 | 1900-01-02 |
siren | 123456789 | 000000000 |
siret | 12345678901234 | 00000000000000 |
vat | FRXX123456789 | FR12000000000 |
nir | a valid NIR | 100010100000036 |
postal | 75001 | 00000 |
Names, streets and free text stay tok_…. Amounts and invoice dates stay in clear: do not send them. On a direct call, standard issues a new tok_… every time, even for the same value. The gateway keeps one token per value. fpt does not: the mask is stable, the clear value is found with entityId.
A mask reads back the value it was issued for. Two values of one entityId with different masks are both kept: 0612345678 (0600000000) and +447911123456 (+440000000000) each read back their own number. Two values that give the same mask (two French 06 mobiles, or two emails, since every email gives 00000000@example.com) cannot both live behind it. The second replaces the first, and the response carries "replaced": true. Keep those values in standard mode if both must stay readable.
Some masks share a shape: 000000000 is a SIREN and also a nine-digit zero postal code. To detokenize or shred such a mask, send type with entityId, otherwise the route answers 400 type is required.
Shred a mask with POST /tokens/shred and the same entityId (plus type when the shape is shared). DELETE /tokens/[id] also accepts an API key (by internal id); for an FPT mask, prefer /tokens/shred.
curl -s -X POST "https://your-domain.com/api/tokens/shred" \
-H "Authorization: Bearer $VEILIO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "token": "00000000@example.com", "entityId": "BP10042", "type": "email" }'POST /tokenize/bulk accepts mode per field. A field without mode stays tok_….
POST /tokenize/format rewrites JSON, CSV or SQL in place. It returns tok_…, not a mask. POST /detokenize/format only reveals tok_…: an FPT mask in the file stays as-is (no error, no plaintext). To reveal a mask, use /detokenize or /detokenize/bulk with entityId (and type when the shape is shared, e.g. SIREN vs postal code). Send entityId or entityIdColumn when you need the vault link. CSV accepts comma, semicolon, tab and pipe. The delimiter is detected when options.csv.delimiter is omitted. A file without a header row needs options.csv.hasHeaders: false; its paths are then row[1][0], row[1][1]. A cell that holds the delimiter keeps its quotes on the way back. A trailing newline and CRLF do not add an empty row. options.fields accepts a column name (email), a wildcard (lignes[*].email), a dotted path (lignes.email) or *.email for any top-level key. SQL support is INSERT … VALUES only, every tuple, including a doubled quote (O''Brien). UPDATE is not parsed and returns 400 No fields found to tokenize. Paths look like INSERT.clients[0].email. If an announced field is still in clear in the result, the route returns 400 and does not hand back the file.
POST /flows/ingest with no schema on the key tokenizes every field in the payload, including company. policyApplied is then null. With a schema, only the schema fields are tokenized.
Tokenize and detokenize are capped at 1,000 requests per minute per key. The commercial limit is the yearly quota in the license. The dashboard shows that quota, and requests since the license start, not an unlimited admin plan.
7. Retention and auto-shred (required if you use TTL)
If your apps pass retention.ttlDays or retention.retentionUntil when tokenizing, you must schedule the auto-shred job yourself. Setting retention alone does not destroy data.
- Set
VEILIO_CRON_SECRETin.envand restart the app if needed. - Schedule an hourly call (crontab, systemd timer, or your orchestrator):
curl -fsS -X POST "https://your-domain.com/api/internal/tokens/auto-shred?limit=500" \
-H "Authorization: Bearer $VEILIO_CRON_SECRET"- Confirm a
200JSON response with aprocessedfield.
Full details: Data Lifecycle: Retention and Crypto Shredding.
8. Update the application
A restart does not pull a new image (pull_policy: missing, tag pinned in VEILIO_IMAGE). To move to another version, change VEILIO_IMAGE, then:
docker compose pull && docker compose up -dPostgreSQL volumes and license.jwt are preserved. Back up both databases before changing version (see Backups).
APP_PORT upgrade trap. Older kits sometimes used APP_PORT=127.0.0.1:3000. Current compose expects a bare port (APP_PORT=3000); the 127.0.0.1 bind is already in docker-compose.yml. Keeping the host prefix breaks docker compose up. Fix the .env, then pull/up again.
Stale session after DB wipe / restore. An old browser cookie no longer matches any session row. The app clears it and redirects to /login with a relative Location (so reverse proxies keep the public host) instead of looping on « Chargement… ». If the cookie remains, clear site cookies manually and open /login.
The schema is updated by versioned migrations, never by prisma db push. The log shows Applying migration … for each new one. On first start after a schema-era upgrade, an instance installed with an earlier layout is recorded at the baseline (Existing installation, recording the baseline migration as applied), with no change to its data. Always upgrade through the supported path; do not skip required baselines. If a database matches neither the expected schema nor the baseline, the instance refuses to start and changes nothing: restore the backup and contact support.
Weekly usage report. With VEILIO_LICENSE_CONTROL_PLANE_URL and VEILIO_LICENSE_REFRESH_TOKEN set, the instance sends token and request consumption for the license period to Veilio once a week (and about one minute after start). Veilio Admin → On-Premise then shows the last report. Set VEILIO_USAGE_REPORT=false to disable. Air-gapped instances cannot report.
Telemetry last-seen. Same URL + token: heartbeat every 3 days (~90 s after start). No new required env var — it turns on when URL + vlr_... are already set. Admin → On-Premise shows last-seen. Set VEILIO_TELEMETRY=false only to disable.
Administrators see an in-app banner when a newer version is on the control plane. That banner needs outbound HTTPS to https://app.veilio.fr (or VEILIO_ONPREMISE_CONTROL_PLANE_URL if you override it).
9. Create an admin manually (alternative)
Use this when you do not set BOOTSTRAP_*.
docker compose exec veilio-app pnpm onpremise:seed-admin -- \
--email admin@acme.internal --password 'ChangeMe123!' --company "Acme"10. Coolify deployment
- New Resource → Docker Compose (image on Docker Hub, do not build from Git).
- Deploy the kit's
docker-compose.yml. - Set environment variables (section 3), including
VEILIO_CRON_SECRETif you use retention. - Set
VEILIO_LICENSE_KEYas a secret (option B, section 4). - Assign domain to
veilio-app, port 3000. APP_URL= the Coolify HTTPS URL. SetNEXTAUTH_URLto the same value.- Configure an external cron / Coolify scheduled job for auto-shred (section 7).
Verify: curl -s https://your-domain/api/health?db=1
License lifecycle
| State | Behavior |
|---|---|
| Valid | Everything works |
| 30 days before expiry | Warning banner + health: expiring_soon |
| Expired (0–30 days) | Grace period: read-only, no new tokens |
| Expired > 30 days | Instance refuses to start |
Annual quotas: each instance hard-caps token creations and request units for the license period. Email alerts fire at 80% / 95% / 100%. Beyond the cap, new tokenizations are blocked until a quote amendment (upgrade Pro, extra instance, or custom quotas). Detokenize / lookup / shred / export stay available (same idea as grace-period read-only), so a capped instance is not a full outage for apps that only reveal data.
The contract is annual and quote-based. After each invoiced renewal, Veilio re-issues a JWT per instance.
Automatic renewal. If you received a vlr_... token, the instance can fetch the new license itself. Add to .env:
VEILIO_LICENSE_AUTO_REFRESH=true
VEILIO_LICENSE_CONTROL_PLANE_URL=https://app.veilio.fr
VEILIO_LICENSE_REFRESH_TOKEN=vlr_...On startup and once a day the instance calls POST /api/license/refresh. The new license is applied live, without a restart, and written to disk when ./veilio is mounted read-write. Only the signed JWT is exchanged. No business data leaves the instance.
Manual swap (air-gap). Leave auto-refresh off. Replace the file and restart:
cp ~/Downloads/license-mysite.jwt veilio/license.jwt
docker compose restart veilio-appRe-download anytime from Dashboard → Integration → Download JWT.
Backups
You must back up, off the server:
- Both PostgreSQL volumes (
veilio-dataandclients-data) ENCRYPTION_KEY. Without it, tokens cannot be recovered.license.jwtfor each instance, plusVEILIO_LICENSE_REFRESH_TOKENif you use auto-renewalNEXTAUTH_SECRET,VEILIO_CRON_SECRETandMETRICS_SECRET. Rotating the cron secret without updating the scheduled job stops auto-shred.
TLS and reverse proxy
Terminate HTTPS in front of the container:
# nginx example
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}Set APP_URL to your public HTTPS URL.
Support
Questions: support@veilio.fr. Re-download a JWT or the kit from Dashboard → Integration.