Vibecode Landingi
track this build4 phases, 12 steps, beginner friendly0%The visible landing-page platform loop is buildable, but a credible replacement needs more than the first screen. Landingi earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Landingi. 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 ===== # Landingi · indie build A landing page builder whose interesting half is the experiment engine: consistent variant assignment, conversion attribution that survives a page reload, and a statistical method that does not let you stop a test the moment it looks good. 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 | | --- | --- | --- | | Pages | Static generation with a small runtime for assignment | a landing page that loads slowly converts worse than one that says less | | Assignment | Deterministic from a stable visitor identifier | a visitor must see the same variant on every visit or the test is meaningless | | Attribution | Conversion tied to the assignment, not to the session | a conversion after a reload still belongs to the variant that was seen | | Statistics | Fixed horizon with hidden results, or a sequential method | these are the two honest options and a running significance figure is neither | | Runtime | TypeScript with PostgreSQL | assignments and conversions are counts that need to be exact | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server, the page generator and the statistics. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Assignments and conversions are counts that have to be exact, with one assignment per visitor enforced. - Get it: Docker or your package manager. - Verify: A unique constraint rejects a second assignment for one visitor - [ ] **Your statistical method, chosen before building** · free to read; a statistician's hour is worth it for a commercial site - Why: Fixed horizon with hidden results and sequential testing lead to different interfaces, and retrofitting either onto the other is a rewrite of the decision path. - Get it: Read how each handles continuous monitoring, pick one, and write down the parameters: your minimum detectable effect and acceptable error rate. - Verify: A written note names the method, the detectable effect and the error rate - [ ] **Enough traffic to reach a conclusion** · free, and sometimes discouraging - Why: A test needing forty thousand visitors per arm on a page receiving two hundred a week will never conclude, and knowing that before building saves the experiment. - Get it: Compute the required sample from your detectable effect and compare it to your actual weekly traffic. - Verify: A calculation exists showing how long a test would take at your traffic - [ ] **What counts as a conversion** · free - Why: It is fixed at experiment start and cannot change while running, so an imprecise definition invalidates the test rather than being tidied up later. - Get it: Define the event precisely, including repeat and cross-device behaviour. - Verify: A written definition covers the event, repeats and cross-device cases ## 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: - Multivariate testing. - Personalisation engines. - Template libraries and design assets. - Any general claim about what converts. - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing - large template and component libraries If one of those is essential to you, that is the reason to keep paying for Landingi, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Landingi 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 landing pages with split testing that reports a result you can rely on to replace Landingi. 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 The statistics are the part people get wrong, and getting them wrong is worse than not testing, because a wrong result gets acted on with confidence. The specific failure is stopping when the numbers look good. A standard significance test assumes you decided the sample size in advance and looked once. If you watch a running test and stop the moment it crosses the threshold, you will cross it eventually on almost any test, including one between two identical pages. Checking daily on a test with no real difference will produce a significant-looking result far more often than the stated error rate suggests. So this build either fixes the sample size in advance and hides the result until it is reached, or uses a method valid under continuous monitoring. It does not show a running significance figure that invites stopping, and it will refuse to declare a winner on a test that has not met its criterion. The friction is the feature. A tool that lets you stop early is a tool that manufactures results. ### Stack (fixed, do not substitute) - Pages: Static generation with a small runtime for assignment · a landing page that loads slowly converts worse than one that says less - Assignment: Deterministic from a stable visitor identifier · a visitor must see the same variant on every visit or the test is meaningless - Attribution: Conversion tied to the assignment, not to the session · a conversion after a reload still belongs to the variant that was seen - Statistics: Fixed horizon with hidden results, or a sequential method · these are the two honest options and a running significance figure is neither - Runtime: TypeScript with PostgreSQL · assignments and conversions are counts that need to be exact ### Data model (create this before Phase 1) - pages(id, slug, published_version_id, created_at) - versions(id, page_id, content_json, published_at) · immutable once published - experiments(id, page_id, method, target_sample_per_variant, min_detectable_effect, started_at, ended_at, stopped_reason) - variants(id, experiment_id, version_id, weight, label) - assignments(visitor_id, experiment_id, variant_id, assigned_at) · one per visitor, never reassigned - conversions(id, assignment_id, goal, at, value_minor) · attributed to the assignment - results(experiment_id, computed_at, per_variant_json, decision, valid_under_method) - An assignment is written once and never changed, because a visitor moved between variants mid-test corrupts both arms. ### Phase 1 · Pages that load fast enough to test Build: Build pages from a block model and generate static output. Publishing snapshots an immutable version. Keep the runtime script small, since it will be in the path of every measurement. Trap: Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. Done when: A published version is immutable and editing creates a new one. Pages render as static output with a small runtime script. Page load is measured and stated. An experiment binds to versions rather than to the page, so editing cannot alter a running test. Do not build yet: No experiments, assignment or statistics. ### Phase 2 · Assignment that stays put Build: Assign deterministically from a stable visitor identifier, writing the assignment once. Handle a visitor whose storage is cleared. Never reassign an existing visitor when weights change. Trap: Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. Done when: A visitor receives the same variant across sessions, tested over several visits. Changing weights does not reassign existing visitors, verified directly. A visitor with cleared storage is treated as new rather than being double counted. Assignment happens before the page renders, with no visible flicker. Do not build yet: No conversions or statistics yet. ### Phase 3 · Conversions attributed to what was seen Build: Record conversions against the assignment rather than the session. Handle a conversion arriving days later, and a visitor converting more than once. Define the goal precisely before the experiment starts. Trap: Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. Done when: A conversion occurring days after assignment attributes to the original variant, tested with a delayed conversion. Repeat conversions from one visitor are handled per a stated rule. The goal definition is fixed at experiment start and cannot be changed while running. Conversions arriving from a different device are handled by a stated rule rather than silently. Do not build yet: No statistics or decisions yet. ### Phase 4 · Statistics that do not let you cheat Build: Implement either a fixed sample size computed in advance with results hidden until it is reached, or a sequential method valid under continuous monitoring. Refuse to declare a winner before the criterion is met. Validate against simulated data with no real effect. Trap: Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. Done when: Simulating a test between two identical variants produces false winners at no more than the stated rate across a thousand simulated experiments, asserted in a test. The chosen method is named in the interface. A winner cannot be declared before the criterion is met, verified by attempting it. The required sample size is computed from a stated minimum detectable effect. Do not build yet: No reporting or multi-page tests yet. ### Phase 5 · Reporting a result honestly Build: Report the effect with an interval rather than a point estimate, state the method and the sample, and record why a test was stopped. Keep concluded experiments so a claim can be checked later. Trap: Reporting a percentage improvement as a single number implies a precision the data does not support, and a fourteen percent lift with an interval spanning zero to thirty is a different finding from one spanning twelve to sixteen. Report the interval, always, and make the point estimate the smaller number on the page. Done when: Effects are reported as intervals with the point estimate secondary. The method, sample size and stopping reason appear on every result. Concluded experiments are retained with their data so a claim can be rechecked. A test stopped early for a business reason is recorded as such rather than reported as a result. Do not build yet: Nothing after this. ### Out of scope (and why) - Multivariate testing, which needs far more traffic than most sites have and produces results people over-read. - Personalisation engines, which is a different problem and one where evaluation is much harder. - Template libraries and design assets, which is most of what the hosted products actually sell. - Any claim about what will convert, which depends on your audience rather than on any general finding. ### README must contain - That stopping a test when it looks good produces false winners, and which method this build uses instead - That a running significance figure is not shown, deliberately - That assignments are written once and weight changes never reassign existing visitors - That conversions attribute to the assignment rather than to the current variant - That effects are reported as intervals with the point estimate secondary ===== AGENTS.md ===== # Agent instructions · Landingi indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Static generation with a small runtime for assignment, Deterministic from a stable visitor identifier, Conversion tied to the assignment, not to the session, Fixed horizon with hidden results, or a sequential method, TypeScript with PostgreSQL. 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 - Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. - Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. - Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. - Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. ===== BUILD_PLAN.md ===== # Build plan · Landingi A landing page builder whose interesting half is the experiment engine: consistent variant assignment, conversion attribution that survives a page reload, and a statistical method that does not let you stop a test the moment it looks good. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Pages that load fast enough to test A landing page renders quickly and is versioned immutably once published. ### Steps 1. Snapshot immutable versions at publish An edited variant mid-test invalidates the whole experiment. Files: `src/versions.ts` 2. Generate static output The page is in the measurement path. Slow pages change what you measure. Files: `src/build.ts` 3. Keep the runtime small It loads before assignment. Measure its size and hold it. Files: `runtime/assign.ts` ### Done when - [ ] Published versions are immutable - [ ] Pages render as static output - [ ] Load time is measured and stated - [ ] Experiments bind to versions, not pages ### Watch out - Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. ## Phase 2 · Assignment that stays put A visitor sees the same variant every time, and you know they did. ### Steps 1. Assign once, deterministically From a stable identifier, written once and never updated. Files: `src/assign.ts`, `src/assign.test.ts` 2. Never reassign on weight change New weights apply to new visitors only. Files: `src/assign.ts` 3. Avoid the flicker Rendering the control then swapping is visible and it biases the result. Files: `runtime/assign.ts` ### Done when - [ ] A visitor sees the same variant across sessions - [ ] Weight changes do not reassign existing visitors - [ ] Cleared storage is handled without double counting - [ ] There is no variant flicker on load ### Watch out - Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. ## Phase 3 · Conversions attributed to what was seen A conversion counts for the variant the visitor actually saw. ### Steps 1. Attribute to the assignment It is the only record of what the visitor actually saw. Files: `src/conversions.ts` 2. Fix the goal at start Changing what counts as success mid-test is choosing the answer. Files: `src/experiments.ts` 3. State the repeat and cross-device rules Both happen. Both need an answer written down, not an accident. Files: `src/conversions.ts` ### Done when - [ ] A delayed conversion attributes to the original variant - [ ] Repeat conversions follow a stated rule - [ ] The goal cannot change mid-experiment - [ ] Cross-device conversions follow a stated rule ### Watch out - Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. ## Phase 4 · Statistics that do not let you cheat A winner is declared only when the method says it may be. ### Steps 1. Choose one honest method and name it Fixed horizon with hidden results, or sequential. Not a running figure. Files: `src/stats.ts` 2. Validate against a null simulation A thousand tests between identical variants. Count the false winners. Files: `src/stats.test.ts` 3. Refuse early declarations In the decision path. The friction is the feature. Files: `src/decide.ts` ### Done when - [ ] A null simulation produces false winners at no more than the stated rate - [ ] The method is named in the interface - [ ] Early declaration is refused - [ ] Sample size derives from a stated detectable effect ### Watch out - Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. ## Not in this build - Multivariate testing. - Personalisation engines. - Template libraries and design assets. - Any general claim about what converts. ## After v1, if you want it - Sample size guidance before a test starts, which prevents tests that cannot conclude - Segment analysis with a correction for testing several segments, which is the next place people fool themselves - Sequential monitoring with a proper spending function, if you started with fixed horizon ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Assignments and conversions. DATABASE_URL=postgres://localhost:5432/landing # Required. Named in the interface. The other honest option is a sequential method. STATS_METHOD=fixed_horizon_hidden # Required. Present as a constant. A running figure invites the stopping that manufactures results. SHOW_RUNNING_SIGNIFICANCE=never # Required. Drives the required sample size. Smaller effects need far more traffic. MIN_DETECTABLE_EFFECT=0.05 # Required. Validated against a null simulation in the test suite. ACCEPTABLE_ERROR_RATE=0.05 # Required. How long a visitor keeps their variant. Shorter means more reassignment as new visitors. ASSIGNMENT_COOKIE_DAYS=90
You are building a lean indie version of Landingi. 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 ===== # Landingi · indie build A landing page builder whose interesting half is the experiment engine: consistent variant assignment, conversion attribution that survives a page reload, and a statistical method that does not let you stop a test the moment it looks good. 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 | | --- | --- | --- | | Pages | Static generation with a small runtime for assignment | a landing page that loads slowly converts worse than one that says less | | Assignment | Deterministic from a stable visitor identifier | a visitor must see the same variant on every visit or the test is meaningless | | Attribution | Conversion tied to the assignment, not to the session | a conversion after a reload still belongs to the variant that was seen | | Statistics | Fixed horizon with hidden results, or a sequential method | these are the two honest options and a running significance figure is neither | | Runtime | TypeScript with PostgreSQL | assignments and conversions are counts that need to be exact | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server, the page generator and the statistics. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Assignments and conversions are counts that have to be exact, with one assignment per visitor enforced. - Get it: Docker or your package manager. - Verify: A unique constraint rejects a second assignment for one visitor - [ ] **Your statistical method, chosen before building** · free to read; a statistician's hour is worth it for a commercial site - Why: Fixed horizon with hidden results and sequential testing lead to different interfaces, and retrofitting either onto the other is a rewrite of the decision path. - Get it: Read how each handles continuous monitoring, pick one, and write down the parameters: your minimum detectable effect and acceptable error rate. - Verify: A written note names the method, the detectable effect and the error rate - [ ] **Enough traffic to reach a conclusion** · free, and sometimes discouraging - Why: A test needing forty thousand visitors per arm on a page receiving two hundred a week will never conclude, and knowing that before building saves the experiment. - Get it: Compute the required sample from your detectable effect and compare it to your actual weekly traffic. - Verify: A calculation exists showing how long a test would take at your traffic - [ ] **What counts as a conversion** · free - Why: It is fixed at experiment start and cannot change while running, so an imprecise definition invalidates the test rather than being tidied up later. - Get it: Define the event precisely, including repeat and cross-device behaviour. - Verify: A written definition covers the event, repeats and cross-device cases ## 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: - Multivariate testing. - Personalisation engines. - Template libraries and design assets. - Any general claim about what converts. - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing - large template and component libraries If one of those is essential to you, that is the reason to keep paying for Landingi, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Landingi 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 landing pages with split testing that reports a result you can rely on to replace Landingi. 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 The statistics are the part people get wrong, and getting them wrong is worse than not testing, because a wrong result gets acted on with confidence. The specific failure is stopping when the numbers look good. A standard significance test assumes you decided the sample size in advance and looked once. If you watch a running test and stop the moment it crosses the threshold, you will cross it eventually on almost any test, including one between two identical pages. Checking daily on a test with no real difference will produce a significant-looking result far more often than the stated error rate suggests. So this build either fixes the sample size in advance and hides the result until it is reached, or uses a method valid under continuous monitoring. It does not show a running significance figure that invites stopping, and it will refuse to declare a winner on a test that has not met its criterion. The friction is the feature. A tool that lets you stop early is a tool that manufactures results. ### Stack (fixed, do not substitute) - Pages: Static generation with a small runtime for assignment · a landing page that loads slowly converts worse than one that says less - Assignment: Deterministic from a stable visitor identifier · a visitor must see the same variant on every visit or the test is meaningless - Attribution: Conversion tied to the assignment, not to the session · a conversion after a reload still belongs to the variant that was seen - Statistics: Fixed horizon with hidden results, or a sequential method · these are the two honest options and a running significance figure is neither - Runtime: TypeScript with PostgreSQL · assignments and conversions are counts that need to be exact ### Data model (create this before Phase 1) - pages(id, slug, published_version_id, created_at) - versions(id, page_id, content_json, published_at) · immutable once published - experiments(id, page_id, method, target_sample_per_variant, min_detectable_effect, started_at, ended_at, stopped_reason) - variants(id, experiment_id, version_id, weight, label) - assignments(visitor_id, experiment_id, variant_id, assigned_at) · one per visitor, never reassigned - conversions(id, assignment_id, goal, at, value_minor) · attributed to the assignment - results(experiment_id, computed_at, per_variant_json, decision, valid_under_method) - An assignment is written once and never changed, because a visitor moved between variants mid-test corrupts both arms. ### Phase 1 · Pages that load fast enough to test Build: Build pages from a block model and generate static output. Publishing snapshots an immutable version. Keep the runtime script small, since it will be in the path of every measurement. Trap: Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. Done when: A published version is immutable and editing creates a new one. Pages render as static output with a small runtime script. Page load is measured and stated. An experiment binds to versions rather than to the page, so editing cannot alter a running test. Do not build yet: No experiments, assignment or statistics. ### Phase 2 · Assignment that stays put Build: Assign deterministically from a stable visitor identifier, writing the assignment once. Handle a visitor whose storage is cleared. Never reassign an existing visitor when weights change. Trap: Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. Done when: A visitor receives the same variant across sessions, tested over several visits. Changing weights does not reassign existing visitors, verified directly. A visitor with cleared storage is treated as new rather than being double counted. Assignment happens before the page renders, with no visible flicker. Do not build yet: No conversions or statistics yet. ### Phase 3 · Conversions attributed to what was seen Build: Record conversions against the assignment rather than the session. Handle a conversion arriving days later, and a visitor converting more than once. Define the goal precisely before the experiment starts. Trap: Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. Done when: A conversion occurring days after assignment attributes to the original variant, tested with a delayed conversion. Repeat conversions from one visitor are handled per a stated rule. The goal definition is fixed at experiment start and cannot be changed while running. Conversions arriving from a different device are handled by a stated rule rather than silently. Do not build yet: No statistics or decisions yet. ### Phase 4 · Statistics that do not let you cheat Build: Implement either a fixed sample size computed in advance with results hidden until it is reached, or a sequential method valid under continuous monitoring. Refuse to declare a winner before the criterion is met. Validate against simulated data with no real effect. Trap: Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. Done when: Simulating a test between two identical variants produces false winners at no more than the stated rate across a thousand simulated experiments, asserted in a test. The chosen method is named in the interface. A winner cannot be declared before the criterion is met, verified by attempting it. The required sample size is computed from a stated minimum detectable effect. Do not build yet: No reporting or multi-page tests yet. ### Phase 5 · Reporting a result honestly Build: Report the effect with an interval rather than a point estimate, state the method and the sample, and record why a test was stopped. Keep concluded experiments so a claim can be checked later. Trap: Reporting a percentage improvement as a single number implies a precision the data does not support, and a fourteen percent lift with an interval spanning zero to thirty is a different finding from one spanning twelve to sixteen. Report the interval, always, and make the point estimate the smaller number on the page. Done when: Effects are reported as intervals with the point estimate secondary. The method, sample size and stopping reason appear on every result. Concluded experiments are retained with their data so a claim can be rechecked. A test stopped early for a business reason is recorded as such rather than reported as a result. Do not build yet: Nothing after this. ### Out of scope (and why) - Multivariate testing, which needs far more traffic than most sites have and produces results people over-read. - Personalisation engines, which is a different problem and one where evaluation is much harder. - Template libraries and design assets, which is most of what the hosted products actually sell. - Any claim about what will convert, which depends on your audience rather than on any general finding. ### README must contain - That stopping a test when it looks good produces false winners, and which method this build uses instead - That a running significance figure is not shown, deliberately - That assignments are written once and weight changes never reassign existing visitors - That conversions attribute to the assignment rather than to the current variant - That effects are reported as intervals with the point estimate secondary ===== AGENTS.md ===== # Agent instructions · Landingi indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Static generation with a small runtime for assignment, Deterministic from a stable visitor identifier, Conversion tied to the assignment, not to the session, Fixed horizon with hidden results, or a sequential method, TypeScript with PostgreSQL. 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 - Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. - Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. - Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. - Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. ===== BUILD_PLAN.md ===== # Build plan · Landingi A landing page builder whose interesting half is the experiment engine: consistent variant assignment, conversion attribution that survives a page reload, and a statistical method that does not let you stop a test the moment it looks good. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Pages that load fast enough to test A landing page renders quickly and is versioned immutably once published. ### Steps 1. Snapshot immutable versions at publish An edited variant mid-test invalidates the whole experiment. Files: `src/versions.ts` 2. Generate static output The page is in the measurement path. Slow pages change what you measure. Files: `src/build.ts` 3. Keep the runtime small It loads before assignment. Measure its size and hold it. Files: `runtime/assign.ts` ### Done when - [ ] Published versions are immutable - [ ] Pages render as static output - [ ] Load time is measured and stated - [ ] Experiments bind to versions, not pages ### Watch out - Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. ## Phase 2 · Assignment that stays put A visitor sees the same variant every time, and you know they did. ### Steps 1. Assign once, deterministically From a stable identifier, written once and never updated. Files: `src/assign.ts`, `src/assign.test.ts` 2. Never reassign on weight change New weights apply to new visitors only. Files: `src/assign.ts` 3. Avoid the flicker Rendering the control then swapping is visible and it biases the result. Files: `runtime/assign.ts` ### Done when - [ ] A visitor sees the same variant across sessions - [ ] Weight changes do not reassign existing visitors - [ ] Cleared storage is handled without double counting - [ ] There is no variant flicker on load ### Watch out - Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. ## Phase 3 · Conversions attributed to what was seen A conversion counts for the variant the visitor actually saw. ### Steps 1. Attribute to the assignment It is the only record of what the visitor actually saw. Files: `src/conversions.ts` 2. Fix the goal at start Changing what counts as success mid-test is choosing the answer. Files: `src/experiments.ts` 3. State the repeat and cross-device rules Both happen. Both need an answer written down, not an accident. Files: `src/conversions.ts` ### Done when - [ ] A delayed conversion attributes to the original variant - [ ] Repeat conversions follow a stated rule - [ ] The goal cannot change mid-experiment - [ ] Cross-device conversions follow a stated rule ### Watch out - Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. ## Phase 4 · Statistics that do not let you cheat A winner is declared only when the method says it may be. ### Steps 1. Choose one honest method and name it Fixed horizon with hidden results, or sequential. Not a running figure. Files: `src/stats.ts` 2. Validate against a null simulation A thousand tests between identical variants. Count the false winners. Files: `src/stats.test.ts` 3. Refuse early declarations In the decision path. The friction is the feature. Files: `src/decide.ts` ### Done when - [ ] A null simulation produces false winners at no more than the stated rate - [ ] The method is named in the interface - [ ] Early declaration is refused - [ ] Sample size derives from a stated detectable effect ### Watch out - Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. ## Not in this build - Multivariate testing. - Personalisation engines. - Template libraries and design assets. - Any general claim about what converts. ## After v1, if you want it - Sample size guidance before a test starts, which prevents tests that cannot conclude - Segment analysis with a correction for testing several segments, which is the next place people fool themselves - Sequential monitoring with a proper spending function, if you started with fixed horizon ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Assignments and conversions. DATABASE_URL=postgres://localhost:5432/landing # Required. Named in the interface. The other honest option is a sequential method. STATS_METHOD=fixed_horizon_hidden # Required. Present as a constant. A running figure invites the stopping that manufactures results. SHOW_RUNNING_SIGNIFICANCE=never # Required. Drives the required sample size. Smaller effects need far more traffic. MIN_DETECTABLE_EFFECT=0.05 # Required. Validated against a null simulation in the test suite. ACCEPTABLE_ERROR_RATE=0.05 # Required. How long a visitor keeps their variant. Shorter means more reassignment as new visitors. ASSIGNMENT_COOKIE_DAYS=90
You are building a production product version of Landingi. 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 ===== # Landingi · product brief ## Problem The visible landing-page platform loop is buildable, but a credible replacement needs more than the first screen. Landingi earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A landing page tool whose experiments produce results you can act on, because it will not let you stop a test at the moment it happens to look good. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - deploy target or static hosting - user-owned domain for publishing ## Explicit non-goals for v1 - Multivariate testing. - Personalisation engines. - Template libraries and design assets. - Any general claim about what converts. - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing - large template and component libraries ## Success criteria - A null simulation is confirmed to produce false winners at no more than the stated rate - Weight changes are confirmed not to reassign existing visitors - A delayed conversion is confirmed to attribute to the original variant - Early declaration is confirmed refused - Effects are confirmed reported as intervals ===== BRIEF.md ===== # Build brief · Landingi 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 landing pages with split testing that reports a result you can rely on to replace Landingi. 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 The statistics are the part people get wrong, and getting them wrong is worse than not testing, because a wrong result gets acted on with confidence. The specific failure is stopping when the numbers look good. A standard significance test assumes you decided the sample size in advance and looked once. If you watch a running test and stop the moment it crosses the threshold, you will cross it eventually on almost any test, including one between two identical pages. Checking daily on a test with no real difference will produce a significant-looking result far more often than the stated error rate suggests. So this build either fixes the sample size in advance and hides the result until it is reached, or uses a method valid under continuous monitoring. It does not show a running significance figure that invites stopping, and it will refuse to declare a winner on a test that has not met its criterion. The friction is the feature. A tool that lets you stop early is a tool that manufactures results. ### Stack (fixed, do not substitute) - Pages: Static generation with a small runtime for assignment · a landing page that loads slowly converts worse than one that says less - Assignment: Deterministic from a stable visitor identifier · a visitor must see the same variant on every visit or the test is meaningless - Attribution: Conversion tied to the assignment, not to the session · a conversion after a reload still belongs to the variant that was seen - Statistics: Fixed horizon with hidden results, or a sequential method · these are the two honest options and a running significance figure is neither - Runtime: TypeScript with PostgreSQL · assignments and conversions are counts that need to be exact ### Data model (create this before Phase 1) - pages(id, slug, published_version_id, created_at) - versions(id, page_id, content_json, published_at) · immutable once published - experiments(id, page_id, method, target_sample_per_variant, min_detectable_effect, started_at, ended_at, stopped_reason) - variants(id, experiment_id, version_id, weight, label) - assignments(visitor_id, experiment_id, variant_id, assigned_at) · one per visitor, never reassigned - conversions(id, assignment_id, goal, at, value_minor) · attributed to the assignment - results(experiment_id, computed_at, per_variant_json, decision, valid_under_method) - An assignment is written once and never changed, because a visitor moved between variants mid-test corrupts both arms. ### Phase 1 · Pages that load fast enough to test Build: Build pages from a block model and generate static output. Publishing snapshots an immutable version. Keep the runtime script small, since it will be in the path of every measurement. Trap: Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. Done when: A published version is immutable and editing creates a new one. Pages render as static output with a small runtime script. Page load is measured and stated. An experiment binds to versions rather than to the page, so editing cannot alter a running test. Do not build yet: No experiments, assignment or statistics. ### Phase 2 · Assignment that stays put Build: Assign deterministically from a stable visitor identifier, writing the assignment once. Handle a visitor whose storage is cleared. Never reassign an existing visitor when weights change. Trap: Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. Done when: A visitor receives the same variant across sessions, tested over several visits. Changing weights does not reassign existing visitors, verified directly. A visitor with cleared storage is treated as new rather than being double counted. Assignment happens before the page renders, with no visible flicker. Do not build yet: No conversions or statistics yet. ### Phase 3 · Conversions attributed to what was seen Build: Record conversions against the assignment rather than the session. Handle a conversion arriving days later, and a visitor converting more than once. Define the goal precisely before the experiment starts. Trap: Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. Done when: A conversion occurring days after assignment attributes to the original variant, tested with a delayed conversion. Repeat conversions from one visitor are handled per a stated rule. The goal definition is fixed at experiment start and cannot be changed while running. Conversions arriving from a different device are handled by a stated rule rather than silently. Do not build yet: No statistics or decisions yet. ### Phase 4 · Statistics that do not let you cheat Build: Implement either a fixed sample size computed in advance with results hidden until it is reached, or a sequential method valid under continuous monitoring. Refuse to declare a winner before the criterion is met. Validate against simulated data with no real effect. Trap: Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. Done when: Simulating a test between two identical variants produces false winners at no more than the stated rate across a thousand simulated experiments, asserted in a test. The chosen method is named in the interface. A winner cannot be declared before the criterion is met, verified by attempting it. The required sample size is computed from a stated minimum detectable effect. Do not build yet: No reporting or multi-page tests yet. ### Phase 5 · Reporting a result honestly Build: Report the effect with an interval rather than a point estimate, state the method and the sample, and record why a test was stopped. Keep concluded experiments so a claim can be checked later. Trap: Reporting a percentage improvement as a single number implies a precision the data does not support, and a fourteen percent lift with an interval spanning zero to thirty is a different finding from one spanning twelve to sixteen. Report the interval, always, and make the point estimate the smaller number on the page. Done when: Effects are reported as intervals with the point estimate secondary. The method, sample size and stopping reason appear on every result. Concluded experiments are retained with their data so a claim can be rechecked. A test stopped early for a business reason is recorded as such rather than reported as a result. Do not build yet: Nothing after this. ### Out of scope (and why) - Multivariate testing, which needs far more traffic than most sites have and produces results people over-read. - Personalisation engines, which is a different problem and one where evaluation is much harder. - Template libraries and design assets, which is most of what the hosted products actually sell. - Any claim about what will convert, which depends on your audience rather than on any general finding. ### README must contain - That stopping a test when it looks good produces false winners, and which method this build uses instead - That a running significance figure is not shown, deliberately - That assignments are written once and weight changes never reassign existing visitors - That conversions attribute to the assignment rather than to the current variant - That effects are reported as intervals with the point estimate secondary ===== ARCHITECTURE.md ===== # Architecture · Landingi ## Stack | Part | Choice | Why | | --- | --- | --- | | Pages | Static generation with a small runtime for assignment | a landing page that loads slowly converts worse than one that says less | | Assignment | Deterministic from a stable visitor identifier | a visitor must see the same variant on every visit or the test is meaningless | | Attribution | Conversion tied to the assignment, not to the session | a conversion after a reload still belongs to the variant that was seen | | Statistics | Fixed horizon with hidden results, or a sequential method | these are the two honest options and a running significance figure is neither | | Runtime | TypeScript with PostgreSQL | assignments and conversions are counts that need to be exact | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Versions | Immutable published snapshots. | Any store, provided a running experiment cannot be edited underneath. | | Assignment | Write-once deterministic assignment. | Any scheme, provided existing visitors are never reassigned. | | Attribution | Conversions tied to assignments. | Any implementation, provided attribution is to the assignment rather than the current variant. | | Statistics | A named method with a validated error rate. | Either honest method, provided a running significance figure is not shown. | | Reporting | Intervals and stopping reasons. | Any presentation, provided the interval is primary. | ## 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 · Assignments and conversions. - `STATS_METHOD` · required · Named in the interface. The other honest option is a sequential method. - `SHOW_RUNNING_SIGNIFICANCE` · required · Present as a constant. A running figure invites the stopping that manufactures results. - `MIN_DETECTABLE_EFFECT` · required · Drives the required sample size. Smaller effects need far more traffic. - `ACCEPTABLE_ERROR_RATE` · required · Validated against a null simulation in the test suite. - `ASSIGNMENT_COOKIE_DAYS` · required · How long a visitor keeps their variant. Shorter means more reassignment as new visitors. ## 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 · Landingi product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Static generation with a small runtime for assignment, Deterministic from a stable visitor identifier, Conversion tied to the assignment, not to the session, Fixed horizon with hidden results, or a sequential method, TypeScript with PostgreSQL. - 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 - Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. - Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. - Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. - Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. - Reporting a percentage improvement as a single number implies a precision the data does not support, and a fourteen percent lift with an interval spanning zero to thirty is a different finding from one spanning twelve to sixteen. Report the interval, always, and make the point estimate the smaller number on the page. ===== MILESTONES.md ===== # Delivery milestones · Landingi Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Pages that load fast enough to test A landing page renders quickly and is versioned immutably once published. ### Steps 1. Snapshot immutable versions at publish An edited variant mid-test invalidates the whole experiment. Files: `src/versions.ts` 2. Generate static output The page is in the measurement path. Slow pages change what you measure. Files: `src/build.ts` 3. Keep the runtime small It loads before assignment. Measure its size and hold it. Files: `runtime/assign.ts` ### Done when - [ ] Published versions are immutable - [ ] Pages render as static output - [ ] Load time is measured and stated - [ ] Experiments bind to versions, not pages ### Watch out - Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. ## M2 · Assignment that stays put A visitor sees the same variant every time, and you know they did. ### Steps 1. Assign once, deterministically From a stable identifier, written once and never updated. Files: `src/assign.ts`, `src/assign.test.ts` 2. Never reassign on weight change New weights apply to new visitors only. Files: `src/assign.ts` 3. Avoid the flicker Rendering the control then swapping is visible and it biases the result. Files: `runtime/assign.ts` ### Done when - [ ] A visitor sees the same variant across sessions - [ ] Weight changes do not reassign existing visitors - [ ] Cleared storage is handled without double counting - [ ] There is no variant flicker on load ### Watch out - Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. ## M3 · Conversions attributed to what was seen A conversion counts for the variant the visitor actually saw. ### Steps 1. Attribute to the assignment It is the only record of what the visitor actually saw. Files: `src/conversions.ts` 2. Fix the goal at start Changing what counts as success mid-test is choosing the answer. Files: `src/experiments.ts` 3. State the repeat and cross-device rules Both happen. Both need an answer written down, not an accident. Files: `src/conversions.ts` ### Done when - [ ] A delayed conversion attributes to the original variant - [ ] Repeat conversions follow a stated rule - [ ] The goal cannot change mid-experiment - [ ] Cross-device conversions follow a stated rule ### Watch out - Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. ## M4 · Statistics that do not let you cheat A winner is declared only when the method says it may be. ### Steps 1. Choose one honest method and name it Fixed horizon with hidden results, or sequential. Not a running figure. Files: `src/stats.ts` 2. Validate against a null simulation A thousand tests between identical variants. Count the false winners. Files: `src/stats.test.ts` 3. Refuse early declarations In the decision path. The friction is the feature. Files: `src/decide.ts` ### Done when - [ ] A null simulation produces false winners at no more than the stated rate - [ ] The method is named in the interface - [ ] Early declaration is refused - [ ] Sample size derives from a stated detectable effect ### Watch out - Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. ## M5 · Reporting a result honestly (production only) The report says what was found and what it does not support. ### Steps 1. Report intervals, not point estimates A single percentage implies precision the data does not have. Files: `src/report.ts` 2. State method, sample and stopping reason On the result itself, where someone reading it will see them. Files: `src/report.ts` 3. Retain concluded experiments So a claim made from one can be checked a year later. Files: `src/archive.ts` ### Done when - [ ] Effects report as intervals - [ ] Method, sample and stopping reason are on every result - [ ] Concluded experiments are retained with their data - [ ] An early business stop is recorded as such ### Watch out - Reporting a percentage improvement as a single number implies a precision the data does not support, and a fourteen percent lift with an interval spanning zero to thirty is a different finding from one spanning twelve to sixteen. Report the interval, always, and make the point estimate the smaller number on the page. ===== OPERATIONS.md ===== # Operations · Landingi ## Backup Assignments and conversions are the experiment. Losing part of either biases the result in a way you cannot detect afterwards, so back them up and confirm counts rather than only that a restore completed. ## Restore Restore and confirm assignment counts per variant match what was recorded before, since an uneven loss silently changes the answer. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Assignment balance across variants is the health check: a drift from the configured weights means assignment is failing somewhere, and every result after that point is suspect. ## Incident checklist A declared winner that later fails to hold is the failure that matters, and the usual cause is an early stop or a mid-test change. The stopping reason and version binding are recorded precisely so that can be diagnosed rather than argued about. 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 - [ ] A null simulation is confirmed to produce false winners at no more than the stated rate - [ ] Weight changes are confirmed not to reassign existing visitors - [ ] A delayed conversion is confirmed to attribute to the original variant - [ ] Early declaration is confirmed refused - [ ] Effects are confirmed reported as intervals ## Launch constraint Do not market omitted Landingi 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. Assignments and conversions. DATABASE_URL=postgres://localhost:5432/landing # Required. Named in the interface. The other honest option is a sequential method. STATS_METHOD=fixed_horizon_hidden # Required. Present as a constant. A running figure invites the stopping that manufactures results. SHOW_RUNNING_SIGNIFICANCE=never # Required. Drives the required sample size. Smaller effects need far more traffic. MIN_DETECTABLE_EFFECT=0.05 # Required. Validated against a null simulation in the test suite. ACCEPTABLE_ERROR_RATE=0.05 # Required. How long a visitor keeps their variant. Shorter means more reassignment as new visitors. ASSIGNMENT_COOKIE_DAYS=90
# Landingi · indie build A landing page builder whose interesting half is the experiment engine: consistent variant assignment, conversion attribution that survives a page reload, and a statistical method that does not let you stop a test the moment it looks good. 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 | | --- | --- | --- | | Pages | Static generation with a small runtime for assignment | a landing page that loads slowly converts worse than one that says less | | Assignment | Deterministic from a stable visitor identifier | a visitor must see the same variant on every visit or the test is meaningless | | Attribution | Conversion tied to the assignment, not to the session | a conversion after a reload still belongs to the variant that was seen | | Statistics | Fixed horizon with hidden results, or a sequential method | these are the two honest options and a running significance figure is neither | | Runtime | TypeScript with PostgreSQL | assignments and conversions are counts that need to be exact | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server, the page generator and the statistics. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **PostgreSQL 16 or newer** · free locally - Why: Assignments and conversions are counts that have to be exact, with one assignment per visitor enforced. - Get it: Docker or your package manager. - Verify: A unique constraint rejects a second assignment for one visitor - [ ] **Your statistical method, chosen before building** · free to read; a statistician's hour is worth it for a commercial site - Why: Fixed horizon with hidden results and sequential testing lead to different interfaces, and retrofitting either onto the other is a rewrite of the decision path. - Get it: Read how each handles continuous monitoring, pick one, and write down the parameters: your minimum detectable effect and acceptable error rate. - Verify: A written note names the method, the detectable effect and the error rate - [ ] **Enough traffic to reach a conclusion** · free, and sometimes discouraging - Why: A test needing forty thousand visitors per arm on a page receiving two hundred a week will never conclude, and knowing that before building saves the experiment. - Get it: Compute the required sample from your detectable effect and compare it to your actual weekly traffic. - Verify: A calculation exists showing how long a test would take at your traffic - [ ] **What counts as a conversion** · free - Why: It is fixed at experiment start and cannot change while running, so an imprecise definition invalidates the test rather than being tidied up later. - Get it: Define the event precisely, including repeat and cross-device behaviour. - Verify: A written definition covers the event, repeats and cross-device cases ## 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: - Multivariate testing. - Personalisation engines. - Template libraries and design assets. - Any general claim about what converts. - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing - large template and component libraries If one of those is essential to you, that is the reason to keep paying for Landingi, and the README should say so rather than pretend.
# Build brief · Landingi 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 landing pages with split testing that reports a result you can rely on to replace Landingi. 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 The statistics are the part people get wrong, and getting them wrong is worse than not testing, because a wrong result gets acted on with confidence. The specific failure is stopping when the numbers look good. A standard significance test assumes you decided the sample size in advance and looked once. If you watch a running test and stop the moment it crosses the threshold, you will cross it eventually on almost any test, including one between two identical pages. Checking daily on a test with no real difference will produce a significant-looking result far more often than the stated error rate suggests. So this build either fixes the sample size in advance and hides the result until it is reached, or uses a method valid under continuous monitoring. It does not show a running significance figure that invites stopping, and it will refuse to declare a winner on a test that has not met its criterion. The friction is the feature. A tool that lets you stop early is a tool that manufactures results. ### Stack (fixed, do not substitute) - Pages: Static generation with a small runtime for assignment · a landing page that loads slowly converts worse than one that says less - Assignment: Deterministic from a stable visitor identifier · a visitor must see the same variant on every visit or the test is meaningless - Attribution: Conversion tied to the assignment, not to the session · a conversion after a reload still belongs to the variant that was seen - Statistics: Fixed horizon with hidden results, or a sequential method · these are the two honest options and a running significance figure is neither - Runtime: TypeScript with PostgreSQL · assignments and conversions are counts that need to be exact ### Data model (create this before Phase 1) - pages(id, slug, published_version_id, created_at) - versions(id, page_id, content_json, published_at) · immutable once published - experiments(id, page_id, method, target_sample_per_variant, min_detectable_effect, started_at, ended_at, stopped_reason) - variants(id, experiment_id, version_id, weight, label) - assignments(visitor_id, experiment_id, variant_id, assigned_at) · one per visitor, never reassigned - conversions(id, assignment_id, goal, at, value_minor) · attributed to the assignment - results(experiment_id, computed_at, per_variant_json, decision, valid_under_method) - An assignment is written once and never changed, because a visitor moved between variants mid-test corrupts both arms. ### Phase 1 · Pages that load fast enough to test Build: Build pages from a block model and generate static output. Publishing snapshots an immutable version. Keep the runtime script small, since it will be in the path of every measurement. Trap: Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. Done when: A published version is immutable and editing creates a new one. Pages render as static output with a small runtime script. Page load is measured and stated. An experiment binds to versions rather than to the page, so editing cannot alter a running test. Do not build yet: No experiments, assignment or statistics. ### Phase 2 · Assignment that stays put Build: Assign deterministically from a stable visitor identifier, writing the assignment once. Handle a visitor whose storage is cleared. Never reassign an existing visitor when weights change. Trap: Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. Done when: A visitor receives the same variant across sessions, tested over several visits. Changing weights does not reassign existing visitors, verified directly. A visitor with cleared storage is treated as new rather than being double counted. Assignment happens before the page renders, with no visible flicker. Do not build yet: No conversions or statistics yet. ### Phase 3 · Conversions attributed to what was seen Build: Record conversions against the assignment rather than the session. Handle a conversion arriving days later, and a visitor converting more than once. Define the goal precisely before the experiment starts. Trap: Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. Done when: A conversion occurring days after assignment attributes to the original variant, tested with a delayed conversion. Repeat conversions from one visitor are handled per a stated rule. The goal definition is fixed at experiment start and cannot be changed while running. Conversions arriving from a different device are handled by a stated rule rather than silently. Do not build yet: No statistics or decisions yet. ### Phase 4 · Statistics that do not let you cheat Build: Implement either a fixed sample size computed in advance with results hidden until it is reached, or a sequential method valid under continuous monitoring. Refuse to declare a winner before the criterion is met. Validate against simulated data with no real effect. Trap: Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. Done when: Simulating a test between two identical variants produces false winners at no more than the stated rate across a thousand simulated experiments, asserted in a test. The chosen method is named in the interface. A winner cannot be declared before the criterion is met, verified by attempting it. The required sample size is computed from a stated minimum detectable effect. Do not build yet: No reporting or multi-page tests yet. ### Phase 5 · Reporting a result honestly Build: Report the effect with an interval rather than a point estimate, state the method and the sample, and record why a test was stopped. Keep concluded experiments so a claim can be checked later. Trap: Reporting a percentage improvement as a single number implies a precision the data does not support, and a fourteen percent lift with an interval spanning zero to thirty is a different finding from one spanning twelve to sixteen. Report the interval, always, and make the point estimate the smaller number on the page. Done when: Effects are reported as intervals with the point estimate secondary. The method, sample size and stopping reason appear on every result. Concluded experiments are retained with their data so a claim can be rechecked. A test stopped early for a business reason is recorded as such rather than reported as a result. Do not build yet: Nothing after this. ### Out of scope (and why) - Multivariate testing, which needs far more traffic than most sites have and produces results people over-read. - Personalisation engines, which is a different problem and one where evaluation is much harder. - Template libraries and design assets, which is most of what the hosted products actually sell. - Any claim about what will convert, which depends on your audience rather than on any general finding. ### README must contain - That stopping a test when it looks good produces false winners, and which method this build uses instead - That a running significance figure is not shown, deliberately - That assignments are written once and weight changes never reassign existing visitors - That conversions attribute to the assignment rather than to the current variant - That effects are reported as intervals with the point estimate secondary
# Agent instructions · Landingi indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Static generation with a small runtime for assignment, Deterministic from a stable visitor identifier, Conversion tied to the assignment, not to the session, Fixed horizon with hidden results, or a sequential method, TypeScript with PostgreSQL. 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 - Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. - Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. - Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. - Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose.
# Build plan · Landingi A landing page builder whose interesting half is the experiment engine: consistent variant assignment, conversion attribution that survives a page reload, and a statistical method that does not let you stop a test the moment it looks good. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Pages that load fast enough to test A landing page renders quickly and is versioned immutably once published. ### Steps 1. Snapshot immutable versions at publish An edited variant mid-test invalidates the whole experiment. Files: `src/versions.ts` 2. Generate static output The page is in the measurement path. Slow pages change what you measure. Files: `src/build.ts` 3. Keep the runtime small It loads before assignment. Measure its size and hold it. Files: `runtime/assign.ts` ### Done when - [ ] Published versions are immutable - [ ] Pages render as static output - [ ] Load time is measured and stated - [ ] Experiments bind to versions, not pages ### Watch out - Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. ## Phase 2 · Assignment that stays put A visitor sees the same variant every time, and you know they did. ### Steps 1. Assign once, deterministically From a stable identifier, written once and never updated. Files: `src/assign.ts`, `src/assign.test.ts` 2. Never reassign on weight change New weights apply to new visitors only. Files: `src/assign.ts` 3. Avoid the flicker Rendering the control then swapping is visible and it biases the result. Files: `runtime/assign.ts` ### Done when - [ ] A visitor sees the same variant across sessions - [ ] Weight changes do not reassign existing visitors - [ ] Cleared storage is handled without double counting - [ ] There is no variant flicker on load ### Watch out - Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. ## Phase 3 · Conversions attributed to what was seen A conversion counts for the variant the visitor actually saw. ### Steps 1. Attribute to the assignment It is the only record of what the visitor actually saw. Files: `src/conversions.ts` 2. Fix the goal at start Changing what counts as success mid-test is choosing the answer. Files: `src/experiments.ts` 3. State the repeat and cross-device rules Both happen. Both need an answer written down, not an accident. Files: `src/conversions.ts` ### Done when - [ ] A delayed conversion attributes to the original variant - [ ] Repeat conversions follow a stated rule - [ ] The goal cannot change mid-experiment - [ ] Cross-device conversions follow a stated rule ### Watch out - Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. ## Phase 4 · Statistics that do not let you cheat A winner is declared only when the method says it may be. ### Steps 1. Choose one honest method and name it Fixed horizon with hidden results, or sequential. Not a running figure. Files: `src/stats.ts` 2. Validate against a null simulation A thousand tests between identical variants. Count the false winners. Files: `src/stats.test.ts` 3. Refuse early declarations In the decision path. The friction is the feature. Files: `src/decide.ts` ### Done when - [ ] A null simulation produces false winners at no more than the stated rate - [ ] The method is named in the interface - [ ] Early declaration is refused - [ ] Sample size derives from a stated detectable effect ### Watch out - Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. ## Not in this build - Multivariate testing. - Personalisation engines. - Template libraries and design assets. - Any general claim about what converts. ## After v1, if you want it - Sample size guidance before a test starts, which prevents tests that cannot conclude - Segment analysis with a correction for testing several segments, which is the next place people fool themselves - Sequential monitoring with a proper spending function, if you started with fixed horizon
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Assignments and conversions. DATABASE_URL=postgres://localhost:5432/landing # Required. Named in the interface. The other honest option is a sequential method. STATS_METHOD=fixed_horizon_hidden # Required. Present as a constant. A running figure invites the stopping that manufactures results. SHOW_RUNNING_SIGNIFICANCE=never # Required. Drives the required sample size. Smaller effects need far more traffic. MIN_DETECTABLE_EFFECT=0.05 # Required. Validated against a null simulation in the test suite. ACCEPTABLE_ERROR_RATE=0.05 # Required. How long a visitor keeps their variant. Shorter means more reassignment as new visitors. ASSIGNMENT_COOKIE_DAYS=90
# Landingi · product brief ## Problem The visible landing-page platform loop is buildable, but a credible replacement needs more than the first screen. Landingi earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A landing page tool whose experiments produce results you can act on, because it will not let you stop a test at the moment it happens to look good. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - deploy target or static hosting - user-owned domain for publishing ## Explicit non-goals for v1 - Multivariate testing. - Personalisation engines. - Template libraries and design assets. - Any general claim about what converts. - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing - large template and component libraries ## Success criteria - A null simulation is confirmed to produce false winners at no more than the stated rate - Weight changes are confirmed not to reassign existing visitors - A delayed conversion is confirmed to attribute to the original variant - Early declaration is confirmed refused - Effects are confirmed reported as intervals
# Build brief · Landingi 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 landing pages with split testing that reports a result you can rely on to replace Landingi. 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 The statistics are the part people get wrong, and getting them wrong is worse than not testing, because a wrong result gets acted on with confidence. The specific failure is stopping when the numbers look good. A standard significance test assumes you decided the sample size in advance and looked once. If you watch a running test and stop the moment it crosses the threshold, you will cross it eventually on almost any test, including one between two identical pages. Checking daily on a test with no real difference will produce a significant-looking result far more often than the stated error rate suggests. So this build either fixes the sample size in advance and hides the result until it is reached, or uses a method valid under continuous monitoring. It does not show a running significance figure that invites stopping, and it will refuse to declare a winner on a test that has not met its criterion. The friction is the feature. A tool that lets you stop early is a tool that manufactures results. ### Stack (fixed, do not substitute) - Pages: Static generation with a small runtime for assignment · a landing page that loads slowly converts worse than one that says less - Assignment: Deterministic from a stable visitor identifier · a visitor must see the same variant on every visit or the test is meaningless - Attribution: Conversion tied to the assignment, not to the session · a conversion after a reload still belongs to the variant that was seen - Statistics: Fixed horizon with hidden results, or a sequential method · these are the two honest options and a running significance figure is neither - Runtime: TypeScript with PostgreSQL · assignments and conversions are counts that need to be exact ### Data model (create this before Phase 1) - pages(id, slug, published_version_id, created_at) - versions(id, page_id, content_json, published_at) · immutable once published - experiments(id, page_id, method, target_sample_per_variant, min_detectable_effect, started_at, ended_at, stopped_reason) - variants(id, experiment_id, version_id, weight, label) - assignments(visitor_id, experiment_id, variant_id, assigned_at) · one per visitor, never reassigned - conversions(id, assignment_id, goal, at, value_minor) · attributed to the assignment - results(experiment_id, computed_at, per_variant_json, decision, valid_under_method) - An assignment is written once and never changed, because a visitor moved between variants mid-test corrupts both arms. ### Phase 1 · Pages that load fast enough to test Build: Build pages from a block model and generate static output. Publishing snapshots an immutable version. Keep the runtime script small, since it will be in the path of every measurement. Trap: Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. Done when: A published version is immutable and editing creates a new one. Pages render as static output with a small runtime script. Page load is measured and stated. An experiment binds to versions rather than to the page, so editing cannot alter a running test. Do not build yet: No experiments, assignment or statistics. ### Phase 2 · Assignment that stays put Build: Assign deterministically from a stable visitor identifier, writing the assignment once. Handle a visitor whose storage is cleared. Never reassign an existing visitor when weights change. Trap: Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. Done when: A visitor receives the same variant across sessions, tested over several visits. Changing weights does not reassign existing visitors, verified directly. A visitor with cleared storage is treated as new rather than being double counted. Assignment happens before the page renders, with no visible flicker. Do not build yet: No conversions or statistics yet. ### Phase 3 · Conversions attributed to what was seen Build: Record conversions against the assignment rather than the session. Handle a conversion arriving days later, and a visitor converting more than once. Define the goal precisely before the experiment starts. Trap: Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. Done when: A conversion occurring days after assignment attributes to the original variant, tested with a delayed conversion. Repeat conversions from one visitor are handled per a stated rule. The goal definition is fixed at experiment start and cannot be changed while running. Conversions arriving from a different device are handled by a stated rule rather than silently. Do not build yet: No statistics or decisions yet. ### Phase 4 · Statistics that do not let you cheat Build: Implement either a fixed sample size computed in advance with results hidden until it is reached, or a sequential method valid under continuous monitoring. Refuse to declare a winner before the criterion is met. Validate against simulated data with no real effect. Trap: Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. Done when: Simulating a test between two identical variants produces false winners at no more than the stated rate across a thousand simulated experiments, asserted in a test. The chosen method is named in the interface. A winner cannot be declared before the criterion is met, verified by attempting it. The required sample size is computed from a stated minimum detectable effect. Do not build yet: No reporting or multi-page tests yet. ### Phase 5 · Reporting a result honestly Build: Report the effect with an interval rather than a point estimate, state the method and the sample, and record why a test was stopped. Keep concluded experiments so a claim can be checked later. Trap: Reporting a percentage improvement as a single number implies a precision the data does not support, and a fourteen percent lift with an interval spanning zero to thirty is a different finding from one spanning twelve to sixteen. Report the interval, always, and make the point estimate the smaller number on the page. Done when: Effects are reported as intervals with the point estimate secondary. The method, sample size and stopping reason appear on every result. Concluded experiments are retained with their data so a claim can be rechecked. A test stopped early for a business reason is recorded as such rather than reported as a result. Do not build yet: Nothing after this. ### Out of scope (and why) - Multivariate testing, which needs far more traffic than most sites have and produces results people over-read. - Personalisation engines, which is a different problem and one where evaluation is much harder. - Template libraries and design assets, which is most of what the hosted products actually sell. - Any claim about what will convert, which depends on your audience rather than on any general finding. ### README must contain - That stopping a test when it looks good produces false winners, and which method this build uses instead - That a running significance figure is not shown, deliberately - That assignments are written once and weight changes never reassign existing visitors - That conversions attribute to the assignment rather than to the current variant - That effects are reported as intervals with the point estimate secondary
# Architecture · Landingi ## Stack | Part | Choice | Why | | --- | --- | --- | | Pages | Static generation with a small runtime for assignment | a landing page that loads slowly converts worse than one that says less | | Assignment | Deterministic from a stable visitor identifier | a visitor must see the same variant on every visit or the test is meaningless | | Attribution | Conversion tied to the assignment, not to the session | a conversion after a reload still belongs to the variant that was seen | | Statistics | Fixed horizon with hidden results, or a sequential method | these are the two honest options and a running significance figure is neither | | Runtime | TypeScript with PostgreSQL | assignments and conversions are counts that need to be exact | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Versions | Immutable published snapshots. | Any store, provided a running experiment cannot be edited underneath. | | Assignment | Write-once deterministic assignment. | Any scheme, provided existing visitors are never reassigned. | | Attribution | Conversions tied to assignments. | Any implementation, provided attribution is to the assignment rather than the current variant. | | Statistics | A named method with a validated error rate. | Either honest method, provided a running significance figure is not shown. | | Reporting | Intervals and stopping reasons. | Any presentation, provided the interval is primary. | ## 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 · Assignments and conversions. - `STATS_METHOD` · required · Named in the interface. The other honest option is a sequential method. - `SHOW_RUNNING_SIGNIFICANCE` · required · Present as a constant. A running figure invites the stopping that manufactures results. - `MIN_DETECTABLE_EFFECT` · required · Drives the required sample size. Smaller effects need far more traffic. - `ACCEPTABLE_ERROR_RATE` · required · Validated against a null simulation in the test suite. - `ASSIGNMENT_COOKIE_DAYS` · required · How long a visitor keeps their variant. Shorter means more reassignment as new visitors. ## 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 · Landingi product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Static generation with a small runtime for assignment, Deterministic from a stable visitor identifier, Conversion tied to the assignment, not to the session, Fixed horizon with hidden results, or a sequential method, TypeScript with PostgreSQL. - 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 - Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. - Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. - Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. - Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. - Reporting a percentage improvement as a single number implies a precision the data does not support, and a fourteen percent lift with an interval spanning zero to thirty is a different finding from one spanning twelve to sixteen. Report the interval, always, and make the point estimate the smaller number on the page.
# Delivery milestones · Landingi Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Pages that load fast enough to test A landing page renders quickly and is versioned immutably once published. ### Steps 1. Snapshot immutable versions at publish An edited variant mid-test invalidates the whole experiment. Files: `src/versions.ts` 2. Generate static output The page is in the measurement path. Slow pages change what you measure. Files: `src/build.ts` 3. Keep the runtime small It loads before assignment. Measure its size and hold it. Files: `runtime/assign.ts` ### Done when - [ ] Published versions are immutable - [ ] Pages render as static output - [ ] Load time is measured and stated - [ ] Experiments bind to versions, not pages ### Watch out - Editing a published page during a running experiment changes what the variant is halfway through, and the results describe two things that no longer exist. Snapshot the version at publish and bind the experiment to versions rather than to pages. ## M2 · Assignment that stays put A visitor sees the same variant every time, and you know they did. ### Steps 1. Assign once, deterministically From a stable identifier, written once and never updated. Files: `src/assign.ts`, `src/assign.test.ts` 2. Never reassign on weight change New weights apply to new visitors only. Files: `src/assign.ts` 3. Avoid the flicker Rendering the control then swapping is visible and it biases the result. Files: `runtime/assign.ts` ### Done when - [ ] A visitor sees the same variant across sessions - [ ] Weight changes do not reassign existing visitors - [ ] Cleared storage is handled without double counting - [ ] There is no variant flicker on load ### Watch out - Reassigning visitors when you adjust traffic weights moves people between arms mid-test, which contaminates both and produces a result describing neither. Keep existing assignments fixed and apply new weights only to visitors who have not been assigned. ## M3 · Conversions attributed to what was seen A conversion counts for the variant the visitor actually saw. ### Steps 1. Attribute to the assignment It is the only record of what the visitor actually saw. Files: `src/conversions.ts` 2. Fix the goal at start Changing what counts as success mid-test is choosing the answer. Files: `src/experiments.ts` 3. State the repeat and cross-device rules Both happen. Both need an answer written down, not an accident. Files: `src/conversions.ts` ### Done when - [ ] A delayed conversion attributes to the original variant - [ ] Repeat conversions follow a stated rule - [ ] The goal cannot change mid-experiment - [ ] Cross-device conversions follow a stated rule ### Watch out - Attributing a conversion to whichever variant is current at conversion time rather than to the recorded assignment mixes the arms whenever anything about the test changes, and the corruption is invisible in the totals. Attribute to the assignment, which is the only thing that records what was actually seen. ## M4 · Statistics that do not let you cheat A winner is declared only when the method says it may be. ### Steps 1. Choose one honest method and name it Fixed horizon with hidden results, or sequential. Not a running figure. Files: `src/stats.ts` 2. Validate against a null simulation A thousand tests between identical variants. Count the false winners. Files: `src/stats.test.ts` 3. Refuse early declarations In the decision path. The friction is the feature. Files: `src/decide.ts` ### Done when - [ ] A null simulation produces false winners at no more than the stated rate - [ ] The method is named in the interface - [ ] Early declaration is refused - [ ] Sample size derives from a stated detectable effect ### Watch out - Displaying a running significance figure invites stopping the moment it crosses the line, and doing that on two identical pages produces a false winner a large fraction of the time. Either hide the numbers until the planned sample is reached, or use a method whose guarantee holds when you look continuously, and say which one you chose. ## M5 · Reporting a result honestly (production only) The report says what was found and what it does not support. ### Steps 1. Report intervals, not point estimates A single percentage implies precision the data does not have. Files: `src/report.ts` 2. State method, sample and stopping reason On the result itself, where someone reading it will see them. Files: `src/report.ts` 3. Retain concluded experiments So a claim made from one can be checked a year later. Files: `src/archive.ts` ### Done when - [ ] Effects report as intervals - [ ] Method, sample and stopping reason are on every result - [ ] Concluded experiments are retained with their data - [ ] An early business stop is recorded as such ### Watch out - Reporting a percentage improvement as a single number implies a precision the data does not support, and a fourteen percent lift with an interval spanning zero to thirty is a different finding from one spanning twelve to sixteen. Report the interval, always, and make the point estimate the smaller number on the page.
# Operations · Landingi ## Backup Assignments and conversions are the experiment. Losing part of either biases the result in a way you cannot detect afterwards, so back them up and confirm counts rather than only that a restore completed. ## Restore Restore and confirm assignment counts per variant match what was recorded before, since an uneven loss silently changes the answer. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Assignment balance across variants is the health check: a drift from the configured weights means assignment is failing somewhere, and every result after that point is suspect. ## Incident checklist A declared winner that later fails to hold is the failure that matters, and the usual cause is an early stop or a mid-test change. The stopping reason and version binding are recorded precisely so that can be diagnosed rather than argued about. 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 - [ ] A null simulation is confirmed to produce false winners at no more than the stated rate - [ ] Weight changes are confirmed not to reassign existing visitors - [ ] A delayed conversion is confirmed to attribute to the original variant - [ ] Early declaration is confirmed refused - [ ] Effects are confirmed reported as intervals ## Launch constraint Do not market omitted Landingi 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. Assignments and conversions. DATABASE_URL=postgres://localhost:5432/landing # Required. Named in the interface. The other honest option is a sequential method. STATS_METHOD=fixed_horizon_hidden # Required. Present as a constant. A running figure invites the stopping that manufactures results. SHOW_RUNNING_SIGNIFICANCE=never # Required. Drives the required sample size. Smaller effects need far more traffic. MIN_DETECTABLE_EFFECT=0.05 # Required. Validated against a null simulation in the test suite. ACCEPTABLE_ERROR_RATE=0.05 # Required. How long a visitor keeps their variant. Shorter means more reassignment as new visitors. ASSIGNMENT_COOKIE_DAYS=90
$ choose a build depth, inspect the files, then open the complete pack in your agent
Landingi: A single site is easy to code. People pay for safe ongoing edits, templates, publishing, domains, collaboration, and no-maintenance hosting.
xCMS collaboration and client handoff
xmanaged hosting, forms, and analytics
xgeneral-purpose visual editing
xlarge template and component libraries
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Landingi pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| build | $29/workspace | $24/workspace | 10 active landing pages; 2,000 visits/month; 1 custom domain; 5 users; 2,500 credits/month |
| optimize | $149/workspace | $119/workspace | 100 active landing pages; 30,000 visits/month; 3 custom domains; 10 users; 10,000 credits/month |
| scale | $299/workspace | $229/workspace | Unlimited active pages; 100,000-500,000 visits/month depending configuration; 10 custom domains; unlimited users; 20,000 credits/month |
| enterprise | $1499/workspace | $1199/workspace | Unlimited active pages; 1M+ visits/month; 100 custom domains; unlimited users; 60,000 credits/month |
free tierno free tier; 14-day trial capped at 200 visits, after which the trial ends
billingmonthly + annual (annual advertised as 2+ months free; current annual promotional equivalents are lower than a simple two-month discount)
hidden costsExtra domains cost $5/month each (with plan-specific add-on ceilings); credit top-ups cost $15 for 5,000, $25 for 10,000, or $40 for 20,000; Scale pricing increases with selected monthly traffic.
verified 2026-08-14 · source ↗
Vibecode Landingi
Kinda. The core of Landingi is buildable in a weekend with the prompt on this page, but there are real gaps: CMS collaboration and client handoff, managed hosting, forms, and analytics. Read the honest list above before committing.
How much does Landingi cost?
Landingi costs about $29/month (Lite, checked 2026-07-31), which is $348 per year.
What do I lose by replacing Landingi?
Honestly: CMS collaboration and client handoff; managed hosting, forms, and analytics; general-purpose visual editing; large template and component libraries. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Landingi?
Yes: Mautic (Pages, forms, campaigns and A/B tests without licence fees; database and email operations replace the subscription.) systeme.io (Hosted pages, popups, forms and one split test on a permanent free plan; agency workflow is thin.) The prompt is for when you want it exactly your way.