Pilot atproto-native commerce backend
5

Configure Feed

Select the types of activity you want to include in your feed.

Rust 97.4%
PLpgSQL 1.8%
Shell 0.4%
Dockerfile 0.3%
JavaScript 0.1%
Other 0.1%
99 2 4

Clone this repository

https://tangled.org/lgtm.shop/commerce https://tangled.org/did:plc:dvfz3oewlgdnidgnptzqnczr
git@tangled.org:lgtm.shop/commerce git@tangled.org:did:plc:dvfz3oewlgdnidgnptzqnczr

For self-hosted knots, clone URLs may differ based on your setup.



README.md

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 PublicWriter stub).
  • Onto the protocol (Phase 3)AtprotoPermissionedStore, member-grant → space-credential exchange, LtHash signed-commit sync (P1 uses PgPermissionedStore), and publishing the lexicons under store.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 pinesquema-codegen and jetstream workspace deps have placeholder REPLACE_WITH_*_REV values; 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.