Setup, common commands, architecture overview, and the database/testing gotchas actually hit while working in this repo recently: migration files that can drift to the point of being unrunnable (not just stale), inconsistent platform-string casing between Go constants and SQL objects, global (unscoped) mock-platform NOTIFY channels, the require.Eventually-runs-on-a-goroutine hazard, and why domains/reports' fixtures use Etsy rather than Amazon. CLAUDE.md already points here via @AGENTS.md, so no change needed there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XEDaCB7C2NEBgyvqEtZuxY
10 KiB
10 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.
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.