Skip to content

[Epic] Pluggable createStorefront sources — fold demo-hand-rolled infra into attestomcp-storefront #29

Description

@dzuluaga

Why

attestomcp-from-scratch is 28 lines because the storefront module hides the whole ceremony. But every real deployment re-implements the same infrastructure our shopping demo hand-rolls — persistence and a live catalog loader — because createStorefront doesn't own them. That makes "using AttestoMCP" look far heavier than the quickstart.

Goal: make createStorefront accept pluggable stores / sources so consumers never hand-write adapters, and the demo collapses back toward the 28-line quickstart.

Children

Design

One combined spec (via spec-kit) covering the shared seam — pluggable stores + catalog sources — with these invariants held:

  • In-memory stores and the static catalog array remain the zero-config defaults (quickstart unchanged).
  • Explicit store/catalog injection still supported and precedence-clear (custom-backend escape hatch).
  • Security: per-order verification scoping (invariant 4), server-side price/age re-derivation (invariant 2), catalog fail-closed on empty/unreachable load.
  • @upstash/redis / Firebase Admin stay peer/optional so in-memory/static users keep a lean install.

API contract (maintainer-owned — anchor for the spec)

The public signatures below are the contract; the spec fleshes out behavior, edge cases, and tests within them. Any change to these signatures needs maintainer (@dzuluaga) sign-off — this is a separate, explicit line in the review gate, because a published API shape is not reversible the way an implementation is.

interface CreateStorefrontOptions {
  // …existing: catalog, reviews, baseUrl, signingKey, cartStore, orderStore, … …

  // #27 persistence — where cart/order/verification state lives; in-memory is the default
  storage?: StorageAdapter;

  // #28 catalog — a SOURCE, not only a static array; the array stays the default
  catalog?: Product[] | CatalogSource;
}

// symmetric factories — the backend name lives in the helper, never the option

// #27 helper — builds cart + created/completed-order + verification stores from one handle
function redisStorage(opts: {
  url: string;
  token: string;                             // @upstash/redis-compatible (HTTP/REST)
  namespace?: string;                        // key prefix; isolates deployments on a shared instance
}): StorageAdapter;

// #28 helper — the module owns loader + TTL cache + fail-closed
function firestoreCatalog(opts: {
  credentials?: FirebaseAdminCreds;          // else GOOGLE_APPLICATION_CREDENTIALS
  collection?: string;
  ttlMs?: number;                            // default 300_000
}): CatalogSource;

interface StorageAdapter {
  // provides the four stores createStorefront needs, each scoped per order id (Security invariant 4)
}
interface CatalogSource {
  load(): Promise<Product[]>;                // fail-closed: throws on empty/unreachable cold load
}

Precedence / defaults (also maintainer-owned):

  • Explicit store injection (cartStore / createdOrderStore / orderStore / verificationStore) wins over storage — the custom-backend escape hatch.
  • No storage and no explicit stores → in-memory (zero-config default; quickstart unchanged).
  • catalog as Product[] → static (default); as CatalogSource → dynamic.
  • @upstash/redis and firebase-admin stay peer/optional deps.
  • Naming rule: options name the concept (storage, catalog); the backend appears only in the factory (redisStorage, firestoreCatalog) — so a future postgresStorage() / sqlCatalog() needs no interface change.

Related

Metadata

Metadata

Assignees

Labels

enhancementNew feature or requestepicTracking epic

Type

No type

Fields

Priority

None yet

Projects

Status
In progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions