Build Ecwid
KINDAreplaces $35/mosaves $420/yrback to the verdict
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - accountfree 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 - accountpennies 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 - 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 - 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_URLsecret | required | postgres://user:pass@localhost/shop | Orders and entitlements. This is the money record; back it up. |
STORAGE_BUCKET | required | creator-files | Where purchased files live. Never public. |
STORAGE_KEYsecret | required | AKIA...redacted | Used only to mint signed URLs on the server. |
PROCESSOR_SECRET_KEYsecret | required | sk_live_...redacted | Creates checkout sessions and retrieves them during fulfilment. |
PROCESSOR_WEBHOOK_SECRETsecret | required | whsec_...redacted | Verifies every event before it can grant anything. |
SIGNED_URL_TTL_SECONDS | required | 300 | How long a download link lives. Short, and minted per request. |
PUBLIC_BASE_URL | required | https://shop.example.com | Canonical product pages, the embed script and the checkout return address. |
The build, in order
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.
Neither side should be able to break the other's layout.
Files
src/embed/mount.tstests/isolation.test.tsIt is the only storage you are reliably allowed to use there.
Files
src/embed/cart.tstests/no-cookie.test.tsOtherwise your first customer's site silently blocks you.
Files
docs/embedding.md
done when · tick each as it passeswatch 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.
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.
It is the thing people can send to each other.
Files
src/app/p/[slug]/page.tsxA back button that does nothing is a bug report you will get.
Files
src/embed/router.tsOne address per product, everywhere.
Files
src/embed/product.ts
done when · tick each as it passeswatch 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.
Access comes from the event, not the page
A payment grants access exactly once, whether or not the buyer's browser ever came back.
It will be called from two places and retried by a third.
Files
src/fulfil.tstests/fulfil-idempotent.test.tsThe browser is the least reliable participant in the payment.
Files
src/webhooks.tsAn edited boolean cannot answer 'when did they lose access'.
Files
src/entitlements.ts
done when · tick each as it passeswatch 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.
Delivering the thing they bought
A large file downloads over a bad connection, and access ends when entitlement does.
A long-lived link is a product you gave away.
Files
src/delivery/sign.tstests/delivery.test.tsLarge files and hotel wifi are the normal case.
Files
src/delivery/stream.tsA pile of incomplete downloads is a support ticket waiting.
Files
src/delivery/log.ts
done when · tick each as it passeswatch 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.
Running a shop by yourselfproduct builder
You can answer a customer, find an order, and get everything out, at eleven at night.
It is the only screen you will use under pressure.
Files
src/admin/orders.tsxSix months later you will be asked why.
Files
src/entitlements.tsA wrong calculation is worse than an honest absence.
Files
src/admin/orders.tsxREADME.md
done when · tick each as it passeswatch 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.
That is the whole plan for Ecwid. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- 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
- 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
Need the files? The project pack on the verdict page hands your agent the whole brief · more creator commerce.