Changelog
All notable changes. Versions follow SemVer; the image and the app share one version. The SDKs — @spectosh/browser, @spectosh/node, @spectosh/react-native — have their own and are published only when they change.
The public site's image ships. The 0.3.0 release stopped at the image scan, so specto-site has no 0.3.0 build; the app is the same as in 0.3.0. Upgrading: change the image tag to v0.3.1 and roll both deployments; no migrations. The SDKs are unchanged.
Security
- The
specto-site image drops the nginx modules it never loads (GeoIP, image filter, JavaScript and XSLT), the libraries they bring in (libgd, libtiff, libxslt) and curl, and takes Alpine's latest fixes. libtiff 4.7.1 (CVE-2026-4775) is what stopped the 0.3.0 release.
The hosted product splits in two: specto.sh is the public site and app.specto.sh is the app. Self-hosted instances keep one address and see no change. Upgrading a self-hosted instance: change the image tag to v0.3.0 and roll both deployments; no migrations, no new settings. The hosted split is a Kustomize component you turn on (deploy/k8s/components/split, steps in deploy/runbooks/split.md). The SDKs are unchanged: @spectosh/browser 0.2.5, @spectosh/node and @spectosh/react-native 0.2.0.
Added
- specto.sh is its own site. The landing page, pricing, docs, changelog and legal pages are built to plain HTML and served from their own small pod, with no Node, no secrets, no database and nothing it can call out to. Pages load without React; one small script runs the tour's tabs and pricing's monthly and yearly prices. Each release ships a second image,
specto-site. - Search engines and AI assistants are welcome on specto.sh.
robots.txt lets them in (GPTBot, ClaudeBot, PerplexityBot, Google-Extended and others), next to a sitemap, an llms.txt that says what Specto is and where the docs are, canonical links, social cards and structured data. SITE_URL and SDK_URL say where an instance's public pages and its SDK live when that isn't the app's address. With SITE_URL set elsewhere, the app sends its public pages there (301) and asks not to be indexed. Left unset, everything stays on BETTER_AUTH_URL as before.
Changed
- The hosted app moves to app.specto.sh. Old links still work: sign-in, invitations, recordings and signed DPAs on specto.sh forward to the app with their path, and
specto.sh/#replay/… links in emails, Slack or tickets open the replay. Everyone signs in once after the move, since the session cookie belongs to the app's address. Passkeys keep working: they stay registered to specto.sh. - Customers' sites change nothing: the install snippet is still
https://specto.sh/specto.js, and recordings still go to specto.sh. - Emails link to the app for anything you sign in to and to the site for the docs.
Security
- The public site's pod runs as a non-root user on a read-only filesystem, with every capability dropped, no service-account token, and a network policy that admits only the tunnel and allows no outbound traffic. Its Content Security Policy allows only its own scripts, and the build fails if a page carries an inline script.
- The app is never indexed, and its session cookie never reaches the public site.
Repository
packages/ui (@specto/ui) holds the brand and the public pages, shared by the app (self-hosted instances still serve them) and the site.apps/site (@specto/site) builds specto.sh with Astro and packages it in its own nginx image.@specto/core's modules sit in folders by area: db, ingest, billing, email, security, compliance, insights and integrations. Import names (@specto/core/plans) are unchanged.
No passwords anywhere, one sign-in page, and "Confirm it's you" before sensitive changes. Upgrading: change the image tag to v0.2.9 and roll both deployments; migration 0042 removes any stored password (production already kept none). PASSWORD_SIGN_IN, PASSWORD_BREACH_CHECK, BLOCK_COMMON_PASSWORDS and DEMO_ACCOUNT_PASSWORD are no longer read: remove them from your configuration (PASSWORD_SIGN_IN logs a warning). The SDKs are unchanged: @spectosh/browser 0.2.5, @spectosh/node and @spectosh/react-native 0.2.0.
Security
- No passwords, in any environment. Signing in, signing up, resetting or changing a password is gone from development and test instances too: every password route answers 404, and migration
0042 deletes stored passwords. Production's password reset by emailed code (/email-otp/reset-password) was still registered. Its code was never sent, so it couldn't be used, but it would have set a password on any account. - Routes Specto doesn't use are closed (404 before anything runs): reading the authenticator's secret back (
/two-factor/get-totp-uri gave it to any signed-in session of an account without a password, with no confirmation), changing the account's email, Better Auth's own account deletion, linking or unlinking a provider while signed in, reading a linked provider's tokens, checking a sign-in code without using it up, and writing to the session record. - "Confirm it's you" before sensitive changes. Turning two-factor on or off, new backup codes, adding or removing a passkey and deleting the account need a sign-in in the last 10 minutes, or a 6-digit code emailed to the account (plus the authenticator's code when two-factor is on). Until now a stolen session cookie could add its own passkey or turn two-factor off.
- An organization's "require two-factor" rule, its member list and the access review judge by how a session signed in: an emailed code alone is one factor. A session from before sign-in methods were recorded counts as one factor until its owner signs in again.
- Continue tells the sign-in page whether an address has a passkey (to ask for it); that lookup is limited to 20 a minute per IP address.
Added
- Passkeys on this device. Your account offers "Use Windows Hello" ("Use Touch ID" on a Mac, Face ID on an iPhone, the screen lock on Android) next to "Use a phone or security key", and the passkey offer after an email sign-in puts it on this device: signing in there is your PIN, fingerprint or face, no phone. The list shows where each passkey lives (Windows Hello, Google Password Manager, iCloud Keychain).
Changed
- One sign-in page. Continue finds the account or creates it, so there's no switch between signing in and creating an account. One dark page with the product line and the recorder timeline; one line covers the Terms and Privacy Policy; the data region is one line, shown when there are other regions.
- The passkey is asked for after Continue. An address with a passkey goes straight to the browser's prompt, with "Email me a sign-in link instead"; the others get the link as before. The domain's single sign-on still comes first.
- Two-factor's "Turn off" and "New backup codes" ask "are you sure" instead of a password.
- The privacy policy no longer mentions password hashes.
Removed
PASSWORD_SIGN_IN, PASSWORD_BREACH_CHECK, BLOCK_COMMON_PASSWORDS (with the vendored list of common passwords) and DEMO_ACCOUNT_PASSWORD. The demo account signs in by code like everyone; development prints the sign-in email to the server's log.
Friction where it happened, and a calmer sign-in screen. Upgrading: change the image tag to v0.2.8 and roll both deployments; no migrations. @spectosh/browser is 0.2.5 (errors and rage clicks carry their page); @spectosh/node and @spectosh/react-native stay 0.2.0.
Added
- Sign in with Microsoft (
MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET, optional MICROSOFT_TENANT_ID, default common). It isn't trusted for linking to an existing account by email: a work directory can issue an address its owner never proved. - The hosted instance shows Google, GitHub and Microsoft on the sign-in screen, the ones not set up yet disabled; other instances show only the ones configured.
Changed
- Sign-up asks only for your email. The account is named after it, as sign-ups by code already were; rename it in Your account.
- The sign-in screen is balanced on wide windows: the brand side and the form each take half, their content centered.
Fixed
- Friction is counted on the page it happened. Analytics' "Friction by page" and the weekly digest's "most friction" filed errors and rage clicks under the page each session started on, so a checkout that failed after arriving on the home page showed as
/. The browser SDK now sends the page with every error and rage click; events from older SDKs still fall back to the entry page.
A front page that shows the product, the pilot in the Terms, and a fix for the feedback button. Upgrading: change the image tag to v0.2.7 and roll both deployments; no migrations. @spectosh/browser is 0.2.4 (the script-tag build's fix; pinned /sdk/0.2.4/specto.js carries it); @spectosh/node and @spectosh/react-native stay 0.2.0. Set SUPPORT_EMAIL to show a support address in the landing page's footer.
Added
- The landing page shows the product: a tour of the real app — replay, feedback, issues, funnels, heatmaps — over a demo shop recorded by the real SDK; everything Specto does beyond replay; why Specto; the security controls a review asks for; an FAQ; "Talk to sales" on Enterprise (
SALES_EMAIL); and a footer with contact addresses and the operator's name (LEGAL_ENTITY). - The pricing page mentions the Pro pilot, with a link to apply.
Changed
- The Terms describe the pilot and say new organizations start on Free; a trial appears in them only where
TRIAL_DAYS turns one on. - A passkey sign-in counts as the second factor for instance admins, as it already did for organizations that require two-factor. The pilot applications list asks for it too: it holds applicants' names and emails.
Fixed
- The feedback button broke a snippet early in
<head>: with data-feedback="true" and no page body yet, the script threw on every page load — the host page saw "Script error.", and window.Specto (identify(), track(), consent) was never defined. The button now waits for the body, and can no longer cost the recording. - Feedback marked "question" had an empty badge and no filter in the inbox.
Sign in with one click, emails that look like Specto, a front page that shows a replay, and a Pro pilot instead of trials. Upgrading: change the image tag to v0.2.6 and roll both deployments; migration 0041 (two tables, three columns) runs at boot. Trials are off unless TRIAL_DAYS is set — Stripe instances used to give every new organization 14 days of Business; set TRIAL_DAYS=14 to keep that. No SDK changed: @spectosh/browser stays 0.2.3, @spectosh/node and @spectosh/react-native 0.2.0.
Added
- Sign in by link. The sign-in email's button signs you in: straight away in the browser that asked for it, after one tap anywhere else — so a mail scanner that opens the link neither signs in nor uses it up (the code rides in the link's
#fragment and is submitted by the page). The tab waiting for the email moves on by itself; the six-digit code stays in the email for signing in on another device. - One design for every email: the Specto mark and name in a header band with a dot for what it's about, one button, details in mono and a footer that says why you got it — light, with a dark variant for mail apps in dark mode, and a plain-text part saying the same. The mark is embedded, so nothing loads from the web (SMTP, Microsoft 365 and Resend all carry it). The weekly digest preview under Alerts shows the email as sent.
- New emails: a sign-in from a browser the account hasn't used before; new feedback to the project's admins, gathered (at most every
FEEDBACK_ALERT_COOLDOWN_MIN, 15), with a switch for each person under Alerts; your first replay is in; and, three days after a project is created with nothing recorded, a nudge that names the origin ingest refused when that's the reason (NUDGE_AFTER_DAYS). - The Pro pilot (Stripe instances): Free organizations apply on Plan & billing; instance admins approve or decline on the Instance page. An approved organization runs on Pro for
PILOT_DAYS (90), reminded two days before the end and moved to Free unless it subscribes. The landing page offers it. PILOT=off hides it. - A new front page: a checkout replaying — the rage clicks, the failed payment, the visitor's note — then what came attached to that session, privacy, the one-line install, alerts, and pricing from the plan catalog.
Changed
- New organizations start on Free. Trials are opt-in (
TRIAL_DAYS); the pilot takes their place on specto.sh.
Removed
- The Feature Portal — the public roadmap with votes, its screen, setting and search results. People ask for features through the feedback widget. Its tables stay (no data is deleted); old
/p/<key> links show “Page not found”. - Password-reset and email-verification emails: production has no passwords, and signing in with the emailed link confirms the address (an account whose provider didn't vouch for it is sent a sign-in link). The invitation-accepted email: Members & roles shows who joined.
Fixed
- The signed-DPA email linked to a page that didn't exist; it opens the signed copy now.
Settings and the sidebar, tidied. Upgrading: change the image tag to v0.2.5 and roll both deployments; no migrations. No SDK changed: @spectosh/browser stays 0.2.3, @spectosh/node and @spectosh/react-native 0.2.0.
Changed
- The Settings list sits beside its page. On a wide window the section list was at the window's far edge; it now sits next to the page and stays in view as the page scrolls. Narrower than 1100px it is still the row above the page.
- The sidebar says what's recording. The day's session count is gone — that number is the Overview's. The line under the logo says "3 recording now" or "Nothing recording", and opens Sessions.
No passwords in production, an install that checks itself, React done right, and email through Microsoft 365. Upgrading: set up email first. Production no longer offers, accepts or keeps passwords — the server deletes stored password credentials when it starts — so before rolling out, give the instance an email transport (SMTP_URL, Microsoft 365 through GRAPH_*, or RESEND_API_KEY, with EMAIL_FROM) or make sure people have a passkey or a sign-in provider. Then change the image tag to v0.2.4 and roll both deployments; migration 0040 (four new columns) runs at boot. @spectosh/browser is 0.2.3; @spectosh/node and @spectosh/react-native stay 0.2.0.
Added
- Email through Microsoft 365. A mailbox of your own tenant sends through Microsoft Graph, signed in with a certificate (no password, no expiring secret) and limited by Exchange to that one mailbox (
GRAPH_TENANT_ID, GRAPH_CLIENT_ID, GRAPH_CERT, GRAPH_CERT_KEY; deploy/runbooks/go-live.md, step 3). @spectosh/browser/react. SpectoProvider starts recording before any component's effects run — once per page, StrictMode- and hot-reload-safe, inert on the server (a Next.js 'use client' provider works as is) — and identifies user when it changes. useSpecto(), SpectoErrorBoundary and spectoErrorHandler() for React 19's root error hooks report the errors React catches, with the component stack, once.- TypeScript types for
@spectosh/browser, /react and /destination. - The key is the whole install.
pk_… belongs to the hosted service; pk_…@eu.specto.sh or pk_…@your-host names a project elsewhere, and host overrides it (endpoint still works). The SDK asks that host for the project's settings before sending anything: an unknown key records nothing and the console says why. - Capture from the first moment. Errors, console, requests and the DOM are held in memory while the project's settings load, then filtered by them and sent — or dropped. A page load's first requests and errors are in the replay.
captureError(error) (also Specto.captureError). - The install checks itself. When ingest refuses an origin that isn't allowed, the Install page names it — "Sessions from http://localhost:3001 were refused 2m ago" — with Allow for project admins.
- Development sessions. Sessions from localhost and other development hosts are tagged dev, counted outside the plan quota up to
DEV_SESSIONS_PER_MONTH (1,000) per organization, and left out of the Overview and the Sessions list once the project has real web traffic ("Show development sessions (N)"). - The Install page has React and Next.js tabs; every snippet takes just the key.
Changed
- No passwords in production. Sign-in is by emailed code, passkey, Google/GitHub or single sign-on; every password endpoint answers 404 and
PASSWORD_SIGN_IN is ignored there (development and test instances keep it). Two-factor changes and account deletion confirm with a recent sign-in. An instance with no way to sign in warns at boot. - Settings has one menu. The sidebar is a single list of the project's screens with Settings at the bottom; Settings shows one section per page, picked from a list at the right edge — this project's sections, then the organization's under its name, whose pages say they apply to every project.
Passwordless sign-in, an Overview that opens on today, and a calmer Your account. Upgrading: change the image tag to v0.2.3 and roll both deployments; no migrations. Where the instance can send email, the sign-in screen stops offering passwords — accounts with one sign in with an emailed code instead; set PASSWORD_SIGN_IN=on to keep offering them. No SDK changed: @spectosh/browser stays 0.2.2, @spectosh/node and @spectosh/react-native 0.2.0.
Added
- Passwordless sign-in. A passkey, Google or GitHub (when configured), and an email field: Continue sends an address whose domain has single sign-on to its identity provider, and any other a six-digit code by email (10 minutes, single use, 5 tries, hashed at rest). Every address gets the same step, so the screen doesn't reveal which accounts exist. A new address becomes an account on its first code; afterwards the app offers a passkey once. Passwords remain where the instance can't send email, or alongside codes with
PASSWORD_SIGN_IN=on; off never offers them. - The Overview's range, in the chart's corner:
‹ Today › steps through Today, the last 7, 30 and 90 days, remembered in this browser. Days start at your local midnight; Today and 7 days are hour by hour, 30 and 90 day by day, and the rest of today is shaded. One range drives the chart and the four cards, which compare with the period before up to the same point — Today with this time yesterday — or say there's nothing to compare with when that period is past your plan's retention, which the chart hatches.
Changed
- Two-factor after an emailed code. An account with an authenticator app is asked for it after a code sign-in too, and an organization's 2FA requirement is judged per session: password and code sessions need the authenticator app, passkey, Google, GitHub and SSO sessions are covered. Accounts without a password turn 2FA on and off without one.
- Your account: active sessions read as devices — this one first, the others behind Show N others, each with Sign out; two-factor is one Set up button, and the password comes in a Confirm it's you step when you act. Change password shows only on accounts that have one.
- Overview: the Get set up checklist can be dismissed (per project); empty cards say what will appear and how — Install the snippet for no sessions yet.
- Releases publish an SDK only when its code changed; each SDK has its own version.
Settings reorganized around who a change reaches, the organization shown only once it matters, and charts that draw what was recorded. Upgrading: change the image tag to v0.2.2 and roll both deployments; no migrations.
Changed
- Settings, grouped by scope. Project (Install & setup, General, Members & roles, Privacy & masking, Alerts, Integrations) and Organization (Organization, Security, Plan & billing, Audit log, Access review). The new Security section holds the two-factor requirement, the access policy, single sign-on and SCIM, moved out of Team & roles — now Members & roles. Your account and Instance admin open from the menu under your name;
#settings/account, #settings/instance and #settings/team still work. - The organization appears once it groups something — a second project, another person, or a second organization. Until then Settings' second group is Account & billing and no page names it. Then a new Organization page lets owners and admins rename it (audited) and lists its owners, people, plan and projects; owners and admins are asked once to name it if its name is one nobody chose. The project switcher lists each project with its key, grouped by organization only when you are in more than one.
- Skip idle is on by default, says when it skips (skipping idle · N×), and the replay scrubber shades the stretches it skips.
- Instance admins see the real checkout on Plan & billing; assigning a plan directly is a separate box that says it bypasses Stripe.
Fixed
- The activity charts draw what was recorded. The Overview's week, each session row's sparkline and the replay scrubber's trace were decorative waves. Now: sessions and feedback per hour over the last 7 days with errors and rage clicks at the hour they happened and an hourly readout; a session's own activity across its length with its errors marked; the session's activity density behind the scrubber's markers.
- Load-balancer probes, uptime monitors and
curl get the page instead of a failed server render (and its log error); /metrics on the public port is a 404.
Fixes from running 0.2.0 on specto.sh. Upgrading: change the image tag to v0.2.1 and roll both deployments (specto, specto-ingest); no migrations.
Fixed
- Play works from a fresh load. Pressing Play before seeking did nothing: the button turned to Pause and the clock stayed at 00:00. rrweb's clock has no baseline until the first play, and the player resumed from that reading (about −56 years). It now reads the clock clamped into the recording — Play, the arrow keys, speed and skip-idle changes.
- Public pages scroll with the mouse wheel again — the landing page, pricing, docs, changelog, legal pages, the public feature portal and the access and 2FA gates. Their full-page wrapper was a scroll container that never scrolls, with overscroll containment; current Firefox and Chromium gave it the wheel and the page didn't move.
- A new pod waits for Postgres (up to 30 s) instead of crashing when the database isn't reachable yet — k3s's NetworkPolicy admits a new pod's address only after ~2 s. The same wait is in
migrate.mjs and the backup job. - A restricted Stripe test key (
rk_test_) on a production instance now needs ALLOW_STRIPE_TEST_MODE=true, like a secret test key (sk_test_).
Security
- The image applies Debian's security updates at build time (perl-base had 3 critical and 4 high findings fixed upstream), and
source-map-js is 1.2.2 (GHSA-68fv-2mgg-jv7q).
The production revamp: billing that bills, tenancy that holds, ingest that can't be fooled, and a deployment that fails closed. Upgrading a v0.1 instance: deploy/k8s/README.md → "Upgrading the live specto.sh instance".
Breaking
- Enterprise features need a license. Organization SSO, SCIM provisioning and audit log streaming moved to
packages/core/ee/ (Specto Enterprise License); setting them up needs SPECTO_LICENSE_KEY on the instance — and, on instances that bill through Stripe, a Business or Enterprise plan. Existing setups keep working. The rest of Specto is now AGPL-3.0 (LICENSE); the SDKs stay MIT. - Organizations own plans. Plans, seats, project limits and usage moved from projects to organizations; each existing project becomes its own organization keeping its plan (
team → Business), project admins become org admins and one is promoted to owner (migration 0007). - Snippet keys. The snippet now carries a rotatable public key (
sites.publicKey) separate from the internal project id. Existing snippets keep working (the public key of migrated projects equals the old key) until you rotate it. - Invites are tokens. Pending email-matched invites no longer auto-join; admins send invite links (single use, 7-day expiry).
- SDK v0.2 sends numbered batches and
text/plain uploads; the server still accepts v0.1 SDKs. - Pinned SDK URL moved from
/sdk/v1/specto.js (which was never actually immutable) to /sdk/<version>/specto.js. - Schema comes only from versioned migrations, applied at boot under an advisory lock; the boot-time DDL is gone (legacy databases are baselined automatically).
- Node 24 is the supported runtime.
Added
- Stripe billing: Checkout, in-place prorated plan switches, Customer Portal, signed idempotent webhooks, past-due grace; public pricing page.
- Atomic monthly session metering per organization; per-plan ingest rates and retention;
429 quota_exceeded. - Teams: invite links, member and role management, project key rotation, project deletion (with object purge), audit log.
- SDK: sessions across page loads (30-min idle, 4-h max), retry with backoff, chunked
sendBeacon, periodic full snapshots, remote project config (stricter wins), consent mode, opt-out, sampling, identify, track, sessionUrl, PII scrubbing of console/errors/URLs/event props. - Issues: JavaScript errors grouped across sessions (normalized message + first app frame), each linked to the replays of the sessions that hit it. Open / Resolved / Ignored triage (developers and admins); a resolved issue hit again in a session that started after it was resolved comes back to Open flagged Regressed (migration 0018,
issue_states). - Issue alerts: an error a project has never produced, or a resolved issue hit again in a newer session, emails the project's developers and admins a digest (at most every
ISSUE_ALERT_COOLDOWN_MIN, default 15) and sends issue.new / issue.regressed webhook events. Admins switch alerts per project, everyone can opt out of the emails (Settings → Alerts); links open the issue (#issues/<project>/<issue>). The first run learns existing errors quietly (migration 0019). - Webhooks are retried. Every event is stored in an outbox, attempted at once and, on a non-2xx answer, timeout or network error, retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h (
Retry-After honored); the project's admins get an email when one gives up. Bodies carry a stable id (also x-specto-delivery) for de-duplication, plus x-specto-event and x-specto-attempt headers. Settings → Integrations shows each delivery's state with "Retry now", "Redeliver" and "Send test event" (webhook.test). Delivered bodies aren't kept; settled rows are swept after DELIVERY_RETENTION_DAYS (30) as before, queued ones never. Query strings are no longer stored with the logged URL (migration 0020). - Project privacy settings (recording on/off, strict privacy, console, network details, sample rate) enforced in the SDK and at ingest.
- Frozen, SRI-pinnable SDK releases under
/sdk/<version>/specto.js. - Operator-configurable legal pages (
LEGAL_*), subprocessor list. - Two-factor authentication (TOTP + backup codes) for password accounts; security notices when the password changes or 2FA is turned off; organizations can require 2FA for every password-based member.
- Service emails through a deduplicated outbox: welcome with the install snippet, invite accepted (to the inviter), 80% and 100% of the monthly sessions, payment failed, subscription ended.
- Trials: new hosted organizations start on a 14-day Business trial (no card); reminder two days before the end, then Free unless subscribed.
- Downgrade grace: a plan change that shortens retention keeps the previous window for 14 days instead of deleting history at the next sweep.
- Abuse controls: instance admins can suspend an organization (ingest and feedback refused, data kept); optional disposable-email sign-up blocking.
- Error tracking to any Sentry-compatible endpoint (
ERROR_TRACKING_DSN), privacy-minimal events; server-function CSRF middleware made explicit. - Integration secrets and OAuth tokens encrypted at rest (
ENCRYPTION_KEY). - Private Prometheus metrics port;
/api/live; request ids; queue gauges. - Kubernetes: NetworkPolicies, Pod Security
restricted, PDBs, HPA connection budget, Redis auth, Cloudflare Tunnel with path routing, offsite backups to R2, CloudNativePG option, restore runbook. - CI/CD: core tests + 12 e2e suites + production smoke on Postgres, image scan, SHA-pinned actions; releases gated on CI with SBOM, provenance and npm trusted publishing.
Product
- Docs in depth (ROADMAP R2-5):
/docs/sdk (the full SDK reference), /docs/frameworks (Next.js, Remix, Nuxt, SvelteKit), /docs/consent (Cookiebot, OneTrust, any consent manager), /docs/csp and /docs/troubleshooting (a "no sessions arriving" walkthrough with every ingest error code), with a shared docs layout and page list. - Public changelog at
/changelog, rendered from this file at build time, and "What's new" in the sidebar, marked until you've read the latest release notes (ROADMAP R2-8). - Session keys (security backlog): knowing a session id — a replay link shows it — no longer lets anyone append to that session with the project's public key. The browser and React Native SDKs send a random 128-bit key with every batch; ingest keeps its SHA-256 and refuses appends without it (
403 session_key, after which the SDK starts a new session). Sessions from older SDKs are unaffected. Migration 0039; the 0.2.0 SDK build is re-frozen. - Public pages as real HTML (ROADMAP R2-4): pricing, docs, the changelog and the legal pages are rendered once per server process with the instance's own settings (plans, prices, operator details, its address in snippets) and served as HTML that then hydrates — for search engines and a faster first paint. With
ALLOW_INDEXING (the hosted site), / is the landing page for signed-out visitors, sign-in moves to /login, and /welcome redirects to /. Link previews get Open Graph cards (scripts/og-images.mjs); Lighthouse SEO scores 100 on the public pages. - License keys (ROADMAP E4-7): Specto Enterprise license keys, signed with Ed25519 and verified offline (licensee, features, expiry, optional instance domain); Settings → Instance shows the instance's license, and SSO, SCIM and the SIEM stream explain what's missing — the license or, with a link to billing, the plan.
packages/core/ee/bin/specto-license.mjs makes signing keys and licenses. - Data regions (ROADMAP E4-3): an operator running Specto in several regions — each a separate instance — sets
DATA_REGION and DATA_REGIONS. Sign-up shows the data region and links to the other regions' sign-up; sign-in links to them; Settings → General, the privacy policy and the DPA say where data is stored. In production, an EU region won't boot on an R2 bucket outside the EU jurisdiction. Setup: deploy/k8s/README.md → "Running another region". - Connection pooling (ROADMAP L1-6): Specto runs behind PgBouncer in transaction mode. Point
DATABASE_URL at the pooler and the new DATABASE_DIRECT_URL at Postgres itself — advisory locks (leader-elected jobs) and migrations use it, because transaction pooling would release a session lock in the wrong session. deploy/k8s/postgres-cnpg.example.yaml moves to Postgres 17 on CloudNativePG with its pooler and point-in-time recovery; the move-over is in deploy/k8s/README.md and the PITR drill in deploy/runbooks/restore.md. - Access reviews (ROADMAP E4-6), Settings → Access review (organization owners and admins): everyone who can reach the organization's data, with their organization and project roles, how they sign in (password, passkeys, social, SSO), 2FA and last sign-in — flagged when an admin has a password without 2FA, nobody signed in for 90 days, someone can't sign in under required SSO, a seat has no project access, or SCIM deactivated someone still listed. Export it as CSV, record the review with notes (the list is kept as it was), and see when the next one is due (every 90 days). Sign-ins now record
lastSignInAt. Migration 0038. - SOC 2 readiness pack (
compliance/): the Trust Services Criteria mapped to Specto's controls and where their evidence is, policy templates (information security, access control, change management, vendor management, incident response, retention, business continuity) and a risk register seeded from SECURITY.md. - Signed DPAs (ROADMAP E4-8), Settings → Plan & billing: an organization owner or admin reviews the Data Processing Addendum and signs it electronically for their company (name, title, company, authority statement). The signature keeps the document exactly as signed with its SHA-256, the signer's address and browser, and time; the signer gets a copy by email, and a printable signed copy (
/legal/dpa-signed/<id>) is available to the organization's admins. When the DPA text or the operator's details change, organizations are asked to sign the new version. Instance admins see every signature (Settings → Instance). Offered when the instance names its operator (LEGAL_ENTITY). The DPA text now lives in @specto/core/dpa, shared by the page and signatures. Migration 0037. - Service levels in the Terms:
LEGAL_SLA ("Plan|availability|first response;…") lists the operator's service levels and support response times; without it the Terms keep the no-SLA wording. - SCIM 2.0 user provisioning (ROADMAP E4-1): an organization with verified SSO creates a SCIM token (shown once) for its IdP — Okta, Entra ID, OneLogin… — at
/api/scim/v2. Users are created at the SSO domain and join with the default project role; deactivating someone in the IdP removes them, signs them out and refuses their SSO sign-ins until they're reactivated; deleting removes them. Owners can't be deprovisioned. Migration 0036. - Organization single sign-on (ROADMAP E4-1), Settings → Team & roles: connect one OpenID Connect or SAML 2.0 identity provider per organization (Okta, Entra ID, Google Workspace, Keycloak…). Sign-in waits for the email domain to be proven by a DNS TXT record (or an instance admin on self-hosted instances); the IdP can only sign in addresses at that domain. People who arrive by SSO join the organization and its projects with a chosen role (viewer by default). Require single sign-on holds every other kind of session at a page that signs in through the IdP — turned on only from an SSO session, so admins can't lock themselves out. "Sign in with single sign-on" on the sign-in screen finds the provider from the work email.
@better-auth/sso 1.7.7; the IdP client secret is sealed at rest. Docs: /docs/sso. Migration 0035. @spectosh/react-native (ROADMAP P3-8): React Native apps record into the same project as the website — screens (React Navigation or screen(name)), requests, console output, uncaught and caught errors, identify and track. App sessions appear in Sessions (Device: mobile or tablet; the user agent names the app and version), their errors in Issues and their screens in funnels and paths; their page shows the timeline and panels (apps have no screen replay). Numbered batches are retried with the same number; 30 minutes in the background starts a new session. No dependencies; outside the pnpm workspace, tested and published on its own. Docs: /docs/react-native.- The dashboard on phones (ROADMAP P3-8): below 820px the sidebar is a drawer behind a menu button, tables keep their key columns, the inbox opens the full report on tap, settings sections become a scrolling row of tabs, and a replay stacks the stage over the console/network rail — nothing scrolls sideways. Sessions → + Filter → Device (mobile, tablet, desktop, from the user agent), and replay metadata names the device.
- Organization access policy (ROADMAP E4-5), Settings → Team & roles: an IP allowlist (IPv4/IPv6 addresses and ranges) and a sign-in lifetime (8 hours to 7 days). Members outside it keep their membership but get a gate instead of the organization's projects — "Not available from this network" or "Sign in again". An admin can't save a list that leaves out their own address. Audited; migration 0034.
- Project retention and legal hold (ROADMAP E4-4), Settings → General: a project can keep recordings, events and backend spans for less than its plan's window (7 days to a year, never more than the plan); organization owners and admins can place a project on legal hold with a reason — retention then deletes nothing of it, and the project (or an account that would take it along) can't be deleted until the hold is lifted. Placing and lifting are audited. Migration 0033.
- Audit log export (ROADMAP E4-2), Settings → Audit log: CSV export (last 30/90/365 days or everything kept; organization admins get the whole organization) and a SIEM stream — every new entry posted in order as HMAC-signed JSON batches to the organization's endpoint, retried until accepted, with a test event and a delivery status — plus audit retention per organization (instance default, 90 days to 7 years). Migration 0032.
- Passkeys: sign in with a fingerprint, face or device PIN — Settings → Account adds and removes them, the sign-in screen has "Sign in with a passkey" (
@better-auth/passkey, Better Auth 1.7.7, migration 0031). Every use must verify the person: the server refuses passkeys that only prove possession. Adding or removing one emails a security notice; PASSKEY_RP_ID shares passkeys across subdomains. - AI summaries v2 (ROADMAP P3-7):
- Session summaries are generated once and stored (
recording_sessions.summary, migration 0030), shown when the replay opens and regenerated on request. They now cover the navigation path, product events and failed backend spans; issues filed from the replay and Slack shares include them. The LLM client moved to @specto/core/llm. - Weekly digest: each Monday (
WEEKLY_DIGEST_DAY/_HOUR, UTC) every member of a project gets last week's sessions against the week before, sessions with errors, feedback, new issues, regressions and the pages with the most friction — plus a short paragraph written from those numbers when an LLM is configured (aggregates only). Quiet projects get none; Settings → Alerts turns it off per person and previews it. Leader-elected, one email per project and week. - Similar feedback: the inbox's Similar tab groups open reports describing the same problem, and each report lists its similar reports — TF-IDF over title and description (
@specto/core/clusters), so it needs no LLM; with one, groups get a short label.
- Issue trackers and Slack sharing (ROADMAP P3-4): Send to… on a feedback item or a replay files it in GitHub Issues, Linear or Jira (or Symbiome) — title, signals, browser, user and a link back to the replay — once per tracker; the issue's key then shows as a link and the feedback's activity records it. Share to Slack posts it to the project's channel with the page, signals and a "Watch the replay" button. Trackers are connected and tested in Settings → Integrations (Linear lists its teams to pick from); credentials are sealed and write-only, and a Jira token is dropped when the site changes.
GITHUB_API_URL points at GitHub Enterprise. - Backend traces (ROADMAP P3-1): a request in a replay shows what the server did — a span waterfall with timings, child spans, attributes and the exception. The browser SDK sends a W3C
traceparent header with same-origin requests when asked (traces: true / data-traces="true", more origins with traceOrigins) and records the trace id; servers send spans to the new OTLP/HTTP JSON endpoint /api/otlp/v1/traces with a secret server key (Settings → Install → Backend traces; hashed, shown once, rotatable) — from any OpenTelemetry SDK via the standard OTEL_EXPORTER_OTLP_TRACES_* variables, or the new zero-dependency @spectosh/node (middleware for Express/Connect/node:http, a wrapper for fetch-style handlers, child spans). Spans are validated, bounded and scrubbed on arrival, kept 30 days (trace_spans, migrations 0028–0029; the demo-only traces table is gone) and shown only through a session of their project. Guide: /docs/traces. - Heatmaps and scroll maps (ROADMAP P3-6): Analytics → Heatmaps shows, per page and device class (desktop, tablet, mobile), where visitors click, painted on a recorded snapshot of the page stretched to its full height, plus the most clicked elements and how far down page views get (25/50/75 % and bottom lines, the fold of the recorded screen). The SDK sends each click's page position, viewport width and a tag/id/class selector (never text, at most 300 per page view) and each page view's deepest scroll point; ingest stores them as
click / scroll rows (partial index, migration 0027) and replays leave them out. Heatmaps fill in from new sessions. - Session search (ROADMAP P3-5): Sessions → + Filter narrows the list by a page visited (entry or navigation), a custom event, an identify trait (
plan=pro), a JavaScript error message, a request status (5xx, 4xx, failed or a code) or identified/anonymous visitors. Filters combine in SQL (pagination stays exact; a new partial index on custom, navigation and request events — migration 0026), suggest the project's recent event names and trait keys, and save as segments shared with the project (developers and admins save; whoever saved one, or an admin, deletes it). The page buttons are now windowed (1 … 5 6 7 … 120). - Funnels see every page of a visit: page loads inside a running session are recorded as navigations (multi-page sites used to count only the entry page), and hash-routed apps can use their routes as steps (
/#settings).
Alerts
- Signal alert rules (ROADMAP P3-3, Settings → Alerts): error spike and rage-click surge (the share of sessions in the last hour vs. the past week) and funnel drop (conversion over the last 24 hours vs. the past week), each with a low / medium / high sensitivity and minimums that keep small numbers quiet. A rule alerts once when it trips — email to the project's developers and admins, a
signal.alert webhook event, a Slack message — and posts signal.resolved / a Slack message on recovery (migration 0024: signal_rules, signal_hourly). - Slack (Settings → Integrations): paste an incoming-webhook URL to get issue digests and signal alerts in a channel, with "Send test message". The URL is encrypted at rest, never shown again and logged as
hooks.slack.com/…; messages go through the webhook outbox (retries, delivery log) and escape text that comes from visitors. - The funnel computation moved to
@specto/core/funnel (Analytics and the funnel-drop rule share it).
Billing
- Paid overage (ROADMAP R2-1): organizations on monthly Pro or Business can keep recording past their quota at
OVERAGE_PRICE_PER_1000 ($2.50) per 1,000 extra sessions, up to a monthly cap they set (Settings → Plan & billing). Extra sessions are counted atomically at ingest and reported to a Stripe Billing meter (STRIPE_PRICE_OVERAGE, STRIPE_METER_EVENT) through a ledger written before sending, so retries can't double-bill; the invoice line reconciles with overage_reports. Emails when overage starts and when the cap is reached; the quota emails offer overage. New alert SpectoOverageReportsStuck (migration 0023). - Yearly plans (ROADMAP R2-2): Pro and Business can be billed yearly for ten months' price (two months free) where
STRIPE_PRICE_PRO_ANNUAL / STRIPE_PRICE_BUSINESS_ANNUAL are set. /pricing and Settings → Plan & billing get a Monthly / Yearly toggle; subscribers switch between them in place, prorated by Stripe. The billing interval is mirrored from Stripe (organizations.billingInterval, migration 0022; existing subscriptions are monthly). Displayed prices are env-overridable (PLAN_PRO_PRICE, PLAN_PRO_PRICE_ANNUAL, …). Terms updated.
Operations
- Unusual ingest per organization (ROADMAP L1-8): when an organization's last hour has at least
INGEST_ANOMALY_MIN_SESSIONS (1,000) sessions and INGEST_ANOMALY_FACTOR (5) × its busiest hour of the past week, its owners are emailed (allowed origins, key rotation), the instance admins too (how to suspend), the audit log records it and specto_ingest_anomalous_orgs feeds the new SpectoIngestAnomaly alert (migration 0021, usage_hourly). specto-admin CLI for instance operators (packages/core/bin): top, org <id|project key|email>, suspend, reinstate — on any organization, audited; runs in the app pod with kubectl exec.- Activation funnel (ROADMAP R2-7): organizations per sign-up week → snippet loaded → first session → paid, with the median time to the first session, in Settings → Instance (instance admins) and
specto-admin activation. Each step is a timestamp written once — sites.sdkSeenAt (the snippet's first settings request), sites.firstSessionAt, organizations.paidAt — so the funnel outlives retention (migration 0025, backfilled from kept sessions and the audit log; organizations made from v0.1 projects are re-dated to their oldest project). - The dashboard can record itself (ROADMAP R2-7): with
SELF_RECORDING_KEY (a project's publishable key, on this instance or SELF_RECORDING_ENDPOINT) Specto records its own dashboard, sign-in screen included, with the account id as the user and product milestones (project_created, snippet_copied, checkout_started, invite_sent) as custom events. What it can capture is fixed in code: strict privacy, no console text, no request details, and the replay stage is blocked, so customers' visitors are never recorded. Users can turn it off per browser (Settings → Account); the privacy policy describes it while it's on. Off by default. CI runs every e2e suite with it on, and e2e/selfrec.mjs checks what it captures. - Settings → Install says when the snippet has loaded on a page but no session arrived (allowed origins, consent mode, sampling), and an installed project stays "Connected" after retention deletes its sessions.
GET /api/status: one word per component (API, database, service email), cached 15 s, readable from any origin. deploy/status is a static status page for off-cluster hosting with live checks and hand-posted incidents.deploy/runbooks/incident.md: severities, the first 15 minutes, a section per alert (each alert's runbook_url links to it), security incidents and GDPR notification, communication and post-incident reviews. New alert: SpectoWebhooksFailing.
Security
- Security review (ROADMAP L1-7, SECURITY.md §12): OWASP ASVS 5.0 Level 1 walk-through and a ZAP baseline scan of the production build.
- Invite and password-reset tokens no longer appear in request logs.
- Unexpected server-function errors reach the browser only as a generic message with a reference (no SQL text or stack).
- Deleting an account asks for the password (rate-limited), or a sign-in within the last 10 minutes for accounts without one.
- Instance-admin actions require two-factor authentication in production.
- Invitation emails are capped per organization (
INVITES_PER_HOUR, 20). - The 10,000 most common passwords are refused (
BLOCK_COMMON_PASSWORDS, on by default in production; vendored list, no lookups). /robots.txt, /sitemap.xml and /api/live send nosniff too.
- No more
'unsafe-inline' scripts. Pages allow inline scripts only with a nonce that is new in every response (TanStack Router's nonce support, filled into the prerendered shell by server.mjs), so an injected script doesn't run even if one got past the render layer. The page shell is no-store. CI checks it in a browser against the production server (e2e/csp.mjs). - Strict privacy keeps only the path of recorded URLs: requests, SPA navigations and the page address lose their query strings, and hash routes their parameters. The SDK does it in the browser; ingest enforces it for strict projects whatever the SDK sends.
Fixed
- Password reset links lead somewhere. The link in the reset email landed on the sign-in form with no way to set a new password; it now opens "Choose a new password" (the token leaves the address bar), and a used or expired link says so.
e2e/reset.mjs follows the real link. - Rate limits keyed on a spoofable
X-Forwarded-For (and a shared bucket for everyone when it was missing) — now the trusted client IP (TRUST_PROXY), shared via Redis, with a tighter credential limiter. - SSRF through webhook and integration URLs — egress guard with DNS pinning.
- Ingest trusted client timestamps, kinds and sizes; retries duplicated events; replicas could hand out overlapping sequence numbers; the write queue could drop rows on shutdown.
- Unverified accounts could read data in production; production booted without an email transport.
- Dashboard KPIs and signals were partly hard-coded; several reads were unbounded.
- Retention could run on every pod at once in one huge transaction.
- Secrets (webhook secret) round-tripped to the browser.
- The page address in replay metadata skipped the SDK's URL scrubbing, so a token in the page's query string could be uploaded.
- Sites whose Content Security Policy doesn't allow
blob: workers (a script-src without worker-src is enough) recorded nothing: the upload worker was blocked without an exception and its batches were dropped. The worker now confirms it runs; until it does the SDK keeps the batches, and if it doesn't within 4 seconds (or reports an error) the page uploads them. The page uploader's own requests are not recorded: each upload used to show up as a network event that triggered the next upload, so an idle page kept uploading every few seconds. - Identify traits over 4 KB were cut mid-JSON; whole traits are now dropped until they fit.
identify() and track() called right after startRecording() — the documented pattern — were dropped while the project's settings loaded; they are now kept (up to 50) and sent when recording begins.
Session replay v2 (full-screen player, network details), Google/GitHub sign-in, Symbiome work items, npm package, Kubernetes manifests, TanStack Start migration. See git log v0.1.4.