Privacy statement
Alcove is open-source software that lets two partner agencies find the records they have in common without revealing the records they do not share. This statement describes what the project itself collects, transmits, and retains, and what the supporting services an exchange relies on can observe. It is written for the agency security, compliance, and privacy reviewers who ask for a privacy policy by name.
Owner: the Alcove maintainers. See Review and ownership.
This is not a privacy notice for your agency's own data subjects, and it is not a Privacy Impact Assessment (PIA). A PIA is completed per deployment and turns on facts only the deploying agency holds -- the system owner, the authority for collection, the populations involved, and the retention schedule. This statement and docs/COMPLIANCE.md are the source material an agency uses to complete one.
The project's role
- Alcove is software you run, not a service run on your behalf. The one exception is the hosted deployment of the web application, covered below.
- The deploying agency is the sole controller of the data it processes. The project is not a controller, processor, or business associate for that data. It never receives it.
- The project collects, transmits, and retains no personal data on its own behalf, in either deployment. There are no accounts, no registration, no license check, no update ping, no usage analytics, and no telemetry.
- What the two parties disclose to each other is governed by their data sharing agreement, not by this project. Alcove enforces the protocol; the agreement decides what may be exchanged under it.
The statements in this document about what Alcove connects to have a mechanical backstop: a repository check (npm run check:egress-claims) fails the build when the shipped source trees gain a URL literal naming a host under one of the schemes it reads -- http, https, and the STUN and TURN schemes -- outside a reviewed allowlist. The check is a backstop rather than a proof of no egress, and it is narrower than the claims it guards: a literal under another scheme (a wss:// beacon), one added to build configuration outside the scanned trees, a host assembled at runtime, a URL an author spelled around the check by splitting or encoding it, and a connection made inside a dependency are all outside its reach. Its limits are in docs/SECURITY_DESIGN.md.
Two deployments
The answers below differ by deployment. Read the section matching what you are deploying. Who operates each part of a deployment, area by area, is in docs/SHARED_RESPONSIBILITY.md.
Container deployment (CLI and local console)
Both deployments are suitable for real exchanges. This one runs every component on infrastructure you control.
- What the project operates: nothing. The container runs on your machine or in your infrastructure. No component of it reports to the project.
- What it connects to: only the SFTP server or shared directory you configure for the exchange, or for a WebRTC exchange the coordination server, STUN and TURN servers, WebSocket relay, and relay registrar its configuration names (see What supporting services can observe). One more request is made when you invite at a web app's address (
alcove invite https://..., orhttp://..., in which case the read is unencrypted): the CLI reads that app's public/alcove.jsonfile, once, to learn which coordination server the app uses. The request sends nothing about the exchange; the app's host sees the request and your IP address, as for any page load. The container makes no other network connection. - The local console is served by that same container to your own machine over loopback and is not reachable beyond that host (docs/CONSOLE.md). It runs every exchange by starting the CLI in the container and runs none in the browser, so it connects to what the CLI connects to for the same configuration: the SFTP server or shared directory, or for WebRTC the partner's computer and the coordination server, STUN servers, relays, and registrar that the operator, an accepted invitation, or an opened
alcove.yamlnames, with the CLI's default STUN server (stun.l.google.com:19302) where none names a STUN server. An openedalcove.yamlwhoseconnection.server.provisionblock is in start mode also makes the run call that provisioning endpoint before each exchange, asalcove exchangedoes. Given a web app's address, it reads that app's public/alcove.jsononce, asalcove invitedoes. - What the project can observe about your exchanges: nothing, unless a WebRTC exchange uses its coordination server or its relays. It receives no data and no record that an exchange occurred. The exception is a WebRTC exchange through the coordination server the public Alcove web app names, which
alcove invite https://psi.data-bridge.organd the console's Use the Alcove web app's server choose, or one that uses the project's relays (Hosted web application): those services then see what What supporting services can observe states, and never exchange content. - Distribution is the one third-party touch. Pulling the container image from a public registry tells that registry's operator that the pull happened (What supporting services can observe). The project does not operate the registry.
Hosted web application
- Status: suitable for real exchanges, including important one-off ones. The project operates it on a best-effort basis, with no uptime commitment.
- Where data is handled: entirely in your browser. Cloudflare Pages delivers the application code. Your input file is read, matched, and written locally. What leaves your browser is encrypted before it is sent to your partner: the coordination server helps the two sides set up the connection, and a relay that carries the connection forwards encrypted packets it cannot read. This is a reviewed property of the codebase rather than a browser-enforced one; the limits of that claim against an injected script are stated in docs/SECURITY_DESIGN.md.
- Which coordination server an exchange uses: the one its invitation names. An invitation created on a hosted site names the project's coordination server, below, unless the inviting user set their own coordination server in relay settings; that setting is kept in their browser and applies to the invitations it creates from then on (docs/WEB_APP.md). If the browser cannot read the saved setting, a new invitation it creates names the project's server, and the review step before it is created warns of this; a re-invite from a saved exchange names the server that exchange already uses. Both parties of an exchange whose invitation names another server contact that server and not the project's. An invitation from another deployment of the application names that deployment's server, which a hosted site accepting it contacts.
- When an exchange uses the project's relays. The project runs a TURN relay, a WebSocket relay, and the relay's registrar, below. On
psi.data-bridge.organdstaging.data-bridge.org, a browser exchange uses none of them unless the user turned them on, under Use this site's relay server in relay settings, the browser's own relay setting names them, or the exchange was accepted from an invitation that names them, whichever site or party made that invitation; an exchange accepted from an invitation uses each relay only where the invitation names that relay. A command-line party uses them where its connection configuration names them or it accepts an invitation that names them; an invitation from a hosted site names them where its inviter uses them. Either way, the exchange's Allowed relays can leave them out, and you can use your own relay instead; to keep one out of an exchange, both parties leave it out (docs/COMMUNICATION.md). - The demo site,
demo.data-bridge.org, is a second deployment of this application. Its build names the same coordination server and the same relays aspsi.data-bridge.org, so what this section states about those services holds for an exchange on it. It differs in one default: every browser exchange on it uses the project's relays, unless its Allowed relays leave them out (docs/DEPLOYMENT.md). - What the project operates, and what it can therefore observe (verified 2026-10-07 on the Cloudflare account, the AWS account and the host):
- No server for the application's pages. The pages are static files served by Cloudflare Pages with no server-side code, and the project keeps no log of page loads. Cloudflare's own records of those requests are covered below.
- The peer-coordination server at
signal.data-bridge.org, which brokers the browser-to-browser connection: it handles the signaling it relays, the derived rendezvous identifiers, connection timing, and client IP addresses. Its TLS front also receives the headers a browser sends with every request, such as the user agent and the page's origin. Its access log records neither. It never sees exchange content, which is end-to-end encrypted under the application-layer encryption keyed by the two parties' shared secret (see docs/SECURITY_DESIGN.md). It runs on a host of the project's own behind a TLS front the project also runs, and Cloudflare does not carry its traffic. The host is infrastructure from Amazon Web Services (AWS), inus-west-2.
- The project keeps one log: that host's system journal. It stays on the host; nothing copies it elsewhere. The host's configuration deletes an archived journal file, whole, once its newest entry is 90 days old, so an entry is kept for about 90 days after the newest entry in its file. The configuration archives the active file daily, but a file can span more than a day: the archived files on the host, measured on 2026-10-09, each span 1 to 8 days, so an entry can be kept up to that many days beyond 90. The journal also has size limits, journald's defaults, which can only delete entries sooner. It holds:
- The host's 443 front's error log: one listener on port 443 passes each connection to the TURN relay or to another service on the same host, the coordination server's TLS front among them, by the server name it asks for. It decrypts the TLS of a TURN connection itself, which holds only the TURN messages and the relayed traffic's own encryption, and of a WebSocket relay connection, which holds what the WebSocket relay below sees, and passes every other connection on unopened. It keeps no access log. Its error log holds warnings and errors; when the service a connection asks for refuses it, does not accept it in time, or resets it, the line names the client IP address beside that service's local port, which identifies the service. It holds at most 8 connections to the WebSocket relay from one IP address at once, and writes such a line for each further connection it closes.
- The TLS front's access log: for each request, the time, the client IP address, the method, the request path without its query string, the status, the Upgrade header, the bytes sent, the duration, and the request's length in bytes; for a request passed to the coordination server, also that server's status, the time taken to connect to it, and the time it took to answer. No user agent. A request that arrives through port 443 is logged with the 443 front's address,
127.0.0.1, in place of the client IP address; one made to port 8443 is logged with the client's. Because the query string is not recorded, a signaling connection's line holds no rendezvous identifier and no client token. - Earlier access log lines with the full request: lines written before 14:00 UTC on 2026-10-07 also hold the query string, which carries the rendezvous identifier and the client token beside the client IP address. Those lines were deleted from the host on 2026-10-09: 56 journal entries, which record 14 requests, all written before that time. None were written after it.
- The TLS front's error log: critical errors only (nginx's
critlevel and above). A request the coordination server refuses, resets or does not answer, a request body that is too large or is buffered to disk, a malformed request line, malformed headers and an oversized header line each write no line, so the error log holds no request line, no query string, and so no rendezvous identifier or client token, and no client IP address beside them. A check run against the front's image on each pull request that changes its configuration or image checks this for each of those requests (infra/broker/README.md, Logging). A critical error about a request, such as the front failing to write a buffered request body to its temporary storage, can still write a line that holds the request line, and a critical error about a connection, such as some failed TLS handshakes, can write a line that names the client IP address. - Earlier error log lines with the request line: before the error log was limited to critical errors, it held warnings and errors. Measured on 2026-10-26 with the front's image, nginx 1.31.6: a line about a request, written when the coordination server refuses, resets or does not answer the connection, or when a request body is too large or is buffered to disk, holds the client IP address as the access log gives it, the request line with its query string, and so the rendezvous identifier and the client token, and the request's Host and Referer headers. It holds no user agent and no Origin header. Such lines already in the journal are kept under the retention above.
- The coordination server's own output: start-up and service lines, with no client IP address and no rendezvous identifier.
- The project's TURN relay at
turn.data-bridge.orgruns on the same host. An exchange uses it on the terms in Hosted web application, above; what it observes is in What supporting services can observe. It forwards encrypted traffic it cannot read and stores none of it. Every client reaches it through the 443 front, which passes it the client's IP address, so where its lines name a client's address and port, as when a connection fails, the address is the client's. They also hold the credential's username when authentication fails. The username is an expiry time and a fixed label; it identifies no party and no exchange (docs/notes/webrtc-relay-deployment.md). - The project's WebSocket relay at
relay.data-bridge.orgruns on the same host, behind the 443 front, for a network that blocks connections through the TURN relay. It is the WebSocket relay the hosted sites' builds name (checked 2026-10-25), and an exchange uses it on the same terms as the TURN relay. The exchange moves onto it only when no WebRTC connection opens. Like the TURN relay, it forwards encrypted traffic it cannot read and stores none of it. The front passes it no client IP address, so its own lines name none: a line when it starts, a warning when it cannot read the relay's key table, and a fault with one exchange's key, which names that exchange's id (infra/relay/README.md). - The relay's registrar at
registrar.data-bridge.org, on the same host, where each browser exchange that lists the project's relay, and each command-line party accepting an invitation that names it, enrolls, before it connects, the relay key derived from its shared secret, under an exchange id derived from the same secret (docs/spec/PROTOCOL.md). It keeps the key and the id in the relay's table until the enrollment lapses, about 70 minutes later. That key is the exchange's one relay key: a party's own relay and registrar, where it has them, accept the same key until it rotates. Its lines name the exchange id, the time and the outcome of each enrollment, and no IP address and no key (infra/relay/README.md). - The host's own system lines, including the SSH server's, which can hold the address of a machine that connected to it.
- Cloudflare, a third party, serves the pages at
psi.data-bridge.org,staging.data-bridge.organddemo.data-bridge.organd keeps its own records under its own retention, not the project's:- Measured on the account: the zone has no Logpush job, and the Pages project serving
psi.data-bridge.organdstaging.data-bridge.orghas no server-side functions, stores no log, and has Cloudflare Web Analytics turned off. The Pages project servingdemo.data-bridge.org, uploaded the same way with no server-side functions, has Cloudflare Web Analytics turned off too. - Request analytics: Cloudflare documents that its HTTP request analytics "retain at least 31 days of data" for a zone on the Free plan, which this one is. That is a minimum; Cloudflare states no maximum, and does not state which fields of each request it stores for a Free plan zone.
- Raw request logs: Cloudflare documents that "By default, your HTTP request logs are not retained", and that when retention is turned on they can be retrieved going back at least 3 and up to 7 days. Whether it is turned on for this zone has not been checked.
- Not established: what Cloudflare keeps for a request made straight to a project's
pages.devaddress, such asalcove-link.pages.dev, which does not pass through the zone; and how long Cloudflare keeps its own internal edge logs. - Not carried by Cloudflare:
signal.data-bridge.org,turn.data-bridge.org,relay.data-bridge.organdregistrar.data-bridge.orgare DNS-only names in the zone. Cloudflare answers DNS lookups for them and carries none of their traffic.
- Measured on the account: the zone has no Logpush job, and the Pages project serving
- Google, a third party, runs the default STUN server at
stun.l.google.com:19302. A browser on this site contacts it only where neither its relay settings nor the invitation it accepted names a STUN server; where either names one, the browser contacts the STUN servers they name in place of Google's. An invitation names the STUN servers its inviter uses, so an inviter who names their own keeps Google's server out of the exchange for both parties, unless the partner runs an older copy of the web app, which still adds Google's server beside the invitation's (docs/WEB_APP.md). - What it does not do: no accounts, no cookies, no analytics or third-party tracking scripts, and no script, style, or font loaded from a third-party host. The application makes no request to any host other than the supporting services named below.
- What it stores stays on your device. A managed (recurring) exchange keeps its record -- the partnership label, the agreed column shape, the rendezvous locator, the schedule, the run outcomes, the rotating shared secret, and the address of any relay registrar you enroll it at -- in browser storage. None of it is sent to a server; an exchange you enroll at your relay's registrar sends that registrar the relay key derived from the current secret after each run, as the table below describes. Removing the managed exchange erases it (see docs/MANAGED_EXCHANGE.md). The at-rest threat model for that shared secret is in docs/SECURITY_DESIGN.md.
What supporting services can observe
An exchange relies on services that are operated by one of the parties, by the project, or by a third party, depending on the channel and the deployment. Who operates a service is who answers for it. None of them sees the identifiers used for matching: those never leave the party that holds them. This table is the one statement of what each service can observe; how the channels are protected is in docs/SECURITY_DESIGN.md.
| Service | Typically operated by | What it can observe |
|---|---|---|
| Page hosting (Cloudflare) | Cloudflare, which serves the hosted web application's pages through Cloudflare Pages for the project; not present if you deploy the web application yourself | Every request for the hosted application's pages: your client IP address, the requested path, timing, and the pages it serves, including a command-line party's one read of /alcove.json when it invites at the site's address. It carries none of the coordination server's, the TURN relay's or the WebSocket relay's traffic, and never data-channel content: the WebRTC session runs browser to browser under DTLS it does not terminate. It keeps request records under its own retention (Hosted web application). |
| Coordination host (AWS) | Amazon Web Services, which provides the infrastructure the project's coordination server, TURN relay, WebSocket relay, relay registrar, and TLS front run on; not present for an exchange that uses none of those services | The host's network metadata (client IP addresses, connection timing, and volume) and its disk. No exchange content: the services on the host see none, as the rows below state. |
| Peer coordination (signaling) | The project, for the hosted web application, unless the invitation names another server, such as one the inviting user set in relay settings; you, if you deploy the web application yourself or set your own server there; or a public third-party service if you point at one | Rendezvous identifiers, connection timing, and client IP addresses. Never exchange content, which is end-to-end encrypted under the application-layer encryption keyed by the two parties' shared secret. Hosted web application lists every log the project's server keeps and how long each is kept. |
| STUN | A third party. Google runs stun.l.google.com:19302, which the CLI uses when a run lists no STUN url (docs/CLI.md) and the hosted web application uses on the terms in Hosted web application. Otherwise whoever runs the STUN servers the configuration, relay settings, or invitation names | The client IP address that queried it, and the fact of a session, and nothing further. STUN is used to discover a public address before the connection is established. |
| TURN relay | The project, for an exchange that uses the project's relay on the terms in Hosted web application; otherwise whoever you configure, commonly a commercial ICE service, or a relay one party operates and names in its browser's relay settings, in its connection configuration, or in the invitation | Each party's network address and when it connected, on every exchange that lists it, whether or not any traffic is relayed: a party's software reserves an address on the relay while it gathers candidates, before it knows if the direct path will work. The address of the machine running a relay check that contacts it (alcove doctor relay, or Test these relays in the browser). When traffic is relayed, also the traffic volume between the two endpoints. It forwards encrypted DTLS packets without terminating the session, so it cannot read content. The reference relay deployment deletes each journal entry about 90 days after the newest entry in its file (see Retention). |
| WebSocket relay | The project, for an exchange that uses the project's relay, on the same terms as its TURN relay; otherwise the party that runs one and names it in its browser's relay settings, in connection.websocket_relay, or in the invitation. No commercial service runs this relay | Each party's network address and when it connected, on every exchange that lists it, whether or not the exchange moves onto it: each party connects to it as its run starts waiting for its partner. The exchange id and role each party presents to be paired. When it carries the exchange, also the volume each way and the size of each message. It forwards the exchange's messages without reading them and stores none of them; they are under the application-layer encryption keyed by the two parties' shared secret, apart from the key-exchange messages that set it up, which hold no exchange data. The reference deployment keeps its lines in the same journal as the TURN relay's, under the same retention (infra/relay/README.md). |
| TURN relay registrar | The project, contacted before it connects by a browser exchange that lists the project's relay, and by a command-line party accepting an invitation that names it; otherwise the party that operates the relay, when its configuration names the registrar (connection.relay_registrar) or a saved exchange in the web application is enrolled there, contacted by the CLI and by the web application's saved exchanges | The exchange id -- the one the operator chose, or, for an enrollment without a token, one derived from the exchange's shared secret -- the relay key derived from the exchange's current shared secret -- which the relay holds to accept credentials, and which the exchange's other relays and their registrars accept as well until it rotates -- the client IP address, and the time of each registration. The client IP address of each alcove doctor relay of a configuration whose run would enroll there. Never the shared secret or anything else about the exchange. The project's registrar keeps an enrollment for about 70 minutes (Hosted web application). |
| Shared SFTP server or shared folder | One of the two parties, or a third party both trust | The exchange's files. What those files reveal depends on the exchange -- see below. |
| Container registry | A public registry the project does not operate | That an image pull happened. The project receives only whatever aggregate pull counts the registry publishes to image owners. |
The SFTP and shared-folder case is the one that turns on how the exchange is set up:
- A recurring, authenticated exchange wraps the exchange in application-layer encryption keyed from the two parties' shared secret, so the server operator sees ciphertext and file timing rather than the exchange's contents. The few files outside that wrap hold no exchange data and no identifiers used for matching (docs/SECURITY_DESIGN.md).
- A zero-setup exchange carries no such key and relies on the transport alone -- SSH in transit, and the server's own access controls at rest. The server operator can read the files that pass through it, which include the payload values disclosed for matched rows. Where the server is outside both parties' control, prefer an authenticated exchange.
What Alcove retains on your systems
- The shared secret in the key file is the only persistent credential; it is stored owner-only and rotates after every successful exchange (see docs/SECURITY_DESIGN.md).
- The output file pairs your own row identifiers with the matched partner records and the columns the partner disclosed. The identifiers used for matching are not part of it. Its retention and disposition are yours to govern.
- The exchange record each party writes is a local, self-attested log of what it disclosed and carries no protected values (see docs/spec/EXCHANGE_RECORD.md).
- Logs contain no PII. Operational logging is limited to non-sensitive metadata.
The complete data-handling account is in docs/SECURITY_DESIGN.md.
Requests from data subjects
The project holds no personal data and cannot respond to access, correction, or deletion requests. Direct them to the agency that operates the deployment, which is the controller of the data it processed.
Reporting a privacy concern
- If the concern is not security-sensitive, open a GitHub issue tagged
compliance. - If it is security-sensitive -- anything that could expose data -- follow the private reporting process in SECURITY.md instead. Do not open a public issue.
Review and ownership
- Owner: the Alcove maintainers. Privacy review is a maintainer responsibility rather than a named individual's; use the reporting channels above rather than contacting a person.
- Last reviewed: the
last_revieweddate in the front matter at the top of this document. - Cadence: this statement is reviewed on any change that affects what Alcove collects, transmits, or retains, and at least annually regardless of whether anything changed. Every revision and its date are recorded in this repository's version history.
See also
- docs/COMPLIANCE.md - regulatory framings, data classification, and the considerations a privacy review should cover
- docs/SHARED_RESPONSIBILITY.md - the deployment model and what the project operates versus what the deploying agency operates
- docs/SECURITY_DESIGN.md - threat model, data handling, channel security, and the at-rest model for managed exchanges
- SECURITY.md - vulnerability reporting and supported versions
- SUPPORT.md - where to direct other questions