[0.6.0] - 2026-10-15
⚠️ ACTION REQUIRED — MANUAL DATA MIGRATION for PostgreSQL, MySQL/MariaDB and MongoDB
0.6.0 renames every table / collection to
consenti_*(users→consenti_users,profiles→consenti_profiles, …). Your existing data is NOT migrated automatically. Before starting 0.6.0, back up the database, stop the app and run the one-off rename script — see Migration below:node node_modules/@consenti/api/scripts/migrate-to-consenti-prefix.mjs # dry run — changes nothing node node_modules/@consenti/api/scripts/migrate-to-consenti-prefix.mjs --apply # renameUntil you do, the app refuses to start with a message saying so; nothing is changed or lost. The script only renames (no rows are read, copied or deleted). SQLite and the JSON file store are not affected.
Summary
A minor release — it upgrades existing databases automatically (schema v2), adds a per-consent
profileVersion and a complianceMap override key, and validates profile names more strictly — on top
of the following. (1) The United Kingdom never resolved under compliance.type: 'auto': geo providers
(geoip, maxmind), the built-in timezone table and en-GB language tags all emit ISO GB, while
EMBEDDED_COMPLIANCE_MAP keys it as UK, so GET /resolve-profile returned
{ path: null, complianceGroup: 'opt-in', found: false } and the widget showed the default banner even
with an active opt-in profile (storage-independent). Alongside it, compliance.complianceMap gains
an override key so changing one country no longer means re-declaring every country served, and custom
geoDataProvider functions now receive the full browser hints the widget already sends. (2) On
Windows, saving or activating a profile failed with
EPERM: operation not permitted, rename '…\profiles\<tenant>\<group>.new-xxxxxxxx' -> '…\profiles\<tenant>\<group>'
(HTTP 500): the hot-serve directory for a compliance group is a junction and Windows refuses to rename
over an existing junction, whereas POSIX rename() atomically replaces a symlink. The version files
were written but the live {complianceGroup}/ path was never re-pointed, so the banner kept serving
the old snapshot. (3) The json storage adapter did not work on Windows at all. (4) On MariaDB (10.5+),
PostgreSQL and SQLite a profile that had ever recorded a consent or notice could not be deleted —
consent_records.profile_id and notice_shown.profile_id had foreign keys to profiles(id), so
DELETE /profiles/:id returned HTTP 500 (Cannot delete or update a parent row: a foreign key constraint fails). Oracle MySQL parsed but ignored the inline constraint, and the JSON and MongoDB adapters never had
one, so this also closes an adapter-parity gap. (5) Hardening so a deleted or edited profile never loses
evidence: each consent now records the profile version it was given, profile actions are attributed to the
admin who made them, and compliance-group / locale names are validated as safe directory and file names.
(6) PostgreSQL, MySQL/MariaDB and MongoDB tables/collections are now prefixed consenti_ (consenti_users,
consenti_profiles, …) so Consenti can never collide with, or silently adopt, a generic users/profiles
table that already exists in the database. This one needs a manual step — rename the existing ones once
with the script shipped in the package (see Migration). The same release hardens first-start on shared
hosting and restricted database users: connection pools default to 5, a first start that dies halfway is
now recoverable on MySQL/MariaDB, several instances booting at once no longer race, and search in the
dashboard (consents, visitors, audit log) works on PostgreSQL and MySQL/MariaDB again.
Added
@consenti/ui
- Standalone-mode
geoDataProvideris called with the browser hints.
@consenti/api
scripts/migrate-to-consenti-prefix.mjs, shipped in the package: renames the 17 unprefixed tables (or the collections) of a pre-0.6.0 install to theirconsenti_names. Dry run by default,--applyto rename; reads the sameCONSENTI_DB_*variables as the app; renames only (no rows read, copied or deleted); safe to re-run; refuses to overwrite an existingconsenti_*table.- Schema v2 (all SQL drivers; MongoDB/JSON simply gain an optional field):
consent_recordsandconsent_historyget a nullableprofile_version. The service stamps it on every create/update (after any plugin hook, never from the request body). Withprofile_idit pins the exact on-disk snapshot — the banner text the visitor saw — andverify()resolves a deleted profile through that version. Records created before this have no version and resolve to the newest snapshot. profile_versionin the consent CSV (appended as the last column) and XLSX exports, and in theConsentRecordOpenAPI schema.- Admin profile actions (create, copy, update, delete, activate, deactivate) now record the acting admin's
userIdin the audit log. /resolve-profileforwards the widget's fulllanguageslist and requestedlocaletogeoDataProvider(also parsed fromAccept-Languagewhen the widget hints are absent); input is sanitised (max 20 languages, tags up to 35 chars).
@consenti/types
ConsentDbRecord.profileVersion?,ConsentHistoryEntry.profileVersion?,UpdateConsentInput.profileVersion?(and onCreateConsentInput) — the profileversionin force when a consent was given.ComplianceMapData.override?: boolean;CountryResolverFncontext gainslanguages: string[]andlocale: string;WidgetCountryResolverFnreceives{ timezone, language, languages, locale }.
@consenti/utils
findCountryEntry()— country lookup that treatsGBandUKas the same country.
Changed
@consenti/api: PostgreSQL, MySQL/MariaDB, MongoDB — every table / collection is now named with aconsenti_prefix (consenti_tenants,consenti_profiles,consenti_consent_records,consenti_schema_version, …; index names too). The prefix is fixed, not configurable. SQLite and the JSON store are unchanged — their data lives in a file Consenti owns. If the database still holds the old unprefixed Consenti tables/collections and no prefixed ones, the adapter refuses to start and says to run the migration script, rather than quietly creating an empty set beside your data (it only triggers when Consenti's owntenants,profilesandconsent_records— plusschema_versionon SQL — all exist, so another app'susersorprofilestable doesn't set it off). Code that reads the database directly (reports, BI) needs the new names.@consenti/api: connection pool default is now 5 (was 10 for PostgreSQL/MySQL, and the MongoDB driver's own 100): shared hosts commonly cap connections per database user at 5–25, shared with every other app.storage.poolMaxsets it, for all three drivers, on a dedicated server.@consenti/api:storage.pathis documented as required for every driver — PostgreSQL, MySQL and MongoDB too: the database holds consent records, users and templates, but compiled profiles are always files under${storage.path}/profiles/, so keep it on persistent storage (shared between instances).@consenti/utils:resolveComplianceMapOverride()merges an operator map over the embedded map when the document setsoverride: false(country-level replacement — an operator entry replaces the embedded entry wholesale, never deep-merged);override: true/omitted keeps the existing replace behaviour; a non-booleanoverridefails validation and falls back to the embedded map.@consenti/ui: standalone-modecomplianceMaphonoursoverride: true(an unlisted country gets the strictest group,'opt-in', instead of falling back to the embedded mapping); omitted/falsekeeps the existing fall-back — the widget default intentionally differs from the server's.@consenti/api: setup-wizard config summary states whether a custom map merges or replaces.@consenti/api:consent_records.profile_idandnotice_shown.profile_idno longer have a foreign key toprofiles(fresh-install schema, all SQL dialects). A consent record is evidence (GDPR Art. 7(1): the controller must be able to demonstrate consent) and must outlive the profile it was collected against; the profile's version snapshots stay on disk after deletion by design. Existing databases are upgraded automatically (see Migration). Nothing else is removed on delete — consent records, notice records, version snapshots and the audit trail are all kept.@consenti/api: theprofile:deletedaudit entry now also records the profile'sname,complianceGroupandwasActive(previously onlyprofileIdandversion) — once the row is gone, the append-only audit log is the only place those survive.@consenti/api:GET /consent/:visitorId/verifyresolves a record whose profile was deleted to the profile's newest archived snapshot instead of failing blind: the result still reportsprofile_changed, now withconsentProfileId, andcurrentProfileId(the profile that replaced it in the same compliance group) andconsent_expiredare derived from the snapshot.
Fixed
@consenti/api: MySQL/MariaDB — a first start that died partway (dropped connection, host limit) used to leave tables behind and then fail on every later start withDuplicate key name, because DDL auto-commits andCREATE INDEXhas noIF NOT EXISTS. Indexes are now declared inside eachCREATE TABLE IF NOT EXISTS,consenti_schema_versionis still stamped last, so starting again simply resumes.@consenti/api: several instances starting at once against an empty database no longer race — a named lock (GET_LOCK) on MySQL/MariaDB, an advisory lock on PostgreSQL (both need no special privilege), and idempotent upserts for the seed data on MongoDB (a concurrent first start could fail withE11000 duplicate key).@consenti/api: first-start failures on a restricted database user now say which privileges are needed (MySQL/MariaDB:SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, REFERENCES; PostgreSQL:CREATEon the schema) and that Consenti never creates databases, schemas, extensions or users.@consenti/api: PostgreSQL, MySQL/MariaDB — theqsearch on the consents, visitors and audit-log lists failed with a SQL syntax error (ESCAPEplaced after the closing parenthesis; present since 0.3.0).@consenti/api: MySQL/MariaDB —createTenant/updateTenantfailed withIncorrect datetime value: '2026-…T…Z'(strict mode rejects an ISO-8601 timestamp for aDATETIMEcolumn);purgeExpiredConsentscompares with the same format.- Docs: the server
compliancereference now covers every option (gpc,gpp,tcf.publisherCC,parentalConsentTokenTtlDays,complianceMap, …) and a newgppsection;handleCache's second argument is documented as the profile id (a string — it was shown as a version number); the widgetcompliance.tcfrow listspublisherCC,vendorListVersionandgvlVersion. @consenti/api: MariaDB 10.5+, PostgreSQL, SQLite —DELETE /profiles/:idno longer fails with a foreign-key error for a profile that has consent or notice records (see Changed).@consenti/api: saving an active profile that has no compliance group no longer points the "live directory" at the tenant's wholeprofiles/folder (which the old code would have emptied); such a save only writes its version files, likeactivate()already did.@consenti/api: the worker now refuses to touch the file system for an unsafe compliance-group or locale name, even if request validation were bypassed.- Repo / install on Windows: root
npm run buildnow goes through Turborepo, so the sharedtypesandutilspackages are built beforeui/api(a fresh clone used to fail withCould not resolve "@consenti/utils");package-lock.jsonwas regenerated and is now complete for every OS/CPU (it was out of sync —npm cifailed withMissing: postcss@8.5.30 from lock file— and only recorded macOS native packages such as@rollup/rollup-darwin-arm64); the docsdev/buildscripts no longer use the Unix-onlyNEXT_IGNORE_INCORRECT_LOCKFILE=1prefix; newnpm run check:lockfileverifies the lockfile against Windows, Linux and macOS using the pinned npm. @consenti/api: Windows — activating a profile no longer fails withEPERMwhen the compliance-group junction already exists; the existing junction is removed just before the swap (the previous version's own directory is untouched). The swap stays a single atomicrename()on macOS/Linux. The temporary junction is now cleaned up if the swap fails.@consenti/api: Windows,jsonstorage — the adapter derived its parent directory with a/-only regex, so on a backslash path it created a directory namedconsenti-data.json; every write then failed (logged as a warning only) and the data was lost on restart. It now usespath.dirname().@consenti/api:/resolve-profileresolvesEurope/London/en-GB/ geoipGBvisitors to the UK group (opt-in).@consenti/ui: standalonecompliance.type: 'auto'resolvesen-GBbrowsers.@consenti/ui:profile-resolverintegration test harness no longer throws on Node 21+ (navigatoris getter-only).
Breaking changes
PostgreSQL, MySQL/MariaDB and MongoDB: tables/collections are renamed consenti_* and an existing install
must be migrated by hand once (see Migration). Until then the app refuses to start with a message saying so;
nothing is changed or lost. SQLite and the JSON store are not affected.
Stricter names for anything that becomes a directory or file name: profileJson.complianceGroup /
customComplianceGroup must be 1–64 characters of a-z 0-9 - _, start with a letter or digit, not be a
reserved Windows device name (con, nul, com1…) and not start with prof_; defaultLocale,
locales and localeContent keys must be language tags (en, pt-BR) with no case-insensitive
duplicates. POST/PUT /profiles answer 400 otherwise, and activating a profile whose stored group
violates this fails — rename it (upper-case or spaced custom groups are the likely case). All built-in
groups and normal locales are unaffected.
Migration
Step 1 — PostgreSQL, MySQL/MariaDB and MongoDB only (SQLite and JSON: skip to "Automatic" below). Rename
the existing tables/collections to their consenti_ names once, before starting 0.6.0:
-
Back up the database and stop the app.
-
From your project, with the same
CONSENTI_DB_*environment variables the app uses (or pass--driver,--uri/--host --port --user --password,--database):# shows what would be renamed, changes nothing node node_modules/@consenti/api/scripts/migrate-to-consenti-prefix.mjs # renames node node_modules/@consenti/api/scripts/migrate-to-consenti-prefix.mjs --apply -
Start the app. It finishes any remaining schema upgrade itself (below).
The script renames only — no rows are read, copied or deleted — and is quick regardless of database size.
MySQL/MariaDB renames all tables in one atomic statement and PostgreSQL in one transaction; MongoDB renames
collection by collection (each keeps its data and indexes), and a re-run skips the ones already done. It needs
more than the app does: MySQL ALTER + DROP on the old tables and CREATE + INSERT on the new ones
(ALL PRIVILEGES on the database, which shared hosts grant by default), PostgreSQL table ownership, MongoDB the
renameCollection action (readWrite includes it). It refuses to overwrite a consenti_* table that already
exists. A fresh database needs nothing. Everything else that reads these tables directly must use the new names.
Automatic. The first start on 0.6.0 upgrades an existing database to schema v2 — drops the two
profile_id foreign keys and adds profile_version — on MariaDB, PostgreSQL and SQLite; MySQL (Oracle)
only gains the column, MongoDB and the JSON store need nothing. Take a backup first for production
databases. The upgrade changes table structure only and never edits rows. The database user needs the
ALTER privilege (MySQL/MariaDB) or must own the tables (PostgreSQL) — otherwise the API refuses to start
with an explanatory error and nothing is changed. PostgreSQL runs in one transaction. SQLite first saves a
consenti.db.pre-v2-<timestamp>.bak copy (delete it when satisfied; allow ~2× the database size in free
disk), rebuilds the two affected tables in one transaction, verifies row counts before dropping anything,
and refuses to rebuild a table that has a column this version doesn't know. MySQL/MariaDB DDL cannot be
rolled back, so each step is idempotent and an interrupted upgrade resumes on the next start. Starting 0.5.0
against an upgraded database still works. Verified with full before/after dumps (18,837 rows across 17
tables, awkward values included) on MariaDB 10.5 and 10.11, MySQL 8.4, PostgreSQL 16, and SQLite
(node:sqlite, node-sqlite3-wasm; 5,000 consents plus every other table), plus restricted-account and
mid-upgrade-failure cases; MongoDB 7 and the JSON store gain only an optional field. The prefix rename, the migration script, the
restricted-user and shared-database cases, concurrent first starts and about 70 adapter operations were run
against PostgreSQL 16, MySQL 8.4, MariaDB 10.11 and MongoDB 7 (before and after the rename, including a
simulated 0.5.0 database upgraded through both steps). MariaDB 10.6+ / MySQL 8.0.4+ is needed for the dashboard
statistics (JSON_TABLE). To change only some countries, add override: false to your complianceMap document. A profile
whose activation failed earlier on Windows is repaired by saving it again; on Windows with json
storage, delete the stray db\consenti-data.json\ directory if one exists.