ObolNetwork / ObolNetwork/obol-stack
Architecture proposal: customizable public storefront via A2UI-style surface contract
Nobody has claimed this yet.
- Dominant language
- Go
- Stars
- 11
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
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/reset- an operator-owned storefront profile with
displayName,tagline, andlogoUrl - 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
- transport: REST, JSONL, SSE, WebSocket, etc.
References:
- A2UI data flow: https://a2ui.org/concepts/data-flow/
- A2UI message format: https://a2ui.org/concepts/data-flow/#message-format
- Related implementation under review: https://github.com/ObolNetwork/obol-stack/pull/663
Current PR review notes to address before or alongside this direction
1. Relative custom logos break /opengraph-image
The CLI currently allows site-relative logo URLs, for example /custom.png, via ValidateLogoURL:
The OG route then passes storefront.logoUrl directly to ImageResponse:
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
logoUrlfrom 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
Passed.
cd web/public-storefront
npm ci
npm run build
Passed. npm ci reported 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.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
- unsafe future pressure toward custom HTML/CSS/script
- hard-to-test rendering branches as fields multiply
Goals
- Let operators customize the public storefront in a structured, validated way.
- Keep payment/routing/security behavior separate from presentation.
- Preserve existing public/private route boundaries:
- public:
/,/services/*,/skill.md,/api/services.json,/api/storefront.json, discovery docs - local-only: internal frontend, eRPC, dashboards, admin surfaces
- public:
- 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.
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:
- Short term: ConfigMap in
x402, same operational model as PR #663. - Longer term: a
StorefrontConfigCRD 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
schemaVersionnow. - 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:
StorefrontShellHeaderHeroServiceSectionServiceCardPaymentBadgesTrustBannerIdentitySummaryMarkdownBlockwith a safe markdown subsetFooterLinksEmptyState
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.jsonor/api/storefront.surface.jsonl - validate message schema and catalog IDs
- render only known components
- continue to support current
/api/services.jsonpolling 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.
Suggested implementation phases
Phase 0: tighten PR #663
- Fix relative logo handling in OG image rendering.
- Run gofmt.
- Add
schemaVersionto/api/storefront.jsonif low-risk. - Add a test case for relative logo URL behavior.
- Add a test that storefront publication resolves public/default logo URLs consistently.
Phase 1: versioned snapshot
- Introduce
internal/storefront/schema.gowith typed desired vs published structs. - Keep ConfigMap storage, but make the payload versioned.
- Add
StorefrontConfigandStorefrontSnapshotbuilders. - Publish a richer
/api/storefront.jsonwhile preserving the existing flat fields for compatibility. - Define clear merge semantics for partial updates and field clearing.
Phase 2: Obol catalog and static surface
- Define
web/public-storefront/src/catalog/obol-storefront-v1.ts. - Add
/api/storefront.surface.jsonor/api/storefront.surface.jsonlfrom the controller. - Build a small renderer that maps catalog components to existing React components.
- Use the surface for page layout while still using
/api/services.jsonfor service refresh. - Add malformed-surface fallback tests.
Phase 3: preview and local editor
- Add
obol sell storefront previewto render a draft locally or publish a temporary draft endpoint. - Optionally add a local-only editor in the internal frontend.
- Add action handling for preview/publish/reset.
- Consider CRD migration if we need status conditions or draft/published state.
Phase 4: optional live updates
- If useful, add SSE or WebSocket transport for live preview.
- Keep public storefront static/request-response unless there is a concrete need for streaming.
Security and validation constraints
- No arbitrary HTML, script, remote React, or iframe components from operator config.
- Remote images must be validated and rendered safely. Prefer resolved public URLs or managed uploaded assets.
- Keep public routes intentionally limited. Do not expose local frontend/admin routes through the tunnel hostname.
- Theme tokens should be validated against contrast/accessibility constraints where practical.
- Markdown content, if allowed, should use a small safe subset and sanitized links.
- Surface renderer should reject unknown catalog IDs, unknown components, and invalid data bindings.
- Metadata and OG routes should degrade to defaults rather than returning 500s.
Open questions for validation
Product / UX
- What is the minimum useful customization set beyond name/tagline/logo?
- Do we want templates such as
minimal,marketplace,agent-profile,docs-first? - Should sellers be able to add custom markdown sections, or only structured fields?
- Should service grouping/order live in
ServiceOffer.spec.listing, storefront config, or both?
Frontend
- Should the Next app render from A2UI-style messages directly, or should we compile the snapshot into a simpler internal view model first?
- Should
/api/storefront.surface.jsonbe a JSON array or JSONL from day one? - How much of the existing components can be reused unchanged behind a renderer adapter?
- What is the fallback behavior for a malformed surface?
Controller / Kubernetes
- Is ConfigMap storage enough for v1, or do we want a
StorefrontConfigCRD early? - If CRD, do we need
status.conditionsfor validation and publish state? - Should the controller publish draft and active snapshots separately?
- Should storefront profile be tied to AgentIdentity eventually?
Security
- What asset sources should be allowed for logos and images?
- Should custom external images be proxied/cached, copied into a managed asset store, or referenced directly?
- How do we prevent public metadata endpoints from becoming an accidental admin/config leak?
- Which routes need explicit hostname restrictions as this grows?
Compatibility
- How long should the flat
/api/storefront.jsonfields remain stable? - Do we expect external clients to consume storefront metadata, or only browsers?
- Should the catalog ID be a local version string, public URL, or both?
Proposed acceptance criteria for the architecture spike
- Team agrees on desired vs published state split.
- Team agrees whether v1 storage is ConfigMap or CRD.
- Team agrees on the initial Obol Storefront Catalog component set.
- Team agrees whether the surface endpoint is JSON array or JSONL.
- Team agrees how relative/custom assets are represented and resolved.
- A small prototype can render the existing PR #663 storefront from a generated surface without changing public route posture.
Proposed prototype deliverable
A follow-up PR could include:
internal/storefronttyped schema withschemaVersion- controller-generated
/api/storefront.surface.json web/public-storefrontrenderer for 5 to 7 catalog components- tests for malformed surface fallback
- tests for logo URL resolution in metadata and OG image paths
- no public write actions
- no CRD unless the team explicitly chooses that path
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading PR #663 and the existing paths in internal/storefront/profile.go, internal/serviceoffercontroller/controller.go, and web/public-storefront/src/app/opengraph-image.tsx. Review the proposed StorefrontConfig, StorefrontSnapshot, catalog, and surface endpoint phases before choosing a bounded implementation. Done requires an agreed, versioned contract and an explicitly scoped phase with validation and tests.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go, kubernetes, next.js
- Domain
- backend-api-design, infrastructure, web-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 32/100