commerce#
Lexicons and backend implementation for piloting ATmospheric commerce—digital storefronts own their catalog and orders and can move them across the ATmosphere.
commerce.lgtm.shop includes:
- generic lexicon definitions
- self-hostable, pluggable backend for atproto storefronts in a single docker image
At this stage, the pilot namespace is shop.lgtm.commerce.* (on lgtm.shop). If this evolves into a proven workflow, we'd like to adopt a neutral standard namespace. See SPEC.md for the full technical specification.
Lexicons#
Records live in the shop's repo (merchant DID). Permissioned and buyer-owned records move to atproto permissioned spaces over time.
Public — the shop window#
| Lexicon | Description |
|---|---|
shop.lgtm.commerce.storefront |
Store identity, branding, currencies, shipping zones, policies (singleton: self) |
shop.lgtm.commerce.product |
Catalog entries — name, description, images, options; status (draft/active/discontinued/archived) |
shop.lgtm.commerce.variant |
Purchasable units — SKU, price, option values, kind, availability (available/sold_out/unavailable) |
Public — proof (opt-in, keytrace-shaped)#
| Lexicon | Description |
|---|---|
shop.lgtm.commerce.purchaseClaim |
Buyer-owned "I bought / I back this" badge |
shop.lgtm.commerce.purchaseAttestation |
Shop signature vouching for a claim |
shop.lgtm.commerce.attestationKey |
Rotating shop signing key |
Permissioned — the private counterpart#
| Lexicon | Description |
|---|---|
shop.lgtm.commerce.order |
Order + line items |
shop.lgtm.commerce.address |
Buyer-authored shipping address |
shop.lgtm.commerce.receipt |
Signed entitlement proof |
shop.lgtm.commerce.fulfillmentStatus |
Fulfillment / tracking state |
Running the pilot (Phase 1)#
One-command browsable demo (no external accounts)#
No Stripe account, no Printful account needed. seed-demo provisions the store and signing key; --fake-payments routes checkout to a local /dev/pay endpoint instead of Stripe so the full loop runs in-process.
# 1. Generate secrets (32 random bytes each, base64-encoded).
export LGTM_MASTER_KEY=$(head -c32 /dev/urandom | base64)
export LGTM_SESSION_SECRET=$(head -c32 /dev/urandom | base64)
# 2. Set remaining env vars.
export LGTM_DATABASE_URL=postgres://postgres@localhost:5433/lgtm
export LGTM_STORE_DID=did:web:demo.lgtm.shop
export LGTM_PUBLIC_URL=http://localhost:8080
export LGTM_TENANCY=single
export LGTM_FAKE_PAYMENTS=1
# These are read by Config::load() but unused in fake-payments mode:
export LGTM_STRIPE_SECRET_KEY=sk_unused
export LGTM_STRIPE_WEBHOOK_SECRET=whsec_unused
export LGTM_PRINTFUL_API_KEY=pf_unused
export LGTM_PRINTFUL_WEBHOOK_SECRET=pfwh_unused
export LGTM_ATPROTO_OAUTH_CLIENT_ID=https://demo.lgtm.shop/client-metadata.json
# 3. Seed the store (migrations + tenant + signing key + storefront + product/variant).
# IMPORTANT: use the SAME LGTM_MASTER_KEY here and when starting the server —
# the signing key is sealed with it and won't round-trip with a different key.
cargo run -p server --bin seed-demo
# 4. Start the server (fake-payments mode, worker in-process).
cargo run -p server -- --role all --fake-payments
# 5. POST a checkout. Get a variant_id from the seed-demo output, or list the catalog:
# curl -fsS http://localhost:8080/api/products # active products + their variants
curl -fsS http://localhost:8080/api/checkout \
-H 'content-type: application/json' \
-d '{"buyer":{"did":{"did":"did:web:demo.buyer"}},"items":[{"variant_id":"<variant-uuid>","quantity":1}]}'
# Response: {"checkout_url":"http://localhost:8080/dev/pay?order_id=<order-uuid>","order_id":"<order-uuid>"}
# 6. Simulate payment (open the /dev/pay URL or curl it).
curl -fsS 'http://localhost:8080/dev/pay?order_id=<order-uuid>'
# The worker fulfills the order and signs a receipt. Then:
# 7. Issue an attestation.
curl -fsS http://localhost:8080/api/claims/attest \
-H 'content-type: application/json' \
-H 'x-buyer-did: did:web:demo.buyer' \
-d '{"order_id":"<order-uuid>"}'
# 8. View the backer wall.
curl -fsS http://localhost:8080/api/backers
Production setup (real Stripe + Printful)#
Single-tenant, direct Stripe (operator is merchant of record), Printful fulfillment, web checkout, signed receipts, and the opt-in backer wall. One image, Postgres alongside.
# 1. Configure. Set LGTM_STRIPE_*, LGTM_PRINTFUL_*, LGTM_STORE_DID, LGTM_MASTER_KEY
# (base64 32 bytes), LGTM_SESSION_SECRET, LGTM_PUBLIC_URL.
cp .env.example .env
$EDITOR .env
# 2. Build + run (Postgres + app). The app migrates on boot under an advisory lock,
# then serves on :8080 with the worker in-process (--role all).
docker compose up -d --build
curl -fsS localhost:8080/readyz # DB + integration health -> 200
# 3. Happy path (after seeding a storefront/product/variant via the admin UI).
# a) start checkout -> Stripe checkout_url + a PendingPayment order
curl -fsS localhost:8080/api/checkout -H 'content-type: application/json' -d '{
"buyer": { "guest": { "email": "buyer@example.com" } },
"currency": "USD",
"items": [{ "variant_id": "<variant-uuid>", "quantity": 1 }]
}'
# b) Stripe posts checkout.session.completed -> /webhooks/stripe (signature-verified,
# deduped, enqueued). The worker marks the order paid, submits Printful fulfillment,
# writes the signed receipt, and moves the order to Fulfilled.
# c) issue a shop attestation; the buyer then publishes a public purchaseClaim.
curl -fsS localhost:8080/api/claims/attest -H 'content-type: application/json' \
-H 'x-buyer-did: did:...' -d '{
"order_id": "<order-uuid>"
}'
# d) the Jetstream indexer verifies the claim against the store key; the backer wall lists it.
curl -fsS localhost:8080/api/backers
Roles: the default --role all runs migrations, HTTP, and the worker in one process. Split them by overriding the entrypoint (server --role server / server --role worker); both still migrate on boot under the same advisory lock, so a rolling deploy is safe.
Deploying a published image#
The demo above builds from source. To run an instance from the published image
(lgtmsupplyco/commerce on Docker Hub) instead of building — with a Railway quickstart,
the seed one-off, and the go-live cutover to real payments — see
docs/deploy/running-an-instance.md.
docker-compose.prod.yml is the consumer shape: pin the image
tag, supply a .env, bump the tag to upgrade. Releases are cut by bumping VERSION
and pushing to main (see RELEASING.md).
Environment variables#
| Variable | Required | Example | Notes |
|---|---|---|---|
LGTM_TENANCY |
yes | single |
single-tenant pilot |
LGTM_DATABASE_URL |
yes | postgres://lgtm:lgtm@postgres:5432/lgtm |
Postgres connection |
LGTM_MASTER_KEY |
yes | base64 of 32 bytes | seals the store signing key; seed + app must share it |
LGTM_PUBLIC_URL |
yes | https://lgtm.shop |
public base URL |
LGTM_STORE_DID |
yes | did:web:lgtm.shop |
store authority DID |
LGTM_SESSION_SECRET |
yes | base64 | admin session signing |
LGTM_ATPROTO_OAUTH_CLIENT_ID |
yes | https://lgtm.shop/client-metadata.json |
admin/buyer OAuth |
LGTM_STRIPE_SECRET_KEY |
yes* | sk_live_… |
*any non-empty value in fake mode |
LGTM_STRIPE_WEBHOOK_SECRET |
yes* | whsec_… |
*any non-empty value in fake mode |
LGTM_PRINTFUL_API_KEY |
yes* | pf_… |
*any non-empty value in fake mode |
LGTM_PRINTFUL_WEBHOOK_SECRET |
yes* | pfwh_… |
*any non-empty value in fake mode |
LGTM_FAKE_PAYMENTS |
no | 1 |
1 = simulate payments (no external accounts) |
PORT |
no | 8080 |
listen port; PaaS platforms inject this |
Deferred to later phases#
Out of scope for the Phase 1 pilot (schema/lexicons present but dormant, or not built):
- Multi-tenant + hosted (Phase 2) — Stripe Connect, tenant isolation/RLS, custom storefront domains, listing/claim indexing at scale, live public-repo writes (the P1
PublicWriterstub). - Onto the protocol (Phase 3) —
AtprotoPermissionedStore, member-grant → space-credential exchange, LtHash signed-commit sync (P1 usesPgPermissionedStore), and publishing the lexicons understore.standard.*. - Funding surface (Phase 4) — pay-what-you-want donations, the embeddable donate widget, and the
/funding/*routes. - Memberships (Phase 4) — Stripe Billing subscriptions, recurring
variant.kind,recurring_interval, the subscription lifecycle (renew/dun/cancel). - Benefit providers (Phase 4) — Discord / GitHub / tangled.sh grants,
external_account_link,benefit_grant, account linking (/api/links/:provider/*). - Git revs to pin —
esquema-codegenandjetstreamworkspace deps have placeholderREPLACE_WITH_*_REVvalues; pin to real commit SHAs before enabling those crates in CI.
Resources#
- AT Protocol — The underlying protocol
- Lexicon spec — AT Protocol lexicon documentation
- SPEC.md — Commerce & funding backend technical specification
Acknowledgments#
Inspired by standard.site, which provides shared lexicons for personal websites on AT Protocol.
License#
MIT — see LICENSE.