Skip to content
DocsSecurity and privacy

Workspace

Security, privacy and your data

Read-only access, credential encryption, what is and isn't stored about visitors and customers, consent, and what happens when you disconnect.

Joinsight reads your billing, search and traffic accounts and joins them to what a small script records on your site. That is a lot of access, so this page sets out exactly what it is: what Joinsight can do with each account, how credentials are kept, what is and isn't stored about your visitors and customers, what an AI model gets to see, and what happens when you disconnect something. The privacy policy is the binding version; this page explains the mechanics behind it.

The short version

  • Every source connection is read-only. Stripe only accepts a restricted key with Read permissions; secret keys are refused.
  • Each stored credential is encrypted with AES-256-GCM under its own data key.
  • Every workspace's data sits behind row-level security in the database, so one workspace's queries can't return another's rows.
  • Customer emails are stored only as a keyed hash plus a masked form such as j•••@e•••.com.
  • The snippet stores nothing on a visitor's device before consent, and Joinsight stores no visitor IP addresses.
  • Claude, when it's used for sentence rewriting, only receives aggregates and labels.
  • The MCP server is read-only, bound to one workspace per connection, and logs every call.

What Joinsight can and cannot do with your accounts

AccountWhat Joinsight holdsWhat it can doWhat it can't do
StripeA restricted key (rk_live_ or rk_test_)Read customers, subscriptions, invoices, prices, products, coupons, events, payment methods and chargesCreate charges, refunds, customers or anything else. Secret (sk_) and publishable (pk_) keys are rejected at connect time
RevenueCatAn API v2 secret key with Read only access to Customer information, Project configuration and Charts metrics, plus a webhook secretRead customers, subscriptions, purchases and RevenueCat's own MRR figure; receive webhook events you configureChange products, entitlements or customers
GoogleAn OAuth refresh token with webmasters.readonly and analytics.readonly, plus openid and email to show which account is connectedRead Search Console and GA4 reportsChange Search Console or Analytics settings
PlausibleA Stats API key and site IDRead visitors by entry page and sourceChange your Plausible site
App Store ConnectOne stored key, meant to be a Sales and Reports or Finance key. An Admin key entered to request reports is used for that one call and never storedDownload the analytics reports; the Admin key only creates the report requestsChange your apps, users or listings. If you connected an Admin key as the stored key, the card asks you to swap it for a Sales and Reports key
InstagramAn Instagram Login token with instagram_business_basic and instagram_business_manage_insightsRead your professional account's posts and their metricsPublish, comment or message
MCP clientsNothing of yours; the client holds a token Joinsight issuedRead MRR, movements, customers, page, keyword and App Store revenue, alerts and data healthChange any data or settings, see your API keys or tokens, see full customer emails, or touch billing or members

Inside a workspace, only owners can connect or disconnect sources, change notification settings, rotate the secret key and delete the workspace. Members can read the dashboard and connect their own AI assistants.

How credentials are stored

Credentials use envelope encryption. Each value, such as a Stripe key or a Google refresh token, is encrypted with AES-256-GCM under a fresh random data key. That data key is then encrypted with a master key that lives in the server's configuration, not in the database. A copy of the database alone doesn't reveal any credential.

Rotating the master key only re-wraps the data keys; the encrypted values themselves are untouched. A single workspace's credentials can also be revoked on their own.

The same encryption covers your Slack and Discord webhook URLs and the RevenueCat webhook secret. Other secrets are stored as one-way hashes, never in a readable form:

  • Your workspace's secret key (js_sk_…) is shown once: when you create the workspace, and again each time you rotate it in Settings, Keys. Joinsight keeps a keyed hash and the last four characters.
  • Sign-in links, sign-in codes, sessions and MCP tokens are stored as hashes.

Once you've pasted a key, Joinsight never shows it again. The App Store card, for example, says the .p8 file is "stored encrypted and never shown again".

How workspaces are kept apart

Every table that holds workspace data has a workspace_id column and a forced row-level security policy in Postgres. The application connects as a role that can't bypass those policies. Each request that touches workspace data runs in a transaction that first declares which workspace it's for, and the database then only exposes that workspace's rows. A query that forgets to filter returns nothing, not someone else's data.

The MCP server, the alert rules, the brief and the dashboard all go through the same mechanism.

Sign-in and sessions

