README had drifted: an unresolved merge-conflict marker left over from a past edit, a directory-structure diagram that no longer matches the repo (/internal/site, /internal/domains/platforms/tiktok - actual layout is server/, domains/, etc per AGENTS.md), and a duplicated dev-workflow blurb that AGENTS.md already documents more accurately. Splits the roadmap checklist out to ROADMAP.md and the Etsy API compliance checklist to domains/platforms/etsy/COMPLIANCE.md (next to the code it governs, where someone touching that integration will actually look for it) instead of burying both in one large README. README itself becomes a short front door with a "Where things live" index up top, since scattering docs across files only helps if there's an obvious map to them. Moves the CQRS/event-sourcing architecture note into AGENTS.md's Architecture section (with a new domains/raw_events bullet) rather than leaving it as prose in README, since AGENTS.md is the maintained engineering reference and that's where a reader would already be looking for how the domains are structured. 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/raw_events- the event-sourcing store:Saveappends anEvent,LoadEventsForStorereplays a platform+store's series. Other domains (reports,domains/accounts/mocks.go) read from views/queries that project over this event series rather than mutating their own standalone state. See also the CQRS note above.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.