← Ventures — GCStore Constellation
GCStore Shop — Level-2 Deep Dive
GCStore Shop is the company's own marketplace storefront: the public place a customer browses, configures and buys the refurbished and modified machines the LLC sells, paying directly rather than through a third-party marketplace. It is live with real products and has taken real payments end to end. Two audiences use it — anonymous shoppers (who never have to sign in to browse) and identified buyers (who authenticate to check out) — and one owner, who runs the local admin and previews listings before they go live.
Internal architecture
The storefront is two deployable services behind one edge. A Spring Boot API holds all the business logic and data, exposed as an OAuth2 resource server. A separate Angular SSR web service renders every page on the server so the catalog is fast and crawlable, then hydrates in the browser; it is also a full PWA (installable, offline shell, web push). The edge routes API and webhook traffic to the API and everything else to the SSR service, so the two scale and deploy independently.
- Options / variants engine (the centerpiece). A base listing carries options (axes such as memory or storage), each with choices; the sellable variants are the combinations, each with its own SKU, price, compare-at and stock. The buyer composes the exact machine and the catalog stays small. A variant is addressed by a key that encodes its selection; the order of axes inside that key belongs to the source system, so the storefront parses keys rather than assembling them — assuming the order would break the day it changed. A listing defaults to its cheapest in-stock variant, never merely the cheapest, so a shopper never lands on a sold-out combination. Money is a decimal string end to end — DECIMAL in the database, string on the wire, never parsed into a floating-point number.
- Catalog projection. The storefront keeps its own read model of the catalog and never calls the ERP on a page view — a page must not depend on another machine being awake. It is kept current by a notify-then-fetch loop: the ERP posts a tiny "listing changed" notification (id + version), which is recorded and de-duplicated; a scheduler then fetches the full listing and re-projects it. The notification is never treated as the data, which keeps delivery cheap and idempotent, turns a swapped photo or a corrected price into a first-class resync signal, and makes an outage a non-event — the storefront catches up by paging "changed since". A per-listing version guards against applying a stale update. Rejected alternative: pushing full listing payloads on every edit — fragile, order-sensitive, and it couples availability to delivery.
- Virtual Seller (Milton). A chat seller on every page, signed in only. There is no search tool and no lookup: the entire published catalogue — descriptions in full — is assembled into one text block and sent as a cached prefix, identical for every customer, so a single load serves the whole shop. A filter would only ever return what matched; the catalogue in full is what lets him offer the machine the customer did not know to ask for.
The block is assembled through the catalogue service rather than off the tables, because price and availability here are computed rather than stored — the figure on a card is the cheapest configuration actually in stock, found by walking the combinations. It is dropped and rebuilt on a CatalogChanged event published wherever the projection moves: the ERP sending a listing, a local listing edited or published, and every movement of stock. A long freshness window remains as a backstop, not as the mechanism.
The model never writes a number. It returns a listing id, a configuration and a pitch; name, link, image, price and availability are resolved afterwards through the same function order placement uses — which also catches a unit selling while the answer was being written, before anything reaches the customer.
Plans are Keycloak roles arriving in the token the API already validates, resolved by precedence. A plan buys a better seller, not a differently briefed one: which model answers, whether the web is reachable, and how much may be asked per day and per product. All of it is tuned from the admin, as is the persona itself, because tone is found by correcting real conversations.
Conversations are stored raw and whole, one per customer per store day, and read by person in an admin logbook. No link is recorded between a conversation and a sale: a customer asks about one machine, browses away, and buys a third. Usage is metered per answer with cache reads, cache writes and output kept apart, since they are priced an order of magnitude from one another.
- Cart & checkout. An anonymous cart lives in the browser; the moment someone signs in it merges into a server-side cart keyed to their account, so it survives logout, a new device, or a redeploy. Checkout quotes tax and payment pricing live. Tax is destination-based: resolved to the delivery address through the state authority's live lookup, with a committed quarterly rate table as fallback; destinations with no obligation are simply untaxed. Delivery addresses are verified against a public lookup. The processing fee of the card rail is passed to the buyer, framed as a discount for paying by the fee-free method.
- Payments. PayPal (which also clears cards, so cards work with no second processor): create-order then synchronous capture, with a signature-verified webhook as the durable confirmation and the channel for reversals, refunds and slow-settling e-checks. Zelle has no API by nature — the storefront shows instructions, the buyer declares payment, the owner confirms against the bank, and a reservation window holds the stock meanwhile.
- Order lifecycle & fulfilment. Reservation (stock held with an expiry) → awaiting payment → paid → shipped → delivered, with a sweeper that expires unpaid holds and returns the units. Each stage that matters to the buyer sends transactional email, and — where the buyer opted in — a web-push notification. A watchlist lets a buyer follow a variant and is transition-based: it fires once when an item reaches its last unit, sells out, or comes back, driven by real stock transitions rather than on every change.
- Sales reporting (outbox). A paid order is reported to the ERP through a transactional outbox — written in the same transaction as the order, delivered by a relay with backoff. Reporting is idempotent (create/correct, timing-agnostic), so it survives the ERP being down and can never double-count a sale. Money leaving the building is the one thing that must not be lost or duplicated.
- Cross-cutting. Every call to an external system is recorded in an integration-call audit for reconciliation (including a periodic check that the real processing fee still matches the configured estimate). The audit writer runs in its own transaction, so a failed business transaction still leaves its trail.
Interfaces
- ← ERP (catalog): consumes a versioned listing contract — a thin change-notification (authenticated system-to-system) plus a pull endpoint by id and a "changed since" paging endpoint for catch-up; projected into the shop's own read model. The storefront holds no credential that reaches the ERP's financial data.
- → ERP (sales): paid orders reported through the idempotent sales contract via the outbox.
- Media: images referenced by id, served by the ERP as immutable, resizable webp; the storefront asks for the width it renders (a content-addressed local mirror exists as optional fallback).
- ⇆ Identity (Keycloak): OIDC Authorization Code + PKCE, one public client, the shared customers realm (single sign-on across the constellation). The API verifies tokens offline against the realm's key set.
- → GCStore Core: at checkout the API resolves purchase eligibility and profile from GCStore Core, server to server.
- ⇆ PayPal: create/capture and signature-verified webhooks.
- → Anthropic (Claude API): one outbound call per customer message, with the catalogue as a cached prefix; usage metered per answer.
- → Buyer: transactional email (SMTP) and web push (VAPID); the same mailbox receives a customer's question when the seller hands it over.
Tech stack
API: Java 21, Spring Boot, Spring Security (OAuth2 resource server), Spring Data JPA, Liquibase migrations, MariaDB; web push via VAPID (BouncyCastle payload encryption); Anthropic Java SDK. Web: Angular with @angular/ssr (Node server-rendering service), signals, standalone components; PWA (manifest, service worker, installable, offline page, push). Deploy: two containers behind the shared TLS edge; MariaDB over the shared internal network; configuration by environment with fail-fast guards.
Status & roadmap
In production. The storefront is live, selling real machines, with the full path running end to end: configure → cart → checkout with live tax and fee pass-through → PayPal (real payments) and Zelle → order lifecycle → transactional email → the sales outbox back to the ERP → the integration audit. The catalog projection, the options/variants engine, the durable cart, watchlist transitions and the installable PWA with web push are all in service. The seller ships dark by default: with no key configured the widget still appears and invites the visitor to sign in, but it never reaches the model, and nothing else in the shop depends on it.
Next: in-window payment retry (a transient processor hiccup never costs the sale); faceted filters and pagination in the catalog grid; a buyer address book; templated, multi-language emails; PayPal dispute and delayed-payment events wired into the existing webhook; line-level returns; a shipping-label API with automatic tracking; consolidating the last local listings onto the ERP for a single catalog. For the seller: an enforced answer schema, an outcome per conversation computed backwards over the stored log, and letting a visitor converse before registering. Native mobile apps stay an option for later — the PWA already delivers an installable app on both platforms.