Build Ecwid

KINDAreplaces $35/mosaves $420/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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-daythe files for this build are in the project pack

RuntimeNext.js with TypeScript, your own domain, serving both the embed script and the catalogue pagesEmbeddingOne small script that renders into a shadow root and keeps its cart token in the host page's own storageStoragePostgreSQL for orders and entitlements, object storage for the files people boughtPaymentsHosted checkout on the processor's own page, fulfilment driven by webhooksDeliveryShort-lived signed URLs with resumable range requests

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Runs the storefront, the API and the embed build.

    Get it nodejs.org or your version manager. open ↗

    Verify node --version prints v22 or higher

  2. 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. open ↗

    Verify A test payment produces a verified event at your endpoint

  3. 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

  4. have readyfree

    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

  5. decidefree 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

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- 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

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
DATABASE_URLsecretrequiredpostgres://user:pass@localhost/shopOrders and entitlements. This is the money record; back it up.
STORAGE_BUCKETrequiredcreator-filesWhere purchased files live. Never public.
STORAGE_KEYsecretrequiredAKIA...redactedUsed only to mint signed URLs on the server.
PROCESSOR_SECRET_KEYsecretrequiredsk_live_...redactedCreates checkout sessions and retrieves them during fulfilment.
PROCESSOR_WEBHOOK_SECRETsecretrequiredwhsec_...redactedVerifies every event before it can grant anything.
SIGNED_URL_TTL_SECONDSrequired300How long a download link lives. Short, and minted per request.
PUBLIC_BASE_URLrequiredhttps://shop.example.comCanonical product pages, the embed script and the checkout return address.

The build, in order

  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.

    1. Neither side should be able to break the other's layout.

      Files src/embed/mount.tstests/isolation.test.ts

    2. It is the only storage you are reliably allowed to use there.

      Files src/embed/cart.tstests/no-cookie.test.ts

    3. Otherwise your first customer's site silently blocks you.

      Files docs/embedding.md

    done when · tick each as it passes
    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.
  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.

    1. It is the thing people can send to each other.

      Files src/app/p/[slug]/page.tsx

    2. A back button that does nothing is a bug report you will get.

      Files src/embed/router.ts

    3. One address per product, everywhere.

      Files src/embed/product.ts

    done when · tick each as it passes
    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.
  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.

    1. It will be called from two places and retried by a third.

      Files src/fulfil.tstests/fulfil-idempotent.test.ts

    2. The browser is the least reliable participant in the payment.

      Files src/webhooks.ts

    3. An edited boolean cannot answer 'when did they lose access'.

      Files src/entitlements.ts

    done when · tick each as it passes
    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.
  4. Delivering the thing they bought

    A large file downloads over a bad connection, and access ends when entitlement does.

    1. A long-lived link is a product you gave away.

      Files src/delivery/sign.tstests/delivery.test.ts

    2. Large files and hotel wifi are the normal case.

      Files src/delivery/stream.ts

    3. A pile of incomplete downloads is a support ticket waiting.

      Files src/delivery/log.ts

    done when · tick each as it passes
    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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more creator commerce.