Autonnel v0.1.0
Back to blog

Picocart revisited: what replacing Shopify in development taught us

A follow-up after building Picocart: what Shopify API compatibility actually bought, where the compatibility illusion leaks, and when to stay put.

· 9 min read

In May I wrote why Picocart exists when Medusa already does. That post was an argument made in advance: Autonnel only touches a small slice of a commerce backend, so build that slice on the same edge stack rather than operate a second runtime. I still believe the argument. This post is the part I could not write then, which is what happened when the thing got built.

Some of it went better than I predicted. One fairly central prediction was simply wrong. And there is a category of problem I did not anticipate at all, which is what happens when “API-compatible” is a claim you have to keep being honest about.

The compatibility bet paid off harder than I described

The old post said the Picocart adapter in Autonnel would be “essentially the Shopify adapter with a different base URL and a thin transform layer.” I described a picocart.ts sitting next to shopify.ts.

There is no picocart.ts. There is no Picocart adapter at all.

In src/modules/commerce-gateway/infra/backends/backend-resolver.ts, the provider kinds shopify and picocart fall into the same branch and construct the same ShopifyGraphqlClient, the same catalog adapter, the same order adapter, the same fulfillment adapter. The entire Picocart-specific surface in Autonnel is one function in backend-credentials.ts that strips the protocol and any trailing slash off the configured base URL, because the Shopify client builds its own https:// prefix. That function is three lines. Everything else is the Shopify path.

That is a better outcome than a thin adapter, and it changes the maintenance story. There is no second code path to keep in sync, so Picocart cannot drift from Shopify by accident inside Autonnel. It can only drift on the Picocart side, where it is visible.

Credentials settled as baseUrl, apiVersion, and apiKey in Settings → Ecommerce, with notifications forced off for Picocart since Picocart sends no email. The ecommerce adapter docs cover the rest of the configuration.

The prediction that was wrong: REST became GraphQL

The old post was specific and confident about the wire format. It said Picocart implements “the subset of the Shopify Admin REST API,” with URLs like /admin/api/2024-01/products.json and /admin/api/2024-01/orders.json.

None of those exist. Picocart has exactly one external route: POST /admin/api/2024-01/graphql.json.

The reason is not that REST turned out to be a bad idea. It is that I was compatible with the wrong thing. Autonnel’s Shopify integration is a GraphQL client, reading contextualPricing for per-country prices, plus priceRangeV2, displayFinancialStatus, media(first: 5). A REST surface would have made Picocart compatible with a Shopify API that Autonnel does not speak, and the “no integration work” promise would have quietly become “write a REST adapter.”

The part of the prediction that survived intact was authentication. X-Shopify-Access-Token is still the header, tokens still live in the ApiKey table, and mutations still require write access on the key. Authorization is accepted as an alternative. That part transferred exactly as designed.

The general lesson is that “compatible with Shopify” is not a target. “Compatible with the exact operations one specific client sends” is. Those are very different projects, and only the second one is finishable.

Where the compatibility illusion leaks

Picocart does not contain a GraphQL server. There is no schema, no parser, no execution engine, no field resolution.

src/lib/shopify/graphql/router.ts extracts the operation name with a regular expression and looks it up in a table of twelve entries. handler.ts calls the matching resolver with the raw variables. The response is built by hand in projectors.ts, whose own comment is the most honest line in the codebase: each object emits “the SUPERSET of fields requested by autonnel’s two clients,” so a single resolver serves both and each client reads the subset it selected.

That is the whole trick, and it is worth being explicit about what it costs.

Field selection is not honoured. Picocart returns what the projector emits, not what you asked for. Ask for a field that exists in Shopify and not in the projector and you do not get a schema error, you get undefined, which surfaces later as a null in a funnel page. Ask for fewer fields and you still pay for all of them.

Operation names are the API. Rename a query in the client from Products to ListProducts and Picocart answers Unsupported operation: ListProducts. The client and server are coupled through a string that no type system checks.

Shopify’s query syntax is faked. The query: filter argument on products is handled by stripping title: and * characters and doing a lowercase substring match on the title. That covers the search Autonnel actually performs and nothing else. The filter also runs after the page slice, so a filtered page can return fewer rows than the page size while still reporting hasNextPage from the unfiltered set.

Cursors are row ids in a costume. endCursor is a gid://shopify/Product/{cuid} for the last row, and paging is a sinceId lookup. It round-trips correctly and it is not an opaque Shopify cursor.

