Vibecode Escourtly
track this build4 phases, 12 steps, beginner friendly0%The core loop is a weekend build on top of rrweb: snapshot the DOM on each click, inline the assets, and replay the snapshots as a clickable walkthrough. What takes the project from a demo to something you would put in front of a prospect is capture fidelity · real apps are full of canvas, cross-origin iframes, shadow DOM and lazily loaded assets, and every one of them is a separate fix. Past that sit the parts that are infrastructure rather than features: no-login share links and embeds that stay up, custom domains with certificates, funnel analytics, and CRM sync. Build it for your own product and one sitting gets you a long way. Build it to replace the subscription and you have signed up for the edge cases.
You are building a lean indie version of Escourtly. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Escourtly · indie build A demo recorder built on DOM snapshots, whose hard problems are capturing enough of a page that the replay renders without calling home, masking by default rather than by accident, and replaying untrusted captured markup without letting it run. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A Chrome extension recording one DOM snapshot per click with rrweb | snapshots beat video because the replay is a real page | | Masking | Inverted defaults: inputs and text masked at capture, unmasked deliberately | the library masks passwords only, which is not the same as masking data | | Storage | Postgres with Drizzle, snapshots stored as compressed documents | snapshots are large and you will keep more of them than you expect | | Replay | The snapshot rendered in a sandboxed frame with scripts disallowed | captured markup is untrusted input that came from somebody else's page | | Analytics | First-party counters in your own database | a demo page that ships a third-party tracker is a demo page with a compliance problem | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the extension and runs the web application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL** · free locally, a few pounds a month managed - Why: Snapshots are large documents and you will keep more versions of them than you plan to, which is a database question rather than a file question. - Get it: Docker, your package manager, or a managed instance. - Verify: The application connects and migrations run - [ ] **Chrome with developer mode for unpacked extensions** · free - Why: The capture half only runs as an extension, and the whole loop is untestable until one is loaded. - Get it: Chrome, then enable developer mode on the extensions page. - Verify: The unpacked extension loads and its indicator appears on a page - [ ] **An application you are entitled to capture** · free - Why: Capture fidelity and masking are only testable against a real signed-in application, and it must be one you have permission to record. - Get it: Your own product in a staging environment, seeded with obviously fake data. - Verify: A signed-in page captures and replays with the origin blocked - [ ] **Your masking and retention policy** · free - Why: Both concern other people's data appearing in your database, and both are decisions that quietly default to the worst option if nobody makes them. - Get it: Write down what is masked by default, who may unmask, and how long unpublished captures are kept. - Verify: All three are in the configuration and the README ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Custom domains and certificates. - CRM synchronisation. - Canvas, video and cross-origin frame capture. - Lead capture forms inside the demo. - capture fidelity on canvas, cross-origin iframes, shadow DOM and lazily loaded assets - no-login share links and embeds that hold up under real traffic - viewer funnel analytics and lead capture - CRM sync to Salesforce, HubSpot and Pipedrive - custom domains with certificates handled for you If one of those is essential to you, that is the reason to keep paying for Escourtly, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Escourtly The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an interactive demo builder that captures a product as clickable page snapshots to replace Escourtly. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A snapshot of an application you are signed into contains whatever was on the screen: customer names, email addresses, invoice totals, sometimes a token in a data attribute. The capture library's defaults are not a privacy policy. rrweb records with maskAllInputs off by default, masking only password inputs, and its opt-outs are class names you have to apply: rr-block for an element replaced by a placeholder, rr-ignore for an element whose input events are not recorded, rr-mask for text that is masked. Check the current defaults yourself before you rely on any of this. So this build inverts them. Masking of input values and text is on from the first capture, unmasking is a deliberate per-element choice made in the editor, and nothing publishes until somebody has opened the review screen and seen every captured text node that is still visible. Capture only products you are entitled to capture. Never a customer's account, never a competitor's application behind a login. ### Stack (fixed, do not substitute) - Capture: A Chrome extension recording one DOM snapshot per click with rrweb · snapshots beat video because the replay is a real page - Masking: Inverted defaults: inputs and text masked at capture, unmasked deliberately · the library masks passwords only, which is not the same as masking data - Storage: Postgres with Drizzle, snapshots stored as compressed documents · snapshots are large and you will keep more of them than you expect - Replay: The snapshot rendered in a sandboxed frame with scripts disallowed · captured markup is untrusted input that came from somebody else's page - Analytics: First-party counters in your own database · a demo page that ships a third-party tracker is a demo page with a compliance problem ### Data model (create this before Phase 1) - demos(id, slug, title, status, created_at, published_at, cta_label, cta_url) · slug is generated, not taken from the title - steps(id, demo_id, ordinal, snapshot_ref, target_selector, target_box_json, title, description, reviewed_at) - snapshots(id, document_compressed, bytes, asset_count, inline_failures_json, captured_url_host) · the host, not the full URL, because the path often carries identifiers - assets(snapshot_id, original_url, kind, bytes, inlined, failure_reason) · what got inlined and what did not, so a blank replay is diagnosable - redactions(step_id, selector, kind, applied_at) · every unmasking and every extra mask, recorded as an action somebody took - views(demo_id, session_hash, started_at, last_step, completed_at, cta_clicked_at) · first-party, no third-party script anywhere - A published demo is a set of frozen documents. Nothing in the replay path talks to the product that was captured, which is what makes it survive a deploy. ### Phase 1 · Capture with masking on from the first click Build: Build a Chrome extension that starts and stops a capture, records one snapshot per click along with the clicked element's selector and bounding box, and shows a visible recording indicator. Turn on masking of input values and text at capture time, not at publish time. Trap: Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. Done when: A capture of a page containing sample personal data stores no recognisable value from it, asserted by searching the stored document in a test. The recording indicator is visible on the page throughout a capture. Each step records the clicked element's selector and bounding box, verified by replaying a two step capture. Stopping a capture writes the steps atomically, so an interrupted capture leaves nothing half saved. Do not build yet: No asset inlining, editor, replay or publishing. ### Phase 2 · Snapshots that render without calling home Build: Fetch and inline stylesheets, fonts and images into each snapshot, bounded by a size ceiling. Record every asset that could not be inlined with the reason. Show the failures in the editor so a blank replay has an explanation before anybody else sees it. Trap: A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. Done when: A published demo renders correctly in a browser with no access to the captured origin, tested by blocking that host. Every asset that failed to inline is listed in the editor with its reason and its size. A snapshot exceeding the size ceiling is rejected at capture with a message naming the largest asset. Publishing is refused for a step whose stylesheets did not inline. Do not build yet: No editor, replay, publishing or analytics yet. ### Phase 3 · A review screen nobody can skip Build: Show each step with its visible text highlighted, offer per-element masking and unmasking, record every redaction as an action with a timestamp, and require the whole demo to be reviewed before publishing becomes available. Trap: Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. Done when: Publishing is unavailable until every step carries a review timestamp, tested by attempting to publish an unreviewed demo. Recapturing a step clears its review timestamp and blocks publishing again. Every unmasking is stored as a redaction row with what was unmasked and when. The review screen shows the visible text of a step in one list, so scanning it does not mean reading a rendered page. Do not build yet: No replay, sharing or analytics yet. ### Phase 4 · Replaying captured markup without running it Build: Render each snapshot in a sandboxed frame that does not allow scripts, draw the hotspot from the recorded bounding box, and advance on click or on keyboard activation. Add a start overlay and a closing call to action. Trap: Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. Done when: Scripts in a captured page do not execute during replay, tested with a snapshot containing a script that would announce itself. The hotspot is reachable and activatable by keyboard alone, and the current step is announced to a screen reader. A snapshot that fails to render shows a message and an option to skip to the next step, never a blank frame. Canvas and cross-origin frame content are shown as placeholders, which the library records as not captured by default. Do not build yet: No sharing, embedding, analytics or export yet. ### Phase 5 · Sharing, counting and deleting Build: Publish at a generated slug with no login, offer an embed snippet, and set frame-ancestors so you control where it can be embedded. Count views, step completions and call to action clicks in your own database. Support unpublishing and a deletion that removes snapshots and assets. Trap: A demo slug derived from the customer's name, published with no login, tells anybody who guesses it which prospects you are talking to, and slugs derived from titles are guessable by design. Generate slugs with enough randomness that guessing is impractical, and make unpublish take effect immediately rather than at the next cache expiry. Done when: Slugs are generated rather than derived from the title, and a demo cannot be found by guessing a customer name, checked by inspection. Unpublishing makes the link stop working immediately, tested with a warm cache. View, step and call to action counts are recorded first-party with no third-party script on the page, verified by listing the page's network requests. Deleting a demo removes its snapshots and assets and the deletion is verified by querying for orphans. Do not build yet: Nothing after this. ### Out of scope (and why) - Custom domains and certificate management, which is infrastructure with its own failure modes and on-call implications. - CRM synchronisation, where each connector is an authentication project plus a permanent maintenance commitment. - Capturing canvas, video and cross-origin frames, which the capture library leaves off by default for good reasons and which the replay would have to relax its sandbox to show. - Lead capture forms inside the demo, which collect other people's details and turn a demo tool into a data controller. ### README must contain - That masking is on at capture and unmasking is a recorded per-element decision - That publishing is gated on a per-step review and recapturing a step resets it - That the replay frame does not allow scripts, and what that means for canvas and widgets - How to load the extension unpacked, and the size ceiling for a snapshot - What deletion removes, and that share links are generated rather than derived from a title ===== AGENTS.md ===== # Agent instructions · Escourtly indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Chrome extension recording one DOM snapshot per click with rrweb, Inverted defaults: inputs and text masked at capture, unmasked deliberately, Postgres with Drizzle, snapshots stored as compressed documents, The snapshot rendered in a sandboxed frame with scripts disallowed, First-party counters in your own database. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. - A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. - Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. - Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. ===== BUILD_PLAN.md ===== # Build plan · Escourtly A demo recorder built on DOM snapshots, whose hard problems are capturing enough of a page that the replay renders without calling home, masking by default rather than by accident, and replaying untrusted captured markup without letting it run. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture with masking on from the first click The extension records a snapshot per click with input values and text masked by default. ### Steps 1. Set masking explicitly at capture The defaults mask passwords; your users' data is not a password. Files: `extension/record.ts`, `test/masking.test.ts` 2. Record the click target and its box The hotspot in the replay is only as good as what you captured about the click. Files: `extension/clicks.ts` 3. Show that recording is happening Any capture tool without a visible indicator is a surveillance tool by accident. Files: `extension/indicator.ts` ### Done when - [ ] Sample personal data never reaches the stored snapshot - [ ] A recording indicator is visible throughout - [ ] Click selector and bounding box are recorded - [ ] An interrupted capture saves nothing partial ### Watch out - Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. ## Phase 2 · Snapshots that render without calling home Stylesheets, fonts and images are inlined, and whatever fails is recorded rather than silent. ### Steps 1. Inline stylesheets, fonts and images The replay has to be self-contained or it is a screenshot with extra steps. Files: `extension/inline.ts` 2. Record every inlining failure A blank replay with a reason is a bug; one without is a mystery. Files: `src/snapshots/assets.ts` 3. Bound the snapshot size Snapshots grow without limit and the ceiling is what keeps the database usable. Files: `extension/limits.ts` ### Done when - [ ] The demo renders with the origin host blocked - [ ] Inlining failures are listed with reasons - [ ] Oversized snapshots are rejected at capture - [ ] Steps with failed stylesheets cannot publish ### Watch out - A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. ## Phase 3 · A review screen nobody can skip Every captured text node is seen by a person before the demo can be published. ### Steps 1. Gate publishing on a per-step review The gate is the feature; a checklist in the documentation is not. Files: `src/review/gate.ts`, `test/review-gate.test.ts` 2. Record unmasking as an action Someone decided to reveal that. The record says who and when. Files: `src/review/redactions.ts` 3. List the visible text, do not just render it People skim a page and read a list. Files: `src/review/TextList.tsx` ### Done when - [ ] Publishing is blocked until every step is reviewed - [ ] Recapture clears the review and re-blocks publishing - [ ] Unmaskings are recorded with a timestamp - [ ] Visible text is shown as a list for scanning ### Watch out - Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. ## Phase 4 · Replaying captured markup without running it The snapshot renders as a page with a working hotspot and none of its scripts execute. ### Steps 1. Sandbox the frame and keep scripts off Captured markup is input from another application. Treat it that way. Files: `src/replay/Frame.tsx` 2. Make the hotspot keyboard operable A demo nobody can drive with a keyboard excludes people for no reason. Files: `src/replay/Hotspot.tsx` 3. Degrade a failed snapshot visibly A blank frame reads as a broken product, which is the opposite of the point. Files: `src/replay/Fallback.tsx` ### Done when - [ ] Captured scripts never execute in replay - [ ] Hotspots work with a keyboard and are announced - [ ] A failed snapshot degrades with a message - [ ] Canvas and cross-origin content show as placeholders ### Watch out - Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. ## Not in this build - Custom domains and certificates. - CRM synchronisation. - Canvas, video and cross-origin frame capture. - Lead capture forms inside the demo. ## After v1, if you want it - Branching walkthroughs, once single-path replay has survived real prospects - A guided tour overlay on the live product, which is a different tool sharing the step model - Per-step timing in the analytics, which tells you where people stop rather than that they did ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Snapshots and analytics. Snapshots are the sensitive part. DATABASE_URL=postgres://user:redacted@localhost:5432/demos # Required. Where published demos live. Used in embed snippets. PUBLIC_BASE_URL=https://demos.example.com # Required. Per step, after inlining. A capture over this is refused with the largest asset named. SNAPSHOT_MAX_MB=8 # Required. Who may embed a demo. Empty means nobody but you. EMBED_FRAME_ANCESTORS=https://www.example.com https://example.com # Required. Randomness in a share link. The link is the access control. SLUG_BYTES=12 # Required. Unpublished captures expire, because a draft is still captured data. DRAFT_RETENTION_DAYS=90 # Optional · secret. Optional first-draft step descriptions. Without it you write them yourself and nothing else changes. LLM_API_KEY=sk-...redacted
You are building a lean indie version of Escourtly. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Escourtly · indie build A demo recorder built on DOM snapshots, whose hard problems are capturing enough of a page that the replay renders without calling home, masking by default rather than by accident, and replaying untrusted captured markup without letting it run. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A Chrome extension recording one DOM snapshot per click with rrweb | snapshots beat video because the replay is a real page | | Masking | Inverted defaults: inputs and text masked at capture, unmasked deliberately | the library masks passwords only, which is not the same as masking data | | Storage | Postgres with Drizzle, snapshots stored as compressed documents | snapshots are large and you will keep more of them than you expect | | Replay | The snapshot rendered in a sandboxed frame with scripts disallowed | captured markup is untrusted input that came from somebody else's page | | Analytics | First-party counters in your own database | a demo page that ships a third-party tracker is a demo page with a compliance problem | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the extension and runs the web application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL** · free locally, a few pounds a month managed - Why: Snapshots are large documents and you will keep more versions of them than you plan to, which is a database question rather than a file question. - Get it: Docker, your package manager, or a managed instance. - Verify: The application connects and migrations run - [ ] **Chrome with developer mode for unpacked extensions** · free - Why: The capture half only runs as an extension, and the whole loop is untestable until one is loaded. - Get it: Chrome, then enable developer mode on the extensions page. - Verify: The unpacked extension loads and its indicator appears on a page - [ ] **An application you are entitled to capture** · free - Why: Capture fidelity and masking are only testable against a real signed-in application, and it must be one you have permission to record. - Get it: Your own product in a staging environment, seeded with obviously fake data. - Verify: A signed-in page captures and replays with the origin blocked - [ ] **Your masking and retention policy** · free - Why: Both concern other people's data appearing in your database, and both are decisions that quietly default to the worst option if nobody makes them. - Get it: Write down what is masked by default, who may unmask, and how long unpublished captures are kept. - Verify: All three are in the configuration and the README ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Custom domains and certificates. - CRM synchronisation. - Canvas, video and cross-origin frame capture. - Lead capture forms inside the demo. - capture fidelity on canvas, cross-origin iframes, shadow DOM and lazily loaded assets - no-login share links and embeds that hold up under real traffic - viewer funnel analytics and lead capture - CRM sync to Salesforce, HubSpot and Pipedrive - custom domains with certificates handled for you If one of those is essential to you, that is the reason to keep paying for Escourtly, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Escourtly The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an interactive demo builder that captures a product as clickable page snapshots to replace Escourtly. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A snapshot of an application you are signed into contains whatever was on the screen: customer names, email addresses, invoice totals, sometimes a token in a data attribute. The capture library's defaults are not a privacy policy. rrweb records with maskAllInputs off by default, masking only password inputs, and its opt-outs are class names you have to apply: rr-block for an element replaced by a placeholder, rr-ignore for an element whose input events are not recorded, rr-mask for text that is masked. Check the current defaults yourself before you rely on any of this. So this build inverts them. Masking of input values and text is on from the first capture, unmasking is a deliberate per-element choice made in the editor, and nothing publishes until somebody has opened the review screen and seen every captured text node that is still visible. Capture only products you are entitled to capture. Never a customer's account, never a competitor's application behind a login. ### Stack (fixed, do not substitute) - Capture: A Chrome extension recording one DOM snapshot per click with rrweb · snapshots beat video because the replay is a real page - Masking: Inverted defaults: inputs and text masked at capture, unmasked deliberately · the library masks passwords only, which is not the same as masking data - Storage: Postgres with Drizzle, snapshots stored as compressed documents · snapshots are large and you will keep more of them than you expect - Replay: The snapshot rendered in a sandboxed frame with scripts disallowed · captured markup is untrusted input that came from somebody else's page - Analytics: First-party counters in your own database · a demo page that ships a third-party tracker is a demo page with a compliance problem ### Data model (create this before Phase 1) - demos(id, slug, title, status, created_at, published_at, cta_label, cta_url) · slug is generated, not taken from the title - steps(id, demo_id, ordinal, snapshot_ref, target_selector, target_box_json, title, description, reviewed_at) - snapshots(id, document_compressed, bytes, asset_count, inline_failures_json, captured_url_host) · the host, not the full URL, because the path often carries identifiers - assets(snapshot_id, original_url, kind, bytes, inlined, failure_reason) · what got inlined and what did not, so a blank replay is diagnosable - redactions(step_id, selector, kind, applied_at) · every unmasking and every extra mask, recorded as an action somebody took - views(demo_id, session_hash, started_at, last_step, completed_at, cta_clicked_at) · first-party, no third-party script anywhere - A published demo is a set of frozen documents. Nothing in the replay path talks to the product that was captured, which is what makes it survive a deploy. ### Phase 1 · Capture with masking on from the first click Build: Build a Chrome extension that starts and stops a capture, records one snapshot per click along with the clicked element's selector and bounding box, and shows a visible recording indicator. Turn on masking of input values and text at capture time, not at publish time. Trap: Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. Done when: A capture of a page containing sample personal data stores no recognisable value from it, asserted by searching the stored document in a test. The recording indicator is visible on the page throughout a capture. Each step records the clicked element's selector and bounding box, verified by replaying a two step capture. Stopping a capture writes the steps atomically, so an interrupted capture leaves nothing half saved. Do not build yet: No asset inlining, editor, replay or publishing. ### Phase 2 · Snapshots that render without calling home Build: Fetch and inline stylesheets, fonts and images into each snapshot, bounded by a size ceiling. Record every asset that could not be inlined with the reason. Show the failures in the editor so a blank replay has an explanation before anybody else sees it. Trap: A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. Done when: A published demo renders correctly in a browser with no access to the captured origin, tested by blocking that host. Every asset that failed to inline is listed in the editor with its reason and its size. A snapshot exceeding the size ceiling is rejected at capture with a message naming the largest asset. Publishing is refused for a step whose stylesheets did not inline. Do not build yet: No editor, replay, publishing or analytics yet. ### Phase 3 · A review screen nobody can skip Build: Show each step with its visible text highlighted, offer per-element masking and unmasking, record every redaction as an action with a timestamp, and require the whole demo to be reviewed before publishing becomes available. Trap: Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. Done when: Publishing is unavailable until every step carries a review timestamp, tested by attempting to publish an unreviewed demo. Recapturing a step clears its review timestamp and blocks publishing again. Every unmasking is stored as a redaction row with what was unmasked and when. The review screen shows the visible text of a step in one list, so scanning it does not mean reading a rendered page. Do not build yet: No replay, sharing or analytics yet. ### Phase 4 · Replaying captured markup without running it Build: Render each snapshot in a sandboxed frame that does not allow scripts, draw the hotspot from the recorded bounding box, and advance on click or on keyboard activation. Add a start overlay and a closing call to action. Trap: Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. Done when: Scripts in a captured page do not execute during replay, tested with a snapshot containing a script that would announce itself. The hotspot is reachable and activatable by keyboard alone, and the current step is announced to a screen reader. A snapshot that fails to render shows a message and an option to skip to the next step, never a blank frame. Canvas and cross-origin frame content are shown as placeholders, which the library records as not captured by default. Do not build yet: No sharing, embedding, analytics or export yet. ### Phase 5 · Sharing, counting and deleting Build: Publish at a generated slug with no login, offer an embed snippet, and set frame-ancestors so you control where it can be embedded. Count views, step completions and call to action clicks in your own database. Support unpublishing and a deletion that removes snapshots and assets. Trap: A demo slug derived from the customer's name, published with no login, tells anybody who guesses it which prospects you are talking to, and slugs derived from titles are guessable by design. Generate slugs with enough randomness that guessing is impractical, and make unpublish take effect immediately rather than at the next cache expiry. Done when: Slugs are generated rather than derived from the title, and a demo cannot be found by guessing a customer name, checked by inspection. Unpublishing makes the link stop working immediately, tested with a warm cache. View, step and call to action counts are recorded first-party with no third-party script on the page, verified by listing the page's network requests. Deleting a demo removes its snapshots and assets and the deletion is verified by querying for orphans. Do not build yet: Nothing after this. ### Out of scope (and why) - Custom domains and certificate management, which is infrastructure with its own failure modes and on-call implications. - CRM synchronisation, where each connector is an authentication project plus a permanent maintenance commitment. - Capturing canvas, video and cross-origin frames, which the capture library leaves off by default for good reasons and which the replay would have to relax its sandbox to show. - Lead capture forms inside the demo, which collect other people's details and turn a demo tool into a data controller. ### README must contain - That masking is on at capture and unmasking is a recorded per-element decision - That publishing is gated on a per-step review and recapturing a step resets it - That the replay frame does not allow scripts, and what that means for canvas and widgets - How to load the extension unpacked, and the size ceiling for a snapshot - What deletion removes, and that share links are generated rather than derived from a title ===== AGENTS.md ===== # Agent instructions · Escourtly indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Chrome extension recording one DOM snapshot per click with rrweb, Inverted defaults: inputs and text masked at capture, unmasked deliberately, Postgres with Drizzle, snapshots stored as compressed documents, The snapshot rendered in a sandboxed frame with scripts disallowed, First-party counters in your own database. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. - A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. - Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. - Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. ===== BUILD_PLAN.md ===== # Build plan · Escourtly A demo recorder built on DOM snapshots, whose hard problems are capturing enough of a page that the replay renders without calling home, masking by default rather than by accident, and replaying untrusted captured markup without letting it run. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture with masking on from the first click The extension records a snapshot per click with input values and text masked by default. ### Steps 1. Set masking explicitly at capture The defaults mask passwords; your users' data is not a password. Files: `extension/record.ts`, `test/masking.test.ts` 2. Record the click target and its box The hotspot in the replay is only as good as what you captured about the click. Files: `extension/clicks.ts` 3. Show that recording is happening Any capture tool without a visible indicator is a surveillance tool by accident. Files: `extension/indicator.ts` ### Done when - [ ] Sample personal data never reaches the stored snapshot - [ ] A recording indicator is visible throughout - [ ] Click selector and bounding box are recorded - [ ] An interrupted capture saves nothing partial ### Watch out - Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. ## Phase 2 · Snapshots that render without calling home Stylesheets, fonts and images are inlined, and whatever fails is recorded rather than silent. ### Steps 1. Inline stylesheets, fonts and images The replay has to be self-contained or it is a screenshot with extra steps. Files: `extension/inline.ts` 2. Record every inlining failure A blank replay with a reason is a bug; one without is a mystery. Files: `src/snapshots/assets.ts` 3. Bound the snapshot size Snapshots grow without limit and the ceiling is what keeps the database usable. Files: `extension/limits.ts` ### Done when - [ ] The demo renders with the origin host blocked - [ ] Inlining failures are listed with reasons - [ ] Oversized snapshots are rejected at capture - [ ] Steps with failed stylesheets cannot publish ### Watch out - A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. ## Phase 3 · A review screen nobody can skip Every captured text node is seen by a person before the demo can be published. ### Steps 1. Gate publishing on a per-step review The gate is the feature; a checklist in the documentation is not. Files: `src/review/gate.ts`, `test/review-gate.test.ts` 2. Record unmasking as an action Someone decided to reveal that. The record says who and when. Files: `src/review/redactions.ts` 3. List the visible text, do not just render it People skim a page and read a list. Files: `src/review/TextList.tsx` ### Done when - [ ] Publishing is blocked until every step is reviewed - [ ] Recapture clears the review and re-blocks publishing - [ ] Unmaskings are recorded with a timestamp - [ ] Visible text is shown as a list for scanning ### Watch out - Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. ## Phase 4 · Replaying captured markup without running it The snapshot renders as a page with a working hotspot and none of its scripts execute. ### Steps 1. Sandbox the frame and keep scripts off Captured markup is input from another application. Treat it that way. Files: `src/replay/Frame.tsx` 2. Make the hotspot keyboard operable A demo nobody can drive with a keyboard excludes people for no reason. Files: `src/replay/Hotspot.tsx` 3. Degrade a failed snapshot visibly A blank frame reads as a broken product, which is the opposite of the point. Files: `src/replay/Fallback.tsx` ### Done when - [ ] Captured scripts never execute in replay - [ ] Hotspots work with a keyboard and are announced - [ ] A failed snapshot degrades with a message - [ ] Canvas and cross-origin content show as placeholders ### Watch out - Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. ## Not in this build - Custom domains and certificates. - CRM synchronisation. - Canvas, video and cross-origin frame capture. - Lead capture forms inside the demo. ## After v1, if you want it - Branching walkthroughs, once single-path replay has survived real prospects - A guided tour overlay on the live product, which is a different tool sharing the step model - Per-step timing in the analytics, which tells you where people stop rather than that they did ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Snapshots and analytics. Snapshots are the sensitive part. DATABASE_URL=postgres://user:redacted@localhost:5432/demos # Required. Where published demos live. Used in embed snippets. PUBLIC_BASE_URL=https://demos.example.com # Required. Per step, after inlining. A capture over this is refused with the largest asset named. SNAPSHOT_MAX_MB=8 # Required. Who may embed a demo. Empty means nobody but you. EMBED_FRAME_ANCESTORS=https://www.example.com https://example.com # Required. Randomness in a share link. The link is the access control. SLUG_BYTES=12 # Required. Unpublished captures expire, because a draft is still captured data. DRAFT_RETENTION_DAYS=90 # Optional · secret. Optional first-draft step descriptions. Without it you write them yourself and nothing else changes. LLM_API_KEY=sk-...redacted
You are building a production product version of Escourtly. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Escourtly · product brief ## Problem The core loop is a weekend build on top of rrweb: snapshot the DOM on each click, inline the assets, and replay the snapshots as a clickable walkthrough. What takes the project from a demo to something you would put in front of a prospect is capture fidelity · real apps are full of canvas, cross-origin iframes, shadow DOM and lazily loaded assets, and every one of them is a separate fix. Past that sit the parts that are infrastructure rather than features: no-login share links and embeds that stay up, custom domains with certificates, funnel analytics, and CRM sync. Build it for your own product and one sitting gets you a long way. Build it to replace the subscription and you have signed up for the edge cases. ## Product outcome A demo tool where a capture never quietly takes customer data, a published walkthrough renders for someone who has never heard of your network, and the replay cannot run the markup it is showing. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node and a Postgres database - a Chrome MV3 extension build step - an OpenAI/Anthropic API key for step descriptions - somewhere to host the public demo pages ## Explicit non-goals for v1 - Custom domains and certificates. - CRM synchronisation. - Canvas, video and cross-origin frame capture. - Lead capture forms inside the demo. - capture fidelity on canvas, cross-origin iframes, shadow DOM and lazily loaded assets - no-login share links and embeds that hold up under real traffic - viewer funnel analytics and lead capture - CRM sync to Salesforce, HubSpot and Pipedrive - custom domains with certificates handled for you ## Success criteria - Sample personal data is confirmed absent from stored snapshots - A demo is confirmed to render with the captured origin blocked - Publishing is confirmed blocked until every step is reviewed - Captured scripts are confirmed not to execute in replay - Deletion is confirmed to leave no orphaned snapshots or assets ===== BRIEF.md ===== # Build brief · Escourtly The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an interactive demo builder that captures a product as clickable page snapshots to replace Escourtly. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A snapshot of an application you are signed into contains whatever was on the screen: customer names, email addresses, invoice totals, sometimes a token in a data attribute. The capture library's defaults are not a privacy policy. rrweb records with maskAllInputs off by default, masking only password inputs, and its opt-outs are class names you have to apply: rr-block for an element replaced by a placeholder, rr-ignore for an element whose input events are not recorded, rr-mask for text that is masked. Check the current defaults yourself before you rely on any of this. So this build inverts them. Masking of input values and text is on from the first capture, unmasking is a deliberate per-element choice made in the editor, and nothing publishes until somebody has opened the review screen and seen every captured text node that is still visible. Capture only products you are entitled to capture. Never a customer's account, never a competitor's application behind a login. ### Stack (fixed, do not substitute) - Capture: A Chrome extension recording one DOM snapshot per click with rrweb · snapshots beat video because the replay is a real page - Masking: Inverted defaults: inputs and text masked at capture, unmasked deliberately · the library masks passwords only, which is not the same as masking data - Storage: Postgres with Drizzle, snapshots stored as compressed documents · snapshots are large and you will keep more of them than you expect - Replay: The snapshot rendered in a sandboxed frame with scripts disallowed · captured markup is untrusted input that came from somebody else's page - Analytics: First-party counters in your own database · a demo page that ships a third-party tracker is a demo page with a compliance problem ### Data model (create this before Phase 1) - demos(id, slug, title, status, created_at, published_at, cta_label, cta_url) · slug is generated, not taken from the title - steps(id, demo_id, ordinal, snapshot_ref, target_selector, target_box_json, title, description, reviewed_at) - snapshots(id, document_compressed, bytes, asset_count, inline_failures_json, captured_url_host) · the host, not the full URL, because the path often carries identifiers - assets(snapshot_id, original_url, kind, bytes, inlined, failure_reason) · what got inlined and what did not, so a blank replay is diagnosable - redactions(step_id, selector, kind, applied_at) · every unmasking and every extra mask, recorded as an action somebody took - views(demo_id, session_hash, started_at, last_step, completed_at, cta_clicked_at) · first-party, no third-party script anywhere - A published demo is a set of frozen documents. Nothing in the replay path talks to the product that was captured, which is what makes it survive a deploy. ### Phase 1 · Capture with masking on from the first click Build: Build a Chrome extension that starts and stops a capture, records one snapshot per click along with the clicked element's selector and bounding box, and shows a visible recording indicator. Turn on masking of input values and text at capture time, not at publish time. Trap: Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. Done when: A capture of a page containing sample personal data stores no recognisable value from it, asserted by searching the stored document in a test. The recording indicator is visible on the page throughout a capture. Each step records the clicked element's selector and bounding box, verified by replaying a two step capture. Stopping a capture writes the steps atomically, so an interrupted capture leaves nothing half saved. Do not build yet: No asset inlining, editor, replay or publishing. ### Phase 2 · Snapshots that render without calling home Build: Fetch and inline stylesheets, fonts and images into each snapshot, bounded by a size ceiling. Record every asset that could not be inlined with the reason. Show the failures in the editor so a blank replay has an explanation before anybody else sees it. Trap: A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. Done when: A published demo renders correctly in a browser with no access to the captured origin, tested by blocking that host. Every asset that failed to inline is listed in the editor with its reason and its size. A snapshot exceeding the size ceiling is rejected at capture with a message naming the largest asset. Publishing is refused for a step whose stylesheets did not inline. Do not build yet: No editor, replay, publishing or analytics yet. ### Phase 3 · A review screen nobody can skip Build: Show each step with its visible text highlighted, offer per-element masking and unmasking, record every redaction as an action with a timestamp, and require the whole demo to be reviewed before publishing becomes available. Trap: Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. Done when: Publishing is unavailable until every step carries a review timestamp, tested by attempting to publish an unreviewed demo. Recapturing a step clears its review timestamp and blocks publishing again. Every unmasking is stored as a redaction row with what was unmasked and when. The review screen shows the visible text of a step in one list, so scanning it does not mean reading a rendered page. Do not build yet: No replay, sharing or analytics yet. ### Phase 4 · Replaying captured markup without running it Build: Render each snapshot in a sandboxed frame that does not allow scripts, draw the hotspot from the recorded bounding box, and advance on click or on keyboard activation. Add a start overlay and a closing call to action. Trap: Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. Done when: Scripts in a captured page do not execute during replay, tested with a snapshot containing a script that would announce itself. The hotspot is reachable and activatable by keyboard alone, and the current step is announced to a screen reader. A snapshot that fails to render shows a message and an option to skip to the next step, never a blank frame. Canvas and cross-origin frame content are shown as placeholders, which the library records as not captured by default. Do not build yet: No sharing, embedding, analytics or export yet. ### Phase 5 · Sharing, counting and deleting Build: Publish at a generated slug with no login, offer an embed snippet, and set frame-ancestors so you control where it can be embedded. Count views, step completions and call to action clicks in your own database. Support unpublishing and a deletion that removes snapshots and assets. Trap: A demo slug derived from the customer's name, published with no login, tells anybody who guesses it which prospects you are talking to, and slugs derived from titles are guessable by design. Generate slugs with enough randomness that guessing is impractical, and make unpublish take effect immediately rather than at the next cache expiry. Done when: Slugs are generated rather than derived from the title, and a demo cannot be found by guessing a customer name, checked by inspection. Unpublishing makes the link stop working immediately, tested with a warm cache. View, step and call to action counts are recorded first-party with no third-party script on the page, verified by listing the page's network requests. Deleting a demo removes its snapshots and assets and the deletion is verified by querying for orphans. Do not build yet: Nothing after this. ### Out of scope (and why) - Custom domains and certificate management, which is infrastructure with its own failure modes and on-call implications. - CRM synchronisation, where each connector is an authentication project plus a permanent maintenance commitment. - Capturing canvas, video and cross-origin frames, which the capture library leaves off by default for good reasons and which the replay would have to relax its sandbox to show. - Lead capture forms inside the demo, which collect other people's details and turn a demo tool into a data controller. ### README must contain - That masking is on at capture and unmasking is a recorded per-element decision - That publishing is gated on a per-step review and recapturing a step resets it - That the replay frame does not allow scripts, and what that means for canvas and widgets - How to load the extension unpacked, and the size ceiling for a snapshot - What deletion removes, and that share links are generated rather than derived from a title ===== ARCHITECTURE.md ===== # Architecture · Escourtly ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A Chrome extension recording one DOM snapshot per click with rrweb | snapshots beat video because the replay is a real page | | Masking | Inverted defaults: inputs and text masked at capture, unmasked deliberately | the library masks passwords only, which is not the same as masking data | | Storage | Postgres with Drizzle, snapshots stored as compressed documents | snapshots are large and you will keep more of them than you expect | | Replay | The snapshot rendered in a sandboxed frame with scripts disallowed | captured markup is untrusted input that came from somebody else's page | | Analytics | First-party counters in your own database | a demo page that ships a third-party tracker is a demo page with a compliance problem | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Extension | Capture, masking defaults and click targets. | Any capture library, provided masking is set explicitly rather than inherited. | | Inlining | Self-contained snapshots and recorded failures. | Any fetcher, provided failures are recorded and block publishing. | | Review | The per-step gate and redaction history. | Not swappable for an optional review, which is the same as no review. | | Replay | Sandboxed rendering and accessible hotspots. | Any renderer, provided scripts stay disallowed. | | Publishing | Generated slugs, embedding rules and first-party counts. | Any delivery, provided unpublish is immediate. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Snapshots and analytics. Snapshots are the sensitive part. - `PUBLIC_BASE_URL` · required · Where published demos live. Used in embed snippets. - `SNAPSHOT_MAX_MB` · required · Per step, after inlining. A capture over this is refused with the largest asset named. - `EMBED_FRAME_ANCESTORS` · required · Who may embed a demo. Empty means nobody but you. - `SLUG_BYTES` · required · Randomness in a share link. The link is the access control. - `DRAFT_RETENTION_DAYS` · required · Unpublished captures expire, because a draft is still captured data. - `LLM_API_KEY` · optional, secret · Optional first-draft step descriptions. Without it you write them yourself and nothing else changes. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Escourtly product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A Chrome extension recording one DOM snapshot per click with rrweb, Inverted defaults: inputs and text masked at capture, unmasked deliberately, Postgres with Drizzle, snapshots stored as compressed documents, The snapshot rendered in a sandboxed frame with scripts disallowed, First-party counters in your own database. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. - A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. - Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. - Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. - A demo slug derived from the customer's name, published with no login, tells anybody who guesses it which prospects you are talking to, and slugs derived from titles are guessable by design. Generate slugs with enough randomness that guessing is impractical, and make unpublish take effect immediately rather than at the next cache expiry. ===== MILESTONES.md ===== # Delivery milestones · Escourtly Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capture with masking on from the first click The extension records a snapshot per click with input values and text masked by default. ### Steps 1. Set masking explicitly at capture The defaults mask passwords; your users' data is not a password. Files: `extension/record.ts`, `test/masking.test.ts` 2. Record the click target and its box The hotspot in the replay is only as good as what you captured about the click. Files: `extension/clicks.ts` 3. Show that recording is happening Any capture tool without a visible indicator is a surveillance tool by accident. Files: `extension/indicator.ts` ### Done when - [ ] Sample personal data never reaches the stored snapshot - [ ] A recording indicator is visible throughout - [ ] Click selector and bounding box are recorded - [ ] An interrupted capture saves nothing partial ### Watch out - Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. ## M2 · Snapshots that render without calling home Stylesheets, fonts and images are inlined, and whatever fails is recorded rather than silent. ### Steps 1. Inline stylesheets, fonts and images The replay has to be self-contained or it is a screenshot with extra steps. Files: `extension/inline.ts` 2. Record every inlining failure A blank replay with a reason is a bug; one without is a mystery. Files: `src/snapshots/assets.ts` 3. Bound the snapshot size Snapshots grow without limit and the ceiling is what keeps the database usable. Files: `extension/limits.ts` ### Done when - [ ] The demo renders with the origin host blocked - [ ] Inlining failures are listed with reasons - [ ] Oversized snapshots are rejected at capture - [ ] Steps with failed stylesheets cannot publish ### Watch out - A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. ## M3 · A review screen nobody can skip Every captured text node is seen by a person before the demo can be published. ### Steps 1. Gate publishing on a per-step review The gate is the feature; a checklist in the documentation is not. Files: `src/review/gate.ts`, `test/review-gate.test.ts` 2. Record unmasking as an action Someone decided to reveal that. The record says who and when. Files: `src/review/redactions.ts` 3. List the visible text, do not just render it People skim a page and read a list. Files: `src/review/TextList.tsx` ### Done when - [ ] Publishing is blocked until every step is reviewed - [ ] Recapture clears the review and re-blocks publishing - [ ] Unmaskings are recorded with a timestamp - [ ] Visible text is shown as a list for scanning ### Watch out - Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. ## M4 · Replaying captured markup without running it The snapshot renders as a page with a working hotspot and none of its scripts execute. ### Steps 1. Sandbox the frame and keep scripts off Captured markup is input from another application. Treat it that way. Files: `src/replay/Frame.tsx` 2. Make the hotspot keyboard operable A demo nobody can drive with a keyboard excludes people for no reason. Files: `src/replay/Hotspot.tsx` 3. Degrade a failed snapshot visibly A blank frame reads as a broken product, which is the opposite of the point. Files: `src/replay/Fallback.tsx` ### Done when - [ ] Captured scripts never execute in replay - [ ] Hotspots work with a keyboard and are announced - [ ] A failed snapshot degrades with a message - [ ] Canvas and cross-origin content show as placeholders ### Watch out - Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. ## M5 · Sharing, counting and deleting (production only) A demo has a link, an embed, first-party counts and a deletion that really deletes. ### Steps 1. Generate unguessable slugs The link is the access control, so it has to be worth something. Files: `src/publish/slug.ts` 2. Set frame-ancestors for the embed You decide which sites can frame your demo, not the internet. Files: `src/publish/headers.ts` 3. Delete snapshots and assets together An orphaned snapshot is the captured data you promised to remove. Files: `src/demos/delete.ts`, `test/orphans.test.ts` ### Done when - [ ] Slugs are generated and not guessable - [ ] Unpublish takes effect immediately - [ ] Counts are first-party with no third-party requests - [ ] Deletion leaves no orphaned snapshots or assets ### Watch out - A demo slug derived from the customer's name, published with no login, tells anybody who guesses it which prospects you are talking to, and slugs derived from titles are guessable by design. Generate slugs with enough randomness that guessing is impractical, and make unpublish take effect immediately rather than at the next cache expiry. ===== OPERATIONS.md ===== # Operations · Escourtly ## Backup The database, including snapshot documents, which are the bulk of it. Consider whether you want long backup retention of captured application screens at all, since a backup is a copy of exactly the data your retention policy is trying to expire. ## Restore Restore and immediately verify that unpublished drafts past their retention are expired again, so a restore does not resurrect captures somebody deliberately let go. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Inlining failure rates per capture, snapshot sizes against the ceiling, and drafts past retention. A rising inlining failure rate usually means the captured product changed how it loads assets. ## Incident checklist A demo showing real customer data is the incident this design exists to prevent, and the response is unpublish first, then the redaction history to find out what was unmasked and when. A blank replay is an inlining failure and the reason is already recorded. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Sample personal data is confirmed absent from stored snapshots - [ ] A demo is confirmed to render with the captured origin blocked - [ ] Publishing is confirmed blocked until every step is reviewed - [ ] Captured scripts are confirmed not to execute in replay - [ ] Deletion is confirmed to leave no orphaned snapshots or assets ## Launch constraint Do not market omitted Escourtly capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Snapshots and analytics. Snapshots are the sensitive part. DATABASE_URL=postgres://user:redacted@localhost:5432/demos # Required. Where published demos live. Used in embed snippets. PUBLIC_BASE_URL=https://demos.example.com # Required. Per step, after inlining. A capture over this is refused with the largest asset named. SNAPSHOT_MAX_MB=8 # Required. Who may embed a demo. Empty means nobody but you. EMBED_FRAME_ANCESTORS=https://www.example.com https://example.com # Required. Randomness in a share link. The link is the access control. SLUG_BYTES=12 # Required. Unpublished captures expire, because a draft is still captured data. DRAFT_RETENTION_DAYS=90 # Optional · secret. Optional first-draft step descriptions. Without it you write them yourself and nothing else changes. LLM_API_KEY=sk-...redacted
# Escourtly · indie build A demo recorder built on DOM snapshots, whose hard problems are capturing enough of a page that the replay renders without calling home, masking by default rather than by accident, and replaying untrusted captured markup without letting it run. Estimated effort: **multi-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A Chrome extension recording one DOM snapshot per click with rrweb | snapshots beat video because the replay is a real page | | Masking | Inverted defaults: inputs and text masked at capture, unmasked deliberately | the library masks passwords only, which is not the same as masking data | | Storage | Postgres with Drizzle, snapshots stored as compressed documents | snapshots are large and you will keep more of them than you expect | | Replay | The snapshot rendered in a sandboxed frame with scripts disallowed | captured markup is untrusted input that came from somebody else's page | | Analytics | First-party counters in your own database | a demo page that ships a third-party tracker is a demo page with a compliance problem | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the extension and runs the web application. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL** · free locally, a few pounds a month managed - Why: Snapshots are large documents and you will keep more versions of them than you plan to, which is a database question rather than a file question. - Get it: Docker, your package manager, or a managed instance. - Verify: The application connects and migrations run - [ ] **Chrome with developer mode for unpacked extensions** · free - Why: The capture half only runs as an extension, and the whole loop is untestable until one is loaded. - Get it: Chrome, then enable developer mode on the extensions page. - Verify: The unpacked extension loads and its indicator appears on a page - [ ] **An application you are entitled to capture** · free - Why: Capture fidelity and masking are only testable against a real signed-in application, and it must be one you have permission to record. - Get it: Your own product in a staging environment, seeded with obviously fake data. - Verify: A signed-in page captures and replays with the origin blocked - [ ] **Your masking and retention policy** · free - Why: Both concern other people's data appearing in your database, and both are decisions that quietly default to the worst option if nobody makes them. - Get it: Write down what is masked by default, who may unmask, and how long unpublished captures are kept. - Verify: All three are in the configuration and the README ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Custom domains and certificates. - CRM synchronisation. - Canvas, video and cross-origin frame capture. - Lead capture forms inside the demo. - capture fidelity on canvas, cross-origin iframes, shadow DOM and lazily loaded assets - no-login share links and embeds that hold up under real traffic - viewer funnel analytics and lead capture - CRM sync to Salesforce, HubSpot and Pipedrive - custom domains with certificates handled for you If one of those is essential to you, that is the reason to keep paying for Escourtly, and the README should say so rather than pretend.
# Build brief · Escourtly The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an interactive demo builder that captures a product as clickable page snapshots to replace Escourtly. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A snapshot of an application you are signed into contains whatever was on the screen: customer names, email addresses, invoice totals, sometimes a token in a data attribute. The capture library's defaults are not a privacy policy. rrweb records with maskAllInputs off by default, masking only password inputs, and its opt-outs are class names you have to apply: rr-block for an element replaced by a placeholder, rr-ignore for an element whose input events are not recorded, rr-mask for text that is masked. Check the current defaults yourself before you rely on any of this. So this build inverts them. Masking of input values and text is on from the first capture, unmasking is a deliberate per-element choice made in the editor, and nothing publishes until somebody has opened the review screen and seen every captured text node that is still visible. Capture only products you are entitled to capture. Never a customer's account, never a competitor's application behind a login. ### Stack (fixed, do not substitute) - Capture: A Chrome extension recording one DOM snapshot per click with rrweb · snapshots beat video because the replay is a real page - Masking: Inverted defaults: inputs and text masked at capture, unmasked deliberately · the library masks passwords only, which is not the same as masking data - Storage: Postgres with Drizzle, snapshots stored as compressed documents · snapshots are large and you will keep more of them than you expect - Replay: The snapshot rendered in a sandboxed frame with scripts disallowed · captured markup is untrusted input that came from somebody else's page - Analytics: First-party counters in your own database · a demo page that ships a third-party tracker is a demo page with a compliance problem ### Data model (create this before Phase 1) - demos(id, slug, title, status, created_at, published_at, cta_label, cta_url) · slug is generated, not taken from the title - steps(id, demo_id, ordinal, snapshot_ref, target_selector, target_box_json, title, description, reviewed_at) - snapshots(id, document_compressed, bytes, asset_count, inline_failures_json, captured_url_host) · the host, not the full URL, because the path often carries identifiers - assets(snapshot_id, original_url, kind, bytes, inlined, failure_reason) · what got inlined and what did not, so a blank replay is diagnosable - redactions(step_id, selector, kind, applied_at) · every unmasking and every extra mask, recorded as an action somebody took - views(demo_id, session_hash, started_at, last_step, completed_at, cta_clicked_at) · first-party, no third-party script anywhere - A published demo is a set of frozen documents. Nothing in the replay path talks to the product that was captured, which is what makes it survive a deploy. ### Phase 1 · Capture with masking on from the first click Build: Build a Chrome extension that starts and stops a capture, records one snapshot per click along with the clicked element's selector and bounding box, and shows a visible recording indicator. Turn on masking of input values and text at capture time, not at publish time. Trap: Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. Done when: A capture of a page containing sample personal data stores no recognisable value from it, asserted by searching the stored document in a test. The recording indicator is visible on the page throughout a capture. Each step records the clicked element's selector and bounding box, verified by replaying a two step capture. Stopping a capture writes the steps atomically, so an interrupted capture leaves nothing half saved. Do not build yet: No asset inlining, editor, replay or publishing. ### Phase 2 · Snapshots that render without calling home Build: Fetch and inline stylesheets, fonts and images into each snapshot, bounded by a size ceiling. Record every asset that could not be inlined with the reason. Show the failures in the editor so a blank replay has an explanation before anybody else sees it. Trap: A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. Done when: A published demo renders correctly in a browser with no access to the captured origin, tested by blocking that host. Every asset that failed to inline is listed in the editor with its reason and its size. A snapshot exceeding the size ceiling is rejected at capture with a message naming the largest asset. Publishing is refused for a step whose stylesheets did not inline. Do not build yet: No editor, replay, publishing or analytics yet. ### Phase 3 · A review screen nobody can skip Build: Show each step with its visible text highlighted, offer per-element masking and unmasking, record every redaction as an action with a timestamp, and require the whole demo to be reviewed before publishing becomes available. Trap: Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. Done when: Publishing is unavailable until every step carries a review timestamp, tested by attempting to publish an unreviewed demo. Recapturing a step clears its review timestamp and blocks publishing again. Every unmasking is stored as a redaction row with what was unmasked and when. The review screen shows the visible text of a step in one list, so scanning it does not mean reading a rendered page. Do not build yet: No replay, sharing or analytics yet. ### Phase 4 · Replaying captured markup without running it Build: Render each snapshot in a sandboxed frame that does not allow scripts, draw the hotspot from the recorded bounding box, and advance on click or on keyboard activation. Add a start overlay and a closing call to action. Trap: Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. Done when: Scripts in a captured page do not execute during replay, tested with a snapshot containing a script that would announce itself. The hotspot is reachable and activatable by keyboard alone, and the current step is announced to a screen reader. A snapshot that fails to render shows a message and an option to skip to the next step, never a blank frame. Canvas and cross-origin frame content are shown as placeholders, which the library records as not captured by default. Do not build yet: No sharing, embedding, analytics or export yet. ### Phase 5 · Sharing, counting and deleting Build: Publish at a generated slug with no login, offer an embed snippet, and set frame-ancestors so you control where it can be embedded. Count views, step completions and call to action clicks in your own database. Support unpublishing and a deletion that removes snapshots and assets. Trap: A demo slug derived from the customer's name, published with no login, tells anybody who guesses it which prospects you are talking to, and slugs derived from titles are guessable by design. Generate slugs with enough randomness that guessing is impractical, and make unpublish take effect immediately rather than at the next cache expiry. Done when: Slugs are generated rather than derived from the title, and a demo cannot be found by guessing a customer name, checked by inspection. Unpublishing makes the link stop working immediately, tested with a warm cache. View, step and call to action counts are recorded first-party with no third-party script on the page, verified by listing the page's network requests. Deleting a demo removes its snapshots and assets and the deletion is verified by querying for orphans. Do not build yet: Nothing after this. ### Out of scope (and why) - Custom domains and certificate management, which is infrastructure with its own failure modes and on-call implications. - CRM synchronisation, where each connector is an authentication project plus a permanent maintenance commitment. - Capturing canvas, video and cross-origin frames, which the capture library leaves off by default for good reasons and which the replay would have to relax its sandbox to show. - Lead capture forms inside the demo, which collect other people's details and turn a demo tool into a data controller. ### README must contain - That masking is on at capture and unmasking is a recorded per-element decision - That publishing is gated on a per-step review and recapturing a step resets it - That the replay frame does not allow scripts, and what that means for canvas and widgets - How to load the extension unpacked, and the size ceiling for a snapshot - What deletion removes, and that share links are generated rather than derived from a title
# Agent instructions · Escourtly indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: A Chrome extension recording one DOM snapshot per click with rrweb, Inverted defaults: inputs and text masked at capture, unmasked deliberately, Postgres with Drizzle, snapshots stored as compressed documents, The snapshot rendered in a sandboxed frame with scripts disallowed, First-party counters in your own database. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. - A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. - Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. - Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect.
# Build plan · Escourtly A demo recorder built on DOM snapshots, whose hard problems are capturing enough of a page that the replay renders without calling home, masking by default rather than by accident, and replaying untrusted captured markup without letting it run. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture with masking on from the first click The extension records a snapshot per click with input values and text masked by default. ### Steps 1. Set masking explicitly at capture The defaults mask passwords; your users' data is not a password. Files: `extension/record.ts`, `test/masking.test.ts` 2. Record the click target and its box The hotspot in the replay is only as good as what you captured about the click. Files: `extension/clicks.ts` 3. Show that recording is happening Any capture tool without a visible indicator is a surveillance tool by accident. Files: `extension/indicator.ts` ### Done when - [ ] Sample personal data never reaches the stored snapshot - [ ] A recording indicator is visible throughout - [ ] Click selector and bounding box are recorded - [ ] An interrupted capture saves nothing partial ### Watch out - Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. ## Phase 2 · Snapshots that render without calling home Stylesheets, fonts and images are inlined, and whatever fails is recorded rather than silent. ### Steps 1. Inline stylesheets, fonts and images The replay has to be self-contained or it is a screenshot with extra steps. Files: `extension/inline.ts` 2. Record every inlining failure A blank replay with a reason is a bug; one without is a mystery. Files: `src/snapshots/assets.ts` 3. Bound the snapshot size Snapshots grow without limit and the ceiling is what keeps the database usable. Files: `extension/limits.ts` ### Done when - [ ] The demo renders with the origin host blocked - [ ] Inlining failures are listed with reasons - [ ] Oversized snapshots are rejected at capture - [ ] Steps with failed stylesheets cannot publish ### Watch out - A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. ## Phase 3 · A review screen nobody can skip Every captured text node is seen by a person before the demo can be published. ### Steps 1. Gate publishing on a per-step review The gate is the feature; a checklist in the documentation is not. Files: `src/review/gate.ts`, `test/review-gate.test.ts` 2. Record unmasking as an action Someone decided to reveal that. The record says who and when. Files: `src/review/redactions.ts` 3. List the visible text, do not just render it People skim a page and read a list. Files: `src/review/TextList.tsx` ### Done when - [ ] Publishing is blocked until every step is reviewed - [ ] Recapture clears the review and re-blocks publishing - [ ] Unmaskings are recorded with a timestamp - [ ] Visible text is shown as a list for scanning ### Watch out - Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. ## Phase 4 · Replaying captured markup without running it The snapshot renders as a page with a working hotspot and none of its scripts execute. ### Steps 1. Sandbox the frame and keep scripts off Captured markup is input from another application. Treat it that way. Files: `src/replay/Frame.tsx` 2. Make the hotspot keyboard operable A demo nobody can drive with a keyboard excludes people for no reason. Files: `src/replay/Hotspot.tsx` 3. Degrade a failed snapshot visibly A blank frame reads as a broken product, which is the opposite of the point. Files: `src/replay/Fallback.tsx` ### Done when - [ ] Captured scripts never execute in replay - [ ] Hotspots work with a keyboard and are announced - [ ] A failed snapshot degrades with a message - [ ] Canvas and cross-origin content show as placeholders ### Watch out - Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. ## Not in this build - Custom domains and certificates. - CRM synchronisation. - Canvas, video and cross-origin frame capture. - Lead capture forms inside the demo. ## After v1, if you want it - Branching walkthroughs, once single-path replay has survived real prospects - A guided tour overlay on the live product, which is a different tool sharing the step model - Per-step timing in the analytics, which tells you where people stop rather than that they did
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Snapshots and analytics. Snapshots are the sensitive part. DATABASE_URL=postgres://user:redacted@localhost:5432/demos # Required. Where published demos live. Used in embed snippets. PUBLIC_BASE_URL=https://demos.example.com # Required. Per step, after inlining. A capture over this is refused with the largest asset named. SNAPSHOT_MAX_MB=8 # Required. Who may embed a demo. Empty means nobody but you. EMBED_FRAME_ANCESTORS=https://www.example.com https://example.com # Required. Randomness in a share link. The link is the access control. SLUG_BYTES=12 # Required. Unpublished captures expire, because a draft is still captured data. DRAFT_RETENTION_DAYS=90 # Optional · secret. Optional first-draft step descriptions. Without it you write them yourself and nothing else changes. LLM_API_KEY=sk-...redacted
# Escourtly · product brief ## Problem The core loop is a weekend build on top of rrweb: snapshot the DOM on each click, inline the assets, and replay the snapshots as a clickable walkthrough. What takes the project from a demo to something you would put in front of a prospect is capture fidelity · real apps are full of canvas, cross-origin iframes, shadow DOM and lazily loaded assets, and every one of them is a separate fix. Past that sit the parts that are infrastructure rather than features: no-login share links and embeds that stay up, custom domains with certificates, funnel analytics, and CRM sync. Build it for your own product and one sitting gets you a long way. Build it to replace the subscription and you have signed up for the edge cases. ## Product outcome A demo tool where a capture never quietly takes customer data, a published walkthrough renders for someone who has never heard of your network, and the replay cannot run the markup it is showing. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node and a Postgres database - a Chrome MV3 extension build step - an OpenAI/Anthropic API key for step descriptions - somewhere to host the public demo pages ## Explicit non-goals for v1 - Custom domains and certificates. - CRM synchronisation. - Canvas, video and cross-origin frame capture. - Lead capture forms inside the demo. - capture fidelity on canvas, cross-origin iframes, shadow DOM and lazily loaded assets - no-login share links and embeds that hold up under real traffic - viewer funnel analytics and lead capture - CRM sync to Salesforce, HubSpot and Pipedrive - custom domains with certificates handled for you ## Success criteria - Sample personal data is confirmed absent from stored snapshots - A demo is confirmed to render with the captured origin blocked - Publishing is confirmed blocked until every step is reviewed - Captured scripts are confirmed not to execute in replay - Deletion is confirmed to leave no orphaned snapshots or assets
# Build brief · Escourtly The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an interactive demo builder that captures a product as clickable page snapshots to replace Escourtly. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero A snapshot of an application you are signed into contains whatever was on the screen: customer names, email addresses, invoice totals, sometimes a token in a data attribute. The capture library's defaults are not a privacy policy. rrweb records with maskAllInputs off by default, masking only password inputs, and its opt-outs are class names you have to apply: rr-block for an element replaced by a placeholder, rr-ignore for an element whose input events are not recorded, rr-mask for text that is masked. Check the current defaults yourself before you rely on any of this. So this build inverts them. Masking of input values and text is on from the first capture, unmasking is a deliberate per-element choice made in the editor, and nothing publishes until somebody has opened the review screen and seen every captured text node that is still visible. Capture only products you are entitled to capture. Never a customer's account, never a competitor's application behind a login. ### Stack (fixed, do not substitute) - Capture: A Chrome extension recording one DOM snapshot per click with rrweb · snapshots beat video because the replay is a real page - Masking: Inverted defaults: inputs and text masked at capture, unmasked deliberately · the library masks passwords only, which is not the same as masking data - Storage: Postgres with Drizzle, snapshots stored as compressed documents · snapshots are large and you will keep more of them than you expect - Replay: The snapshot rendered in a sandboxed frame with scripts disallowed · captured markup is untrusted input that came from somebody else's page - Analytics: First-party counters in your own database · a demo page that ships a third-party tracker is a demo page with a compliance problem ### Data model (create this before Phase 1) - demos(id, slug, title, status, created_at, published_at, cta_label, cta_url) · slug is generated, not taken from the title - steps(id, demo_id, ordinal, snapshot_ref, target_selector, target_box_json, title, description, reviewed_at) - snapshots(id, document_compressed, bytes, asset_count, inline_failures_json, captured_url_host) · the host, not the full URL, because the path often carries identifiers - assets(snapshot_id, original_url, kind, bytes, inlined, failure_reason) · what got inlined and what did not, so a blank replay is diagnosable - redactions(step_id, selector, kind, applied_at) · every unmasking and every extra mask, recorded as an action somebody took - views(demo_id, session_hash, started_at, last_step, completed_at, cta_clicked_at) · first-party, no third-party script anywhere - A published demo is a set of frozen documents. Nothing in the replay path talks to the product that was captured, which is what makes it survive a deploy. ### Phase 1 · Capture with masking on from the first click Build: Build a Chrome extension that starts and stops a capture, records one snapshot per click along with the clicked element's selector and bounding box, and shows a visible recording indicator. Turn on masking of input values and text at capture time, not at publish time. Trap: Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. Done when: A capture of a page containing sample personal data stores no recognisable value from it, asserted by searching the stored document in a test. The recording indicator is visible on the page throughout a capture. Each step records the clicked element's selector and bounding box, verified by replaying a two step capture. Stopping a capture writes the steps atomically, so an interrupted capture leaves nothing half saved. Do not build yet: No asset inlining, editor, replay or publishing. ### Phase 2 · Snapshots that render without calling home Build: Fetch and inline stylesheets, fonts and images into each snapshot, bounded by a size ceiling. Record every asset that could not be inlined with the reason. Show the failures in the editor so a blank replay has an explanation before anybody else sees it. Trap: A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. Done when: A published demo renders correctly in a browser with no access to the captured origin, tested by blocking that host. Every asset that failed to inline is listed in the editor with its reason and its size. A snapshot exceeding the size ceiling is rejected at capture with a message naming the largest asset. Publishing is refused for a step whose stylesheets did not inline. Do not build yet: No editor, replay, publishing or analytics yet. ### Phase 3 · A review screen nobody can skip Build: Show each step with its visible text highlighted, offer per-element masking and unmasking, record every redaction as an action with a timestamp, and require the whole demo to be reviewed before publishing becomes available. Trap: Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. Done when: Publishing is unavailable until every step carries a review timestamp, tested by attempting to publish an unreviewed demo. Recapturing a step clears its review timestamp and blocks publishing again. Every unmasking is stored as a redaction row with what was unmasked and when. The review screen shows the visible text of a step in one list, so scanning it does not mean reading a rendered page. Do not build yet: No replay, sharing or analytics yet. ### Phase 4 · Replaying captured markup without running it Build: Render each snapshot in a sandboxed frame that does not allow scripts, draw the hotspot from the recorded bounding box, and advance on click or on keyboard activation. Add a start overlay and a closing call to action. Trap: Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. Done when: Scripts in a captured page do not execute during replay, tested with a snapshot containing a script that would announce itself. The hotspot is reachable and activatable by keyboard alone, and the current step is announced to a screen reader. A snapshot that fails to render shows a message and an option to skip to the next step, never a blank frame. Canvas and cross-origin frame content are shown as placeholders, which the library records as not captured by default. Do not build yet: No sharing, embedding, analytics or export yet. ### Phase 5 · Sharing, counting and deleting Build: Publish at a generated slug with no login, offer an embed snippet, and set frame-ancestors so you control where it can be embedded. Count views, step completions and call to action clicks in your own database. Support unpublishing and a deletion that removes snapshots and assets. Trap: A demo slug derived from the customer's name, published with no login, tells anybody who guesses it which prospects you are talking to, and slugs derived from titles are guessable by design. Generate slugs with enough randomness that guessing is impractical, and make unpublish take effect immediately rather than at the next cache expiry. Done when: Slugs are generated rather than derived from the title, and a demo cannot be found by guessing a customer name, checked by inspection. Unpublishing makes the link stop working immediately, tested with a warm cache. View, step and call to action counts are recorded first-party with no third-party script on the page, verified by listing the page's network requests. Deleting a demo removes its snapshots and assets and the deletion is verified by querying for orphans. Do not build yet: Nothing after this. ### Out of scope (and why) - Custom domains and certificate management, which is infrastructure with its own failure modes and on-call implications. - CRM synchronisation, where each connector is an authentication project plus a permanent maintenance commitment. - Capturing canvas, video and cross-origin frames, which the capture library leaves off by default for good reasons and which the replay would have to relax its sandbox to show. - Lead capture forms inside the demo, which collect other people's details and turn a demo tool into a data controller. ### README must contain - That masking is on at capture and unmasking is a recorded per-element decision - That publishing is gated on a per-step review and recapturing a step resets it - That the replay frame does not allow scripts, and what that means for canvas and widgets - How to load the extension unpacked, and the size ceiling for a snapshot - What deletion removes, and that share links are generated rather than derived from a title
# Architecture · Escourtly ## Stack | Part | Choice | Why | | --- | --- | --- | | Capture | A Chrome extension recording one DOM snapshot per click with rrweb | snapshots beat video because the replay is a real page | | Masking | Inverted defaults: inputs and text masked at capture, unmasked deliberately | the library masks passwords only, which is not the same as masking data | | Storage | Postgres with Drizzle, snapshots stored as compressed documents | snapshots are large and you will keep more of them than you expect | | Replay | The snapshot rendered in a sandboxed frame with scripts disallowed | captured markup is untrusted input that came from somebody else's page | | Analytics | First-party counters in your own database | a demo page that ships a third-party tracker is a demo page with a compliance problem | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Extension | Capture, masking defaults and click targets. | Any capture library, provided masking is set explicitly rather than inherited. | | Inlining | Self-contained snapshots and recorded failures. | Any fetcher, provided failures are recorded and block publishing. | | Review | The per-step gate and redaction history. | Not swappable for an optional review, which is the same as no review. | | Replay | Sandboxed rendering and accessible hotspots. | Any renderer, provided scripts stay disallowed. | | Publishing | Generated slugs, embedding rules and first-party counts. | Any delivery, provided unpublish is immediate. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_URL` · required, secret · Snapshots and analytics. Snapshots are the sensitive part. - `PUBLIC_BASE_URL` · required · Where published demos live. Used in embed snippets. - `SNAPSHOT_MAX_MB` · required · Per step, after inlining. A capture over this is refused with the largest asset named. - `EMBED_FRAME_ANCESTORS` · required · Who may embed a demo. Empty means nobody but you. - `SLUG_BYTES` · required · Randomness in a share link. The link is the access control. - `DRAFT_RETENTION_DAYS` · required · Unpublished captures expire, because a draft is still captured data. - `LLM_API_KEY` · optional, secret · Optional first-draft step descriptions. Without it you write them yourself and nothing else changes. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Escourtly product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: A Chrome extension recording one DOM snapshot per click with rrweb, Inverted defaults: inputs and text masked at capture, unmasked deliberately, Postgres with Drizzle, snapshots stored as compressed documents, The snapshot rendered in a sandboxed frame with scripts disallowed, First-party counters in your own database. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. - A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. - Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. - Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. - A demo slug derived from the customer's name, published with no login, tells anybody who guesses it which prospects you are talking to, and slugs derived from titles are guessable by design. Generate slugs with enough randomness that guessing is impractical, and make unpublish take effect immediately rather than at the next cache expiry.
# Delivery milestones · Escourtly Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capture with masking on from the first click The extension records a snapshot per click with input values and text masked by default. ### Steps 1. Set masking explicitly at capture The defaults mask passwords; your users' data is not a password. Files: `extension/record.ts`, `test/masking.test.ts` 2. Record the click target and its box The hotspot in the replay is only as good as what you captured about the click. Files: `extension/clicks.ts` 3. Show that recording is happening Any capture tool without a visible indicator is a surveillance tool by accident. Files: `extension/indicator.ts` ### Done when - [ ] Sample personal data never reaches the stored snapshot - [ ] A recording indicator is visible throughout - [ ] Click selector and bounding box are recorded - [ ] An interrupted capture saves nothing partial ### Watch out - Recording with the library's defaults masks passwords and nothing else, so the first capture of a real admin screen carries live customer names and addresses into your database, where they are now your responsibility and your breach if it leaks. Set the masking options explicitly at capture, and write a test that captures a page of sample data and asserts none of it appears in the stored document. ## M2 · Snapshots that render without calling home Stylesheets, fonts and images are inlined, and whatever fails is recorded rather than silent. ### Steps 1. Inline stylesheets, fonts and images The replay has to be self-contained or it is a screenshot with extra steps. Files: `extension/inline.ts` 2. Record every inlining failure A blank replay with a reason is a bug; one without is a mystery. Files: `src/snapshots/assets.ts` 3. Bound the snapshot size Snapshots grow without limit and the ceiling is what keeps the database usable. Files: `extension/limits.ts` ### Done when - [ ] The demo renders with the origin host blocked - [ ] Inlining failures are listed with reasons - [ ] Oversized snapshots are rejected at capture - [ ] Steps with failed stylesheets cannot publish ### Watch out - A snapshot that still references the origin's asset host renders perfectly on your machine, where you are signed in and inside the network, and renders as unstyled text for the prospect outside it, or three weeks later when the asset hash changes on a deploy. Inline everything at capture, record the failures, and refuse to publish a step whose stylesheet did not inline. ## M3 · A review screen nobody can skip Every captured text node is seen by a person before the demo can be published. ### Steps 1. Gate publishing on a per-step review The gate is the feature; a checklist in the documentation is not. Files: `src/review/gate.ts`, `test/review-gate.test.ts` 2. Record unmasking as an action Someone decided to reveal that. The record says who and when. Files: `src/review/redactions.ts` 3. List the visible text, do not just render it People skim a page and read a list. Files: `src/review/TextList.tsx` ### Done when - [ ] Publishing is blocked until every step is reviewed - [ ] Recapture clears the review and re-blocks publishing - [ ] Unmaskings are recorded with a timestamp - [ ] Visible text is shown as a list for scanning ### Watch out - Making review optional means it happens for the first demo and never again, and the demo that leaks a customer name is always the sixth one, made in a hurry before a call. Gate publishing on a review timestamp per step, and reset every timestamp when a step is recaptured. ## M4 · Replaying captured markup without running it The snapshot renders as a page with a working hotspot and none of its scripts execute. ### Steps 1. Sandbox the frame and keep scripts off Captured markup is input from another application. Treat it that way. Files: `src/replay/Frame.tsx` 2. Make the hotspot keyboard operable A demo nobody can drive with a keyboard excludes people for no reason. Files: `src/replay/Hotspot.tsx` 3. Degrade a failed snapshot visibly A blank frame reads as a broken product, which is the opposite of the point. Files: `src/replay/Fallback.tsx` ### Done when - [ ] Captured scripts never execute in replay - [ ] Hotspots work with a keyboard and are announced - [ ] A failed snapshot degrades with a message - [ ] Canvas and cross-origin content show as placeholders ### Watch out - Turning on script execution in the replay frame to make an interactive widget look right removes the protection that stops captured markup from running, and the library's own canvas replay option carries exactly that warning about adding script permission to the frame. Keep scripts off, accept that some widgets render as static, and say so in the product rather than trading a sandbox for a hover effect. ## M5 · Sharing, counting and deleting (production only) A demo has a link, an embed, first-party counts and a deletion that really deletes. ### Steps 1. Generate unguessable slugs The link is the access control, so it has to be worth something. Files: `src/publish/slug.ts` 2. Set frame-ancestors for the embed You decide which sites can frame your demo, not the internet. Files: `src/publish/headers.ts` 3. Delete snapshots and assets together An orphaned snapshot is the captured data you promised to remove. Files: `src/demos/delete.ts`, `test/orphans.test.ts` ### Done when - [ ] Slugs are generated and not guessable - [ ] Unpublish takes effect immediately - [ ] Counts are first-party with no third-party requests - [ ] Deletion leaves no orphaned snapshots or assets ### Watch out - A demo slug derived from the customer's name, published with no login, tells anybody who guesses it which prospects you are talking to, and slugs derived from titles are guessable by design. Generate slugs with enough randomness that guessing is impractical, and make unpublish take effect immediately rather than at the next cache expiry.
# Operations · Escourtly ## Backup The database, including snapshot documents, which are the bulk of it. Consider whether you want long backup retention of captured application screens at all, since a backup is a copy of exactly the data your retention policy is trying to expire. ## Restore Restore and immediately verify that unpublished drafts past their retention are expired again, so a restore does not resurrect captures somebody deliberately let go. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Inlining failure rates per capture, snapshot sizes against the ceiling, and drafts past retention. A rising inlining failure rate usually means the captured product changed how it loads assets. ## Incident checklist A demo showing real customer data is the incident this design exists to prevent, and the response is unpublish first, then the redaction history to find out what was unmasked and when. A blank replay is an inlining failure and the reason is already recorded. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Sample personal data is confirmed absent from stored snapshots - [ ] A demo is confirmed to render with the captured origin blocked - [ ] Publishing is confirmed blocked until every step is reviewed - [ ] Captured scripts are confirmed not to execute in replay - [ ] Deletion is confirmed to leave no orphaned snapshots or assets ## Launch constraint Do not market omitted Escourtly capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Snapshots and analytics. Snapshots are the sensitive part. DATABASE_URL=postgres://user:redacted@localhost:5432/demos # Required. Where published demos live. Used in embed snippets. PUBLIC_BASE_URL=https://demos.example.com # Required. Per step, after inlining. A capture over this is refused with the largest asset named. SNAPSHOT_MAX_MB=8 # Required. Who may embed a demo. Empty means nobody but you. EMBED_FRAME_ANCESTORS=https://www.example.com https://example.com # Required. Randomness in a share link. The link is the access control. SLUG_BYTES=12 # Required. Unpublished captures expire, because a draft is still captured data. DRAFT_RETENTION_DAYS=90 # Optional · secret. Optional first-draft step descriptions. Without it you write them yourself and nothing else changes. LLM_API_KEY=sk-...redacted
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Escourtly because the first capture is the easy part. The subscription absorbs the long tail: the app that renders blank because its fonts were behind a CDN, the embed that has to load fast from someone else's marketing site, the custom domain, the lead that has to land in HubSpot without anyone copying it across. A founder who builds this for one product will probably be happy. A team that wants it to work on every product they ship is buying maintenance, not software.
xcapture fidelity on canvas, cross-origin iframes, shadow DOM and lazily loaded assets
xno-login share links and embeds that hold up under real traffic
xviewer funnel analytics and lead capture
xCRM sync to Salesforce, HubSpot and Pipedrive
xcustom domains with certificates handled for you
Escourtly pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| founder | $49/workspace | — | Unlimited walkthroughs, steps and plays; all features; custom domains |
free tierno free tier
billingmonthly only; no annual plan published; cancel anytime
verified 2026-08-14 · source ↗
Vibecode Escourtly
Kinda. The core of Escourtly is buildable in a weekend with the prompt on this page, but there are real gaps: capture fidelity on canvas, cross-origin iframes, shadow DOM and lazily loaded assets, no-login share links and embeds that hold up under real traffic. Read the honest list above before committing.
How much does Escourtly cost?
Escourtly costs about $49/month (Founder, checked 2026-08-03), which is $588 per year.
What do I lose by replacing Escourtly?
Honestly: capture fidelity on canvas, cross-origin iframes, shadow DOM and lazily loaded assets; no-login share links and embeds that hold up under real traffic; viewer funnel analytics and lead capture; CRM sync to Salesforce, HubSpot and Pipedrive; custom domains with certificates handled for you. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Escourtly?
Yes: rrweb (MIT-licensed DOM record and replay · the capture engine this whole category is built on.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.