Joinsight has no passwords.

  • Email sign-in. You get a link and a 6-digit code. Both expire after 15 minutes and work once. The link opens a confirmation page where you press Sign in, and the token travels in the part of the URL after #, which browsers don't send to servers. Email security scanners that open links automatically therefore can't use it up.
  • Rate limits. At most 5 sign-in emails per address and 20 per network address in 15 minutes, and 5 wrong code attempts per code. The sign-in page responds the same way whether or not an account exists.
  • Continue with Google, where it's offered, asks only for openid email profile, uses PKCE, and only accepts email addresses Google has verified. It grants no access to your Google data. Accounts are keyed by email, so both methods reach the same account.
  • Sessions last 30 days in an HttpOnly cookie called js_session. Each sign-in replaces the browser's previous session. Settings, Account has Sign out everywhere, which ends every session on every device.

To rate-limit sign-in requests and AI assistant registrations, Joinsight stores a keyed hash of the requesting network address alongside the sign-in token or the registered app. It doesn't store the address itself.

The app sends X-Frame-Options: DENY and frame-ancestors 'none' so its pages, including the MCP consent screen, can't be framed. It also sends nosniff, a strict-origin-when-cross-origin referrer policy, and a permissions policy that turns off camera, microphone and geolocation.

What the snippet stores about visitors

The snippet is written to collect as little as attribution needs. It doesn't set cookies.

Nothing is written to the visitor's device and nothing is sent. On a landing page, the snippet works out the first touch (landing path, referrer, campaign tags) and keeps it in a JavaScript variable. If the visitor signs up before that page is left or reloaded, which in a single-page app includes signups after moving between its routes, identify() sends that first touch along with your user ID. A page reached from another page of your site isn't a landing, so a signup there sends no first touch at all, and the customer stays unattributed. Otherwise the touch disappears with the page.

Once you call joinsight("consent", true), or if the script tag has data-consent="granted", the snippet stores a random anonymous ID and sends one event per browser session, from the landing page. Consent given on a later page stores the ID but sends nothing for that session. The event contains:

  • your site key and the anonymous ID,
  • the landing path, without its query string,
  • the referrer's hostname only,
  • the five UTM parameters, each cut to 200 characters,
  • which ad click parameter was present (gclid, gbraid, wbraid, msclkid or fbclid), but not its value.

Joinsight adds the time of arrival. A returning visitor keeps their original first touch.

At signup

identify() sends your user ID, the visitor's email if you pass it, and either the anonymous ID or the in-memory first touch. The email is turned into a keyed hash on arrival and only used as a fallback match against billing records.

What is never stored

No IP addresses, no full referrer URLs, no page titles, no device fingerprints, and no profile across different Joinsight customers' sites. The ingest endpoints use the network address only in memory, to apply rate limits.

Customer data from billing sources

From Stripe and RevenueCat, Joinsight keeps what MRR and matching need: the customer ID, your user ID if you set one, the country, card expiry month and year, subscriptions and payments. Customer emails are stored only as a keyed hash plus a masked display form, so the dashboard shows j•••@e•••.com and never the full address. RevenueCat subscriber attributes are stripped from stored webhook events.

Because the email is hashed with a secret key, the same address produces the same hash, which is what lets a Stripe customer, a RevenueCat customer and an identify() call be recognised as one person. The hash can't be turned back into the address.

What Claude receives

Joinsight can use Anthropic's API to rewrite an alert's cause or a line of the Monday brief into one plain sentence. It is optional for the service: without it, the same sentences come from fixed templates.

The model receives a small set of facts and a draft sentence: formatted amounts, counts, percentages, page paths, search queries, channel and plan names. It never receives customer records, and any value that looks like an email address is removed before the request is sent. The reply is discarded in favour of the draft if it contains a number that wasn't in the facts, runs past 45 words, or breaks the house style.

The MCP server

When you connect Claude, ChatGPT or Cursor, the MCP server gives that assistant read-only tools.

  • One workspace per connection, chosen on the consent screen. Connect again to add another.
  • Customer rows can include the masked email, your user ID, country, plan, MRR and first touch. Full emails are never returned.
  • Tokens are stored hashed. Access tokens last an hour and refresh tokens rotate on every use. If an old refresh token is presented again, Joinsight treats it as leaked and revokes the whole connection. Unused connections lapse after 30 days.
  • Membership is checked on every call. If someone leaves the workspace, their connections stop working.
  • Every tool call is logged with the tool name, its arguments, whether it succeeded and how long it took, whether it succeeds or not.

Once an assistant has received an answer, what happens to it is governed by your agreement with that assistant's provider. You can disconnect any app in Settings, AI assistants.

Disconnecting a source and deleting a workspace

