aboutsummaryrefslogtreecommitdiff

Easel

Easel is a simple, self-hosted store for solo merchants. It gives you a storefront, a product catalog with variants, guest checkout with optional customer accounts, and an admin panel to manage orders, without the complexity of an enterprise retail platform.

Goals

  • Simple. Setup, upgrades and maintenance should be easy for anyone comfortable with Linux. Easel scales vertically, uses SQLite, and stores images on disk.
  • Fast. Pages load instantly. Easel uses Go with server-side templates, works in pure HTML with optional JavaScript, and has no invasive analytics or tracking.
  • Secure. Easel should be an appropriate safeguard of your store's financial data. Evan has built pentested, SOC2 and PCI audited systems with millions of customers and applies the same lessons here.

Support

To support continued development of Easel, buy premium hosting at https://www.easel.engineering/. It includes:

  • PCI compliance
  • Automated backups and recovery
  • Performance optimizations
  • Data migration services
  • Evan's direct phone number for support

Premium customers have a significant influence on the product roadmap.

Running Locally

Install Nix with the flakes experimental feature enabled, then enter the dev shell (nix develop, or automatically with direnv) and run:

make deps   # fetch the remaining Go dependencies
make init   # write env files with fresh secrets; seed a new database
make up     # run the server on http://localhost:3000/
make admin  # optional: run the admin console on http://localhost:3001/

The seed holds two stores, each with its own dashboard login (password password1234 for all). Sign in at http://localhost:3000/ as:

  • dev@example.com for MyStore, an established store with orders, reviews, gift cards, and translations.
  • manager@example.com for MyStore as a manager, who sells and fulfills but can't open the team, billing, tax, or other settings only an owner may.
  • new@example.com for New Shop, which just opened: its dashboard shows the next steps a new merchant sees, with a to do list beneath.

Re-running make init never overwrites existing files, non-empty values, or the local database. make seed resets the database to the seed data.

The server routes storefronts by the Host header, so MyStore (slug site1) is at http://site1.localhost:3000/ and New Shop (slug site2) at http://site2.localhost:3000/. Linux doesn't guarantee that *.localhost resolves to loopback, so add a host entry for each store slug you use:

echo '127.0.0.1 site1.localhost site2.localhost' | sudo tee -a /etc/hosts

MyStore's customer Bob has a storefront account: sign in at http://site1.localhost:3000/account/sign-in/ as bob@example.com with the password password1234.

See CONTRIBUTING.md for architecture, tests, and code conventions.

Configuration

Easel ships two binaries, each configured entirely by environment variables from its own env file, which holds both settings and secrets:

  • easel (./cmd/easel) serves the storefronts, dashboard and API. It reads env_easel.ini, from the template env_easel.ini.example.
  • easel-admin (./cmd/admin) is the admin console. It reads env_easel_admin.ini, from the template env_easel_admin.ini.example, and takes only flags (easel-admin -h). The operator signs in as admin with EASEL_ADMIN_PASSWORD and adds the rest of Easel's staff on the console's Staff page. Billing, tiers, and deleting stores and users are the operator's alone.

The templates document every variable. Server variables use the EASEL_ prefix and console variables use EASEL_ADMIN_, so both files can be loaded into one shell; .envrc and the Makefile load both. Shared values, such as the database path, SMTP and Stripe credentials, and the staff access key (EASEL_STAFF_ACCESS_KEY and EASEL_ADMIN_STAFF_ACCESS_KEY), must be set to the same value in each file. EASEL_INTERNAL_API_SECRET is required for the server to start. Invalid values are all reported at startup, before the server binds.

make build writes both binaries to build/. They never read the env file themselves, so pass it through your service manager, such as systemd:

[Service]
EnvironmentFile=/etc/easel/env_easel.ini
ExecStart=/usr/local/bin/easel

Secrets are better kept out of the environment, which child processes inherit. Both binaries read any variable from the file of that name in $CREDENTIALS_DIRECTORY before the environment, so with systemd each secret can be its own mode-0600 file:

[Service]
EnvironmentFile=/etc/easel/env_easel.ini
LoadCredential=EASEL_STRIPE_SECRET:/etc/easel/credentials/EASEL_STRIPE_SECRET

Stripe

Easel uses Stripe for payments. Store keys may be standard (sk_*) or restricted (rk_*) keys; a restricted store key needs Write access to Payment Intents and Refunds. The platform key, which bills hosted subscriptions, needs Write access to Payment Intents and Read and Write access to Customers and Payment Methods.

Point a webhook endpoint at the store's /stripe/webhook/ URL, such as https://store.example.com/stripe/webhook/, subscribed to payment_intent.succeeded, charge.refunded, charge.dispute.created, and charge.dispute.closed. Copy its signing secret (whsec_*) into EASEL_STRIPE_WEBHOOK_SECRET for a self-hosted store, or into the webhook field on the payment keys page for a hosted store.

When hosting stores, also point a webhook endpoint on the platform account at /stripe/webhook/ on any store host, subscribed to payment_method.automatically_updated, so a billing card a bank reissues shows its new details. Its signing secret goes in EASEL_STRIPE_WEBHOOK_SECRET, which production requires in both modes.

The server charges each hosted store's subscription when it's due, in a job that runs once a day at 09:00 UTC, and charges it once. It never retries: a charge that fails, or one it won't make (a subscription more than a week overdue, one whose last charge didn't succeed, or one with no card), leaves the subscription past due and logs an error, which the console's Errors page shows. Nothing charges a past-due store again until the operator does, with Bill on its page in the console, which the dashboard and the store list's Past due filter point to. A charge that never reached the card, as when Stripe is down, isn't a failure: the subscription stays due, nobody is told, and the next day's run charges it, until it's a week overdue.

Each charge saves its Stripe payment intent before confirming it. When Stripe's answer is lost, as when the connection drops mid-charge, the charge stays pending, and nothing charges the store meanwhile, not even Bill. An hourly job asks Stripe how each charge pending over 15 minutes ended: one that went through is recorded paid, unlocking the store if it was the charge reactivating it, and one that didn't is canceled in Stripe and fails like any other.

Translating Store Content

go run ./cmd/translate -h explains the translate tool, which translates a store's content through the REST API. nix develop exports EASEL_API_URL=http://localhost:3000 and EASEL_API_KEY set to the seed store's write key, for local development only: eas_sto_9bsv0s0pi2qg036t2bbg_de00000000000000000000000000000000000000000000000000000000000001.

Security

If you self-host Easel, you must use HSTS to force connections through HTTPS in the browser.

License

Easel is licensed under the GNU Affero General Public License, version 3 or later (AGPL-3.0-or-later). See LICENSE for details and NOTICE for third-party attributions, including the ISC-licensed thankful-ai/migrate code in internal/migrate/.