API change log
2.0.0-rc.23
Section titled “2.0.0-rc.23”- Grant an app access for an account you control. On platforms with account delegation, the authentication page asks, after sign-in, whom the access is for; an access granted for a controlled account is marked on the server (
accessInfo().delegation.grantedVia: 'app') and the auth request takes anactAsfield ('allow','deny'or a username). Breaking: ending a delegation now also revokes the accesses granted through it. A delegate’s token can no longer accept an OAuth2 consent (403 access_denied) or a cross-account consent (400,delegation-grant-requires-owner) on the controlled account.GET /service/infogainsaccount(the account app’s URL) andfeatures.delegation. - Credential hand-off by one-time shared secret. An auth request may ask for
credentialHandoff: 'shared-secret': theACCEPTEDpoll then carries a one-timehandoffkey instead of the token, redeemed once on the user’s core. Requests that do not ask behave as before; a core that does not support it ignores the field. - Security: a managed shared access no longer outlives its managing app access. A shared access created by an expiring app access without
expireAfternow takes the app’s expiry, and one without expiry is refused once its app access has expired. Breaking (update access): clearing such a shared access’s expiry, or giving its app an expiry while it has none, is refused (invalid-operation, withoffendingChildrenin the second case). - Security: a webhook stops firing once its access has expired (or, for a shared access without expiry, once its managing app access has); it becomes
inactive, andwebhooks.updatewithstate: 'active'reactivates it once the access is valid again.
2.0.0-rc.18 to 2.0.0-rc.22
Section titled “2.0.0-rc.18 to 2.0.0-rc.22”- An auth request can say how each permission should be presented. The new optional
consentobject of the auth request marks permissions as required (mandatory), or as ones the user has to choose deliberately (optIn); a permission in neither list is optional and shown pre-selected, which is what every permission does today.allowUserChoiceis what enables per-permission choice at all. The authentication page then mints only what the user kept, and the server verifies the result against the offer: a grant that does not match is refused withinvalid-consent-grant, and a core that could not perform that check answersconsent-check-unavailablerather than accepting it. Requests without aconsentobject behave exactly as before, and a core that does not support it ignores the object and falls back to all-or-nothing, so the echo ofconsentin the response is how an app detects support. Where consent is your lawful basis, preferoptIn: see the consent guide. - The same
optInannotation is available on OAuth2 and cross-account consent offers, beside the existingmandatory, with identical meaning: it decides whether the entry opens ticked, never what may be granted. See OAuth2 authorization.
Email verification is now general availability, and on by default. services.email.enabled.verifyEmail defaults to true. Upgrading does not stop a working configuration from booting: a deployment that never set the key keeps booting, logs one warning per start, and leaves the feature off until auth.emailVerificationPageURL and a mail setup are in place. Setting the key to true explicitly makes the page URL required at boot, as before; setting it to false turns the feature off silently. “Mail is configured” is decided from configuration alone, with no SMTP probe, and the sender address is not part of that test, so a deployment that was sending mail without one keeps sending mail.
- Email verification at sign-up (optional, off by default). Operators can require a proved address before an account is created, with
account.emailVerification.requireAtRegistration. The client requests a one-time code withPOST {register}email-challenge, the holder pastes it back toPOST {register}email-challenge/verify, and the resultingemailProofis sent with create user. Addresses proved this way recordverificationMethod: 'email-code', a new proved value alongside'email-link'and'operator'. Admin-created accounts are never gated. The core refuses to boot when the gate is on and mail is incomplete, and a delivery failure blocks that registration rather than letting an unverified account through. The rate limits are keyed on the target address, because the endpoints are public: read the caveat in the operator guide before enabling it. - The multiple-emails surface leaves beta, including verify email address and the
emailsoperations of update account information. See the new email verification guide. - The mailed verification link now carries
usernameas well as the token, so a landing page can address/:username/account/verify-emailwithout an email lookup. Existing links keep working, and the parameters are appended with the right separator, so a page URL that already carries its own query keeps it intact. - Mail templates ship with the server.
welcome-email,reset-password,verify-emailandemail-challenge, in English and French, are seeded on first boot when no template directory is configured, so a fresh deployment can send mail without authoring a template first. Deployments that already hold templates are untouched. GET /service/infonow always carriesfeatures.emailVerification: { atRegistration, onAccount }.
2.0.0-rc.17
Section titled “2.0.0-rc.17”- Third-party sign-in (beta, off by default). Pryv.io can act as an OpenID Connect client, letting an account holder sign in through an external identity provider the operator configures. New routes
GET /auth/sso/:provider/startand/callback, plus a publicGET /auth/sso/providersdescriptor for the sign-in buttons. A first sign-in links the provider identity to an account only when that account has proved ownership of the provider’s verified email address, so an address that was never inbox-proved cannot be taken over. The session is handed to the auth app through a one-time shared secret, so the token never appears in a redirect URL. See third-party sign-in. - Fixed: an OAuth2 authorization accept no longer mints an access through a consent that was refused or through an invalidated link.
- Fixed: when several data-grants serve one relationship, a consent revoke now reliably notifies the peer instead of reporting
peerNotified: false(#129). - Fixed:
accesses.createnow rejects a creation stream-id with junk after a valid prefix, or longer than 100 characters, as its error message already promised (#130).
2.0.0-rc.16
Section titled “2.0.0-rc.16”(supersedes the 2.0.0-rc.15 tag, which was cut from a commit that failed CI and was never published.)
- MFA: per-account failed-attempt limit. The failed-second-factor budget now also accrues per account, not only per pending MFA session, so repeated wrong codes across repeated logins no longer reset it. Once an account reaches
services.mfa.attempts.perAccountfailures within the window, the second-factor step is locked andmfa.verify,mfa.confirmandmfa.challengeanswer429 too-many-attemptswith aRetry-Afterheader. Password login is not locked and existing tokens keep working, so a password-holder cannot use this to lock a user out. A lock clears on its own, or immediately throughmfa.recoveror the admin deactivation. New configservices.mfa.attempts:perSession(5),perAccount(20,0disables),perAccountWindowSeconds(900),lockoutSeconds(900). This supersedes the known limitation noted in 2.0.0-rc.14.
2.0.0-rc.14
Section titled “2.0.0-rc.14”MFA: server-side TOTP (authenticator apps), enabled by default, alongside SMS. MFA is no longer SMS-only and is now active by default. A built-in TOTP factor (RFC 6238) works with no configuration and no external service, and is the default method; SMS keeps working unchanged and stays off until configured. Nothing is forced: a user with no enrolled factor logs in exactly as before. mfa.activate takes an optional method and returns an otpauthUri and Base32 secret for TOTP; auth.login returns mfaMethod next to mfaToken; service.info().features.mfa.methods lists the active methods so a client offers only what the operator enabled. Legacy deployments upgrade unchanged: a configuration carrying the old services.mfa.mode keeps precedence. To keep MFA off after upgrading, set services.mfa.active: false. See MFA setup.
- Multiple emails per account (beta). An account can hold more than one address, each with its own verification state, while the singular
emailstays authoritative as the primary.GET /:username/accountreturns anemailsarray;PUT /:username/accountaccepts anemailsoperations object (add,setPrimary,remove,resend);POST /:username/account/verify-emailconsumes the mailed one-time token.verificationMethoddistinguishes proved ownership ('email-link','operator') from asserted ('registration','legacy'), so averifiedstatus alone does not imply proof. Existing accounts need no migration. - Email verification ships OFF by default in this release (beta). Enabling it makes
auth.emailVerificationPageURLa required key, and a required key that is unset refuses the boot, so a default-on sub-feature could stop a previously valid deployment from starting. Opt in with both keys together. (In a later release this becomes on by default, with a soft landing instead of a boot refusal: see the entry at the top of this page.) - Shared secrets: hand a secret to a third party by one-time key. Instead of putting an apiEndpoint in a URL, where it survives in history, referrer headers and access logs, store the payload on the account and hand over a random key redeemable exactly once. Create returns the key once and keeps only its SHA-256; redeem takes no token, since the key is the credential, and travels in the request body; status inspects without consuming. An optional signature binds redemption to a passphrase or an HMAC proof. An access can be barred from minting them with the
secretSharingfeature permission. Config:sharedSecrets.enabled(defaulttrue),maxSizeBytes,maxTtl. - OAuth2: DPoP, sender-constrained tokens (RFC 9449, beta). A client can bind its tokens to a key pair it holds, so a stolen bearer token alone is useless: every call must carry a short-lived
DPoPproof. Opt-in per session and backward compatible. Proofs are single-use cluster-wide, enforcement sits in the shared access-validation path (REST, batch, socket.io and HF series alike, fail-closed), and a refresh must prove the same key. Requires a reverse proxy that overwritesX-Forwarded-Host/X-Forwarded-Proto, since proofs bind to the client-facing URI. - OAuth2:
private_key_jwtclient authentication (RFC 7521/7523, beta). A confidential client can authenticate with a signed JWT instead of a shared secret, by registering its public JWK Set (ES256 only). Verification failures answer uniformly, so the endpoint leaks nothing about why. - OAuth2: revocation reaches live tokens cluster-wide (beta). Revoking a client now also kills already-issued access tokens within
oauth.clientRevokeCheckSeconds(default 30), including open socket.io connections and the HF series token cache, instead of letting them live out their TTL. Operators can also revoke by key thumbprint, which kills everything bound to that key including later refresh attempts, and inspect what a revocation would hit. - OAuth2: an abandoned authorization code no longer leaves its access alive. The access minted at consent is revoked when the token exchange never succeeds. Session accesses now carry an explicit
{ feature: 'selfRevoke', setting: 'allowed' }permission, andaccesses.createacceptssetting: 'allowed'as the explicit form of the default. - Observability rebuilt: no third-party agent. ⚠ If you enabled the optional APM integration in an earlier version, read the note below. Telemetry is now constructed by the platform from a closed vocabulary rather than auto-instrumented by a vendor agent, and shipped over OTLP to any backend, including a collector inside your own infrastructure. What can be emitted is per-method call counts, duration histograms and error counts, labelled with an API method id, a status class and a documented error id, plus service name, version, machine hostname and worker index. Request URLs, parameters, bodies, headers, usernames, identifiers, log records and error messages have no representation in the schema. Error reports are aggregated and time-coarsened, because a precise failure timestamp is a re-identification handle. Earlier versions: the vendor agent’s scrubbing configuration was placed in a file the agent does not read, so it never loaded, and affected deployments sent request URLs, the
Hostheader, route parameters including the username, obfuscated SQL and forwarded log records including their message text. Assume that applied for as long as the integration was enabled and check what your provider account holds. See observability. - Fixed: concurrent
streams.createof the same id returns409 item-already-existsinstead of a500leaking the storage engine’s unique-constraint message (#126). - Fixed: attachment uploads are now bounded by
uploads.maxSizeMb, which previously bounded only JSON bodies, so a deployment without a size-limiting proxy accepted attachments of any size. Oversized parts now answer413 payload-too-largecarryingdata.limitMb. Operator note: a deployment relying on unbounded uploads will receive413until the limit is raised (#125). - Fixed: a peer-rejected OAuth2 consent accept answers
400 invalid_grantwith the peer’s machine-readable reason instead of a bare500. - Fixed: withdrawing consent no longer blocks re-consent through the same multi-use shareable link.
- Fixed: the telemetry endpoint guard refused plain
http://to a collector on a private address, which is the layout it recommends; loopback, private and link-local destinations are now accepted and anything routable still requireshttps:. The rule moved into the emitter, so it holds however the endpoint was configured (#119). - Fixed:
451 unavailable-methodno longer blames the commercial licence when a method is unavailable for another reason, such as an operator turning an optional feature off. - Fixed: a cross-account back-channel handshake could be dropped silently and permanently, leaving every later consent revocation on that relationship undeliverable. Affects 2.0.0-rc.10 and rc.11. Relationships already affected do not heal on upgrade: each needs a fresh request and accept.
- Fixed: two concurrent relationships with the same counterparty under the same app are now told apart, keyed on their per-request scope stream. Previously the newer relationship’s back-channel overwrote the older one’s pointers and deliveries were misrouted.
2.0.0-rc.11
Section titled “2.0.0-rc.11”- OAuth2: refresh-token reuse detection. Replaying an already-rotated refresh token, the signature of a stolen token chain, now revokes the whole chain rather than merely rejecting the call: the durable consent record and all live session accesses for that user and client, plus their descendants, are soft-deleted and dependent webhooks cascaded. A benign double-submit inside
oauth.refreshReuseGraceSeconds(default 10s) is tolerated. The error response is identical whether reuse was detected or not, so the endpoint cannot be used as an oracle. oauth.*events reach the audit trail. Five user-scoped events, including the newoauth.token.reuse_detected, persist to the user’s audit storage and are readable through the usual:_audit:*streams; four user-less events go to syslog only. Audit emission never fails a token grant.- Fixed: audit input validation could never reject anything, because the validators return a diagnostic string on failure and the guard treated it as success.
- Correction to the documentation: cross-account consent revocation was described as a server-orchestrated dual delete in which the peer’s side tears itself down. That was never implemented. A revoke tears down only the accesses on the account where it was written, and the forwarded notification is delivered without running a teardown on the receiving side, so revocation is advisory in the trigger-writer to peer direction. Integrators should delete their own half when they observe a revoke arriving. The enforcing direction is the local one: an accepter revoking destroys the data-grant on their own account, which is what cuts the requester’s read.
2.0.0-rc.10
Section titled “2.0.0-rc.10”- Cross-account consent: revocation reaches the counterparty whatever path performs it. Deleting a relationship access with a plain
accesses.delete, for example from a generic “connected apps” screen, now delivers the same revoke notification as the dedicated helpers, so consent withdrawal is observable on the other side regardless of how it was performed. The forwarded event always carriescontent.accessId, which is also the authoritative selector of the relationship to revoke: with several relationships to one counterparty, the previous matching could tear down the wrong one (#109). - The
:_cmc:*namespace now materialises on reads too. An account’s reserved streams are created on first use, but the trigger covered writes only, so a consumer whose first action was a read, typically an inbox watcher, gotunknown-referenced-resourceon every poll and could never bootstrap (#111). - Scope edits made with a plain
accesses.updatenow reach the counterparty, like the helper flow does. The notification previously targeted a stream that only admits lifecycle events and was silently rejected there.
2.0.0-rc.9
Section titled “2.0.0-rc.9”- Cross-account consent: request a delegable data-grant. A consent request may carry
request.accessType: "app"(default"shared"), so the accepted data-grant is minted as anappaccess and the approved requester can create scoped, individually-named sub-accesses within it: least-privilege re-delegation with per-actor audit attribution. Existing offers and the OAuth2 flow are unchanged.
2.0.0-rc.8
Section titled “2.0.0-rc.8”- Fixed: 2.0.0-rc.7 is dead on arrival, use rc.8. A production image built with
--omit=devpruned a package the OAuth2 route required at module load, so every API worker crash-looped and the core never served (#106).
2.0.0-rc.7
Section titled “2.0.0-rc.7”- OAuth2 authorization-code flow (server-side). Pryv.io can act as an OAuth2 authorization server (RFC 6749 with PKCE), so third-party applications obtain tokens through the standard redirect flow instead of the Pryv-native access-request polling flow. Both flows remain supported. New endpoints:
GET /.well-known/oauth-authorization-server(RFC 8414 discovery),GET /oauth2/authorizeandPOST /oauth2/token(authorization code, refresh, client credentials). The token response carries a PryvapiEndpointextension so multi-core clients build a working connection, and a vanilla client that calls the wrong core receives421with the correctcoreUrl. Clients are registered out of band by the operator. Consent scopes are granular, with no coarse wildcards: thescopeparameter carries exactly one consent-offer reference, the consent screen lets the user untick individual permissions, and the minted access carries exactly the kept subset. Revoking the durable consent record invalidates the refresh chain; narrowing it propagates on the next refresh, while widening always requires a fresh authorization. See OAuth2.
2.0.0-rc.6
Section titled “2.0.0-rc.6”- Access aliases: de-identifying endpoints.
accesses.createacceptsrandomAlias: true, which issues a platform-unique routable alias that replaces the username everywhere the access is addressed, including the returnedapiEndpointandaccess-info. Accesses handed to different parties therefore cannot be cross-matched back to one account. The alias routes exactly like the username, across cores, and is released when the access is deleted. - Changeable username.
POST /account/change-usernamelets a user choose a new one. Accesses issued under the previous username keep working, since the old name is kept as a routable alias, and theiraccess-inforeports the new username. The number of changes is capped by the operator (default 2);GET /account/username-changesreports used, limit and remaining. - Fixed: accepting a cross-account invite with an access name already in use failed permanently with a raw database duplicate-key message, and an internal retry loop kept re-attempting an accept that could never succeed (#105).
- Fixed: a failed transactional email no longer returns a
500carrying the mail-service URL and the upstream error to unauthenticated callers of password reset. The diagnostic is logged server-side instead (#104).
2.0.0-rc.5
Section titled “2.0.0-rc.5”- BREAKING: cross-account consent writes that mint or widen accesses now require a personal token. Writing an accept or a scope-update trigger mints or widens a data-grant on the user’s account, so it now requires user presence at the moment the action is recorded, closing a path where an app token with narrow write permission could trigger creation of a much broader access. Non-personal tokens receive
400 invalid-operationwitherror.data.id = "cmc-accept-requires-personal-token". Revoke is gated differently, on the standard access-permission check rather than the token class, because a revoke is a contraction: a personal token always may, the relationship’s own holder may self-revoke by default, the app that created the access may, and everything else is refused withcmc-revoke-forbidden. Cross-platform protocol deliveries pass through unaffected. Other trigger types, including request, refuse, invalidate-link and the chat and notification families, are unchanged. - Optional encryption-at-rest image variant. A published variant adds optional encryption at rest for the data directories (events, attachments, series, audit, platform database), with pluggable backends and key providers. The base image is unchanged and the variant is off until opted in.
/service/infocan advertise adapters: an optionaladaptersarray of base URLs, each serving an adapter UI and amanifest.jsondescribing its name, type, version and capabilities. Fully additive.
2.0.0-rc.4
Section titled “2.0.0-rc.4”- Multi-core: cores join as non-voters by default, so adding a core can no longer take an existing core’s control plane offline.
- On-demand encrypted backups. The backup tool can encrypt its output, so plaintext personal data never touches the destination disk. Two key models: a recipient public key, where the backup host holds no secret that can decrypt its own output, or a passphrase. Each file is encrypted independently, so chunking, incremental runs and single-user restore keep working, and restore auto-detects an encrypted backup. Opt-in: without the flags, backups behave exactly as before. A lost key makes the backup unrecoverable, which is the point of the feature. See encrypting the backup.
2.0.0-rc.1
Section titled “2.0.0-rc.1”First release candidate of open-pryv.io v2.
- Single-binary topology. One process manages API, HF series and preview workers in one Docker image. No MongoDB, and no separate registration, MFA or mail containers: all merged into the core.
- Two production-grade user-data engines: PostgreSQL (default, cross-user queries through shared tables) or SQLite (per-user files, cleaner erasure semantics). Both pass the same test matrix at parity.
- Multi-core cluster bootstrap over mTLS-protected Raft, with DNS discovery and wildcard certificate renewal across the cluster.
- Cross-account messaging and consent, federated consent, chat and system notifications between two accounts.
- Versioned accesses:
accesses.updateis back, with composite-id versioning and audit history,accesses.getOneaccepts?includeHistory=true, and socket.io emitsaccessUpdated. - Engine-agnostic schema migrations and a v1 to v2 migration path.
- An interactive install wizard (
docker run … init) that writes a commented configuration, a launcher and a configuration checker into the operator’s chosen directory. - BREAKING since
2.0.0-pre: the/reg/accesspolling endpoint response shapes were trimmed (SDK callers onpryv@>=3.5.0are unaffected), and MongoDB was removed as a user-data storage engine, so MongoDB-backed deployments must export and re-import into PostgreSQL or SQLite. Both are documented in the dedicated entries further down this page.
2.0.0-rc.3
Section titled “2.0.0-rc.3”- BREAKING — the deprecated
GET /audit/logsroute has been removed. Query audit logs through the Events API as described in the Audit logs guide: callevents.getwith the audit streams in thestreamsparameter (:_audit:for all,:_audit:access-{access-id}for one access,:_audit:action-{method-id}for one action). Returned items are standard audit events (audit-log/pryv-api/audit-log/pryv-api-error). No official SDK used the route.
Scoped real-time notifications for both notification transports — be notified only of the changes you care about, paired with lib-js pryv@3.7.0 + @pryv/socket.io@3.7.0 + @pryv/monitor@3.7.0 (lockstep).
- Webhooks gained an optional
scopesfield: a map of named scopes, each{ kind, query }wherekindisevents(default),streamsoraccessesandqueryis shaped like the matching read method’s parameters (an events.get query forevents). A scoped webhook fires only on matching changes and its data changes payload carries the matched scope keys instead of the coarseeventsChanged/streamsChangedmessages.scopesis alterable via webhooks.update. Unfiltered webhooks are unchanged. - Websockets gained
subscribe/unsubscribe/getSubscriptionsmessages and a unifiednotificationsChangedmessage. A connection holding at least one scope opts out of the coarse broadcasts and receivesnotificationsChanged({ keys })with the matched scope keys. Back-compatible: servers without the feature rejectsubscribe, and the SDK falls back to the coarse messages. @pryv/monitortransparently registers a scope derived from itseventsGetScopeand usesnotificationsChangedwhen the server supports it, falling back to coarse signals otherwise — no API change for monitor consumers.
2.0.0-rc.2
Section titled “2.0.0-rc.2”Diskless deployment shape for single-core dnsLess installs in full PostgreSQL mode — no persistent filesystem needed on the app host:
storages.platform.engine: postgresqlstores platform data (registrations index, DNS records, TLS certificates, invitation tokens, …) in PostgreSQL; no rqlited process runs. Single-core only — multi-core keeps rqlite; boot-time validation enforces the topology.- New
s3fileStorage engine: event attachments on any S3-compatible object store (storages.file.engine: s3+storages.engines.s3.*). - New
bin/migrate-platform.jsmoves platform data between rqlite and PostgreSQL in either direction (adopt diskless, or go multi-core later). - New
bin/config-to-env.js(+config-to-envdocker subcommand) converts a config file into an env file for pure-ENVdocker run --env-filedeployments. - The install wizard offers the diskless options for dnsLess + postgresql runs and documents them as commented blocks otherwise; it now also generates a
config-to-env.shlauncher. - Account deletion erases attachments through the fileStorage engine, so S3-stored attachments are removed too.
2.0.0-pre.4
Section titled “2.0.0-pre.4”Cross-account Messaging & Consent (CMC) plugin: completeness and security hardening on the open-pryv.io server, paired with lib-js pryv@3.4.0 + @pryv/cmc@1.1.0 + @pryv/monitor@3.4.0 + @pryv/socket.io@3.4.0 (lockstep).
Plugin completeness
- Capability TTL configurable per-invite.
cmc.createInvite({ ..., expiresAt })(orcontent.request.expiresAton a rawconsent/request-cmc) now controls the capability’s lifetime. Plugin bounds the value to[60s, 30d]at mint time; out-of-range rejects withcmc-capability-ttl-out-of-range. Default unchanged at 7 days when omitted. - Features gating binding at send time.
content.request.features.{chat, systemMessaging}is enforced on BOTH sides —cmc.sendChat/cmc.sendSystemAlertagainst a relationship whose negotiated feature flag isfalserejects withcmc-chat-disabled/cmc-system-messaging-disabled. Default-permit on omission (matches the offer-side default).consent/scope-request-cmcandconsent/scope-update-cmcremain protocol-level and permitted regardless. - Doctor-side
revokeRelationship({ inviteEventId })is now reliable. Previously the inbox-mirror droppedinviteEventId; the plugin now stamps it from the capability access’sclientData.cmc.requestEventIdso the SDK’s convenience lookup matches. The{ accessId, scopeStreamId }power-user form keeps working. requestEventIdpopulated on real-deploy capability accesses. Pre-fix the mint hook ran pre-persist whenevent.idwas null; new post-create hook stamps the real id after persistence. This makes theinviteEventIdmirror work end-to-end.
Security hardening (route-level guards)
clientData.cmc.*forge-prevention.accesses.create/accesses.updatereject any user-suppliedclientData.cmc.*withcmc-clientdata-cmc-forbidden. That namespace is plugin-owned end-to-end (role, appCode, counterparty, capability, requestEventId, features); allowing user-set values would let an app forge a counterparty role and bypass the handshake. The CMC plugin’s own internal calls reach storage viamall.accessesdirectly so the handshake is unaffected.- Reserved-root immutability.
streams.deleterejects deletion of the five reserved CMC parents (:_cmc:,:_cmc:inbox,:_cmc:apps,:_cmc:_internal,:_cmc:_internal:retries) +:_cmc:_internal:*+ plugin-managedchats/collectorssegments withcmc-reserved-stream-undeletable— even from a personal token. Without this guard aDELETE :_cmc:would silently break every active relationship on the account. User-creatable:_cmc:apps:<app>:<sub>streams remain deletable. content.fromstamping extended.inboxWriteHookalready stamped from-field on:_cmc:inboxwrites; the new counterparty-from stamping hook extends the same protection to per-appchats:*/collectors:*writes by counterparty-marked accesses. A peer cannot forgecontent.fromonmessage/chat-cmc,notification/alert-cmc,notification/ack-cmc,consent/scope-request-cmc, orconsent/scope-update-cmc.:_cmc:_internal:*defense-in-depth filter. New read-hooks onevents.get(strips internal-stream ids fromparams.streams),events.getOne(returns 404 if event has internal streamId), andstreams.get(prunes the internal subtree from the response tree). Today the internal subtree has no app-visible permissions so explicit queries return empty anyway; this guards against future regressions in the permission system.
Tests / contract
- 7 new error ids on
cmc.errorIdsmirror the server-side additions:CAPABILITY_TTL_OUT_OF_RANGE,HANDLER_MISSING_CAPABILITY_ID,CHAT_DISABLED,SYSTEM_MESSAGING_DISABLED,CLIENTDATA_CMC_FORBIDDEN,RESERVED_STREAM_UNDELETABLE,COUNTERPARTY_IDENTITY_MISSING. New[CMCXEC]J9 catalogue-match test pins all 7 against the server strings. - J3–J10 wire-shape contract tests added to
@pryv/cmc/test/cmc.test.js(listInvitesusesstreamsnotstreamIds,listAcceptedRelationshipscounterparty mapping precedence,waitForAcceptsinceTimefilter,acceptInvitescopeStreamIdrequirement +dataGrantAccessIdresolution).
Upgrade
npm install pryv@3.4.0 @pryv/cmc@1.1.0—@pryv/monitor+@pryv/socket.iofollow via transitive resolution. No source-level breaking change; existing apps onpryv@3.3.x+@pryv/cmc@1.0.xkeep working.- Server deployments:
docker pull pryvio/open-pryv.io:2.0.0-pre.4(Dokku/Docker), orgit pull origin master && npm install --ignore-scripts && npm rebuild && sudo systemctl restart pryv-master.service(raw deploys).
2.0.0-pre
Section titled “2.0.0-pre”First v2 preview — major consolidation and new features.
Open source
- Open Pryv.io v2 is now fully open source under BSD-3-Clause. The former “Enterprise version” and Open Pryv.io are now the same codebase.
- Docker image published as
pryvio/open-pryv.io(waspryvio/corefor v1). Pullpryvio/open-pryv.io:2.0.0-pre.
Consolidated runtime
- Registration, MFA, high-frequency series and previews are now served by a single binary (
bin/master.js), replacing the previous per-service Docker images (pryvio/core,pryvio/hfs,pryvio/preview,service-register,service-mfa). - Cluster mode: configurable N API workers + M HFS workers + optional previews worker under one master process.
- High-frequency series endpoints (
/{user}/events/{id}/series,/{user}/series/batch) are reachable on the same public port as the rest of the API — an in-process dispatcher routes them to the HFS worker on:4000. Setcluster.hfsWorkers: 1(or more) to enable HFS; no external reverse proxy required. For high-throughput installs, front the cluster with nginx (reference vhost:docs/nginx-ingress-sample.conf). SDKs readfeatures.noHFon/service/infoto know whether HFS is served — auto-derived fromcluster.hfsWorkers.
Multi-factor authentication (ported from the former Enterprise version)
- New API:
mfa.activate,mfa.confirm,mfa.challenge,mfa.verify,mfa.deactivate,mfa.recover. - When a user has MFA active,
auth.loginreturns{ mfaToken }instead of a regular access token. Clients must follow up withmfa.verify(SMS code) to obtain the Pryv access token. - Back-office
system.deactivateMfaremains available alongside the user-facingmfa.deactivate. - Disabled by default (
services.mfa.mode: disabled) — existing deployments see no change.
Registration merged into core (was service-register)
- All
/reg/*endpoints are now served by the main binary. No separate register service to deploy. - Backing store is PlatformDB (rqlite) — replaces the previous redis/leveldb storage.
- Legacy routes, invitations, DNS-less registration and multi-core indirection are all preserved.
Multi-core deployments
- Per-user routes (
/:username/*) enforce core affinity. Cross-core requests receive HTTP 421 Misdirected Request with body{ error: { id: 'wrong-core', message, coreUrl } }— clients must retry directly againstcoreUrl. No HTTP redirect is issued (cross-origin redirects stripAuthorizationheaders, WebSockets cannot follow). /reg/*and/system/*remain load-balanceable.- New
core.urlper-core config override for DNSless multi-core deployments where FQDNs cannot be derived from{core.id}.{dns.domain}. Other cores discover the URL viaPlatform.coreIdToUrl()(PlatformDB-backed).
Storage
- PostgreSQL backend (optional): full MongoDB parity for events, streams, accounts. Select via
storages.engines.*config. Note: v1→v2 data migration currently only operates from a MongoDB v1 source; migration into a PostgreSQL v2 backend is not yet supported. See INSTALL.md. - rqlite is now the only platform-storage engine — the legacy sqlite platform store was removed.
master.jsalways spawnsrqlited.
Upgrading from v1
- Toolkit at
dev-migrate-v1-v2: exports v1 user data (MongoDB) and produces a v2-compatible backup directory thatbin/backup.js --restorecan import.
Access versioning
accesses.updateis back (was removed in early v2).PUT /accesses/{id}mutates the head, snapshots the prior state into history, and bumps the access’sserial. Mutable fields:name,deviceName,permissions,expireAfter/expires,clientData. See Update access.- New
GET /accesses/{id}(Get one access) accepting either bare<base>or composite<base>:<serial>ids. Composite with an older serial returns the historical snapshot + acurrenthint. Pass?includeHistory=truefor the full version history. - Composite-id wire format:
access.id/access.createdBy/access.modifiedBynow serialise as<base>:<serial>once an access has been updated at least once. Never-updated accesses still serialise as bare cuid — fully backwards-compatible. Usepryv.utils.parseAccessRef(lib-js ≥ 3.1.0) to extract{ base, serial }. - Composite-id conflict:
accesses.updateandaccesses.deleterequire the caller’s{id}to match the current head’s serial. A stale composite returns409 stale-resourcewithdata: { provided, currentSerial }; refetch and retry. - New socket.io event
accessUpdatedfired after every successfulaccesses.update, alongside the existing coarse-grainedaccessesChanged. Payload:{ type: 'access-updated', accessId: '<base>:<serial>', serial }. - Chain rules: shared-access permissions must remain a subset of their managing app; narrowing a parent rejects with
offendingChildrenif any managed child would be orphaned. Expiry chain enforced on bothaccesses.updateAND retrofitted onaccesses.create(breaking if you previously created shared accesses with longer expiry than the parent app).
Cross-account Messaging & Consent (CMC plugin)
- New
:_cmc:reserved stream-id namespace + ninecmc/*event types power direct cross-platform / cross-core consent + chat + system-channel flows between two Pryv.io accounts — without shared CA, federation auth, or a central registry. The peer’sapiEndpoint(with its existing access token) is the entire auth surface. - Per-app scoping: data lives under
:_cmc:apps:<app-code>:[<user-path>:]chats:<counterparty-slug>and:_cmc:apps:<app-code>:[<user-path>:]collectors:<counterparty-slug>so an access can be scoped at app-level (:_cmc:apps:<app>:*) or per-request (:_cmc:apps:<app>:<request-slug>:*) via natural prefix matching. - One-shot capability-URL handoff for cross-platform acceptance; bidirectional shared accesses post-acceptance.
- Server-stamped
content.fromon inbox writes (unforgeable counterparty identity). - Auto
accesses.updatepost-hook deliverscmc/system-scope-update-v1to the peer whenever a CMC-tagged access has its permissions changed. - Outbound retry queue persists pending retries as events in
:_cmc:_internal:retrieswith exponential backoff; no new storage primitive. - Backwards-compat: nothing legacy changes; deployments that don’t use CMC see the namespace as inert. No migration required.
- Client SDK:
@pryv/cmc(lib-js sibling package,npm install @pryv/cmc) ships slug + stream-id builders, lifecycle wrappers (createInvite,acceptInvite,sendChat,sendSystemAlert,revokeRelationship,waitForAccept,listAcceptedRelationships,readOffer), and a frozenerrorIdscatalogue mirroring the server-sideCmcErrorIds. Source:lib-js/components/pryv-cmc/. Design docs:components/cmc/README.md,IMPLEMENTERS-GUIDE.md,INTERNALS.md.
Fixes
- Password-reset email (2.0.0-pre.3): the
account.requestPasswordResetmail now reliably embeds the platform’s password-reset URL even when that config value is populated by an override or extra-config plugin. Previously, in some boot orderings the captured config slice missed the value and the Pug template rendered a relative?resetToken=…href that Outlook/Apple Mail QuickLook silently dropped. New template substitution#{RESET_LINK}provides the pre-composed full URL — existing templates using#{RESET_URL}?resetToken=#{RESET_TOKEN}keep working unchanged.
Known gaps in 2.0.0-pre
- OAuth2 authorization-code flow (RFC 6749
/oauth2/authorize,/oauth2/token, client registration, refresh tokens, PKCE) is not in this preview. Clients that need OAuth2-style authorization must continue using the existing/reg/accesspolling flow (now core-affinity aware in multi-core deployments).
- Added Audit from Entreprise version to Open-Pryv.io.
- Refactored Attachments (Event Files) Logic to be modular for future cloud storage of files such as S3.
- Implemented ferretDB compatibility allowing full-open source modules
- Replaced rec.la by backloop.dev
Many under-the-hood changes and a couple fixes, including:
- Stream response to
streams.deletemethod to avoid potential timeout - Deleted stream ids are now already reusable when following the auth process
- Username is not available anymore form
usernamesystem stream. It should be retreived fromaccess-info.
Fixes migration issue when upgrading from 1.6.x to 1.8.0
New features:
- Password policy support: rules for password complexity (length, character categories), age (minimum, maximum i.e. expiration) and reuse (i.e. history) can be enabled in the platform settings under ‘Advanced API settings’
- External data stores support (a.k.a. dynamic mapping, or personal data mapping); enterprise users please contact us for the details.
1.7.14
Section titled “1.7.14”- Fixes two issues with
selfRevokepermissions, one of which related to the system streams backward compatibility flag; the issues caused a crash and prevented creation of accesses withselfRevokepermissions.
1.7.13
Section titled “1.7.13”- Fixes for miscellaneous issues, including issues with the system streams backward compatibility flag (
BACKWARD_COMPATIBILITY_SYSTEM_STREAMS_PREFIX), occasionally sluggish performance when querying events by type, and an occasional failure to restart services after a configuration change.
1.7.10
Section titled “1.7.10”- API change: Don’t coerce the event content values according to type
- Fixes: Allow event types validation for array
- Security fix: make password reset token single-use
Changes:
- Audit has been re-implemented, offering improved performance:
- See the Audit logs guide for API usage
- Audit logs are now available through the Events API, deprecating the previous
GET /audit/logsroute and its data structure
- System streams have been modified. Their prefix changes from
.(dot) to:_system:&:system:. See the System streams page for details. - Tags have been removed from Events. In Pryv.io platforms that contained them, they are migrated to streams, See
BACKWARD_COMPATIBILITY_TAGSplatform parameter in your platform configuration. The tags functionality is ensured by Streams queries for the events.get API method. - Permission levels are computed differently: If a child stream has a different permission than a parent, its level is indeed applied on the child (instead of the higher permission taking precendence as was done before).
- Integrity hash is computed for Events, Attachments and Accesses. This functionality can be disabled.
- Automated platform migration using the migrations.get and migrations.apply API methods.
1.6.20
Section titled “1.6.20”New routes:
- Deactivate MFA for admin API, for when the user has lost his 2nd factor.
1.6.19
Section titled “1.6.19”New routes:
- Get core API method that returns the hostname of the core on which a certain user data is stored.
New Features:
Removals:
- Deprecated “GET /who-am-i” API method removed
- Remove pryvuser-cli, as it is now available through the admin API
Changes:
- Custom auth function has now access to all request headers. See custom authentication guide.
Changes:
- increase JSON input payload to 10MB for HF server. See Data format.
v1.6.0
Section titled “v1.6.0”New features:
- System streams:
- Customizable unique and indexed properties for registration
- Account data accessible through Events API
- More details on System streams
- Admin API:
- Edit platform parameters
- Manage platform users
- More details on Admin reference
- Admin Panel:
- Web application for entreprise Pryv.io platform administration
Changes:
- New registration flow, more details on Account creation
Deprecated:
- Old registration flow
v1.5.22
Section titled “v1.5.22”Changes:
- Deleting an app token deletes the shared accesses that were generated from it (if any).
v1.5.18
Section titled “v1.5.18”New Features:
- Call ‘GET /access-info’ now returns the username to avoid having to extract it manually from
pryvApiEndpoint.
Changes:
- Call ‘POST /user’ (create user) on register. The property
serveris now deprecated in favor ofapiEnpoint.
v1.5.8
Section titled “v1.5.8”New Features:
- Socket.io v2
Removals:
- Socket.io v0.9
v1.5.6
Section titled “v1.5.6”Changes:
- Webhooks API routes now available for
sharedaccesses. - Socket.io interface availablel for
sharedaccesses. - Socket.io interface availablel for accesses with
create-onlypermissions.
v1.5.5
Section titled “v1.5.5”New feature:
- Access permission
{ "feature": "selfRevoke", "setting": "forbidden"}, more details on Access data structure.
New Features:
- Events can now be part of multiple streamIds
authUrlreplacesurlin Auth request in-progress responsepryvApiEndpointreplacesusernameandtokenin Auth request accepted responseaccesses.deletehas been extended for self revocation tosharedandappaccesses
Deprecated:
event.streamId: replaced byevent.streamIdsevent.tags: their functionality will soon be totally replaced by streamIdsurlin Auth request in-progress responseusernameandtokenin Auth request accepted response
Removals:
- Timetracking functionalities have been removed
- singleActivity streams are now standard streams
events.startevents.stop
accesses.update
New features:
- Auth request now accepts a custom
serviceInfoobject, which is returned by the polling url. In case of success, apryvApiEndpointfield is returned. See Auth request for more details. - Add
create-onlypermission level. See the Access data structure for more details. - Add multi-factor authentication for login using the optional MFA service. See the MFA API methods for more details.
- Add auditing capabilities through the Audit API. See the Audit API methods for more details.
- Pryv.io API now supports the Basic HTTP Authorization scheme.
- Release of webhooks to notify of data changes. See Webhook data structure and methods for more details.
- Add route
/service/infothat provides a unified way for third party services to access the necessary information related to a Pryv.io platform. See description for more details. - Most API calls now present a
Pryv-Access-Idresponse header that contains the id of the access used for the call. This is the case only when a valid authorization token has been provided during the request (even if the token is expired). See metadata for more details.
Changes:
- Enrich access-info result with exhaustive access properties.
- Improve the update account API call, in particular when it applies a change of email address. It now correctly checks if the email address is not already in use before updating the account and throws consistent errors.
Deprecated:
- Timetracking functionalities
- singleActivity streams
- events.start
- events.stop
New features:
- High Frequency events allow storing data at high frequency and high data density. Create them by using types that start with
series:X, where X is a normal Pryv type. The API also supports inputting data into multiple series at once, this is called a ‘seriesBatch’ (POST/series/batch). - Add
clientDatafield to Accesses. - Add
httpOnlyflag to server-side cookie sent in response to successful/auth/loginrequest. - Deleted accesses can now be retrieved. See accesses.get method for more details.
- Accesses can now be made to expire. See the access data structure documentation for more details.
Changes:
- Some invalid requests that used to return a HTTP status code of 401 (Unauthorized) now return a 403 (Forbidden). Only the requests that are missing some form of authentication will return a 401 code.
updates.ignoreProtectedFieldsis now off by default. This means that updates that address protected fields will result in an error being returned.
Changes:
- Fix login with Firefox (and other browsers using Referer but no Origin).
- Security fix 2018020801: ‘accesses.update’ was missing an authorisation check.
- Update of the API version in API responses.
- Fix events.get JSON formatting.
- Add configuration options to disable resetPassword and welcome emails.
- Add configuration option to ignore updates of read-only fields.
- Tags have a maximum length of 500 characters. An error is returned from the API when this limit is exceeded.
v1.1.8
Section titled “v1.1.8”Changes:
- Fix edge-case behaviour on very large
streams.deleteoperations. - Direct
events.getAPI call now supports really large results. Changes made have improved this call’s performance by around 30%. As a by-product of this change, we now do not send the ‘Content-Size’ HTTP header anymore. - Allow custom cuid-like ids when creating events.
New feature:
- Versioning:
- a new endpoint on
/events/{id}allows to retrieve a specific event by hisid. Setting theincludeHistoryparameter totrue, the response will contain an array of the previous versions of the event in thehistoryfield.
- a new endpoint on
Validated initial set of API features.
Changes:
- Deletion methods now:
- Reply to permanent deletions with a
{item}Deletionfield confirming the deleted item’s identifier. - Always return code 200 on HTTP (that’s a rollback of the v0.7.x change which was a bit too zealous to be practical).
- Reply to permanent deletions with a
New features:
- Event and stream deletions are now kept for sync purposes; they’re accessible via parameter
includeDeletions(events.get) orincludeDeletionsSince(streams.get). Deletions are cleaned up after some time (currently a year).
Major changes here towards more standardization and flexibility:
- All JSON responses (both in HTTP and Socket.IO) are now structured as follows:
{ "{resource}": {...} }if a single resource item is expected; for example:{ "event": {...} },{ "error": {...} }{ "{resources}": [ {...}, ... ] }if an indeterminate number of items is expected; for example:{ "events": [ {...}, ... ] }
- All responses to resource creation and update calls now include the full object instead of respectively its id and nothing; for example:
{ "stream": {...} } - All JSON responses now include
meta.apiVersionandmeta.serverTimeproperties mirroring the originalAPI-VersionandServer-TimeHTTP headers; HTTP headerAPI-Versionremains - Deleting a resource now returns code 204 if the item was permanently deleted; it still returns a 200 when trashed (now including the trashed item in the response)
- Method ids for deletion/trashing are now
{resource}.deleteinstead of{resource}.del - The
attachmentsproperty of events is now an array (instead of an object), with each attachment now identified by a newidproperty (instead offileName) - As a security measure, reading attached files now either requires auth via the
AuthorizationHTTP header or a newreadTokenquery string parameter (authisn’t allowed anymore in this case); the token to use is specific to each file and access, and is defined in thereadTokenproperty of each event attachment - Event batch creation method has been replaced with generic batch method (
callBatch, HTTP:POST /) - Bookmarks have been renamed to “followed slices”, corresponding method ids to
followedSlices.*and HTTP routes to/followed-slices - Getting events: setting the
tagsparameter now returns events with any of the specified tags, instead of all of them - Error ids:
unknown-*errors replaced with eitherunknown-resourceorunknown-referenced-resourceitem-*-already-existsreplaced withitem-already-existsmissing-parameterreplaced withinvalid-parameters-format
- Other improvements and fixes (data validation performance, minor bugs on auth for trusted apps)
New features:
- Getting events: filter for specific event types with the
typesparameter - Accesses can now define tag permissions in
permissions(in addition to the existing stream permissions)- If only tag permissions are set, all streams are considered readable, and vice-versa
- When stream and tag permissions conflict, the highest permission level is considered
- Full support for managing account information, including password change and reset
Changes to HTTP paths and auth for trusted apps:
- Get streams: removed
trashedoption forstateas it was more trouble than anything useful - Accesses now includes property
id(exposed for referencing)- Create access response now includes both
idandtokenproperties - For existing accesses,
idandtokenare equal
- Create access response now includes both
- Events, streams and accesses now includes change tracking properties:
createdandmodified(timestamp)createdByandmodifiedBy(access id or"system")
- Socket.IO method calls now directly use method ids (e.g.
events.createand pass method params, instead of usingcommandand passing an object with method id and params - For trusted apps only: removed the distinction between “admin” methods and others; breaking changes
/admin/login,/admin/logoutand/admin/who-am-imoved to/auth/login,/auth/logoutand/auth/who-am-irespectivelysessionIDrenamed totokenin login response and SSO cookie data- Personal accesses are now automatically created on login; they can’t be created explicitly anymore
/admin/user-infomoved to/user-info/admin/accessesmerged into/accesses/admin/bookmarksmoved to/bookmarks/admin/profilemerged into/profile
This is a major update that will break most libs and clients, which should be updated ASAP.
- Simplified the API by removing channels and renamed folders into “streams”; adjusted the structure of accesses, streams and events accordingly; more details:
- As a consequence, every event now belongs to a stream
- Data migration: former channels will be converted into root-level streams, and former folders into sub-streams of those
- Events structure:
event.typeis now a string of format{class}/{format}(e.g.picture/attached) instead of an object withclassandformatpropertiesevent.valuehas been renamed toevent.content
- Get events:
- Renamed parameter
onlyFoldersto juststreams - Added
runningboolean parameter, replacing “get running periods” method
- Renamed parameter
- Removed “get running periods” (i.e.
GET /events/running, see above) - Removed
hiddenproperty of streams (ex-folders), which was mostly unused and out of place
- New feature: Allow HTTP method overriding by POSTing _method, _json, and _auth parameters in an URL-encoded request
- Improvement: Retrieving events for a specific timeframe now includes all events that overlap that timeframe, including period events that started earlier
- Added event type validation: the API will now check if an event being created or updated has a known type (as listed on our event types directory), and if yes perform data validation on its value (returning a 400 error if invalid)
- All error ids have been changed to use
slug-styleinstead ofC_CONSTANT_STYLE(so that e.g.INVALID_PARAMETERS_FORMATis nowinvalid-parameters-format); this is consistent with the other ids we’re using in the system
Earlier
Section titled “Earlier”Versions earlier than v0.4 are not covered here.