Disconnecting a source deletes its stored credentials straight away. Joinsight stops reading from it at that moment. The data synced from it is removed by a cleanup job that runs once a day:

DisconnectedWhat the cleanup deletes
Stripe or RevenueCatThat source's customers, subscriptions, billing periods and one-time payments, for Stripe the price-change notes on your charts, and for RevenueCat its stored webhook events. Customers left with no billing record are removed, and MRR is rebuilt
GoogleSearch Console data and GA4 traffic
PlausiblePlausible traffic
App Store ConnectApp Store impressions, page views and downloads
InstagramPosts, their metrics, and the post notes on your charts, which quote the captions

The data is usually gone within a day, and the privacy policy commits to deletion within 30 days of disconnecting. The same deletion runs, before the next import, when a connection switches to another source account: another Stripe account or mode, RevenueCat project, App Store app, Instagram account, Search Console or GA4 property, or Plausible site. Two accounts' data never mix. Disconnecting Google also revokes Joinsight's grant at Google. For other sources, delete the key on the source's side too: Joinsight can stop using a Stripe or RevenueCat key, but only you can revoke it.

Snippet data (visitors, sessions and identify() links) isn't tied to a source connection, so it stays until you delete the workspace.

Deleting a workspace, in Settings, Account, is immediate. It removes every connection, credential, synced number, snippet record, alert, brief and MCP log for that workspace, and it can't be undone. To delete your account itself, email hello@joinsight.app.

Sub-processors and GDPR roles

The privacy policy sets out two roles:

  • Controller for data about you as a Joinsight user: your account, sign-ins, billing and support messages.
  • Processor for data handled on your behalf: what Joinsight reads from your sources and what the snippet collects about your visitors. For that data, you are the controller.

The providers that process data for Joinsight are listed on the privacy page: a hosting provider for the app and database, Brevo for sign-in emails, the brief and alerts, Stripe for Joinsight's own billing, and Anthropic for the optional sentence rewriting. Google, Stripe, RevenueCat, Apple, Plausible and Meta also see Joinsight's requests when it reads from them on your behalf. Where a provider processes data outside the European Economic Area, the policy relies on an adequacy decision or the European Commission's standard contractual clauses.

What to tell your own visitors

Because you're the controller for visitor data, your privacy notice and your consent banner need to describe the snippet. What follows is a factual description of what it does, to copy from. It isn't legal advice: whether you need consent, and which legal basis applies, depends on your jurisdiction. The post on analytics, consent and the GDPR sets out the questions to take to an advisor.

What the snippet stores in the browser

KeyStorageValueWrittenRemoved
js_consentlocalStorage1When consent is grantedOn joinsight("consent", false), or when the visitor clears site data
js_aidlocalStorageA random anonymous IDWhen consent is grantedOn joinsight("consent", false), or when the visitor clears site data
js_ssessionStorage1, marking that this session was countedOn the first event of a browser sessionWhen the tab's session ends, or on joinsight("consent", false)

localStorage entries have no expiry date: they last until the visitor clears them or withdraws consent. There are no cookies.

Put the queue line from the Tracking snippet page before the script tag, so calls made before the script loads aren't lost. Then call the consent API from your banner:

// The visitor accepts analytics
joinsight("consent", true);

// The visitor refuses, or withdraws consent later
joinsight("consent", false);

Granting consent stores the keys above and, on a landing page, sends the session's one event. Refusing or withdrawing removes all three keys from the browser. It doesn't delete what was already sent; a visitor asking for that should contact you, and Joinsight will help you answer. Setting data-consent="granted" on the script tag starts tracking on page load without waiting for a banner, so only use it where consent isn't required or is collected some other way.

A description you can adapt

Joinsight (analytics, attribution). With your consent, we store a random identifier in your browser's local storage (js_aid, with js_consent recording your choice, and js_s in session storage for the current visit). Once per visit, it records the page you landed on, the website that referred you, and any campaign tags in the link. When you create an account, we link that first visit to your account so we can tell which pages and campaigns lead to sign-ups and payments. If you don't consent, nothing is stored in your browser; the page you arrived on and its referrer are only sent if you create an account before leaving that page. IP addresses are not stored. Joinsight processes this data on our behalf.

Adjust it to match how you've set things up. If you pass emails to identify(), or call it from your server, say so. If you use data-consent="granted", the sentence about consent doesn't apply.

Reporting a security issue

If you find a vulnerability or think your data has been exposed, email hello@joinsight.app. The privacy policy commits to telling you without undue delay if a breach affects your data.