Interlinked - The Cube
Privacy Notice
What this product records about a person, who decided to record it, how long it is kept, and how to get it back or have it destroyed.
Start here: this product measures people
It is worth saying plainly rather than burying it. When a gallery sends you a viewing room, the room records which artworks you looked at, for how many milliseconds each, what you zoomed into, what you marked, and how many separate times you came back. Those numbers are combined into a percentage the gallery sees next to your name, which it uses to decide who to follow up with. That is the product. It is not an incidental analytics layer bolted onto something else.
Because of that, the measurement only ever starts after you have been told and have agreed. The screen before the first wall is not a formality: choosing BROWSE PRIVATELY opens no visit, sets no cookie and writes no measurement at all, and the rest of the room works normally. Choosing CONTINUE WITH VIEWING RECORD is what switches the recording on.
The consent you give is stamped with the version of this notice that was in force at
that moment (currently 2026-09-11.1), stored alongside the
timestamp. That is deliberate. A consent recorded as nothing but a date is a claim
that somebody agreed to something, and the words they agreed to cannot be
reconstructed once the text has been edited. A consent whose wording is unknown is
not evidence of anything, so we record which wording it was.
Who is responsible
The gallery that sent you the room is the controller. It chose you, it chose the works, it decided that a viewing record would be kept and what it would be used for. If you want to know why you were sent a room, or you want the gallery to stop, the gallery is the party who decides. Its contact address appears at the bottom of the room and in any copy of your data we give you.
Solaas Tech S.R.L. is the processor. We built and run the software, we hold the data on the gallery's documented instruction, and we do not decide who gets measured. We do not sell data, we do not use it to train anything, and we do not combine one gallery's collectors with another's.
Solaas Tech S.R.L., a company registered in Romania, sole registration code (CUI) 54888013, trade register number J2026038411001, with its registered office at Str. Prieteniei nr. 7, bl. P, sc. B, et. 2, ap. 26, Constanta, Romania. Contact: stoiana00@gmail.com. Interlinked, the Cube is a project of the company named above.
We are the controller for a short and separate list: the gallery staff accounts and their sign-in sessions, keeping the service itself secure and working, and the record of data-rights requests described at the bottom of this page.
What is recorded about a collector
Every row below is a real column in this system. Nothing is listed that the software does not write, and nothing the software writes is left out.
Who the gallery sent a room to
| What | Where | Detail |
|---|---|---|
| Name | recipients.name | As the gallery typed it, or as the collector typed it into an inquiry form. |
| Email address | recipients.email | The address the viewing-room link was sent to, or the one given in an inquiry. |
| Company, city, country | recipients.company / city / country | Optional, gallery-supplied. |
| Opt-in timestamp | recipients.opted_in_at | When the gallery recorded the contact as opted in. |
| CRM counters | contacts.rooms_sent / marks / inquiries | Running totals the gallery sees on its contacts screen. |
Why: Letting the gallery address a named person, and letting the platform deliver the link to them.
Lawful basis: The gallery decides who receives a room and on what basis (typically its legitimate interest in its own client relationship, Art. 6(1)(f), or consent it obtained itself). The operator processes it on the gallery’s documented instruction, Art. 28.
The link itself
| What | Where | Detail |
|---|---|---|
| Room token | room_links.token | The unguessable string in the URL. It is the credential for the room. |
| Expiry, maximum opens, opens used, revocation | room_links.expires_at / max_opens / opens_count / revoked_at | How long the link lives and how many visits it has been spent on. |
Why: Controlling who can open a private room and for how long.
Lawful basis: Necessary to perform the service the gallery contracted for, Art. 6(1)(b) between operator and gallery; Art. 28 instruction as to the collector.
Each visit
| What | Where | Detail |
|---|---|---|
| Visit start, last activity, end | room_sessions.started_at / last_seen_at / ended_at | The sitting. One row per visit, not per page. |
| Consent moment | room_sessions.consent_at | The instant the collector pressed CONTINUE WITH VIEWING RECORD. |
| Which text was consented to | room_sessions.consent_policy_version | The version string of this notice as it stood at that instant. |
| Browser user agent | room_sessions.ua | The raw User-Agent header, stored verbatim. |
| Hashed IP address | room_sessions.ip_hash | Salted SHA-256 of the address, truncated to 32 hex characters. The address itself is never written down; the hash exists to count unique visitors and to recognise abuse. |
Why: Recognising a returning visit, proving that consent was given and to which text, and telling one visitor from another without holding an address.
Lawful basis: Consent, Art. 6(1)(a), taken on the consent screen before anything is measured. A visitor who chooses BROWSE PRIVATELY is not measured at all.
What the person looked at, the measurement
| What | Where | Detail |
|---|---|---|
| Event type | signal_events.type | One of: open, consent, wall_view, work_view, zoom, mark, unmark, inquiry, close. |
| Which artwork | signal_events.work_id | The specific work the event was about, when it was about one. |
| Dwell, in milliseconds | signal_events.dwell_ms | How long the work or wall was on screen. Clamped to 10 minutes per event, so a forgotten tab cannot fabricate an evening of interest. |
| Timestamp | signal_events.created_at | Client-supplied, rejected if it is in the future or before the visit started. |
| Marks | marks.work_id / created_at | Which works the collector shortlisted, and when. |
Why: Producing the gallery’s SIGNALS screens: views and average dwell per work, and a 0-100 "strength" score per named recipient computed from return visits, total dwell, marks, zooms and inquiries (src/lib/signals/strength.ts).
Lawful basis: Consent, Art. 6(1)(a). This is the part of the product that measures a named person, and it is the reason the consent screen is not skippable: nothing in this category is written for a visit that did not consent (`needsConsent()` in src/lib/signals/events.ts refuses it twice, at the route and at the writer).
What the person wrote
| What | Where | Detail |
|---|---|---|
| Inquiry text | inquiries.message | Up to 2000 characters, stored as plain text, never as markup. |
| Thread and messages | threads.subject, messages.body / sender / created_at | The conversation the inquiry opened in the gallery’s inbox, and everything said in it since. |
| Inquiry status | inquiries.status | new / replied / closed, as the gallery works it. |
Why: Letting a collector ask about a work and letting the gallery answer.
Lawful basis: Steps taken at the request of the person before entering a contract, Art. 6(1)(b), and the gallery’s legitimate interest in keeping its own commercial correspondence, Art. 6(1)(f).
The follow-up draft, when a gallerist asks for one
| What | Where | Detail |
|---|---|---|
| What was sent | not stored; assembled per request | The works this person opened, with their own view counts, minutes, zoom-ins and marks; their strength score and visit dates; and their inquiry text truncated to 400 characters. |
| What was removed first | src/lib/ai/facts.ts | Name, email, company and city, including inside the free text of an inquiry, replaced by placeholders. The prompt is then read back and the request refused if any of them survived. |
| The draft | not stored | Shown to the gallerist with the real name substituted in here. It is not written to the database. |
Why: Letting a gallerist ask for a first draft of a follow-up email. It happens on a button press, never on a schedule, never in a batch, and never on page load. The feature disappears entirely if no API key is configured.
Lawful basis: The gallery’s legitimate interest in following up its own enquiry, Art. 6(1)(f). Pseudonymisation is a safeguard, not a basis: we hold the key that turns the placeholder back into a person, so this is still personal data and still a transfer to a processor.
Data-rights requests
| What | Where | Detail |
|---|---|---|
| Request kind and status | data_requests.kind / status | export or delete; pending, verified, processing, done or rejected. |
| Address it was made from | data_requests.email | Needed to verify the request and to answer it. |
| Verification token hash | data_requests.verification_token_hash | SHA-256 of the token in the emailed link. The token itself is never stored. |
| Timestamps and outcome | data_requests.created_at / verified_at / completed_at / note | What was done, when, and what was kept and why. |
Why: Being able to prove that a request was answered and within what time, the accountability duty of Art. 5(2). It is the one record an erasure request does not erase.
Lawful basis: Legal obligation, Art. 6(1)(c), read with Art. 5(2).
What is recorded about a gallery user
| What | Where | Detail |
|---|---|---|
| Email and name | users.email / users.name | What the account was registered with. |
| Password hash | users.password_hash | argon2id. The password itself is never stored and cannot be recovered. |
| Membership and role | gallery_members.gallery_id / role | Which gallery the user acts for, as owner, manager or staff. |
| Sign-in sessions | sessions.id / expires_at / user_agent | One row per signed-in device. `id` is the SHA-256 of the token, never the token itself. |
| Linked Apple / Google identity | user_identities.provider / provider_user_id / email | Only if the user chose to sign in with a provider. |
| Push notification devices | push_devices.token / platform / user_agent | Only if the user turned notifications on in the mobile app. |
Why: Running the account, keeping it signed in, and letting it be recovered.
Lawful basis: Performance of the contract with the gallery, Art. 6(1)(b), and the operator’s legitimate interest in the security of the service, Art. 6(1)(f).
| What | Where | Detail |
|---|---|---|
| Plan and status | subscriptions.tier / status | Which plan the gallery is on and whether it is trialling, live, in grace or cancelled. |
| Stripe identifiers | subscriptions.stripe_customer_id / stripe_subscription_id / stripe_price_lookup_key | Pointers into Stripe. They are not billing details in themselves. |
| Billing period dates | subscriptions.current_period_end / trial_ends_at / grace_until / canceled_at | When the period ends, when the trial ends, and how long a failed payment is forgiven before anything is restricted. |
| Webhook event ids | stripe_events.id | So a repeated webhook is applied once. Nothing about a person. |
| Card details | nowhere | Never on this server. Checkout and the billing portal are pages Stripe hosts; the card is typed into Stripe, not into us. |
| VAT identification number | held by Stripe | Collected at checkout because customers are businesses, and because it decides whether the invoice carries Romanian VAT or a reverse charge. |
Why: Taking payment for a subscription, issuing invoices, and applying the right tax treatment.
Lawful basis: Performance of the contract, Art. 6(1)(b), and a legal obligation for the invoicing and tax records, Art. 6(1)(c).
What we do not do
- No third-party analytics, tag manager or advertising pixel. There is
not one in this codebase. Every page is served by our own server and loads scripts
from nowhere else; the content security policy on every response says
default-src 'self'. - No selling or sharing of anything described here.
- No automated decision with a legal effect. The strength score is a number shown to a human, who decides what to do about it. Nothing in the system acts on the score by itself.
- No profiling across galleries. A collector record belongs to one gallery. The same address at two galleries is two unconnected records.
- No storage of your IP address. Only a salted, truncated hash of it, which cannot be turned back into an address.
Who else sees it
Everything runs on one server we control, with its database on the same machine. These are the only parties data can reach beyond it.
| Who | For what | What reaches them | Where |
|---|---|---|---|
| Cloudflare Ltd. (R2 object storage) only when configured | Storage of artwork images and their derivatives | Artwork image files and the object keys that name them. No collector identifiers, no signal data. | EU (bucket region WEUR) |
| [[SMTP_PROVIDER]] (email relay) only when configured | Delivery of transactional email | Recipient address and display name, the subject and body of the message, which includes viewing-room invitations, inquiry notifications, password resets and data-request verification links. | [[SMTP_REGION]] |
| [[HOSTING_PROVIDER]] (server hosting) | The virtual server the application and its PostgreSQL database run on | Everything in this document, at rest and in memory. | [[SERVER_LOCATION]] |
| Apple Inc. / Google Ireland Ltd. only when configured | Identity providers, only for gallery users who choose "Sign in with Apple / Google" | The sign-in exchange itself. They tell us a subject id and, sometimes, a verified address; we tell them nothing about rooms, collectors or signals. | United States / Ireland |
| Anthropic PBC (Claude API) only when configured | Drafting one follow-up email, when a gallerist presses COMPOSE FOLLOW-UP | What a named collector looked at, without who they are. Sent: the gallery’s public details; for the works that person actually opened, the title, artist, year, medium, dimensions and status together with their own views, minutes, zoom-ins and marks against each; their strength score, session count, total minutes and visit dates; and the text of their inquiries truncated to 400 characters, with their name, company and city replaced by placeholders. Not sent: their name, email, company or city; any price; any image; any other collector or gallery; IP addresses, user agents, session tokens or link tokens; works they never opened. The real name is put into the finished draft here, after the model has written it. Pseudonymous is not anonymous: we hold the key, so this is still personal data and still a transfer. | United States. This application does not pin the inference region. |
| Stripe Payments Europe, Ltd. only when configured | Subscription payments, invoices and the billing portal. Galleries only. | The billing contact and company details a gallery types into Stripe Checkout, including the VAT identification number collected from business customers, and the card itself. Card details never touch this server: checkout and the billing portal are pages Stripe hosts. What comes back and is stored here is a customer id, a subscription id, a price key and the billing period dates. No collector data of any kind reaches Stripe. | Ireland, with onward transfers under Stripe’s own terms |
Apple and Google appear on that list only for a gallery user who chooses to sign in
with them, and that exchange is about the sign-in and nothing else. Where a provider
is outside the EU, the transfer rests on the European Commission's standard
contractual clauses and the provider's own adequacy arrangements. The specific
instrument for each is a point docs/LEGAL.md flags for legal review.
How long it is kept
"Until we get around to it" is not an answer, so here are the actual periods. The rows marked as automatic are executed by a scheduled deletion job that runs against this same table of numbers; the rows marked manual say so, rather than pretending to a timer that does not exist.
| What | Kept for | What happens | Why that long |
|---|---|---|---|
Viewing measurement (dwell, zooms, views, opens)signal_events | 2 years | Deleted automatically Deleted by created_at. | Two years is two full seasons of fairs and biennials, long enough for a gallery to compare this year’s room to last year’s, and the point past which attention data is about a person’s past, not their interest. |
Marks (shortlisted works)marks | 2 years | Deleted automatically Deleted with the visit they belong to (ON DELETE CASCADE from room_sessions), and directly by created_at. | Same class of data as the measurement above; kept and dropped with it. |
Visitsroom_sessions | 2 years | Deleted automatically Deleted by started_at, cascading to its events and marks. | A visit with no events left in it is a row about a person that answers no question. |
Hashed IP address and browser user agent on a visitroom_sessions.ip_hash, room_sessions.ua | 90 days | Blanked automatically, the row kept Set to NULL in place, on visits older than 90 days. | These two are security and abuse-detection data, not analytics. The unique-visitor count they support is a short-term question; after three months they are a fingerprint with no live purpose. Scrubbed to NULL while the visit itself stays, so the gallery’s signal history is untouched. |
Dead viewing-room linksroom_links.token | 1 year | Blanked automatically, the row kept Token replaced with a non-resolving tombstone, one year after it expired or was revoked. | A token is a live credential. One that has been expired or revoked for a year has no purpose, and a stored credential with no purpose is only a risk. The row stays so the gallery keeps the record of who was invited to what. |
Sign-in sessionssessions | 30 days | Deleted automatically Rows past expires_at are deleted. | The cookie lifetime. An expired row authorises nothing and only records that a device once existed. |
Password reset tokenspassword_reset_tokens | 7 days | Deleted automatically Rows more than 7 days past expiry are deleted. Also swept on the write path by the reset endpoint itself. | A spent token is kept for a week so that "I did not ask for this" can still be answered, then it goes. |
Push notification devicespush_devices | 90 days | Deleted automatically Revoked rows deleted after 90 days; live rows not seen for 365 days deleted. | A device that unsubscribed is kept for 90 days so it is not accidentally re-subscribed, then deleted. A device not seen for a year has been wiped or reinstalled and its token is dead anyway. |
Data-rights requestsdata_requests | 3 years | Deleted automatically Completed or rejected rows deleted after 3 years. Unverified rows are dropped after 7 days, because an unverified request is not a request. | Three years, to be able to prove a request was answered and when. This is the record an erasure request deliberately does not erase, Art. 5(2) accountability would otherwise be unprovable. |
Collector contact recordsrecipients, contacts | No fixed period | Nothing automatic. See why. | These belong to the gallery’s own client relationship and the gallery decides how long that lasts. Nothing automatic deletes them. They go when the person asks (see below), or when the gallery deletes the contact, or when the gallery’s account is closed. |
Inquiries, threads and messagesinquiries, threads, messages | No fixed period | Nothing automatic. See why. | A sales enquiry is the gallery’s business record and may be subject to its own accounting retention. Nothing automatic deletes it. An erasure request de-identifies who it was from rather than destroying the gallery’s history of the sale, see /legal/privacy, "what erasure actually does". |
Subscription and billing recordssubscriptions, stripe_events | No fixed period | Nothing automatic. See why. | Kept while the gallery’s account is open. The invoices themselves are held by Stripe under its own retention, and Romanian accounting law fixes how long the issuing company must keep its own copies. That period is an accountant’s answer, not an engineer’s, and it is listed in docs/LEGAL.md rather than guessed at here. |
Follow-up drafts and what was sent to the modelnothing is stored | No fixed period | Nothing automatic. See why. | There is nothing to keep. The prompt is assembled per request and the draft is returned to the screen; neither is written to the database. What the provider itself retains is set by the account’s terms with them, which is a contract to check rather than a column to prune. |
Artwork images in object storageCloudflare R2 | No fixed period | Nothing automatic. See why. | Gallery inventory, not personal data. Deleted when the work or the gallery is deleted. |
Database backups[[BACKUP_RETENTION]] | No fixed period | Nothing automatic. See why. | An erasure removes the row from the live database immediately; it leaves backups when they expire on their own schedule, and backups are never used to answer a query. The retention of that schedule has not been fixed yet, it is listed in docs/LEGAL.md as a value to fill in. |
Your rights, and what happens when you use them
You can ask for a copy of everything held about you (Article 15 and Article 20), have it corrected (Article 16), have it erased (Article 17), have its use restricted (Article 18), or object to it (Article 21). Where the basis is your consent, you can withdraw it at any time, and withdrawing does not undo the past but does stop the future.
The form is here. It needs no account. We answer within one month, as Article 12(3) requires.
Why you will be asked to click a link in an email
If you have no account, the only thing identifying you is an email address, and an email address is not proof of anything. So a request made with one does nothing at all until you use the link we send to that address. The link works once, expires in 24 hours, and asking again cancels the previous one. Without that step, anybody who knew your address could read your viewing history, or erase it.
What erasure actually does
Being specific here matters more than sounding reassuring. When you ask us to erase your record:
- Destroyed outright. Every visit, and with it every dwell measurement, every view, every zoom, every mark, the hashed IP and the browser string. The strength score is calculated when a screen is drawn and never stored, so it disappears with the numbers behind it. There is no anonymised copy kept for analytics: the measurement is the thing you objected to, and keeping it in a rearranged form would be answering your request with a rearrangement.
- Revoked. Any viewing-room link cut for you stops working immediately, and its token is replaced, so a copy of the URL in somebody's inbox cannot reopen the room.
- Blanked. Your name, address, company, city and country in each gallery's contact record are replaced with a blank. After that the record identifies nobody.
- Kept, and here is the honest part. An enquiry you sent a gallery, and the conversation that followed, is that gallery's own business record of its trade, and the gallery is the controller of it. We do not destroy a customer's sales history because a third party submitted a form; doing so would be us exceeding our instructions. Those records survive, attached to the blanked contact, so they no longer lead back to you. If you want them gone as well, write to the gallery, whose address is in the copy of your data. The gallery can then have them purged, and we will do it.
- Kept deliberately. The record that you made the request, when it was verified and when it was answered. That record is the only thing that proves we answered you, and Article 5(2) requires us to be able to prove it. It holds your address, the kind of request and its outcome, and nothing else.
Backups are a separate matter and we will not pretend otherwise: an erasure takes effect in the live database at once, and a copy may remain inside an encrypted backup until that backup expires on its own rotation. Backups are only ever used to restore from a failure, never to answer a query, and a restored row is erased again.
Security
- Passwords are hashed with argon2id and never stored or recoverable.
- A sign-in token is held by you; the database keeps only its SHA-256, so a stolen database cannot be replayed as a live session. The same pattern protects password reset links and the verification links on this page.
- Room links use unguessable tokens, can be revoked, can expire, and can be limited to a number of visits.
- The referrer policy on every page is
strict-origin-when-cross-origin, so a room token in a URL is never leaked to a third party in a referer header. - Every query for gallery data is filtered by the gallery the session belongs to, and every collector action is re-checked against the room the token opened.
Contact, and complaints
Write to stoiana00@gmail.com, or use the form.
We have not appointed a Data Protection Officer. Whether this processing requires
one under Article 37(1) is one of the questions docs/LEGAL.md puts to a
lawyer; if the answer is yes, this line will be replaced with their details.
You can also complain to the Romanian supervisory authority without going through us first: Autoritatea Nationala de Supraveghere a Prelucrarii Datelor cu Caracter Personal (ANSPDCP), https://www.dataprotection.ro, anspdcp@dataprotection.ro, B-dul General Gheorghe Magheru 28-30, Sector 1, 010336 Bucharest, Romania.
Changes
This notice carries a version, currently 2026-09-11.1, and the
version is recorded against every consent given while it was in force. When the version
moves, consents recorded under the old one are consents to different words, and the
stamp is what lets us tell the two apart instead of guessing. Re-asking a collector
whose consent predates a material change is a screen that has not been built yet; it is
listed as owed work in docs/LEGAL.md, and this sentence will say
something stronger when it exists rather than before.