DEV Community

Art
Art

Posted on

A web shop without a database or payment provider: the ForgeCMS shop module

Today the shop module for ForgeCMS went live. Your customers can pay in two ways:

  • in June (Ğ1), a free currency, or
  • in euros by plain bank transfer.

There's no database, no payment provider taking a cut, and no monthly fee. The money goes straight from the buyer to the seller.

This afternoon I paid for my own test order with 1 Ğ1. Three minutes later the shop had marked the order as paid and sent me my download link.

👉 How it works for shop owners: https://crowdware.info/shop-module

Some context: what ForgeCMS is

ForgeCMS is a small CMS written in Go. It has no database and no build step. Pages are small SML files plus Markdown in a Git repo on Codeberg. The server fetches them at request time, caches them, and renders HTML. You edit a page in Git, and a minute later it's live.

I wanted a shop that fits into this. It shouldn't need a database, it shouldn't need an account with a payment company, and it shouldn't hand buyers' data to anyone else.

Why June (Ğ1)?

Ğ1 is a libre currency that has been running since 2017. Every member creates a small share of new money each day, the universal dividend, so it works a bit like a basic income built into the currency. Since March 2026 it has been running on Duniter v2, a Substrate-based blockchain. It's not a speculative token, and people in the network use it to pay for goods and services.

For a shop, what matters is that every transfer is public and carries a comment. The buyer puts the order number in the comment, and the shop can find the payment without any access to the seller's wallet.

The catalogue is one text file

Catalog {
    Product { id: "honey-500" title: "Honey 500 g" price: "12.50" priceEur: "9.90" image: "honey.jpg" text: "From our own bees." }
    Product { id: "ebook" title: "My e-book" price: "20" type: "digital" text: "PDF, 120 pages." }
}
Enter fullscreen mode Exit fullscreen mode

price is the price in Ğ1, and priceEur the price in euros. A product with both prices lets the buyer choose. A product with only one price is sold only in that currency. On a page, Products { } renders the product cards with an Order button.

The price always comes from the catalogue, never from the order form.

How an order flows

  1. The buyer fills in a short form: quantity, name, email, and for physical goods a shipping address. Then they click Place order (payment required).
  2. The server writes the order as a small SML file to disk, e.g. G1-7KQ2MXRA.sml, and sends two emails, one to the buyer and one to the seller.
  3. The buyer sees a payment page with the address, the exact amount and the order number.
  4. The buyer pays and puts the order number into the transfer comment.

Detecting Ğ1 payments

A watcher goroutine asks the public Duniter indexer once a minute for new transfers to the shop's address. The indexer has a GraphQL API:

transfers(filter: {toId: {equalTo: $wallet}, blockNumber: {greaterThan: $from}}) {
  nodes { id blockNumber amount fromId comment { remark } }
}
Enter fullscreen mode Exit fullscreen mode

A transfer is matched to its order by the comment. The watcher waits 10 blocks of confirmations (about a minute) before it counts a payment. Split payments add up, and overpayments are recorded. A transfer without a matching order doesn't get lost: the seller gets an email and sorts it out by hand. The last processed block is stored in a small state file, so a restart doesn't skip or double-count anything.

The server never holds a wallet key. It only reads public data.

Euro payments: one click by the seller

For euros the shop doesn't connect to any bank. There's no banking API and nobody else sees your account. The seller's order email contains a secret confirmation link. When the money is on the account, the seller clicks it, checks the amount on the confirmation page and confirms. From there everything runs the same way as with Ğ1.

Digital delivery

For downloads, the paid email contains a license key and a personal download link:

  • The link is valid for 30 days and 5 downloads.
  • The server stores only a SHA-256 hash of the link's token, never the token itself.
  • The files don't live in the public content repo. They live in a data folder on the server, and the download handler rejects path traversal and symlinks.
  • Before ordering a digital product, the buyer ticks a box saying delivery may start immediately. German and EU law require this so the right of withdrawal ends with the download.

What it deliberately doesn't do

  • no shopping cart: one product per order, any quantity
  • no credit cards, PayPal or Stripe
  • no stock management and no automatic invoices
  • euro payments need one click from the seller

For a small shop, a creator, a farm shop or a community project, that's usually enough. Every feature I left out is one less thing that can break.

Tested for real

Before going public, I ran the whole flow on the live site with a hidden test page:

  • Euro: a transfer of 0.01 EUR, confirmed with the link. The order was marked as paid and the emails went out.
  • Ğ1: 1 Ğ1 for a digital test product. It was detected after 10 confirmations, the order was marked as paid, and the download link arrived by email and worked.

Then I removed the test page.

Try it

The shop module is a one-time €39 license, with no subscription. If you'd like a shop on your own website, whether you sell in June, in euros or both, get in touch.

If you already use Ğ1, I'd love to hear how you currently sell things with it and what's missing.

Top comments (0)