- Go 92.8%
- templ 5.9%
- CSS 1%
- HTML 0.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| admin | ||
| assets | ||
| cmd | ||
| internal | ||
| scripts | ||
| .envrc | ||
| .gitignore | ||
| .golangci.yml | ||
| CONTRIBUTING.md | ||
| env_easel.ini.example | ||
| env_easel_admin.ini.example | ||
| flake.lock | ||
| flake.nix | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| Makefile | ||
| NOTICE | ||
| README.md | ||
| sqlc.json | ||
Easel
Easel is a simple, self-hosted store for solo merchants. It gives you a storefront, a product catalog with variants, guest checkout, and an admin panel to manage orders, without the complexity of an enterprise retail platform.
Goals
- Simple. Its setup, upgrades and maintenance should be automatic and easy
to do for anyone comfortable with Linux. This means:
- We scale vertically.
- We use sqlite for the database.
- We store images on disk.
- Fast. Pages load instantly with lightweight footprints and optimized
images.
- We use Go with server-side templates.
- No invasive analytics or tracking.
- It works in pure HTML. JS is optional.
- Secure. It should be an appropriate safeguard of your store's financial
data.
- Evan built pentested, SOC2 and PCI audited systems in the past with millions of customers and is applying those same lessons and approaches here.
Support
If you'd like to support continued development of Easel, you can purchase premium hosting at https://www.easel.engineering/.
Premium hosting gets you:
- 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 and priorities.
Configuration
Easel reads all configuration from environment variables and parses it into native Go types at startup. Each service has one env file that holds both its public settings and its secrets, so there is nothing to keep in sync across separate files.
env_easel.ini— the server. Template:env_easel.ini.example.env_easel_admin.ini— the admin console. Template:env_easel_admin.ini.example.
The server's variables all use the EASEL_ prefix and the console's all use
the EASEL_ADMIN_ prefix, so the two never clash even when both files are
loaded into one shell. Each service also has its own base URL variable
(EASEL_BASE_URL for the server, EASEL_ADMIN_BASE_URL for the console) so
the two can be set independently. Values that must match, such as the SMTP and
Stripe credentials, are named separately in each file but must be set to the
same value.
The binary never reads the file itself. Pass it to your service manager, for example with systemd:
# easel.service
[Service]
EnvironmentFile=/etc/easel/env_easel.ini
ExecStart=/usr/local/bin/easel
# easel-admin.service
[Service]
EnvironmentFile=/etc/easel/env_easel_admin.ini
ExecStart=/usr/local/bin/easel-admin
At deploy time, append the secrets to each service's file and place it under
/etc/easel/.
The app always reads os.Getenv, so the same mechanism works whether the
variables come from systemd, a container, or a shell. Invalid values are
reported all at once at startup, before the server binds, so a typo fails fast
instead of silently falling back to a default.
Local Development
To get dependencies, first install nix, configure the flakes experimental
feature, and enter a nix devShell in this directory. Then run make deps to
retrieve all the remaining Go dependencies.
Copy the templates and fill in the secrets:
cp env_easel.ini.example env_easel.ini
cp env_easel_admin.ini.example env_easel_admin.ini
.envrc loads both files with direnv, so make up, make admin,
make migrate, and make seed pick them up automatically. Keep shared values
(SMTP, Stripe, database) identical in the two files so the server and console
operate on the same store. Generate secret values with
./scripts/generate_internal_api_secret.sh and
./scripts/generate_stripe_key_encryption_key.sh. EASEL_INTERNAL_API_SECRET
is required for the server to start.
Tests
The test suite needs no configuration and runs offline by default. Two things read the environment:
- A 32-byte
EASEL_STRIPE_KEY_ENCRYPTION_KEYfor the payment key setup tests. Tests supply a default when it is unset. - The tests that talk to the real Stripe test API. They run when
EASEL_STRIPE_SECRETis set and skip otherwise. See "Stripe Integration Tests" in CONTRIBUTING.md for the permissions the key needs.
Stripe Configuration
Easel uses Stripe for payment processing. When running in platform mode (hosting multiple stores), each store provides their own Stripe API keys.
Supported Key Types
Stores can use either:
- Standard secret keys (
sk_live_*orsk_test_*) - Restricted API keys (
rk_live_*orrk_test_*)
Restricted keys are recommended for security, as they limit API access to only what's needed.
Required Permissions for Restricted Keys
Easel uses two kinds of Stripe keys, and a restricted key needs different permissions depending on its role.
Store keys process a store's checkout and refunds:
| Resource | Permission |
|---|---|
| Payment Intents | Write |
| Refunds | Write |
Stripe grants read access wherever it grants write access, so Easel validates a new key with read-only requests to the Payment Intents and Refunds endpoints before saving it. Only one record is requested and nothing is stored.
The platform key in the Easel config bills hosted subscriptions. A self-hosted single store that does not use subscriptions only needs the store permissions above:
| Resource | Permission |
|---|---|
| Payment Intents | Write |
| Customers | Read and Write |
| Payment Methods | Read and Write |
A standard secret key (sk_*) already has all of these permissions. Use a
restricted key when you want to limit what Easel can access.
Webhook Events
Easel creates orders from Stripe webhooks, so each Stripe account needs a
webhook endpoint pointing at the store's /stripe/webhook/ URL, such as
https://store.example.com/stripe/webhook/. Subscribe the endpoint to:
| Event | Purpose |
|---|---|
checkout.session.completed |
Create the order after a Checkout payment succeeds. |
checkout.session.async_payment_succeeded |
Create the order when a delayed payment method settles. |
payment_intent.succeeded |
Recover a paid order when embedded checkout confirms the card but the browser closes before the order is created. |
Easel acknowledges every other event without acting on it, so extra events are harmless. It logs, but does not otherwise process, the refund events below, which are useful for monitoring:
refund.createdrefund.updatedcharge.refundedcharge.refund.updated
Copy the endpoint's 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.
Code Organization
We use a clean architecture for organization because it's quite a large codebase and we expect it to grow substantially over time.
Think of it like an onion with dependencies only flowing in one direction.
- Domain defines our core types with no dependencies. Everything can import domain. This is the center of the onion.
- Application holds our business logic. It can import domain.
- Presentation defines the views, such as HTML pages and i18n translations. It can import application and domain.
- Infrastructure is the outermost layer, and it interfaces with the outside world, including with HTTP, SMTP, the database, and external services like Stripe. This can import presentation, application, and domain.
The cmd/easel/main.go file ties these layers together, initializing concrete
implementations for all the interfaces defined in the other packages.
Nix
We use Nix to provide a perfectly consistent development environment to all developers without the overhead of VMs or containers.
We recommend using Nix with direnv, so your environment is automatically configured for you by entering the directory.
You'll need to enable the flakes experimental feature in Nix.
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 the LICENSE file for details, and NOTICE for third-party attributions.
The internal/migrate/ directory contains code from
thankful-ai/migrate licensed under the
ISC license. See internal/migrate/LICENSE for
details.