None of this is a problem for the client Picocart was built for. All of it is a problem the moment someone points a general-purpose Shopify tool at it, which is a thing “Shopify API-compatible” on the Picocart page invites people to try. The compatibility is real at the level of one client’s traffic and a marketing claim at any level above that.

The “ten endpoints” were twelve operations, and five are live

The old post made a point of saying Autonnel touches “roughly ten endpoints. Not ten categories of endpoints, ten endpoints.”

The operation table has twelve entries: Products, SearchProducts, Product, ProductVariants, Variant, VariantInventory, Order, OrderFulfillments, OrderCreate, DraftOrderCreate, DraftOrderComplete, RefundCreate. The count was close. What the count hid is the distribution.

The Autonnel commerce gateway that ships today issues five of them: Products, OrderFulfillments, OrderCreate, DraftOrderCreate, DraftOrderComplete. The other seven exist because Picocart’s golden tests carry two generations of Autonnel’s client verbatim, an older one that reads scalar price through edges, and the current one that reads contextualPricing through nodes. Compatibility is not one target, it is one target per version of the client you still support, and nobody writes that down in the endpoint count.

Two of the five are also stranger than an endpoint list suggests. There is no DraftOrder table in the schema. DraftOrderCreate base64url-encodes the entire order input into the returned id, and DraftOrderComplete decodes it and writes a paid order. It satisfies Autonnel’s two-step handoff exactly and it means drafts have no persistence, no listing, and no lifecycle. That is a legitimate design given what Autonnel needs. It is not what a Shopify user means by a draft order.

The synthesized parts follow the same pattern. inventoryPolicy is derived from a single boolean on the product, DENY when inventory is tracked and CONTINUE when it is not. Fulfillments are at most one node, built from the order’s trackingNumber and trackingCompany columns, empty while unfulfilled. Real Shopify gives you multiple fulfillments per order.

Running two Workers and two databases

The operational claim in the old post was symmetry: same runtime, same wrangler deploy, same Hyperdrive Postgres model, nothing new on the operational surface. That held. Picocart is Astro 6 on Prisma 7 with a wrangler.toml.template that binds HYPERDRIVE and a CACHE_KV namespace, and its Prisma client is per-request through AsyncLocalStorage on Workers, the same pattern this site uses.

What symmetry does not mean is that it feels like one thing. It is two astro build runs against two config files, two Prisma schemas, two migration histories, two secret sets. Picocart runs on port 4322 locally to stay clear of Autonnel’s 4321 and uses a picocart_session cookie so both can be open in one browser. Those details are small individually and they add up to a second deployment you have to remember exists.

The sharpest local-development friction was the least interesting bug. ShopifyGraphqlClient hardcodes https:// when it builds the request URL, because Shopify is always HTTPS. That means Autonnel cannot be pointed at a plain http://localhost:4322. Local Picocart development runs through a tunnel with a real hostname. It is a two-character constant creating a permanent requirement.

Do not use Picocart if

Some of this repeats the old post because it is still true, and some of it is new information from the build.

You want a general Shopify-compatible backend. Compatibility covers twelve named operations with hand-built responses and no schema. Any Shopify app, CLI, or library that issues its own queries will fail on the first unknown operation name.

You need tax, shipping rates, or discount codes computed. The order table has taxPrice, shippingPrice, and discountPrice columns that default to zero and store whatever you pass in. Nothing computes them. There is no discount code entity at all.

You need webhooks. Picocart emits none. Autonnel polls, which is fine for Autonnel and useless if you wanted event-driven integration.

You need multi-location inventory or collections. The schema has eighteen models and none of them are Location, InventoryLevel, Collection, or MetaField. Inventory is one integer per variant.

You need Shopify Markets pricing. Prices are one row per variant per currency, and country to currency resolution is a static map of common markets with a config override. There is no country-based routing, no rounding rules, no price lists.

You have a working store. Stay on Shopify or WooCommerce. Autonnel’s adapter for both is the production-tested path, and the funnel builder for Shopify page describes that setup. Picocart is version 0.0.1. Switching commerce backends also invalidates the external order references on any open order, which is a real migration and not a config change.

The honest position after building it is that the compatibility bet was correct and the compatibility claim needs an asterisk. Picocart is not a Shopify replacement. It is the smallest server that can convincingly answer the questions Autonnel asks, and it is very good at exactly that. If you want to see what the funnel layer costs to run above it, that is on pricing.

  • github.com/autonnel/picocart · Apache-2.0. Issues and feedback welcome.