Privacy & data handling
What we store, how long, who we share it with, and what we explicitly don't store.
DevUpdate.io is a developer tool, so the trust bar is high. This page lists what we keep, what we don't, and the third parties involved.
What we store #
-
Account data: email, hashed password (or OAuth identity), username, timezone, digest preferences. We don't ask you to invent a username when you sign up: we derive one from the part of your email address before the
@(lightly normalized, and suffixed if that handle is already taken), and you can change it any time under Settings → Account Information. It is a display and sign-in handle, nothing more, and it never leaves your account except to team members you share a workspace with. See Your username for the details. If you sign up with GitHub and your primary email is set to private, GitHub gives us ausers.noreply.github.comaddress we can't deliver mail to, so we'll ask you for a reachable email before you can use the app, and verify it via a one-click confirmation link. -
Signup attribution: how you first arrived, recorded once when the account is created and never updated afterwards. Always: the page you landed on and, if another site sent you, its hostname. Additionally, if you arrived through a marketing link: the campaign parameters that link carried (
utm_source,utm_medium,utm_campaign,utm_term,utm_content) and one ad-network click identifier if present (e.g.gclid). We store the path you landed on, not the query string, and the referring site's host, not its full address: another site's URL can carry their own session tokens or your email address, and we don't want them. It is never sold, and it is deleted with your account. Arriving directly, with no referrer, means only the landing page is recorded; the campaign fields stay empty.It is first-party with one deliberate exception, and only with your consent: if you clicked one of our LinkedIn ads, we tell LinkedIn that the click became a signup. See LinkedIn conversion measurement below for exactly what that does and does not include.
To carry this from the page you landed on to the moment you create an account (possibly several clicks, or a trip through GitHub or Google sign-in, later), we set a single first-party cookie named
du_attrwhen you arrive from a campaign link or another site. It holds only the values listed above, expires after 30 days, and isHttpOnly, so no script on the page can read it. It is not set at all if you arrive directly.This one cookie is set before you answer the cookie banner, and is the only non-essential cookie for which that is true. We think that is the honest trade: gating it on the banner would not make it optional so much as make our own acquisition numbers a measure of who accepts banners. If you would rather it were not set, block cookies for this site. Nothing in the product depends on it.
Answering the banner writes a second cookie,
du_consent, holding nothing but which button you pressed (acceptedorrejected) for six months. Our backend cannot read the banner choice otherwise (it lives in your browser's local storage), and one server-side decision now depends on it (LinkedIn conversion measurement, below). Recording the answer is not a use of it, which is why the cookie is written either way. -
Source list: the GitHub URLs and vendor URLs you've added as tracked sources.
-
Tracker requests: if you ask us to build a deterministic parser for a source (the "Request a deterministic tracker" flow), we store the URL you submitted, the optional product name and note, and which account raised it, so we can prioritize by demand and follow up. These rows survive account deletion (the user link is nulled) as an anonymous demand signal.
-
Lockfile snapshots: package names and versions extracted from uploaded lockfiles. The original file is not retained.
-
Package→repository match cache: which GitHub repo publishes a given public package, cached platform-wide so the same package is resolved once rather than once per customer (see Package matching). A row holds the package name, its ecosystem, and what the public registry answered, and no link to any account, because the answer isn't specific to one. We only write a row when the registry confirms the package exists there, so a cached name is public by definition. A package name the public registry does not recognise (the shape an internal package from a private registry takes) is never written to this shared table; those are re-checked per upload instead. Your own lockfile's full package list stays in your account's own data, above.
-
Release analysis output: summaries, risk scores, and detected changes for each release we've processed. Cached so the AI never has to re-analyze the same diff twice.
-
Security-issue matches: for each monitored dependency, which public OSV/GHSA advisories affect the version you pin (advisory ids, severity, fix status, and the advisory's own workaround text). Derived entirely from your parsed package + version pairs and public advisory data; refreshed daily and deleted when the issue resolves or the lockfile is removed.
-
Public discussion metadata. For each tracked GitHub repo we periodically read its public open issues so the Pulse "News & discussions" section can surface ongoing problems. We store the issue title, URL, author handle, reaction/comment counts, labels, and a short excerpt of the body (public data only). Private-repo issues are only read when you've explicitly installed the DevUpdate.io GitHub App on the repo (read-only permissions, revocable from GitHub).
-
Billing & usage metering: subscription tier, billing cadence, invoice history, AI-usage events (token counts per operation, kept for internal cost monitoring and fair use, never billed per use), and the EU CRD §16(m) withdrawal-waiver consent record (timestamp, IP, terms version).
-
Authentication tokens: RS256-signed JWT access tokens (1 h) and refresh tokens (30 d), plus hashed API tokens for programmatic access. Refresh-token reuse is detected via a per-session JTI; a detected replay invalidates every active session for the affected account.
-
Connected AI clients (hosted MCP server): when you authorize an MCP client (Claude, Cursor, etc.) via OAuth, we store the client's self-registered metadata (name, redirect URLs), the scopes you approved, and SHA-256 hashes of the issued access and refresh tokens (never the raw token values), plus created/last-used timestamps so the Settings list can show you what's connected. Revoking a client from Settings → Connected AI Clients deletes its access immediately; consent grants and revocations are recorded in the audit log.
-
GitHub App user access token (when you connect GitHub): a short-lived (8 h) user-to-server token, plus its refresh token, stored against your account row, Fernet-encrypted at rest with a key that lives in the runtime environment, not the database, so a DB snapshot alone doesn't surface the tokens. The backend can call GitHub as you and let GitHub's own permission model decide which repositories we can read. The token is issued by the DevUpdate.io GitHub App with read-only access to repository contents and metadata (no write, ever) and scoped to repositories you explicitly grant when installing the App. The exact GitHub scopes we request are:
- Repository permissions:
contents: read,metadata: read - Account permissions:
email addresses: read
No other repository, organisation, account, or write permission is ever requested. Both tokens are deleted when you disconnect GitHub or delete your account. Revocable at any time from github.com → Settings → Applications. Revocation from GitHub takes effect on our next call without any action on our side.
The Repositories with access panel in the Lockfiles tab lists the repos you've granted the App by calling GitHub's user-installations API as you at view time. This needs no additional scope beyond the read-only access above, and the list itself is not stored: it's fetched on demand and only briefly cached server-side to stay within GitHub's rate limits.
- Repository permissions:
-
Session metadata: IP and User-Agent per session for security audit, with a 30-minute idle timeout and a 3-concurrent-session cap.
-
Audit log: an append-only
audit_eventstable records security-relevant actions on your account (login success/fail, logout, refresh-token reuse detection, password reset, account deletion, AI-client MCP authorizations and revocations) with timestamp, IP, and user-agent. Used internally for incident review; not shared. -
Email send log: an append-only
email_send_logtable records each transactional or lifecycle email we hand to AWS SES for delivery (verification, password reset, the day-4 unverified warning, and the inactive-account nudges described under Retention below), with your address, the email type, and a timestamp. The row reflects that SES accepted the send request, not that the message reached your mailbox (we don't track bounces or opens here). Used internally for abuse defense and to confirm our lifecycle email loops dispatched; not shared.
How tenant isolation is enforced #
Every table that holds user-owned data — tracked sources, lockfiles,
share links, OAuth connections, sessions, notifications, your audit log,
your connected AI clients, and your billing and usage records — is
isolated by two layers. First, requests from your authenticated dashboard
and API scope every query against these tables to your own user id. The
one deliberate exception is a share link you
create, an unauthenticated token that exposes only the specific summary
you chose to share. Second, as defense in depth, those tables carry a
Postgres Row Level Security policy: endpoints that serve your own data
bind your user id to a Postgres session setting, and RLS then narrows
each tenant-scoped table to your own rows, so a bug that forgets a
user_id filter is downgraded from "another tenant's data leaks" to
"the query returns nothing".
A small number of endpoints deliberately read across that boundary and so cannot bind a single user id: the team-wide views described below, and our own operator diagnostics. Those rely on the first layer — an explicit ownership check on every query — rather than the second, which is why the first layer is the one we treat as load-bearing and the second as the backstop beneath it.
Two boundaries are drawn by team rather than by individual, because the data is genuinely shared: your team's record itself, and the AI fair-use credits that pool across your team's seats. Those are scoped to your team in the application layer — a member of one team can never read another team's. And records that outlive an account by design (billing documents we're required to retain, and audit entries) stay isolated after the account is closed: they are readable by no tenant at all rather than falling back into general visibility.
Separately, and layered on top of that, you can control which of your own
lockfiles your own AI agents see: each lockfile has an Available to AI
agents toggle that, when off, excludes it from the
MCP server's get_upgrades and get_security_issues tools
while keeping it on your dashboard. This is a
user-controlled display/scope preference within your account, not the
tenant-isolation mechanism above, and not a limit on your own full-access
API token used directly. It stores no new personal data: it's a single
boolean on a lockfile row you already own, so the What we store list is
unchanged.
Shared, non-tenant tables #
Two tables are deliberately community-wide rather than per-tenant, because what they hold is a fact about a public package or repository rather than about any account: the source catalog behind Explore (one row per source, watched by many users) and the package→repository match cache described above. Neither carries a user id. The paragraphs below cover how a private repo you track is kept out of every public read path.
The source catalog behind Explore is shared across the whole community (a single row per source, watched by many users), so it is not a per-tenant table. A private GitHub repository you track (via the GitHub App) is flagged private on that shared row and excluded from every public read path: it never appears in the explorer list or search, and the explorer's detail page and release-summaries feed return "not found" for a private source regardless of who asks (the public explorer never serves it, not even to you; authorization there isn't the point, the source simply isn't part of the public catalog). The anonymous realtime stream likewise refuses to subscribe to it. You still see the repository and its releases on your own dashboard. In short: tracking a private repo on DevUpdate.io never publishes its name, description, or release notes.
What we don't store #
- Lockfile raw bytes. Parsed and discarded; only package + version pairs persist.
- Source code. We never clone repositories. Diff analysis goes through GitHub's compare API and the result is summarized; the raw diff is not stored.
- OAuth tokens beyond what we need. We hold the minimum permissions required to power the features you use. Google sign-in stays at basic profile + email. GitHub sign-in is handled by the DevUpdate.io GitHub App, whose user-authorization flow grants read-only access to repository contents and metadata, scoped to repositories you select when installing the App on your account or org. There is no "full control" or write permission anywhere in the request, and authorization alone (without an install) is enough for public-repo features. We only call GitHub when you take an action that requires it (e.g. add a source, sync a lockfile), and the call is restricted to the repo you named.
- Anything beyond parsed dependencies from connected repos. When you use a live lockfile connection, we read only the connected lockfile and store only the package + version pairs parsed from it, never the raw file or other repository contents.
LinkedIn conversion measurement #
We run ads on LinkedIn and need to know whether they work. When you click one
of our LinkedIn ads, LinkedIn appends its own click identifier (li_fat_id)
to the link you follow. If you go on to create an account and you accepted
the cookie banner, our server tells LinkedIn that this click became a signup.
- Sent: LinkedIn's own click identifier, a timestamp, and an internal account number.
- Not sent: your email address. LinkedIn accepts a hashed email and it would measurably improve their match rate; we don't send it, because a hashed email is our data about you and lets LinkedIn match against their whole member graph, while their click id only ever concerns someone who already clicked our ad. Also never sent: name, employer, job title, country, or anything about your dependencies, lockfiles or code.
- Who it can concern: only people who clicked a LinkedIn ad of ours. With no click identifier there is nothing to send, and nothing is sent.
- If you reject the banner: nothing is sent and the ad goes unmeasured.
This runs server to server, on our backend. There is no LinkedIn Insight
Tag: no ad-network pixel of any kind on this site, and none in the
signed-in app, and no ad-network cookie. It reads two records already
described above: the du_attr attribution cookie, and the du_consent record
of how you answered the banner.
Third parties #
| Service | What they see |
|---|---|
| OpenAI (LLM, currently the only provider) | Diff text + release notes for analysis, plus public marketplace metadata (extension manifest, install count, publisher info) and source URLs/descriptions when we synthesize a trust score for a VS Code / Open VSX extension or an ad-hoc web source. Not used for training per OpenAI's API terms. We may engage other LLM providers in the future. |
| OpenSSF Scorecard (api.securityscorecards.dev) | Public GitHub repo identifier (owner/name) when computing a trust score. Anonymous public API; we send only the project identifier, never any of your data. |
| Google deps.dev (api.deps.dev) | Public package name + ecosystem (npm, PyPI, Go, Cargo) when mapping a lockfile dependency to its upstream GitHub repository: during source discovery when the package doesn't otherwise resolve to a GitHub repo (its registry metadata lists no GitHub link, or its Go module path isn't on GitHub), and when computing a trust score. Anonymous public API; we send only the package identifier. The discovery hop is only taken for packages not already in the platform-wide match cache. |
| OSV.dev (api.osv.dev) | Public package name + ecosystem when counting open advisories for a trust score, and package name + the pinned version from your lockfile when verifying the security flag on an available upgrade. Anonymous public API; we never send your account identity, repo names, or the rest of the lockfile. |
| Public package registries (registry.npmjs.org, pypi.org, crates.io, packagist.org, Go module proxy) | Public package names when discovering a dependency's source repository (see Package matching), and, for npm/PyPI, when validating that an offered upgrade version was actually published. Anonymous public APIs; only the package identifier is sent. The published-version check reads a short-lived shared cache of just that package's version list, so a recompute re-requests it only for packages whose entry has expired or was never fetched, rather than for every dependency. Because the discovery result is cached platform-wide, a given package name is sent for discovery roughly once per refresh window across all customers, not once per upload. |
| Google (sign-in OAuth) | Email, name, profile picture when you sign in with Google. |
| GitHub (sign-in via DevUpdate.io GitHub App) | Email, username, avatar URL. Sign-in issues a short-lived user-to-server access token (plus refresh token) carrying read-only access to repository contents and metadata for any repos where you've installed the App. Tokens are stored against your account row and deleted on disconnect. |
| GitHub App (per-repo install, required for private repos) | The same DevUpdate.io GitHub App must be installed on repos (or whole orgs) you want private-repo features for: starred-sync of private stars, live lockfile sync from private repos, source-tracking of private repos. You pick which repos at install time; access stays read-only (contents + metadata only) and is revocable from GitHub settings. We store only the parsed dependency list for live lockfile connections. |
| AWS SES | Outbound email (digests, transactional). |
| AWS S3 | Encrypted storage of AI-generated release summaries and analysis output. |
| PayPro Global | Billing details (name, billing address, payment method). |
| PostHog (EU-hosted) | Product analytics: page views, feature usage, clicks on buttons and links, and the campaign parameters (utm_*) and ad-click identifiers that browsers send by default on a marketing link. Pseudonymized by account ID, never shared externally and never sent to ad networks. The LinkedIn report described above is built from our own database, never from anything PostHog holds. Most events are captured in your browser and honor the cookie-banner choice; a small set of server-side events (account signup, a lockfile being added, a digest email being sent, and an AI agent invoking the MCP tools) are captured cookielessly on our backend as operational product analytics (no browser storage). Beyond the pseudonymous account id, the only extra data on those is the campaign label on the signup event, described under Signup attribution below. Both feed the same EU-hosted project. |
| LinkedIn (ad conversion measurement) | Only when you arrived by clicking one of our LinkedIn ads and accepted the cookie banner: on signup we send LinkedIn back its own click identifier (li_fat_id), a timestamp, and an internal account number, so it can report that the ad converted. Server-to-server; no Insight Tag or other LinkedIn code runs on the site. Your email address is never sent, hashed or otherwise. See LinkedIn conversion measurement. |
| Source icons (your browser, not our backend) | To show a logo next to each source, your browser loads a small image directly: a repo owner's public avatar from github.com/<owner>.png for GitHub sources, the extension icon from the VS Code Marketplace's own gallery CDN (*.gallery.vsassets.io) for Marketplace sources, or /favicon.ico from any other web source's own domain. Only the owner name, extension id, or hostname appears in the image URL, the referrer is suppressed, and no account data is sent. These requests go straight from your browser to the source's own platform: no new third-party service is involved, and they fall back to a plain initial if the image can't be loaded. |
Retention #
- Active accounts: all data retained.
- Unverified email/password signups: the row is removed automatically 7 days after signup if the email is still unverified. A reminder email goes out around day 4. OAuth signups are created already verified and are never affected by this sweep.
- Inactive verified free-tier accounts: if you signed up but never added a source and never connected a lockfile, the account is removed automatically 30 days after signup. We send up to five reminder emails over the preceding 27 days, the last of which is an explicit deletion warning. Paid-plan accounts and team members are excluded; adding a source, upgrading, or joining a team takes you out of this cohort.
- Closed accounts: see Account → Deletion. Account-deletion is self-serve and removes personal data within 30 days, with cryptographic confirmation.
- Billing record retention obligation: invoices, subscription history and withdrawal-waiver consent may be retained up to 10 years after account deletion to satisfy German tax/commercial retention obligations (§147 AO, §257 HGB), reduced to the minimum required by law.
Data export #
You can export a JSON dump of your account data (sources, lockfiles,
analysis history, billing) on request. Email info@devupdate.io from
your account address. It's a manual job today, not yet self-serve.