Vibecode Ecwid
track this build4 phases, 12 steps, beginner friendly0%The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Ecwid, embed a small catalog and hosted checkout into an existing site. The hard boundary is commerce integrations, inventory, channels, mobile apps, tax, and support, plus payments, tax, distribution, and operations.
You are building a lean indie version of Ecwid. 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 ===== # Ecwid · indie build A single-creator storefront whose hard problems are behaving as a guest on somebody else's page without depending on cookies you are not allowed to set, making a product a real linkable address rather than a state inside a widget, and granting access from payment events rather than from a browser arriving at a page. 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 | | --- | --- | --- | | Runtime | Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages | the embed is a client of your own public API and should not be a special case | | Embedding | One small script that renders into a shadow root and keeps its cart token in the host page's own storage | browsers partition or block third-party cookies by default, so a cookie set from your domain inside their page is not a plan | | Storage | PostgreSQL for orders and entitlements, object storage for the files people bought | orders are money and belong in something with real transactions | | Payments | Hosted checkout on the processor's own page, fulfilment driven by webhooks | the redirect back is a courtesy, not a guarantee | | Delivery | Short-lived signed URLs with resumable range requests | a large file over a hotel connection is the normal customer, not the edge case | ## 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 storefront, the API and the embed build. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **A payment processor account with hosted checkout and webhooks** · free to set up; per-transaction fees on real sales - Why: Money never touches your code, which is the single biggest risk reduction available in this build. - Get it: Create the account, enable hosted checkout, and register a webhook endpoint with its signing secret. - Verify: A test payment produces a verified event at your endpoint - [ ] **Object storage that can sign time-limited URLs** · pennies per month at creator volume - Why: Delivery depends on short-lived signed URLs, and storage that cannot mint them forces you into a worse design. - Get it: Any provider with a signed URL feature and range request support. - Verify: A signed URL expires on schedule and a range request resumes a partial download - [ ] **A test page on a domain you do not control** · free - Why: The embed's whole job is to work as a guest, and it will behave differently on your own domain. - Get it: Any free static host, with a deliberately strict content security policy and aggressive global CSS. - Verify: The embed works there with third-party cookies blocked - [ ] **Your tax position, decided before the first sale** · free to research; possibly not free to comply - Why: Selling digital goods across borders creates obligations that begin at the first transaction, not when you notice. - Get it: Find out what registration your sales require where your buyers are, and write down what you concluded and when. - Verify: The README states the position and the date it was checked ## 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: - Merchant of record tax remittance. - A buyer marketplace. - Physical inventory and shipping. - Fraud scoring. - commerce integrations, inventory, channels, mobile apps, tax, and support - marketplace discovery - tax remittance - fraud operations - advanced course and community tooling If one of those is essential to you, that is the reason to keep paying for Ecwid, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Ecwid 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 a small catalogue and checkout you can paste into a website you do not control to replace Ecwid. 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. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages · the embed is a client of your own public API and should not be a special case - Embedding: One small script that renders into a shadow root and keeps its cart token in the host page's own storage · browsers partition or block third-party cookies by default, so a cookie set from your domain inside their page is not a plan - Storage: PostgreSQL for orders and entitlements, object storage for the files people bought · orders are money and belong in something with real transactions - Payments: Hosted checkout on the processor's own page, fulfilment driven by webhooks · the redirect back is a courtesy, not a guarantee - Delivery: Short-lived signed URLs with resumable range requests · a large file over a hotel connection is the normal customer, not the edge case ### Data model (create this before Phase 1) - products(id, slug, title, description, price_minor, currency, state, kind) · kind is file or lesson set - variants(id, product_id, name, price_delta_minor, stock) · stock may be null for digital goods, and null is not zero - files(id, product_id, storage_key, filename, bytes, sha256) and lessons(id, product_id, position, title, body) - carts(token, created_at, last_seen_at, items_json) · the token lives in the host page's storage and is opaque - orders(id, processor_session_id, processor_event_id, email, account_ref, amount_minor, currency, state, created_at) · unique on the processor event that created it - entitlements(id, order_id, product_id, granted_at, revoked_at, reason) · granting and revoking are rows, never a boolean edit - downloads(id, entitlement_id, at, bytes_sent, completed, ip_hash) · enough to spot sharing without keeping a log of people ### Phase 1 · A guest on somebody else's page Build: A script that mounts the catalogue into a shadow root so neither side's styles leak. Hold the cart token in the host page's own storage and send it explicitly on every API call. Keep the script small, load nothing else from a third party, and document the content security policy entries a host site needs to add. Trap: Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. Done when: The embed works end to end in a browser with third-party cookies blocked, tested explicitly. Host page styles do not affect the widget and widget styles do not affect the host page, tested against a page with aggressive global CSS. The script loads no third-party resources and its size is stated and enforced in the build. The required content security policy entries are documented and verified against a host page that sets a strict policy. Do not build yet: No checkout, delivery, orders or refunds yet. ### Phase 2 · A product is an address Build: Serve a server-rendered page per product on your own domain, with the full description, price and structured metadata, and treat it as the canonical address. Have the embed update the host page's address as the visitor moves through the catalogue, and restore state from that address on load. Link from the embed to the canonical page. Trap: Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. Done when: Every product has a server-rendered page returning full content without JavaScript, tested by fetching it with scripting disabled. Navigating the embed updates the host page address and a reload restores the same view, tested. Sharing a copied address opens the same product for a visitor who has never seen the site. The canonical link on the embed points at the product page on your domain. Do not build yet: No payment, entitlements or delivery yet. ### Phase 3 · Access comes from the event, not the page Build: Send the buyer to the processor's hosted checkout. Write one fulfilment function that takes a session identifier, is safe to run repeatedly and concurrently, retrieves the session, checks the payment status and grants entitlements. Call it from the webhook handler and also from the landing page, and rely on the webhook for correctness. Trap: Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. Done when: A payment completed with the browser closed still grants access, tested by discarding the redirect. Calling the fulfilment function twenty times concurrently for one session produces exactly one set of entitlements, tested. A refund or chargeback event writes a revocation row and access stops immediately, tested. Every order stores the processor event that created it and the raw payload. Do not build yet: No download delivery, order management or exports yet. ### Phase 4 · Delivering the thing they bought Build: Serve files through short-lived signed URLs minted per request against a live entitlement check. Support range requests so an interrupted download resumes rather than restarting. Record each download attempt with bytes sent and whether it completed. Lessons render as pages behind the same entitlement check rather than as unprotected files. Trap: Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. Done when: A signed URL expires within the configured window and a revoked entitlement stops minting new ones immediately, tested. An interrupted download of a large file resumes from where it stopped, tested with a truncated connection. Every download attempt is recorded with bytes sent and completion. Lesson content is unreachable without an entitlement, tested by requesting it directly. Do not build yet: No order management, tax handling or exports yet. ### Phase 5 · Running a shop by yourself Build: Order search by email, product and date, receipt resend, a manual grant and revoke with a recorded reason, and an append-only event log for every order. Export orders, entitlements and the catalogue as CSV and JSON. State plainly in the interface and the README that tax is the seller's responsibility and that this build calculates none. Trap: Adding a tax percentage field because it looks easy produces a shop that computes a confident wrong number for every cross-border sale, and the person who finds out is you, later, with interest. Calculate nothing, record the amounts the processor reported, and say in the interface that registration and remittance are yours to sort out before your first sale. Done when: An order is findable by email, product or date within a few seconds on a realistic dataset. Manual grants and revocations are recorded with a reason and appear in the order's event log. No tax figure is computed anywhere in the codebase, verified by review, and the responsibility is stated in the interface. Orders, entitlements and catalogue export as CSV and JSON and reimport into an empty instance. Do not build yet: Nothing after this. ### Out of scope (and why) - Acting as merchant of record and remitting tax, which is a registration and a filing obligation in every jurisdiction you sell into rather than a code feature. - A buyer-facing marketplace, which is where the paid product's discovery comes from and is worthless with one shop in it. - Physical inventory, shipping rates and channel synchronisation, which is a different product with different failure modes. - Fraud scoring, which needs a volume of transactions one creator will never have and is dangerous when guessed at. ### README must contain - That browsers partition or block third-party cookies by default, and that the cart token therefore lives in the host page's own storage - The exact content security policy entries a host site needs, and what happens without them - That fulfilment runs from the payment event and that the success page is only a shortcut, because buyers do not always come back - That download links are short lived and minted per request against a live entitlement - That tax registration and remittance are the seller's responsibility and that this build calculates none ===== AGENTS.md ===== # Agent instructions · Ecwid indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages, One small script that renders into a shadow root and keeps its cart token in the host page's own storage, PostgreSQL for orders and entitlements, object storage for the files people bought, Hosted checkout on the processor's own page, fulfilment driven by webhooks, Short-lived signed URLs with resumable range requests. 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 - Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. - Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. - Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. - Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. ===== BUILD_PLAN.md ===== # Build plan · Ecwid A single-creator storefront whose hard problems are behaving as a guest on somebody else's page without depending on cookies you are not allowed to set, making a product a real linkable address rather than a state inside a widget, and granting access from payment events rather than from a browser arriving at a page. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A guest on somebody else's page The embed renders correctly on a site you do not control and never depends on a cookie you cannot set. ### Steps 1. Mount into a shadow root Neither side should be able to break the other's layout. Files: `src/embed/mount.ts`, `tests/isolation.test.ts` 2. Keep the cart token in the host page's storage It is the only storage you are reliably allowed to use there. Files: `src/embed/cart.ts`, `tests/no-cookie.test.ts` 3. Publish the policy entries a host site must add Otherwise your first customer's site silently blocks you. Files: `docs/embedding.md` ### Done when - [ ] The embed works with third-party cookies blocked - [ ] Styles do not leak in either direction - [ ] No third-party resource is loaded by the script - [ ] Required policy entries are documented and tested ### Watch out - Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. ## Phase 2 · A product is an address Every product has a real URL that can be shared, crawled and linked, not just a state inside a widget. ### Steps 1. Server render a canonical page per product It is the thing people can send to each other. Files: `src/app/p/[slug]/page.tsx` 2. Mirror catalogue state into the host page address A back button that does nothing is a bug report you will get. Files: `src/embed/router.ts` 3. Link the embed to the canonical page One address per product, everywhere. Files: `src/embed/product.ts` ### Done when - [ ] Product pages render fully without JavaScript - [ ] Navigating updates the address and reload restores state - [ ] A shared address opens the right product for a stranger - [ ] The embed links to the canonical page ### Watch out - Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. ## Phase 3 · Access comes from the event, not the page A payment grants access exactly once, whether or not the buyer's browser ever came back. ### Steps 1. One fulfilment function, safe to run twice It will be called from two places and retried by a third. Files: `src/fulfil.ts`, `tests/fulfil-idempotent.test.ts` 2. Drive it from the webhook, not the redirect The browser is the least reliable participant in the payment. Files: `src/webhooks.ts` 3. Revoke by writing a row An edited boolean cannot answer 'when did they lose access'. Files: `src/entitlements.ts` ### Done when - [ ] A payment with no redirect still grants access - [ ] Concurrent fulfilment produces one set of entitlements - [ ] A refund revokes access immediately - [ ] Orders retain their processor event and payload ### Watch out - Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. ## Phase 4 · Delivering the thing they bought A large file downloads over a bad connection, and access ends when entitlement does. ### Steps 1. Mint short-lived URLs per request A long-lived link is a product you gave away. Files: `src/delivery/sign.ts`, `tests/delivery.test.ts` 2. Support range requests for resume Large files and hotel wifi are the normal case. Files: `src/delivery/stream.ts` 3. Record attempts, not just successes A pile of incomplete downloads is a support ticket waiting. Files: `src/delivery/log.ts` ### Done when - [ ] Signed URLs expire and revocation stops new ones - [ ] An interrupted large download resumes - [ ] Download attempts record bytes and completion - [ ] Lesson content requires an entitlement ### Watch out - Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. ## Not in this build - Merchant of record tax remittance. - A buyer marketplace. - Physical inventory and shipping. - Fraud scoring. ## After v1, if you want it - Discount codes with a use limit, which is mostly a concurrency problem and worth doing carefully - A customer library page with its own sign-in link, which reduces the number of support emails asking for a download - A second processor behind the same fulfilment function, which is the real test of whether the event-driven design held ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Orders and entitlements. This is the money record; back it up. DATABASE_URL=postgres://user:pass@localhost/shop # Required. Where purchased files live. Never public. STORAGE_BUCKET=creator-files # Required · secret. Used only to mint signed URLs on the server. STORAGE_KEY=AKIA...redacted # Required · secret. Creates checkout sessions and retrieves them during fulfilment. PROCESSOR_SECRET_KEY=sk_live_...redacted # Required · secret. Verifies every event before it can grant anything. PROCESSOR_WEBHOOK_SECRET=whsec_...redacted # Required. How long a download link lives. Short, and minted per request. SIGNED_URL_TTL_SECONDS=300 # Required. Canonical product pages, the embed script and the checkout return address. PUBLIC_BASE_URL=https://shop.example.com
You are building a lean indie version of Ecwid. 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 ===== # Ecwid · indie build A single-creator storefront whose hard problems are behaving as a guest on somebody else's page without depending on cookies you are not allowed to set, making a product a real linkable address rather than a state inside a widget, and granting access from payment events rather than from a browser arriving at a page. 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 | | --- | --- | --- | | Runtime | Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages | the embed is a client of your own public API and should not be a special case | | Embedding | One small script that renders into a shadow root and keeps its cart token in the host page's own storage | browsers partition or block third-party cookies by default, so a cookie set from your domain inside their page is not a plan | | Storage | PostgreSQL for orders and entitlements, object storage for the files people bought | orders are money and belong in something with real transactions | | Payments | Hosted checkout on the processor's own page, fulfilment driven by webhooks | the redirect back is a courtesy, not a guarantee | | Delivery | Short-lived signed URLs with resumable range requests | a large file over a hotel connection is the normal customer, not the edge case | ## 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 storefront, the API and the embed build. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **A payment processor account with hosted checkout and webhooks** · free to set up; per-transaction fees on real sales - Why: Money never touches your code, which is the single biggest risk reduction available in this build. - Get it: Create the account, enable hosted checkout, and register a webhook endpoint with its signing secret. - Verify: A test payment produces a verified event at your endpoint - [ ] **Object storage that can sign time-limited URLs** · pennies per month at creator volume - Why: Delivery depends on short-lived signed URLs, and storage that cannot mint them forces you into a worse design. - Get it: Any provider with a signed URL feature and range request support. - Verify: A signed URL expires on schedule and a range request resumes a partial download - [ ] **A test page on a domain you do not control** · free - Why: The embed's whole job is to work as a guest, and it will behave differently on your own domain. - Get it: Any free static host, with a deliberately strict content security policy and aggressive global CSS. - Verify: The embed works there with third-party cookies blocked - [ ] **Your tax position, decided before the first sale** · free to research; possibly not free to comply - Why: Selling digital goods across borders creates obligations that begin at the first transaction, not when you notice. - Get it: Find out what registration your sales require where your buyers are, and write down what you concluded and when. - Verify: The README states the position and the date it was checked ## 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: - Merchant of record tax remittance. - A buyer marketplace. - Physical inventory and shipping. - Fraud scoring. - commerce integrations, inventory, channels, mobile apps, tax, and support - marketplace discovery - tax remittance - fraud operations - advanced course and community tooling If one of those is essential to you, that is the reason to keep paying for Ecwid, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Ecwid 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 a small catalogue and checkout you can paste into a website you do not control to replace Ecwid. 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. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages · the embed is a client of your own public API and should not be a special case - Embedding: One small script that renders into a shadow root and keeps its cart token in the host page's own storage · browsers partition or block third-party cookies by default, so a cookie set from your domain inside their page is not a plan - Storage: PostgreSQL for orders and entitlements, object storage for the files people bought · orders are money and belong in something with real transactions - Payments: Hosted checkout on the processor's own page, fulfilment driven by webhooks · the redirect back is a courtesy, not a guarantee - Delivery: Short-lived signed URLs with resumable range requests · a large file over a hotel connection is the normal customer, not the edge case ### Data model (create this before Phase 1) - products(id, slug, title, description, price_minor, currency, state, kind) · kind is file or lesson set - variants(id, product_id, name, price_delta_minor, stock) · stock may be null for digital goods, and null is not zero - files(id, product_id, storage_key, filename, bytes, sha256) and lessons(id, product_id, position, title, body) - carts(token, created_at, last_seen_at, items_json) · the token lives in the host page's storage and is opaque - orders(id, processor_session_id, processor_event_id, email, account_ref, amount_minor, currency, state, created_at) · unique on the processor event that created it - entitlements(id, order_id, product_id, granted_at, revoked_at, reason) · granting and revoking are rows, never a boolean edit - downloads(id, entitlement_id, at, bytes_sent, completed, ip_hash) · enough to spot sharing without keeping a log of people ### Phase 1 · A guest on somebody else's page Build: A script that mounts the catalogue into a shadow root so neither side's styles leak. Hold the cart token in the host page's own storage and send it explicitly on every API call. Keep the script small, load nothing else from a third party, and document the content security policy entries a host site needs to add. Trap: Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. Done when: The embed works end to end in a browser with third-party cookies blocked, tested explicitly. Host page styles do not affect the widget and widget styles do not affect the host page, tested against a page with aggressive global CSS. The script loads no third-party resources and its size is stated and enforced in the build. The required content security policy entries are documented and verified against a host page that sets a strict policy. Do not build yet: No checkout, delivery, orders or refunds yet. ### Phase 2 · A product is an address Build: Serve a server-rendered page per product on your own domain, with the full description, price and structured metadata, and treat it as the canonical address. Have the embed update the host page's address as the visitor moves through the catalogue, and restore state from that address on load. Link from the embed to the canonical page. Trap: Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. Done when: Every product has a server-rendered page returning full content without JavaScript, tested by fetching it with scripting disabled. Navigating the embed updates the host page address and a reload restores the same view, tested. Sharing a copied address opens the same product for a visitor who has never seen the site. The canonical link on the embed points at the product page on your domain. Do not build yet: No payment, entitlements or delivery yet. ### Phase 3 · Access comes from the event, not the page Build: Send the buyer to the processor's hosted checkout. Write one fulfilment function that takes a session identifier, is safe to run repeatedly and concurrently, retrieves the session, checks the payment status and grants entitlements. Call it from the webhook handler and also from the landing page, and rely on the webhook for correctness. Trap: Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. Done when: A payment completed with the browser closed still grants access, tested by discarding the redirect. Calling the fulfilment function twenty times concurrently for one session produces exactly one set of entitlements, tested. A refund or chargeback event writes a revocation row and access stops immediately, tested. Every order stores the processor event that created it and the raw payload. Do not build yet: No download delivery, order management or exports yet. ### Phase 4 · Delivering the thing they bought Build: Serve files through short-lived signed URLs minted per request against a live entitlement check. Support range requests so an interrupted download resumes rather than restarting. Record each download attempt with bytes sent and whether it completed. Lessons render as pages behind the same entitlement check rather than as unprotected files. Trap: Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. Done when: A signed URL expires within the configured window and a revoked entitlement stops minting new ones immediately, tested. An interrupted download of a large file resumes from where it stopped, tested with a truncated connection. Every download attempt is recorded with bytes sent and completion. Lesson content is unreachable without an entitlement, tested by requesting it directly. Do not build yet: No order management, tax handling or exports yet. ### Phase 5 · Running a shop by yourself Build: Order search by email, product and date, receipt resend, a manual grant and revoke with a recorded reason, and an append-only event log for every order. Export orders, entitlements and the catalogue as CSV and JSON. State plainly in the interface and the README that tax is the seller's responsibility and that this build calculates none. Trap: Adding a tax percentage field because it looks easy produces a shop that computes a confident wrong number for every cross-border sale, and the person who finds out is you, later, with interest. Calculate nothing, record the amounts the processor reported, and say in the interface that registration and remittance are yours to sort out before your first sale. Done when: An order is findable by email, product or date within a few seconds on a realistic dataset. Manual grants and revocations are recorded with a reason and appear in the order's event log. No tax figure is computed anywhere in the codebase, verified by review, and the responsibility is stated in the interface. Orders, entitlements and catalogue export as CSV and JSON and reimport into an empty instance. Do not build yet: Nothing after this. ### Out of scope (and why) - Acting as merchant of record and remitting tax, which is a registration and a filing obligation in every jurisdiction you sell into rather than a code feature. - A buyer-facing marketplace, which is where the paid product's discovery comes from and is worthless with one shop in it. - Physical inventory, shipping rates and channel synchronisation, which is a different product with different failure modes. - Fraud scoring, which needs a volume of transactions one creator will never have and is dangerous when guessed at. ### README must contain - That browsers partition or block third-party cookies by default, and that the cart token therefore lives in the host page's own storage - The exact content security policy entries a host site needs, and what happens without them - That fulfilment runs from the payment event and that the success page is only a shortcut, because buyers do not always come back - That download links are short lived and minted per request against a live entitlement - That tax registration and remittance are the seller's responsibility and that this build calculates none ===== AGENTS.md ===== # Agent instructions · Ecwid indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages, One small script that renders into a shadow root and keeps its cart token in the host page's own storage, PostgreSQL for orders and entitlements, object storage for the files people bought, Hosted checkout on the processor's own page, fulfilment driven by webhooks, Short-lived signed URLs with resumable range requests. 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 - Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. - Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. - Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. - Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. ===== BUILD_PLAN.md ===== # Build plan · Ecwid A single-creator storefront whose hard problems are behaving as a guest on somebody else's page without depending on cookies you are not allowed to set, making a product a real linkable address rather than a state inside a widget, and granting access from payment events rather than from a browser arriving at a page. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A guest on somebody else's page The embed renders correctly on a site you do not control and never depends on a cookie you cannot set. ### Steps 1. Mount into a shadow root Neither side should be able to break the other's layout. Files: `src/embed/mount.ts`, `tests/isolation.test.ts` 2. Keep the cart token in the host page's storage It is the only storage you are reliably allowed to use there. Files: `src/embed/cart.ts`, `tests/no-cookie.test.ts` 3. Publish the policy entries a host site must add Otherwise your first customer's site silently blocks you. Files: `docs/embedding.md` ### Done when - [ ] The embed works with third-party cookies blocked - [ ] Styles do not leak in either direction - [ ] No third-party resource is loaded by the script - [ ] Required policy entries are documented and tested ### Watch out - Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. ## Phase 2 · A product is an address Every product has a real URL that can be shared, crawled and linked, not just a state inside a widget. ### Steps 1. Server render a canonical page per product It is the thing people can send to each other. Files: `src/app/p/[slug]/page.tsx` 2. Mirror catalogue state into the host page address A back button that does nothing is a bug report you will get. Files: `src/embed/router.ts` 3. Link the embed to the canonical page One address per product, everywhere. Files: `src/embed/product.ts` ### Done when - [ ] Product pages render fully without JavaScript - [ ] Navigating updates the address and reload restores state - [ ] A shared address opens the right product for a stranger - [ ] The embed links to the canonical page ### Watch out - Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. ## Phase 3 · Access comes from the event, not the page A payment grants access exactly once, whether or not the buyer's browser ever came back. ### Steps 1. One fulfilment function, safe to run twice It will be called from two places and retried by a third. Files: `src/fulfil.ts`, `tests/fulfil-idempotent.test.ts` 2. Drive it from the webhook, not the redirect The browser is the least reliable participant in the payment. Files: `src/webhooks.ts` 3. Revoke by writing a row An edited boolean cannot answer 'when did they lose access'. Files: `src/entitlements.ts` ### Done when - [ ] A payment with no redirect still grants access - [ ] Concurrent fulfilment produces one set of entitlements - [ ] A refund revokes access immediately - [ ] Orders retain their processor event and payload ### Watch out - Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. ## Phase 4 · Delivering the thing they bought A large file downloads over a bad connection, and access ends when entitlement does. ### Steps 1. Mint short-lived URLs per request A long-lived link is a product you gave away. Files: `src/delivery/sign.ts`, `tests/delivery.test.ts` 2. Support range requests for resume Large files and hotel wifi are the normal case. Files: `src/delivery/stream.ts` 3. Record attempts, not just successes A pile of incomplete downloads is a support ticket waiting. Files: `src/delivery/log.ts` ### Done when - [ ] Signed URLs expire and revocation stops new ones - [ ] An interrupted large download resumes - [ ] Download attempts record bytes and completion - [ ] Lesson content requires an entitlement ### Watch out - Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. ## Not in this build - Merchant of record tax remittance. - A buyer marketplace. - Physical inventory and shipping. - Fraud scoring. ## After v1, if you want it - Discount codes with a use limit, which is mostly a concurrency problem and worth doing carefully - A customer library page with its own sign-in link, which reduces the number of support emails asking for a download - A second processor behind the same fulfilment function, which is the real test of whether the event-driven design held ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Orders and entitlements. This is the money record; back it up. DATABASE_URL=postgres://user:pass@localhost/shop # Required. Where purchased files live. Never public. STORAGE_BUCKET=creator-files # Required · secret. Used only to mint signed URLs on the server. STORAGE_KEY=AKIA...redacted # Required · secret. Creates checkout sessions and retrieves them during fulfilment. PROCESSOR_SECRET_KEY=sk_live_...redacted # Required · secret. Verifies every event before it can grant anything. PROCESSOR_WEBHOOK_SECRET=whsec_...redacted # Required. How long a download link lives. Short, and minted per request. SIGNED_URL_TTL_SECONDS=300 # Required. Canonical product pages, the embed script and the checkout return address. PUBLIC_BASE_URL=https://shop.example.com
You are building a production product version of Ecwid. 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 ===== # Ecwid · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Ecwid, embed a small catalog and hosted checkout into an existing site. The hard boundary is commerce integrations, inventory, channels, mobile apps, tax, and support, plus payments, tax, distribution, and operations. ## Product outcome A storefront that works as a guest on a stranger's page with third-party cookies blocked, gives every product a real address, grants access from the payment event whether or not the buyer came back, and stops delivering the moment a refund arrives. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Stripe account - PostgreSQL - S3-compatible storage - public HTTPS deployment - email provider ## Explicit non-goals for v1 - Merchant of record tax remittance. - A buyer marketplace. - Physical inventory and shipping. - Fraud scoring. - commerce integrations, inventory, channels, mobile apps, tax, and support - marketplace discovery - tax remittance - fraud operations - advanced course and community tooling ## Success criteria - The embed is confirmed working with third-party cookies blocked - Every product is confirmed to render without JavaScript at its own address - A payment with no redirect is confirmed to grant access - Concurrent fulfilment is confirmed to grant exactly once - A refund is confirmed to stop new signed URLs immediately ===== BRIEF.md ===== # Build brief · Ecwid 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 a small catalogue and checkout you can paste into a website you do not control to replace Ecwid. 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. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages · the embed is a client of your own public API and should not be a special case - Embedding: One small script that renders into a shadow root and keeps its cart token in the host page's own storage · browsers partition or block third-party cookies by default, so a cookie set from your domain inside their page is not a plan - Storage: PostgreSQL for orders and entitlements, object storage for the files people bought · orders are money and belong in something with real transactions - Payments: Hosted checkout on the processor's own page, fulfilment driven by webhooks · the redirect back is a courtesy, not a guarantee - Delivery: Short-lived signed URLs with resumable range requests · a large file over a hotel connection is the normal customer, not the edge case ### Data model (create this before Phase 1) - products(id, slug, title, description, price_minor, currency, state, kind) · kind is file or lesson set - variants(id, product_id, name, price_delta_minor, stock) · stock may be null for digital goods, and null is not zero - files(id, product_id, storage_key, filename, bytes, sha256) and lessons(id, product_id, position, title, body) - carts(token, created_at, last_seen_at, items_json) · the token lives in the host page's storage and is opaque - orders(id, processor_session_id, processor_event_id, email, account_ref, amount_minor, currency, state, created_at) · unique on the processor event that created it - entitlements(id, order_id, product_id, granted_at, revoked_at, reason) · granting and revoking are rows, never a boolean edit - downloads(id, entitlement_id, at, bytes_sent, completed, ip_hash) · enough to spot sharing without keeping a log of people ### Phase 1 · A guest on somebody else's page Build: A script that mounts the catalogue into a shadow root so neither side's styles leak. Hold the cart token in the host page's own storage and send it explicitly on every API call. Keep the script small, load nothing else from a third party, and document the content security policy entries a host site needs to add. Trap: Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. Done when: The embed works end to end in a browser with third-party cookies blocked, tested explicitly. Host page styles do not affect the widget and widget styles do not affect the host page, tested against a page with aggressive global CSS. The script loads no third-party resources and its size is stated and enforced in the build. The required content security policy entries are documented and verified against a host page that sets a strict policy. Do not build yet: No checkout, delivery, orders or refunds yet. ### Phase 2 · A product is an address Build: Serve a server-rendered page per product on your own domain, with the full description, price and structured metadata, and treat it as the canonical address. Have the embed update the host page's address as the visitor moves through the catalogue, and restore state from that address on load. Link from the embed to the canonical page. Trap: Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. Done when: Every product has a server-rendered page returning full content without JavaScript, tested by fetching it with scripting disabled. Navigating the embed updates the host page address and a reload restores the same view, tested. Sharing a copied address opens the same product for a visitor who has never seen the site. The canonical link on the embed points at the product page on your domain. Do not build yet: No payment, entitlements or delivery yet. ### Phase 3 · Access comes from the event, not the page Build: Send the buyer to the processor's hosted checkout. Write one fulfilment function that takes a session identifier, is safe to run repeatedly and concurrently, retrieves the session, checks the payment status and grants entitlements. Call it from the webhook handler and also from the landing page, and rely on the webhook for correctness. Trap: Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. Done when: A payment completed with the browser closed still grants access, tested by discarding the redirect. Calling the fulfilment function twenty times concurrently for one session produces exactly one set of entitlements, tested. A refund or chargeback event writes a revocation row and access stops immediately, tested. Every order stores the processor event that created it and the raw payload. Do not build yet: No download delivery, order management or exports yet. ### Phase 4 · Delivering the thing they bought Build: Serve files through short-lived signed URLs minted per request against a live entitlement check. Support range requests so an interrupted download resumes rather than restarting. Record each download attempt with bytes sent and whether it completed. Lessons render as pages behind the same entitlement check rather than as unprotected files. Trap: Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. Done when: A signed URL expires within the configured window and a revoked entitlement stops minting new ones immediately, tested. An interrupted download of a large file resumes from where it stopped, tested with a truncated connection. Every download attempt is recorded with bytes sent and completion. Lesson content is unreachable without an entitlement, tested by requesting it directly. Do not build yet: No order management, tax handling or exports yet. ### Phase 5 · Running a shop by yourself Build: Order search by email, product and date, receipt resend, a manual grant and revoke with a recorded reason, and an append-only event log for every order. Export orders, entitlements and the catalogue as CSV and JSON. State plainly in the interface and the README that tax is the seller's responsibility and that this build calculates none. Trap: Adding a tax percentage field because it looks easy produces a shop that computes a confident wrong number for every cross-border sale, and the person who finds out is you, later, with interest. Calculate nothing, record the amounts the processor reported, and say in the interface that registration and remittance are yours to sort out before your first sale. Done when: An order is findable by email, product or date within a few seconds on a realistic dataset. Manual grants and revocations are recorded with a reason and appear in the order's event log. No tax figure is computed anywhere in the codebase, verified by review, and the responsibility is stated in the interface. Orders, entitlements and catalogue export as CSV and JSON and reimport into an empty instance. Do not build yet: Nothing after this. ### Out of scope (and why) - Acting as merchant of record and remitting tax, which is a registration and a filing obligation in every jurisdiction you sell into rather than a code feature. - A buyer-facing marketplace, which is where the paid product's discovery comes from and is worthless with one shop in it. - Physical inventory, shipping rates and channel synchronisation, which is a different product with different failure modes. - Fraud scoring, which needs a volume of transactions one creator will never have and is dangerous when guessed at. ### README must contain - That browsers partition or block third-party cookies by default, and that the cart token therefore lives in the host page's own storage - The exact content security policy entries a host site needs, and what happens without them - That fulfilment runs from the payment event and that the success page is only a shortcut, because buyers do not always come back - That download links are short lived and minted per request against a live entitlement - That tax registration and remittance are the seller's responsibility and that this build calculates none ===== ARCHITECTURE.md ===== # Architecture · Ecwid ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages | the embed is a client of your own public API and should not be a special case | | Embedding | One small script that renders into a shadow root and keeps its cart token in the host page's own storage | browsers partition or block third-party cookies by default, so a cookie set from your domain inside their page is not a plan | | Storage | PostgreSQL for orders and entitlements, object storage for the files people bought | orders are money and belong in something with real transactions | | Payments | Hosted checkout on the processor's own page, fulfilment driven by webhooks | the redirect back is a courtesy, not a guarantee | | Delivery | Short-lived signed URLs with resumable range requests | a large file over a hotel connection is the normal customer, not the edge case | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Embed | Isolation, cart token, host page integration. | Any renderer, provided it never depends on a cookie from your domain. | | Catalogue | Canonical product pages and metadata. | Any renderer, provided every product keeps an address. | | Fulfilment | The idempotent grant and the webhook handler. | Any processor, provided the event is the authority. | | Delivery | Signed URLs, ranges, download records. | Any storage, provided links are short lived and entitlement checked. | | Admin | Order search, manual grants, exports. | Any interface, provided every action is logged with a reason. | ## 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 · Orders and entitlements. This is the money record; back it up. - `STORAGE_BUCKET` · required · Where purchased files live. Never public. - `STORAGE_KEY` · required, secret · Used only to mint signed URLs on the server. - `PROCESSOR_SECRET_KEY` · required, secret · Creates checkout sessions and retrieves them during fulfilment. - `PROCESSOR_WEBHOOK_SECRET` · required, secret · Verifies every event before it can grant anything. - `SIGNED_URL_TTL_SECONDS` · required · How long a download link lives. Short, and minted per request. - `PUBLIC_BASE_URL` · required · Canonical product pages, the embed script and the checkout return address. ## 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 · Ecwid product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages, One small script that renders into a shadow root and keeps its cart token in the host page's own storage, PostgreSQL for orders and entitlements, object storage for the files people bought, Hosted checkout on the processor's own page, fulfilment driven by webhooks, Short-lived signed URLs with resumable range requests. - 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 - Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. - Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. - Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. - Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. - Adding a tax percentage field because it looks easy produces a shop that computes a confident wrong number for every cross-border sale, and the person who finds out is you, later, with interest. Calculate nothing, record the amounts the processor reported, and say in the interface that registration and remittance are yours to sort out before your first sale. ===== MILESTONES.md ===== # Delivery milestones · Ecwid Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A guest on somebody else's page The embed renders correctly on a site you do not control and never depends on a cookie you cannot set. ### Steps 1. Mount into a shadow root Neither side should be able to break the other's layout. Files: `src/embed/mount.ts`, `tests/isolation.test.ts` 2. Keep the cart token in the host page's storage It is the only storage you are reliably allowed to use there. Files: `src/embed/cart.ts`, `tests/no-cookie.test.ts` 3. Publish the policy entries a host site must add Otherwise your first customer's site silently blocks you. Files: `docs/embedding.md` ### Done when - [ ] The embed works with third-party cookies blocked - [ ] Styles do not leak in either direction - [ ] No third-party resource is loaded by the script - [ ] Required policy entries are documented and tested ### Watch out - Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. ## M2 · A product is an address Every product has a real URL that can be shared, crawled and linked, not just a state inside a widget. ### Steps 1. Server render a canonical page per product It is the thing people can send to each other. Files: `src/app/p/[slug]/page.tsx` 2. Mirror catalogue state into the host page address A back button that does nothing is a bug report you will get. Files: `src/embed/router.ts` 3. Link the embed to the canonical page One address per product, everywhere. Files: `src/embed/product.ts` ### Done when - [ ] Product pages render fully without JavaScript - [ ] Navigating updates the address and reload restores state - [ ] A shared address opens the right product for a stranger - [ ] The embed links to the canonical page ### Watch out - Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. ## M3 · Access comes from the event, not the page A payment grants access exactly once, whether or not the buyer's browser ever came back. ### Steps 1. One fulfilment function, safe to run twice It will be called from two places and retried by a third. Files: `src/fulfil.ts`, `tests/fulfil-idempotent.test.ts` 2. Drive it from the webhook, not the redirect The browser is the least reliable participant in the payment. Files: `src/webhooks.ts` 3. Revoke by writing a row An edited boolean cannot answer 'when did they lose access'. Files: `src/entitlements.ts` ### Done when - [ ] A payment with no redirect still grants access - [ ] Concurrent fulfilment produces one set of entitlements - [ ] A refund revokes access immediately - [ ] Orders retain their processor event and payload ### Watch out - Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. ## M4 · Delivering the thing they bought A large file downloads over a bad connection, and access ends when entitlement does. ### Steps 1. Mint short-lived URLs per request A long-lived link is a product you gave away. Files: `src/delivery/sign.ts`, `tests/delivery.test.ts` 2. Support range requests for resume Large files and hotel wifi are the normal case. Files: `src/delivery/stream.ts` 3. Record attempts, not just successes A pile of incomplete downloads is a support ticket waiting. Files: `src/delivery/log.ts` ### Done when - [ ] Signed URLs expire and revocation stops new ones - [ ] An interrupted large download resumes - [ ] Download attempts record bytes and completion - [ ] Lesson content requires an entitlement ### Watch out - Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. ## M5 · Running a shop by yourself (production only) You can answer a customer, find an order, and get everything out, at eleven at night. ### Steps 1. Make order search fast and boring It is the only screen you will use under pressure. Files: `src/admin/orders.tsx` 2. Log every grant and revoke with a reason Six months later you will be asked why. Files: `src/entitlements.ts` 3. Compute no tax, and say so where it matters A wrong calculation is worse than an honest absence. Files: `src/admin/orders.tsx`, `README.md` ### Done when - [ ] Order search is fast on a realistic dataset - [ ] Manual grants and revokes carry recorded reasons - [ ] No tax figure is computed anywhere - [ ] Everything exports and reimports ### Watch out - Adding a tax percentage field because it looks easy produces a shop that computes a confident wrong number for every cross-border sale, and the person who finds out is you, later, with interest. Calculate nothing, record the amounts the processor reported, and say in the interface that registration and remittance are yours to sort out before your first sale. ===== OPERATIONS.md ===== # Operations · Ecwid ## Backup The database nightly and offsite, plus a copy of the files people bought. Orders and entitlements are the record of what somebody paid for, and rebuilding them from processor history is possible but not something you want to do at speed. ## Restore Restore the database, confirm order and entitlement counts against the processor's own totals for the period, then confirm one signed URL mints for a known entitlement. A restore that recovers orders but not entitlements looks fine and delivers nothing. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Webhook failures and retries, fulfilment functions that ran more than once, incomplete downloads as a share of attempts, signed URL errors, and refunds followed by a successful download which should never happen. ## Incident checklist The incidents are a buyer who paid and got nothing, and a refunded buyer who kept downloading. The first is caught by replaying the event, the second by the revocation test, and both need the order event log to explain afterwards. 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 - [ ] The embed is confirmed working with third-party cookies blocked - [ ] Every product is confirmed to render without JavaScript at its own address - [ ] A payment with no redirect is confirmed to grant access - [ ] Concurrent fulfilment is confirmed to grant exactly once - [ ] A refund is confirmed to stop new signed URLs immediately ## Launch constraint Do not market omitted Ecwid 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. Orders and entitlements. This is the money record; back it up. DATABASE_URL=postgres://user:pass@localhost/shop # Required. Where purchased files live. Never public. STORAGE_BUCKET=creator-files # Required · secret. Used only to mint signed URLs on the server. STORAGE_KEY=AKIA...redacted # Required · secret. Creates checkout sessions and retrieves them during fulfilment. PROCESSOR_SECRET_KEY=sk_live_...redacted # Required · secret. Verifies every event before it can grant anything. PROCESSOR_WEBHOOK_SECRET=whsec_...redacted # Required. How long a download link lives. Short, and minted per request. SIGNED_URL_TTL_SECONDS=300 # Required. Canonical product pages, the embed script and the checkout return address. PUBLIC_BASE_URL=https://shop.example.com
# Ecwid · indie build A single-creator storefront whose hard problems are behaving as a guest on somebody else's page without depending on cookies you are not allowed to set, making a product a real linkable address rather than a state inside a widget, and granting access from payment events rather than from a browser arriving at a page. 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 | | --- | --- | --- | | Runtime | Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages | the embed is a client of your own public API and should not be a special case | | Embedding | One small script that renders into a shadow root and keeps its cart token in the host page's own storage | browsers partition or block third-party cookies by default, so a cookie set from your domain inside their page is not a plan | | Storage | PostgreSQL for orders and entitlements, object storage for the files people bought | orders are money and belong in something with real transactions | | Payments | Hosted checkout on the processor's own page, fulfilment driven by webhooks | the redirect back is a courtesy, not a guarantee | | Delivery | Short-lived signed URLs with resumable range requests | a large file over a hotel connection is the normal customer, not the edge case | ## 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 storefront, the API and the embed build. - Get it: nodejs.org or your version manager. - Verify: node --version prints v22 or higher - [ ] **A payment processor account with hosted checkout and webhooks** · free to set up; per-transaction fees on real sales - Why: Money never touches your code, which is the single biggest risk reduction available in this build. - Get it: Create the account, enable hosted checkout, and register a webhook endpoint with its signing secret. - Verify: A test payment produces a verified event at your endpoint - [ ] **Object storage that can sign time-limited URLs** · pennies per month at creator volume - Why: Delivery depends on short-lived signed URLs, and storage that cannot mint them forces you into a worse design. - Get it: Any provider with a signed URL feature and range request support. - Verify: A signed URL expires on schedule and a range request resumes a partial download - [ ] **A test page on a domain you do not control** · free - Why: The embed's whole job is to work as a guest, and it will behave differently on your own domain. - Get it: Any free static host, with a deliberately strict content security policy and aggressive global CSS. - Verify: The embed works there with third-party cookies blocked - [ ] **Your tax position, decided before the first sale** · free to research; possibly not free to comply - Why: Selling digital goods across borders creates obligations that begin at the first transaction, not when you notice. - Get it: Find out what registration your sales require where your buyers are, and write down what you concluded and when. - Verify: The README states the position and the date it was checked ## 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: - Merchant of record tax remittance. - A buyer marketplace. - Physical inventory and shipping. - Fraud scoring. - commerce integrations, inventory, channels, mobile apps, tax, and support - marketplace discovery - tax remittance - fraud operations - advanced course and community tooling If one of those is essential to you, that is the reason to keep paying for Ecwid, and the README should say so rather than pretend.
# Build brief · Ecwid 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 a small catalogue and checkout you can paste into a website you do not control to replace Ecwid. 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. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages · the embed is a client of your own public API and should not be a special case - Embedding: One small script that renders into a shadow root and keeps its cart token in the host page's own storage · browsers partition or block third-party cookies by default, so a cookie set from your domain inside their page is not a plan - Storage: PostgreSQL for orders and entitlements, object storage for the files people bought · orders are money and belong in something with real transactions - Payments: Hosted checkout on the processor's own page, fulfilment driven by webhooks · the redirect back is a courtesy, not a guarantee - Delivery: Short-lived signed URLs with resumable range requests · a large file over a hotel connection is the normal customer, not the edge case ### Data model (create this before Phase 1) - products(id, slug, title, description, price_minor, currency, state, kind) · kind is file or lesson set - variants(id, product_id, name, price_delta_minor, stock) · stock may be null for digital goods, and null is not zero - files(id, product_id, storage_key, filename, bytes, sha256) and lessons(id, product_id, position, title, body) - carts(token, created_at, last_seen_at, items_json) · the token lives in the host page's storage and is opaque - orders(id, processor_session_id, processor_event_id, email, account_ref, amount_minor, currency, state, created_at) · unique on the processor event that created it - entitlements(id, order_id, product_id, granted_at, revoked_at, reason) · granting and revoking are rows, never a boolean edit - downloads(id, entitlement_id, at, bytes_sent, completed, ip_hash) · enough to spot sharing without keeping a log of people ### Phase 1 · A guest on somebody else's page Build: A script that mounts the catalogue into a shadow root so neither side's styles leak. Hold the cart token in the host page's own storage and send it explicitly on every API call. Keep the script small, load nothing else from a third party, and document the content security policy entries a host site needs to add. Trap: Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. Done when: The embed works end to end in a browser with third-party cookies blocked, tested explicitly. Host page styles do not affect the widget and widget styles do not affect the host page, tested against a page with aggressive global CSS. The script loads no third-party resources and its size is stated and enforced in the build. The required content security policy entries are documented and verified against a host page that sets a strict policy. Do not build yet: No checkout, delivery, orders or refunds yet. ### Phase 2 · A product is an address Build: Serve a server-rendered page per product on your own domain, with the full description, price and structured metadata, and treat it as the canonical address. Have the embed update the host page's address as the visitor moves through the catalogue, and restore state from that address on load. Link from the embed to the canonical page. Trap: Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. Done when: Every product has a server-rendered page returning full content without JavaScript, tested by fetching it with scripting disabled. Navigating the embed updates the host page address and a reload restores the same view, tested. Sharing a copied address opens the same product for a visitor who has never seen the site. The canonical link on the embed points at the product page on your domain. Do not build yet: No payment, entitlements or delivery yet. ### Phase 3 · Access comes from the event, not the page Build: Send the buyer to the processor's hosted checkout. Write one fulfilment function that takes a session identifier, is safe to run repeatedly and concurrently, retrieves the session, checks the payment status and grants entitlements. Call it from the webhook handler and also from the landing page, and rely on the webhook for correctness. Trap: Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. Done when: A payment completed with the browser closed still grants access, tested by discarding the redirect. Calling the fulfilment function twenty times concurrently for one session produces exactly one set of entitlements, tested. A refund or chargeback event writes a revocation row and access stops immediately, tested. Every order stores the processor event that created it and the raw payload. Do not build yet: No download delivery, order management or exports yet. ### Phase 4 · Delivering the thing they bought Build: Serve files through short-lived signed URLs minted per request against a live entitlement check. Support range requests so an interrupted download resumes rather than restarting. Record each download attempt with bytes sent and whether it completed. Lessons render as pages behind the same entitlement check rather than as unprotected files. Trap: Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. Done when: A signed URL expires within the configured window and a revoked entitlement stops minting new ones immediately, tested. An interrupted download of a large file resumes from where it stopped, tested with a truncated connection. Every download attempt is recorded with bytes sent and completion. Lesson content is unreachable without an entitlement, tested by requesting it directly. Do not build yet: No order management, tax handling or exports yet. ### Phase 5 · Running a shop by yourself Build: Order search by email, product and date, receipt resend, a manual grant and revoke with a recorded reason, and an append-only event log for every order. Export orders, entitlements and the catalogue as CSV and JSON. State plainly in the interface and the README that tax is the seller's responsibility and that this build calculates none. Trap: Adding a tax percentage field because it looks easy produces a shop that computes a confident wrong number for every cross-border sale, and the person who finds out is you, later, with interest. Calculate nothing, record the amounts the processor reported, and say in the interface that registration and remittance are yours to sort out before your first sale. Done when: An order is findable by email, product or date within a few seconds on a realistic dataset. Manual grants and revocations are recorded with a reason and appear in the order's event log. No tax figure is computed anywhere in the codebase, verified by review, and the responsibility is stated in the interface. Orders, entitlements and catalogue export as CSV and JSON and reimport into an empty instance. Do not build yet: Nothing after this. ### Out of scope (and why) - Acting as merchant of record and remitting tax, which is a registration and a filing obligation in every jurisdiction you sell into rather than a code feature. - A buyer-facing marketplace, which is where the paid product's discovery comes from and is worthless with one shop in it. - Physical inventory, shipping rates and channel synchronisation, which is a different product with different failure modes. - Fraud scoring, which needs a volume of transactions one creator will never have and is dangerous when guessed at. ### README must contain - That browsers partition or block third-party cookies by default, and that the cart token therefore lives in the host page's own storage - The exact content security policy entries a host site needs, and what happens without them - That fulfilment runs from the payment event and that the success page is only a shortcut, because buyers do not always come back - That download links are short lived and minted per request against a live entitlement - That tax registration and remittance are the seller's responsibility and that this build calculates none
# Agent instructions · Ecwid indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages, One small script that renders into a shadow root and keeps its cart token in the host page's own storage, PostgreSQL for orders and entitlements, object storage for the files people bought, Hosted checkout on the processor's own page, fulfilment driven by webhooks, Short-lived signed URLs with resumable range requests. 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 - Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. - Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. - Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. - Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it.
# Build plan · Ecwid A single-creator storefront whose hard problems are behaving as a guest on somebody else's page without depending on cookies you are not allowed to set, making a product a real linkable address rather than a state inside a widget, and granting access from payment events rather than from a browser arriving at a page. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A guest on somebody else's page The embed renders correctly on a site you do not control and never depends on a cookie you cannot set. ### Steps 1. Mount into a shadow root Neither side should be able to break the other's layout. Files: `src/embed/mount.ts`, `tests/isolation.test.ts` 2. Keep the cart token in the host page's storage It is the only storage you are reliably allowed to use there. Files: `src/embed/cart.ts`, `tests/no-cookie.test.ts` 3. Publish the policy entries a host site must add Otherwise your first customer's site silently blocks you. Files: `docs/embedding.md` ### Done when - [ ] The embed works with third-party cookies blocked - [ ] Styles do not leak in either direction - [ ] No third-party resource is loaded by the script - [ ] Required policy entries are documented and tested ### Watch out - Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. ## Phase 2 · A product is an address Every product has a real URL that can be shared, crawled and linked, not just a state inside a widget. ### Steps 1. Server render a canonical page per product It is the thing people can send to each other. Files: `src/app/p/[slug]/page.tsx` 2. Mirror catalogue state into the host page address A back button that does nothing is a bug report you will get. Files: `src/embed/router.ts` 3. Link the embed to the canonical page One address per product, everywhere. Files: `src/embed/product.ts` ### Done when - [ ] Product pages render fully without JavaScript - [ ] Navigating updates the address and reload restores state - [ ] A shared address opens the right product for a stranger - [ ] The embed links to the canonical page ### Watch out - Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. ## Phase 3 · Access comes from the event, not the page A payment grants access exactly once, whether or not the buyer's browser ever came back. ### Steps 1. One fulfilment function, safe to run twice It will be called from two places and retried by a third. Files: `src/fulfil.ts`, `tests/fulfil-idempotent.test.ts` 2. Drive it from the webhook, not the redirect The browser is the least reliable participant in the payment. Files: `src/webhooks.ts` 3. Revoke by writing a row An edited boolean cannot answer 'when did they lose access'. Files: `src/entitlements.ts` ### Done when - [ ] A payment with no redirect still grants access - [ ] Concurrent fulfilment produces one set of entitlements - [ ] A refund revokes access immediately - [ ] Orders retain their processor event and payload ### Watch out - Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. ## Phase 4 · Delivering the thing they bought A large file downloads over a bad connection, and access ends when entitlement does. ### Steps 1. Mint short-lived URLs per request A long-lived link is a product you gave away. Files: `src/delivery/sign.ts`, `tests/delivery.test.ts` 2. Support range requests for resume Large files and hotel wifi are the normal case. Files: `src/delivery/stream.ts` 3. Record attempts, not just successes A pile of incomplete downloads is a support ticket waiting. Files: `src/delivery/log.ts` ### Done when - [ ] Signed URLs expire and revocation stops new ones - [ ] An interrupted large download resumes - [ ] Download attempts record bytes and completion - [ ] Lesson content requires an entitlement ### Watch out - Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. ## Not in this build - Merchant of record tax remittance. - A buyer marketplace. - Physical inventory and shipping. - Fraud scoring. ## After v1, if you want it - Discount codes with a use limit, which is mostly a concurrency problem and worth doing carefully - A customer library page with its own sign-in link, which reduces the number of support emails asking for a download - A second processor behind the same fulfilment function, which is the real test of whether the event-driven design held
# Copy to .env and fill in. Never commit .env; this file documents it. # Required · secret. Orders and entitlements. This is the money record; back it up. DATABASE_URL=postgres://user:pass@localhost/shop # Required. Where purchased files live. Never public. STORAGE_BUCKET=creator-files # Required · secret. Used only to mint signed URLs on the server. STORAGE_KEY=AKIA...redacted # Required · secret. Creates checkout sessions and retrieves them during fulfilment. PROCESSOR_SECRET_KEY=sk_live_...redacted # Required · secret. Verifies every event before it can grant anything. PROCESSOR_WEBHOOK_SECRET=whsec_...redacted # Required. How long a download link lives. Short, and minted per request. SIGNED_URL_TTL_SECONDS=300 # Required. Canonical product pages, the embed script and the checkout return address. PUBLIC_BASE_URL=https://shop.example.com
# Ecwid · product brief ## Problem The core loop is buildable, but a dependable replacement becomes a real weekend or multi-day project. For Ecwid, embed a small catalog and hosted checkout into an existing site. The hard boundary is commerce integrations, inventory, channels, mobile apps, tax, and support, plus payments, tax, distribution, and operations. ## Product outcome A storefront that works as a guest on a stranger's page with third-party cookies blocked, gives every product a real address, grants access from the payment event whether or not the buyer came back, and stops delivering the moment a refund arrives. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Stripe account - PostgreSQL - S3-compatible storage - public HTTPS deployment - email provider ## Explicit non-goals for v1 - Merchant of record tax remittance. - A buyer marketplace. - Physical inventory and shipping. - Fraud scoring. - commerce integrations, inventory, channels, mobile apps, tax, and support - marketplace discovery - tax remittance - fraud operations - advanced course and community tooling ## Success criteria - The embed is confirmed working with third-party cookies blocked - Every product is confirmed to render without JavaScript at its own address - A payment with no redirect is confirmed to grant access - Concurrent fulfilment is confirmed to grant exactly once - A refund is confirmed to stop new signed URLs immediately
# Build brief · Ecwid 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 a small catalogue and checkout you can paste into a website you do not control to replace Ecwid. 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. ### Stack (fixed, do not substitute) - Runtime: Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages · the embed is a client of your own public API and should not be a special case - Embedding: One small script that renders into a shadow root and keeps its cart token in the host page's own storage · browsers partition or block third-party cookies by default, so a cookie set from your domain inside their page is not a plan - Storage: PostgreSQL for orders and entitlements, object storage for the files people bought · orders are money and belong in something with real transactions - Payments: Hosted checkout on the processor's own page, fulfilment driven by webhooks · the redirect back is a courtesy, not a guarantee - Delivery: Short-lived signed URLs with resumable range requests · a large file over a hotel connection is the normal customer, not the edge case ### Data model (create this before Phase 1) - products(id, slug, title, description, price_minor, currency, state, kind) · kind is file or lesson set - variants(id, product_id, name, price_delta_minor, stock) · stock may be null for digital goods, and null is not zero - files(id, product_id, storage_key, filename, bytes, sha256) and lessons(id, product_id, position, title, body) - carts(token, created_at, last_seen_at, items_json) · the token lives in the host page's storage and is opaque - orders(id, processor_session_id, processor_event_id, email, account_ref, amount_minor, currency, state, created_at) · unique on the processor event that created it - entitlements(id, order_id, product_id, granted_at, revoked_at, reason) · granting and revoking are rows, never a boolean edit - downloads(id, entitlement_id, at, bytes_sent, completed, ip_hash) · enough to spot sharing without keeping a log of people ### Phase 1 · A guest on somebody else's page Build: A script that mounts the catalogue into a shadow root so neither side's styles leak. Hold the cart token in the host page's own storage and send it explicitly on every API call. Keep the script small, load nothing else from a third party, and document the content security policy entries a host site needs to add. Trap: Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. Done when: The embed works end to end in a browser with third-party cookies blocked, tested explicitly. Host page styles do not affect the widget and widget styles do not affect the host page, tested against a page with aggressive global CSS. The script loads no third-party resources and its size is stated and enforced in the build. The required content security policy entries are documented and verified against a host page that sets a strict policy. Do not build yet: No checkout, delivery, orders or refunds yet. ### Phase 2 · A product is an address Build: Serve a server-rendered page per product on your own domain, with the full description, price and structured metadata, and treat it as the canonical address. Have the embed update the host page's address as the visitor moves through the catalogue, and restore state from that address on load. Link from the embed to the canonical page. Trap: Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. Done when: Every product has a server-rendered page returning full content without JavaScript, tested by fetching it with scripting disabled. Navigating the embed updates the host page address and a reload restores the same view, tested. Sharing a copied address opens the same product for a visitor who has never seen the site. The canonical link on the embed points at the product page on your domain. Do not build yet: No payment, entitlements or delivery yet. ### Phase 3 · Access comes from the event, not the page Build: Send the buyer to the processor's hosted checkout. Write one fulfilment function that takes a session identifier, is safe to run repeatedly and concurrently, retrieves the session, checks the payment status and grants entitlements. Call it from the webhook handler and also from the landing page, and rely on the webhook for correctness. Trap: Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. Done when: A payment completed with the browser closed still grants access, tested by discarding the redirect. Calling the fulfilment function twenty times concurrently for one session produces exactly one set of entitlements, tested. A refund or chargeback event writes a revocation row and access stops immediately, tested. Every order stores the processor event that created it and the raw payload. Do not build yet: No download delivery, order management or exports yet. ### Phase 4 · Delivering the thing they bought Build: Serve files through short-lived signed URLs minted per request against a live entitlement check. Support range requests so an interrupted download resumes rather than restarting. Record each download attempt with bytes sent and whether it completed. Lessons render as pages behind the same entitlement check rather than as unprotected files. Trap: Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. Done when: A signed URL expires within the configured window and a revoked entitlement stops minting new ones immediately, tested. An interrupted download of a large file resumes from where it stopped, tested with a truncated connection. Every download attempt is recorded with bytes sent and completion. Lesson content is unreachable without an entitlement, tested by requesting it directly. Do not build yet: No order management, tax handling or exports yet. ### Phase 5 · Running a shop by yourself Build: Order search by email, product and date, receipt resend, a manual grant and revoke with a recorded reason, and an append-only event log for every order. Export orders, entitlements and the catalogue as CSV and JSON. State plainly in the interface and the README that tax is the seller's responsibility and that this build calculates none. Trap: Adding a tax percentage field because it looks easy produces a shop that computes a confident wrong number for every cross-border sale, and the person who finds out is you, later, with interest. Calculate nothing, record the amounts the processor reported, and say in the interface that registration and remittance are yours to sort out before your first sale. Done when: An order is findable by email, product or date within a few seconds on a realistic dataset. Manual grants and revocations are recorded with a reason and appear in the order's event log. No tax figure is computed anywhere in the codebase, verified by review, and the responsibility is stated in the interface. Orders, entitlements and catalogue export as CSV and JSON and reimport into an empty instance. Do not build yet: Nothing after this. ### Out of scope (and why) - Acting as merchant of record and remitting tax, which is a registration and a filing obligation in every jurisdiction you sell into rather than a code feature. - A buyer-facing marketplace, which is where the paid product's discovery comes from and is worthless with one shop in it. - Physical inventory, shipping rates and channel synchronisation, which is a different product with different failure modes. - Fraud scoring, which needs a volume of transactions one creator will never have and is dangerous when guessed at. ### README must contain - That browsers partition or block third-party cookies by default, and that the cart token therefore lives in the host page's own storage - The exact content security policy entries a host site needs, and what happens without them - That fulfilment runs from the payment event and that the success page is only a shortcut, because buyers do not always come back - That download links are short lived and minted per request against a live entitlement - That tax registration and remittance are the seller's responsibility and that this build calculates none
# Architecture · Ecwid ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages | the embed is a client of your own public API and should not be a special case | | Embedding | One small script that renders into a shadow root and keeps its cart token in the host page's own storage | browsers partition or block third-party cookies by default, so a cookie set from your domain inside their page is not a plan | | Storage | PostgreSQL for orders and entitlements, object storage for the files people bought | orders are money and belong in something with real transactions | | Payments | Hosted checkout on the processor's own page, fulfilment driven by webhooks | the redirect back is a courtesy, not a guarantee | | Delivery | Short-lived signed URLs with resumable range requests | a large file over a hotel connection is the normal customer, not the edge case | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Embed | Isolation, cart token, host page integration. | Any renderer, provided it never depends on a cookie from your domain. | | Catalogue | Canonical product pages and metadata. | Any renderer, provided every product keeps an address. | | Fulfilment | The idempotent grant and the webhook handler. | Any processor, provided the event is the authority. | | Delivery | Signed URLs, ranges, download records. | Any storage, provided links are short lived and entitlement checked. | | Admin | Order search, manual grants, exports. | Any interface, provided every action is logged with a reason. | ## 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 · Orders and entitlements. This is the money record; back it up. - `STORAGE_BUCKET` · required · Where purchased files live. Never public. - `STORAGE_KEY` · required, secret · Used only to mint signed URLs on the server. - `PROCESSOR_SECRET_KEY` · required, secret · Creates checkout sessions and retrieves them during fulfilment. - `PROCESSOR_WEBHOOK_SECRET` · required, secret · Verifies every event before it can grant anything. - `SIGNED_URL_TTL_SECONDS` · required · How long a download link lives. Short, and minted per request. - `PUBLIC_BASE_URL` · required · Canonical product pages, the embed script and the checkout return address. ## 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 · Ecwid product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js with TypeScript, your own domain, serving both the embed script and the catalogue pages, One small script that renders into a shadow root and keeps its cart token in the host page's own storage, PostgreSQL for orders and entitlements, object storage for the files people bought, Hosted checkout on the processor's own page, fulfilment driven by webhooks, Short-lived signed URLs with resumable range requests. - 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 - Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. - Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. - Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. - Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. - Adding a tax percentage field because it looks easy produces a shop that computes a confident wrong number for every cross-border sale, and the person who finds out is you, later, with interest. Calculate nothing, record the amounts the processor reported, and say in the interface that registration and remittance are yours to sort out before your first sale.
# Delivery milestones · Ecwid Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A guest on somebody else's page The embed renders correctly on a site you do not control and never depends on a cookie you cannot set. ### Steps 1. Mount into a shadow root Neither side should be able to break the other's layout. Files: `src/embed/mount.ts`, `tests/isolation.test.ts` 2. Keep the cart token in the host page's storage It is the only storage you are reliably allowed to use there. Files: `src/embed/cart.ts`, `tests/no-cookie.test.ts` 3. Publish the policy entries a host site must add Otherwise your first customer's site silently blocks you. Files: `docs/embedding.md` ### Done when - [ ] The embed works with third-party cookies blocked - [ ] Styles do not leak in either direction - [ ] No third-party resource is loaded by the script - [ ] Required policy entries are documented and tested ### Watch out - Identifying a cart with a cookie set by your domain from inside the widget fails quietly on the browsers that partition or block third-party cookies by default, and access from inside a frame is only possible by asking through the storage access interface, so the shop appears to work for you and loses every cart for a large share of visitors. Keep the cart token in the host page's own storage, pass it as a parameter, and test in a browser with third-party cookies blocked. ## M2 · A product is an address Every product has a real URL that can be shared, crawled and linked, not just a state inside a widget. ### Steps 1. Server render a canonical page per product It is the thing people can send to each other. Files: `src/app/p/[slug]/page.tsx` 2. Mirror catalogue state into the host page address A back button that does nothing is a bug report you will get. Files: `src/embed/router.ts` 3. Link the embed to the canonical page One address per product, everywhere. Files: `src/embed/product.ts` ### Done when - [ ] Product pages render fully without JavaScript - [ ] Navigating updates the address and reload restores state - [ ] A shared address opens the right product for a stranger - [ ] The embed links to the canonical page ### Watch out - Rendering the whole catalogue from JavaScript inside somebody else's page means no product has an address, so nothing can be linked in a newsletter, shared in a message or found by a search engine, and the shop is invisible outside the one page it sits on. Give every product a server-rendered canonical page and mirror the selection into the address of the host page. ## M3 · Access comes from the event, not the page A payment grants access exactly once, whether or not the buyer's browser ever came back. ### Steps 1. One fulfilment function, safe to run twice It will be called from two places and retried by a third. Files: `src/fulfil.ts`, `tests/fulfil-idempotent.test.ts` 2. Drive it from the webhook, not the redirect The browser is the least reliable participant in the payment. Files: `src/webhooks.ts` 3. Revoke by writing a row An edited boolean cannot answer 'when did they lose access'. Files: `src/entitlements.ts` ### Done when - [ ] A payment with no redirect still grants access - [ ] Concurrent fulfilment produces one set of entitlements - [ ] A refund revokes access immediately - [ ] Orders retain their processor event and payload ### Watch out - Granting access on the success page means a buyer who paid and then lost their connection before the redirect never gets what they bought, and the processor's own documentation says that fulfilment cannot be triggered only from the landing page because customers are not guaranteed to visit it and that the fulfilment function may be called several times, possibly at once, for the same session. Drive fulfilment from the event, make the function safe to run twice, and treat the landing page as a shortcut for the impatient. ## M4 · Delivering the thing they bought A large file downloads over a bad connection, and access ends when entitlement does. ### Steps 1. Mint short-lived URLs per request A long-lived link is a product you gave away. Files: `src/delivery/sign.ts`, `tests/delivery.test.ts` 2. Support range requests for resume Large files and hotel wifi are the normal case. Files: `src/delivery/stream.ts` 3. Record attempts, not just successes A pile of incomplete downloads is a support ticket waiting. Files: `src/delivery/log.ts` ### Done when - [ ] Signed URLs expire and revocation stops new ones - [ ] An interrupted large download resumes - [ ] Download attempts record bytes and completion - [ ] Lesson content requires an entitlement ### Watch out - Minting a signed URL that lasts a week and emailing it means the link is the product: it gets forwarded, posted, and keeps working long after a refund, and there is nothing you can do about it because the signature is valid. Mint short-lived URLs per request behind an entitlement check, so revoking access actually revokes it. ## M5 · Running a shop by yourself (production only) You can answer a customer, find an order, and get everything out, at eleven at night. ### Steps 1. Make order search fast and boring It is the only screen you will use under pressure. Files: `src/admin/orders.tsx` 2. Log every grant and revoke with a reason Six months later you will be asked why. Files: `src/entitlements.ts` 3. Compute no tax, and say so where it matters A wrong calculation is worse than an honest absence. Files: `src/admin/orders.tsx`, `README.md` ### Done when - [ ] Order search is fast on a realistic dataset - [ ] Manual grants and revokes carry recorded reasons - [ ] No tax figure is computed anywhere - [ ] Everything exports and reimports ### Watch out - Adding a tax percentage field because it looks easy produces a shop that computes a confident wrong number for every cross-border sale, and the person who finds out is you, later, with interest. Calculate nothing, record the amounts the processor reported, and say in the interface that registration and remittance are yours to sort out before your first sale.
# Operations · Ecwid ## Backup The database nightly and offsite, plus a copy of the files people bought. Orders and entitlements are the record of what somebody paid for, and rebuilding them from processor history is possible but not something you want to do at speed. ## Restore Restore the database, confirm order and entitlement counts against the processor's own totals for the period, then confirm one signed URL mints for a known entitlement. A restore that recovers orders but not entitlements looks fine and delivers nothing. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Webhook failures and retries, fulfilment functions that ran more than once, incomplete downloads as a share of attempts, signed URL errors, and refunds followed by a successful download which should never happen. ## Incident checklist The incidents are a buyer who paid and got nothing, and a refunded buyer who kept downloading. The first is caught by replaying the event, the second by the revocation test, and both need the order event log to explain afterwards. 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 - [ ] The embed is confirmed working with third-party cookies blocked - [ ] Every product is confirmed to render without JavaScript at its own address - [ ] A payment with no redirect is confirmed to grant access - [ ] Concurrent fulfilment is confirmed to grant exactly once - [ ] A refund is confirmed to stop new signed URLs immediately ## Launch constraint Do not market omitted Ecwid 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. Orders and entitlements. This is the money record; back it up. DATABASE_URL=postgres://user:pass@localhost/shop # Required. Where purchased files live. Never public. STORAGE_BUCKET=creator-files # Required · secret. Used only to mint signed URLs on the server. STORAGE_KEY=AKIA...redacted # Required · secret. Creates checkout sessions and retrieves them during fulfilment. PROCESSOR_SECRET_KEY=sk_live_...redacted # Required · secret. Verifies every event before it can grant anything. PROCESSOR_WEBHOOK_SECRET=whsec_...redacted # Required. How long a download link lives. Short, and minted per request. SIGNED_URL_TTL_SECONDS=300 # Required. Canonical product pages, the embed script and the checkout return address. PUBLIC_BASE_URL=https://shop.example.com
$ choose a build depth, inspect the files, then open the complete pack in your agent
People still pay for Ecwid because creators pay to outsource the dangerous and tedious parts around money, delivery, tax, and customer access. The recurring cost buys checkout, webhooks, tax, refunds, fraud, entitlements, storage, email, accessibility, backups, and customer support, not just the visible interface.
xcommerce integrations, inventory, channels, mobile apps, tax, and support
xmarketplace discovery
xtax remittance
xfraud operations
xadvanced course and community tooling
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Ecwid pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| starter | $5/workspace | $5/workspace | Up to 10 products and email support. |
| venture | $35/workspace | $29/workspace | Up to 100 products; 3 additional Instant Site pages; digital files up to 25 GB each. |
| business | $65/workspace | $49/workspace | Up to 2,500 products; 2 staff accounts; 3 active promotions; 10 additional Instant Site pages. |
| unlimited | $149/workspace | $119/workspace | Unlimited products and staff; up to 500 active promotions and 100 additional Instant Site pages. |
free tierno free tier
billingmonthly + annual (up to about -20%)
hidden costsEcwid charges 0 platform transaction fees, but payment processors, paid App Market integrations, domains and compatible POS hardware/services cost extra; app-store subscription prices can differ from web pricing.
price historyVenture monthly: $25 → $35 (2026-03) · Business monthly: $45 → $65 (2026-03) · Unlimited monthly: $105 → $149 (2026-03)
verified 2026-08-14 · source ↗
Vibecode Ecwid
Kinda. The core of Ecwid is buildable in a weekend with the prompt on this page, but there are real gaps: commerce integrations, inventory, channels, mobile apps, tax, and support, marketplace discovery. Read the honest list above before committing.
How much does Ecwid cost?
Ecwid costs about $35/month (Venture, checked 2026-08-14), which is $420 per year.
What do I lose by replacing Ecwid?
Honestly: commerce integrations, inventory, channels, mobile apps, tax, and support; marketplace discovery; tax remittance; fraud operations; advanced course and community tooling. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Ecwid?
Yes: Square Payment Links (A buy button, product card and hosted checkout for zero monthly dollars; Square must operate in your country.) Stripe Payment Links (Hosted product pages and embeddable buy buttons without a monthly plan; catalog management stays deliberately small.) The prompt is for when you want it exactly your way.