You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #663 (feat(sell): add storefront branding profile and /api/storefront.json) introduces the first slice of storefront customization:
obol sell storefront set/show/reset
an operator-owned storefront profile with displayName, tagline, and logoUrl
controller publication of /api/storefront.json
public storefront consumption of the profile for header, metadata, manifest, favicon, and OG image
That is a useful phase 0, but we should agree on the next architecture before adding more one-off JSON fields to the Next app. The likely next requests are theme colors, hero layout, section ordering, custom copy blocks, richer service grouping, preview/publish, and maybe authenticated local editing. Those can become hard to reason about if each field is threaded directly through bespoke frontend conditionals.
A2UI looks like a good conceptual fit for the next layer because it separates:
UI structure: component tree / surface definition
data model: profile, services, identity, payment metadata, theme tokens
actions: user interactions that can be validated before mutating state
I reproduced a failure with logoUrl: "/custom.png": /opengraph-image returns an error because ImageResponse requires an absolute image URL or data URL in that context.
Possible fixes:
Resolve relative logo paths against the public site URL before rendering OG images.
Or reject relative custom logos until we add explicit asset hosting.
Or distinguish logoUrl from stored asset references and let the controller publish fully resolved asset URLs only.
2. gofmt needed
internal/serviceoffercontroller/controller.go compiles, but gofmt -d reports alignment changes around the new storefrontProfileInformer fields and initializer.
Validation already run on PR head e3510bd
go test ./cmd/obol ./internal/storefront ./internal/serviceoffercontroller -count=1
We need storefront customization without turning the public storefront into a loose, operator-supplied HTML/React execution surface.
The storefront is public on the tunnel hostname. It should remain constrained, predictable, safe to cache, and easy for buyers/agents to consume. At the same time, sellers should be able to differentiate their storefront beyond just display name, tagline, and logo.
If we keep extending /api/storefront.json as a flat profile object consumed ad hoc by Next components, we risk:
tight coupling between config shape and frontend implementation details
no clear way to preview or validate changes before publishing
no stable contract for future non-Next renderers or agent-facing clients
ambiguous ownership between ServiceOffer listing metadata, AgentIdentity metadata, and seller-wide storefront metadata
Keep the public storefront renderer component-catalog based, not arbitrary code based.
Allow the controller to resolve a public, cacheable snapshot from multiple sources: storefront config, ServiceOffers, AgentIdentity, tunnel URL, defaults.
Support a future local editor/preview flow without needing to redesign the data contract.
Make the contract versioned from the start.
Non-goals
Do not allow arbitrary HTML, JavaScript, or remote React components from operator config.
Do not expose the local frontend/admin UI on the public tunnel hostname.
Do not move service pricing, settlement, or x402 verifier behavior into storefront config.
Do not make the public storefront depend on live write access to Kubernetes.
Do not require streaming for the first implementation. Static JSON is enough for phase 1.
Proposed architecture
operator CLI / future local editor
|
v
StorefrontConfig desired state
|
v
serviceoffer-controller
|
+--> StorefrontSnapshot
| - resolved profile
| - resolved theme tokens
| - resolved layout config
| - service catalog projection
| - identity / trust / payment metadata
| - validated public asset URLs
|
+--> /api/storefront.json
+--> /api/storefront.surface.json or /api/storefront.surface.jsonl
+--> /api/services.json
|
v
Next public storefront renderer
|
v
safe Obol storefront component catalog
1. StorefrontConfig: operator-owned desired state
This is what the CLI or future local editor mutates. It should remain sparse and intent-oriented, not fully resolved.
Longer term: a StorefrontConfig CRD if we need status conditions, validation, ownership, preview state, or multiple drafts.
2. StorefrontSnapshot: controller-resolved public state
The controller should resolve all defaults and external references into a public snapshot.
Inputs:
StorefrontConfig desired state
Ready/draining ServiceOffers
AgentIdentity and ERC-8004 registration state
tunnel/base URL from obol-stack-config
static defaults from the codebase
validated assets
Outputs:
/api/storefront.json: compact profile/snapshot for simple clients and metadata
/api/storefront.surface.json: component/data contract for the renderer
/api/services.json: current service catalog, as today
Important behavior:
Add schemaVersion now.
Distinguish sparse desired state from resolved published state.
Publish fully resolved asset URLs for public render paths.
Keep empty values meaningful where needed, so operators can clear a single field without resetting everything.
Add status/validation errors before publishing if we move to CRD.
3. Obol Storefront Catalog
Use an Obol-owned catalog of safe, known components instead of arbitrary UI. This can be A2UI-compatible without exposing arbitrary component execution.
Example catalog components:
StorefrontShell
Header
Hero
ServiceSection
ServiceCard
PaymentBadges
TrustBanner
IdentitySummary
MarkdownBlock with a safe markdown subset
FooterLinks
EmptyState
Component properties should bind to data model paths, not duplicate all data inline.
Example data paths:
/profile/displayName
/profile/tagline
/profile/logoUrl
/theme/accent
/services
/servicesBySection/demo
/identity/agentId
/trust/supportedTrust
4. A2UI-style surface endpoint
A2UI v0.9-style messages would let us define the storefront as a surface:
{"version":"v0.9","createSurface":{"surfaceId":"storefront","catalogId":"https://obol.org/catalogs/storefront/v1.json"}}
{"version":"v0.9","updateDataModel":{"surfaceId":"storefront","path":"/profile","value":{"displayName":"Acme Agents","tagline":"Paid inference and API services","logoUrl":"https://cdn.example.com/logo.png"}}}
{"version":"v0.9","updateDataModel":{"surfaceId":"storefront","path":"/services","value":[/* service catalog projection */]}}
{"version":"v0.9","updateComponents":{"surfaceId":"storefront","components":[{"id":"root","component":"StorefrontShell","children":["header","hero","sections","footer"]},{"id":"header","component":"Header","profile":{"path":"/profile"}},{"id":"hero","component":"Hero","title":"Agent services","subtitle":{"path":"/profile/tagline"}},{"id":"sections","component":"ServiceSections","services":{"path":"/services"}},{"id":"footer","component":"FooterLinks","links":{"path":"/content/footerLinks"}}]}}
For phase 1, this can be a single static JSON array or JSONL document served over HTTP. SSE/WebSocket can wait until we have live editor preview or progressive updates.
Why this helps:
layout is data, not hard-coded branches spread across Next pages
the renderer remains constrained to known Obol components
future editor previews can render the same surface before publish
the catalog becomes the compatibility boundary
service data can update independently from the component tree
5. Public Next renderer
The existing Next app remains the public storefront runtime. It should become a renderer for the Obol storefront catalog.
Responsibilities:
fetch /api/storefront.surface.json or /api/storefront.surface.jsonl
validate message schema and catalog IDs
render only known components
continue to support current /api/services.json polling during migration
keep metadata/manifest/OG generation backed by resolved StorefrontSnapshot
fail closed to a default storefront if the surface is malformed
6. Actions and future editor flow
Public storefront actions should be limited to safe interactions:
open service endpoint
copy endpoint
open docs
filter/sort client-side
A future local authenticated editor could support write actions:
update profile
update theme
reorder sections
preview draft
publish draft
reset to defaults
Those editor actions should target a local-only endpoint or CLI-backed workflow, not the public tunnel route.
Context
PR #663 (
feat(sell): add storefront branding profile and /api/storefront.json) introduces the first slice of storefront customization:obol sell storefront set/show/resetdisplayName,tagline, andlogoUrl/api/storefront.jsonThat is a useful phase 0, but we should agree on the next architecture before adding more one-off JSON fields to the Next app. The likely next requests are theme colors, hero layout, section ordering, custom copy blocks, richer service grouping, preview/publish, and maybe authenticated local editing. Those can become hard to reason about if each field is threaded directly through bespoke frontend conditionals.
A2UI looks like a good conceptual fit for the next layer because it separates:
References:
Current PR review notes to address before or alongside this direction
1. Relative custom logos break
/opengraph-imageThe CLI currently allows site-relative logo URLs, for example
/custom.png, viaValidateLogoURL:obol-stack/internal/storefront/profile.go
Lines 86 to 98 in e3510bd
The OG route then passes
storefront.logoUrldirectly toImageResponse:obol-stack/web/public-storefront/src/app/opengraph-image.tsx
Lines 77 to 82 in e3510bd
I reproduced a failure with
logoUrl: "/custom.png":/opengraph-imagereturns an error becauseImageResponserequires an absolute image URL or data URL in that context.Possible fixes:
logoUrlfrom stored asset references and let the controller publish fully resolved asset URLs only.2.
gofmtneededinternal/serviceoffercontroller/controller.gocompiles, butgofmt -dreports alignment changes around the newstorefrontProfileInformerfields and initializer.Validation already run on PR head
e3510bdPassed.
Passed.
npm cireported existing dependency audit findings: 1 moderate, 1 high.Problem statement
We need storefront customization without turning the public storefront into a loose, operator-supplied HTML/React execution surface.
The storefront is public on the tunnel hostname. It should remain constrained, predictable, safe to cache, and easy for buyers/agents to consume. At the same time, sellers should be able to differentiate their storefront beyond just display name, tagline, and logo.
If we keep extending
/api/storefront.jsonas a flat profile object consumed ad hoc by Next components, we risk:Goals
/,/services/*,/skill.md,/api/services.json,/api/storefront.json, discovery docsNon-goals
Proposed architecture
1.
StorefrontConfig: operator-owned desired stateThis is what the CLI or future local editor mutates. It should remain sparse and intent-oriented, not fully resolved.
Possible shape:
{ "schemaVersion": "storefront.obol.org/v1alpha1", "profile": { "displayName": "Acme Agents", "tagline": "Paid inference and API services", "logo": { "source": "url", "url": "https://cdn.example.com/logo.png" } }, "theme": { "mode": "dark", "accent": "#2FE4AB", "background": "#091011" }, "layout": { "template": "marketplace", "hero": { "title": "Agent services", "subtitlePath": "/profile/tagline" }, "sections": [ { "id": "featured", "title": "Featured", "filter": { "featured": true } }, { "id": "demo", "title": "Demos", "filter": { "category": "demo" } }, { "id": "all", "title": "All services", "filter": {} } ] }, "content": { "footerLinks": [ { "label": "Docs", "href": "/skill.md" }, { "label": "Registration", "href": "/.well-known/agent-registration.json" } ] } }Initial storage options:
x402, same operational model as PR feat(sell): add storefront branding profile #663.StorefrontConfigCRD if we need status conditions, validation, ownership, preview state, or multiple drafts.2.
StorefrontSnapshot: controller-resolved public stateThe controller should resolve all defaults and external references into a public snapshot.
Inputs:
obol-stack-configOutputs:
/api/storefront.json: compact profile/snapshot for simple clients and metadata/api/storefront.surface.json: component/data contract for the renderer/api/services.json: current service catalog, as todayImportant behavior:
schemaVersionnow.3. Obol Storefront Catalog
Use an Obol-owned catalog of safe, known components instead of arbitrary UI. This can be A2UI-compatible without exposing arbitrary component execution.
Example catalog components:
StorefrontShellHeaderHeroServiceSectionServiceCardPaymentBadgesTrustBannerIdentitySummaryMarkdownBlockwith a safe markdown subsetFooterLinksEmptyStateComponent properties should bind to data model paths, not duplicate all data inline.
Example data paths:
/profile/displayName/profile/tagline/profile/logoUrl/theme/accent/services/servicesBySection/demo/identity/agentId/trust/supportedTrust4. A2UI-style surface endpoint
A2UI v0.9-style messages would let us define the storefront as a surface:
{"version":"v0.9","createSurface":{"surfaceId":"storefront","catalogId":"https://obol.org/catalogs/storefront/v1.json"}} {"version":"v0.9","updateDataModel":{"surfaceId":"storefront","path":"/profile","value":{"displayName":"Acme Agents","tagline":"Paid inference and API services","logoUrl":"https://cdn.example.com/logo.png"}}} {"version":"v0.9","updateDataModel":{"surfaceId":"storefront","path":"/services","value":[/* service catalog projection */]}} {"version":"v0.9","updateComponents":{"surfaceId":"storefront","components":[{"id":"root","component":"StorefrontShell","children":["header","hero","sections","footer"]},{"id":"header","component":"Header","profile":{"path":"/profile"}},{"id":"hero","component":"Hero","title":"Agent services","subtitle":{"path":"/profile/tagline"}},{"id":"sections","component":"ServiceSections","services":{"path":"/services"}},{"id":"footer","component":"FooterLinks","links":{"path":"/content/footerLinks"}}]}}For phase 1, this can be a single static JSON array or JSONL document served over HTTP. SSE/WebSocket can wait until we have live editor preview or progressive updates.
Why this helps:
5. Public Next renderer
The existing Next app remains the public storefront runtime. It should become a renderer for the Obol storefront catalog.
Responsibilities:
/api/storefront.surface.jsonor/api/storefront.surface.jsonl/api/services.jsonpolling during migrationStorefrontSnapshot6. Actions and future editor flow
Public storefront actions should be limited to safe interactions:
A future local authenticated editor could support write actions:
Those editor actions should target a local-only endpoint or CLI-backed workflow, not the public tunnel route.
Suggested implementation phases
Phase 0: tighten PR #663
schemaVersionto/api/storefront.jsonif low-risk.Phase 1: versioned snapshot
internal/storefront/schema.gowith typed desired vs published structs.StorefrontConfigandStorefrontSnapshotbuilders./api/storefront.jsonwhile preserving the existing flat fields for compatibility.Phase 2: Obol catalog and static surface
web/public-storefront/src/catalog/obol-storefront-v1.ts./api/storefront.surface.jsonor/api/storefront.surface.jsonlfrom the controller./api/services.jsonfor service refresh.Phase 3: preview and local editor
obol sell storefront previewto render a draft locally or publish a temporary draft endpoint.Phase 4: optional live updates
Security and validation constraints
Open questions for validation
Product / UX
minimal,marketplace,agent-profile,docs-first?ServiceOffer.spec.listing, storefront config, or both?Frontend
/api/storefront.surface.jsonbe a JSON array or JSONL from day one?Controller / Kubernetes
StorefrontConfigCRD early?status.conditionsfor validation and publish state?Security
Compatibility
/api/storefront.jsonfields remain stable?Proposed acceptance criteria for the architecture spike
Proposed prototype deliverable
A follow-up PR could include:
internal/storefronttyped schema withschemaVersion/api/storefront.surface.jsonweb/public-storefrontrenderer for 5 to 7 catalog components