Research to inform which mock platform (Amazon, Shopify, etc.) is worth turning into a real integration next, after Etsy. Adds GMV/seller-count market-size data for every platform in domains/accounts/platform.go, a weighted scoring model (GMV 45%, API/inventory-webhook completeness 30%, growth 15%, integration cost 10%), and the resulting ranked order. Also fills in Etsy's own row, sourced directly from domains/platforms/etsy/generated_client rather than external docs since it's the one platform actually live in this repo: Etsy Open API v3 has no webhook/push mechanism at all, and only the OAuth connection flow is built so far - polling-based order/inventory sync doesn't exist yet. Etsy is included as a reference score (3.85/10) against the same rubric used for the unbuilt platforms, mainly as a sanity check on the model rather than a claim that building it first was wrong. Flags that the existing capability table's BigCommerce row doesn't match an actual platform in this codebase (Tiktok does, and is now researched instead), and that Tiktok Shop's API details are unconfirmed pending a dedicated research pass. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
15 KiB
AGENTS.md
Go backend for a multi-platform ecommerce inventory-sync tool. Etsy is the
one real, live platform integration; everything else (Amazon, BigCartel,
Ebay, Ecwid, Shopify, SquareOnline, Squarespace, Tiktok, WalmartMarketplace,
Wix, WooCommerce, Zoho) exists only as a mock simulation layer used for
development, demos, and testing the sync/reporting logic without needing
real store credentials. See README.md for the product-level roadmap.
Platform integration priority
When picking the next mock platform to turn into a real integration (after
Etsy), use this weighted ranking of the 12 not-yet-live mock platforms. It
scores each on GMV/market opportunity (45%), API/inventory-sync completeness
(30%), growth trajectory (15%), and integration cost (10%) - see
STORE_API_RESEARCH.md's "Weighted priority ranking" section for the full
scoring table, per-criterion reasoning, and the market-size/API research
it's built on.
- Shopify (9.15/10) - cleanest/most complete API (webhooks for both order and inventory events, modern GraphQL), still-strong GMV growth (+29-35% YoY), and a proven self-serve distribution channel (the Shopify App Store, where inventory-sync apps are an established category) that no other platform here has an equivalent of.
- Amazon (8.85/10) - largest raw dollar opportunity (~$575B in 2025
third-party GMV), and inventory-sync pain (suppressed listings, FBA
stranded-inventory fees) is one of the sharpest problems this tool could
solve there.
domains/amazonalready has the most real infrastructure of any mock platform (theProcessEventsLISTEN/NOTIFY loop, SSE wiring), so a real SP-API integration reuses that instead of starting from zero. Scores lower than Shopify mainly because SP-API auth (LWA + AWS SigV4) and rate limiting are the most complex of any platform here, and seller growth has gone flat. - Tiktok Shop (7.15/10) - hypergrowth (global GMV nearly doubled in
2025, projected to double again in 2026) is what earns it this spot, but
its API details are the least confirmed of anything researched -
partner.tiktokshop.comdidn't yield readable docs during this pass. Needs a dedicated research pass to confirm exact webhook/endpoint names before treating this ranking as actionable. - Walmart Marketplace (6.35/10) - smaller in absolute GMV than the top three, but growing ~50% YoY and a natural second-marketplace target for sellers already on Amazon; full order+inventory webhook coverage.
- Wix (6.35/10) - tied with Walmart on score but behind it on the growth tiebreaker; full inventory-webhook coverage and an easy API, just a smaller/less certain GMV number.
- WooCommerce (5.95/10) - large store count (~4-6M) but no
platform-wide GMV figure exists (self-hosted plugin, no central ledger),
and no dedicated inventory webhook (relies on a
product.updatedproxy). - Square Online (5.90/10) - full inventory-webhook coverage and well-documented APIs (shared with the rest of Square's product line), but its ecommerce-specific GMV can't be isolated from Square's much larger in-person POS business.
- Ebay (5.75/10) - most sellers of any platform here (18.3M) but the lowest GMV-per-seller by far, plus no dedicated inventory-change webhook.
- Squarespace (4.75/10) - smaller GMV, no inventory webhook.
- Zoho (3.85/10) - tiny confirmed store count (~2,196 globally).
- Ecwid (3.20/10) - shrinking (-20% YoY store count).
- Big Cartel (1.50/10) - last, and not close: no inventory API or webhook at all is a structural dealbreaker for this tool's core use case, independent of its (also declining, -41% YoY) market size.
(BigCommerce, which appears in the API-capability table in
STORE_API_RESEARCH.md, is excluded here - it isn't one of this codebase's
actual mock platforms; that table predates the platform list settling on
Tiktok instead.)
Etsy isn't in the ranking above (it's already built), but scores 3.85/10 as
a reference point if run through the same rubric - mid-pack-and-declining
GMV, and the only platform researched with zero webhook/push support of
any kind (order or inventory - confirmed directly against
domains/platforms/etsy/generated_client, not external docs). That's not a
retroactive case against having built Etsy - it was presumably chosen for
reasons this rubric doesn't score (an existing relationship, an easier path
to developer credentials) - but it's a reminder that the lack of any
webhook system makes real order/inventory sync a polling loop, which is
exactly the kind of integration cost this rubric undercounts once a
platform is more than superficially wired up. Note also that the OAuth
connection flow is the only part of Etsy that's actually built so far
(domains/platforms/etsy/etsy.go) - polling-based receipt/inventory sync
against the live API doesn't exist yet.
Setup
- Postgres running locally, with an
app_clientrole/database matching.env.example'sDATABASE_URL. cp .env.example .envand fill in real values (Auth0, Etsy). For local dev-only work you don't need real Auth0/Etsy secrets ifDEV_AUTH_ENABLEDistrue(see Auth below) -changemeplaceholders are fine.- Create the dedicated test database once:
createdb -O app_client inventory_2_test.make testmigrates it automatically after that.
Commands
make dev(default: baremake) - applies pending migrations, starts the Tailwind watcher in the background, runs the server in the foreground. Ctrl-C stops both.make run- justgo run ., no migrations or CSS.make test- runs the full test suite against the dedicatedinventory_2_testdatabase (migrates it first). Safe to run anytime; never touches real data.make test-against-dev-db- same suite, pointed at the real dev database instead. Useful for checking behavior against real data; tests clean up after themselves viat.Cleanup, but it's still touching your dev DB.make migrate-up/migrate-down/migrate-version- wrap themigrateCLI usingDATABASE_URLfrom.env, so the DSN is never hand-typed.migrate-test-*variants targetTEST_DATABASE_URL.go build ./...,go vet ./...,gofmt -l .- standard, expected clean before considering work done.go test ./... -race- the test suite is safe to run this way (see Testing below for what made that true).
Architecture
domains/- business logic, one package per bounded concern (accounts,authentication,raw_events,reports,amazon,platforms/etsy). Each domain owns its own DB access; there's no shared ORM/repository layer. Looking to loosely follow CQRS: writes go through domainStoremethods, reads are mostly separate query methods on the sameStore.domains/accounts/mocks.go- the mock-platform simulation layer:CreateMockShop,CreateMockListing,SaveNewMockSale/SaveNewMockRefund/SaveNewMockInventoryReset, etc. These are the real entry points production code (and the simulate-sale/refund/ inventory UI) uses - prefer them over hand-rolled SQL when writing tests or new features that need mock shop/listing/event data.domains/amazon/mock.go- the one platform with a live background processor on top of the mock layer: a PostgresLISTEN/NOTIFY-driven loop ((*Mocks).ProcessEvents) that reacts to newly inserted mock events and dispatches them to aMockEventListener(wired to Server-Sent Events inmain.go, so the UI updates live). No other platform has this yet; if adding one, read this file's history/comments first - it went through several correctness passes (reconnect-on-failure, ack-based retry, configurable poll fallback) worth not re-discovering from scratch.server/- HTTP layer (Gin).server/apiis the JSON/HTMX API,server/uirenders HTML pages,server/authis session/auth middleware,server/sseis the Server-Sent Events plumbing.config/config.go- all runtime configuration comes from environment variables (loaded from.envviagodotenv), never hardcoded. If you add a new required external dependency (a new secret, a new service URL), add it here, not as a literal in the code that uses it.internal/testdb- shared test fixtures:Pool(t)(connects viaTEST_DATABASE_URL, skips the test if unset),Logger(),NewUserID(t),SeedOAuthUser/SeedOAuthSession.
Database & migrations
- Migrations live in
database_migrations/, run viagolang-migrate(migrateCLI). Every migration needs a paired.up.sql/.down.sql. - Migration files can drift from the live schema, and drift can mean
"unrunnable," not just "stale docs." This has happened at least twice:
an interval-literal syntax error that failed on any fresh database, and a
column that existed in the live dev DB but not in what the migration
files would produce (fixed by adding a migration that reconciles the two,
then
migrate force <version>on the DB that already matched by hand). When something about the schema seems off, verify against the live DB (psql \d,pg_get_viewdef(...)) before trusting the migration files, and especially before assuming a fresh database would come up the same way the long-running dev one does. - The mock-platform schema (
mock.*) follows a consistent per-platform pattern:mock.shop_<platform>,mock.shop_<platform>_listings, and for Amazon specificallymock.shop_<platform>_events(the background processor's queue). Some platforms also have a recursivemock.shop_<platform>_listing_countsview (built onmock.shop_<platform>_listing_event_sequence) that computes a running inventory count frommock.raw_shop_events- it's real per-platform logic living in SQL, not a passthrough, and worth reading withpg_get_viewdef(..., true)rather than assuming it matches another platform's shape. - Platform-string casing is inconsistent and matters. Most
accounts.Platformconstants are lowercase/snake_case ("amazon","big_cartel", ...) matching what SQL triggers/views expect, butEtsy/Tiktok/Wixare capitalized Go-side ("Etsy", not"etsy"). Some SQL objects for those platforms expect the capitalized form (e.g. the Etsy listing-counts view filters on'Etsy'), others expect lowercase (e.g. the Etsyraw_shop_eventstrigger checks'etsy'). Don't assume; check the actual SQL (pg_get_viewdef, trigger definitions) for the specific object you're relying on before writing a fixture or a query. - The
mock_shop_<platform>_event_insertedNOTIFYchannels are global, not scoped by shop/account. Any insert intoraw_shop_eventsfor that platform notifies every currently-listening connection for that platform, regardless of which test or code path caused it. See Testing below for how this bit two test packages at once.
Testing
- Tests are real integration tests against a real Postgres database
(
internal/testdb.Pool(t)), not mocks/stubs of the DB layer - this codebase is mostly thinStoremethods wrapping SQL, so testing the Go code in isolation would miss most of the actual risk. - Assertions use
github.com/stretchr/testify'sassert/requirethroughout -requirefor anything the rest of the test depends on (halts immediately),assertfor independent checks. Userequire.Eventuallyfor "poll until true or timeout" patterns instead of hand-rolling one. require.Eventually's condition function runs on a separate goroutine. Never callrequire.*/t.Fatal*(which invoket.FailNow()) from inside one - Go's testing package requiresFailNowto only be called from the test's own goroutine; calling it from a spawned one doesn't panic cleanly, it just silently reports the wrong thing. If a helper used inside anEventuallyclosure needs to check an error, give it a*testing.T-free variant that returns(value, error)and let the closure decide what "not yet satisfied" means.- Every test cleans up its own rows via
t.Cleanup, in FK-safe order (children before parents). Use unique, randomly-generated IDs (testdb.NewUserID(t),uuid.NewString()) so tests are safe to run concurrently with themselves and with real dev data undertest-against-dev-db. - Pick a mock platform deliberately when writing new fixtures that touch
mock.raw_shop_events.domains/amazon's tests process every unprocessed event system-wide (that's correct behavior for its background worker, not a bug) and react to the global Amazon NOTIFY channel - any other package's tests that insert Amazon-platform mock events risk being picked up bydomains/amazon's tests whengo test ./...runs both packages' binaries concurrently (Go's default).domains/reports, for example, deliberately uses Etsy as its example platform for exactly this reason. If you're not testingdomains/amazonitself, don't use Amazon as your fixture platform. - Never disable/re-enable a shared DB trigger to simulate a "notify never
fires" scenario in a test, even temporarily - it's unsafe to do against
a database anything else might be using concurrently (including a real
make devsession someone has running). If you need to prove a timing/fallback path works, find a way to construct the scenario through normal writes instead (seedomains/amazon/mock_test.go's poll-fallback test for an example: it reuses the retry mechanism to guarantee a second dispatch can only come from the poll timer, without touching any trigger). - Killing a real Postgres backend connection (
pg_terminate_backend, found viapg_stat_activity) is a legitimate, safe way to test reconnect/failure-recovery logic when properly scoped - seedomains/amazon/mock_test.go'sterminateListenConnection. Be aware its PID lookup matches on query text globally, not "this test's connection specifically" - check for other live processes (e.g. a runningmake dev) before running a test like this against a shared database.
Auth
- Real auth is Auth0-backed OIDC (
domains/authentication). For local dev, setDEV_AUTH_ENABLED=trueand hitGET /api/auth/dev-login?user_id=whoeverto mint a real session (writes directly tooauth_users/oauth_tokens, same shape a real login produces) without any Auth0 round-trip. Differentuser_idvalues let you test multiple identities/accounts side by side. Never enable this outside local development.
Commit conventions
- Commit messages: imperative mood subject line, no period, body explains why (what problem existed, what the fix changes) rather than re-describing the diff. One logical change per commit - e.g. a bugfix found while doing unrelated work gets its own commit, not folded into the original task's.
- Work summaries: see
work-summaries/for dated records of past sessions' changes and reasoning - useful context before touching an area someone else (human or agent) recently worked on.