Replace Swagger UI with Scalar, add a consumer-first API reference#114
Replace Swagger UI with Scalar, add a consumer-first API reference#114mo4islona wants to merge 1 commit into
Conversation
define-null
left a comment
There was a problem hiding this comment.
I did a quick look and left some suggestions for improvements.
|
|
||
| ### Data sources | ||
|
|
||
| There are two: **archival** (SQD Network) and **real-time** (your RPC, via HotblocksDB). |
There was a problem hiding this comment.
A bit confused by the naming "Your RPC". Is that the right context for the customer? Your RPC provider maybe?
There was a problem hiding this comment.
This data-sources section was operator-facing and has been removed — the API-reference intro is now consumer-only and no longer mentions RPC providers or staking. That context now belongs with the self-hosting docs.
|
|
||
| Serves this API from SQD Network and, optionally, routes requests for the latest blocks to HotblocksDB. | ||
|
|
||
| Downloads an assignment file at startup and periodically re-downloads it. Needs an Arbitrum RPC endpoint (and an Ethereum L1 RPC) to read on-chain state. |
There was a problem hiding this comment.
Is the assignment file information relevant here for the API ? Looks like an implementation detail for me, which the customer does not directly control.
There was a problem hiding this comment.
Agreed — that's an implementation detail the consumer doesn't control. It's no longer in the reference intro (the operator/running content was removed).
| - If `parentBlockHash` matches the parent the portal sees for `fromBlock` → the stream starts normally. | ||
| - If it does **not** match → a fork happened between the client's last seen block and `fromBlock`. The portal returns `409 Conflict`. | ||
|
|
||
| ### What 409 means |
There was a problem hiding this comment.
Instead of describing what 409 mean, I would reframe it to describe what the conflict is. That way we do not focus just on a single status code, but frame it on a more high-level.
In other words I would rename this title and other titles something like: "What a conflict is?" or "What a conflict means for the data consistency", or other appropriate formulation.
There was a problem hiding this comment.
Done — there's now a What a conflict means section that frames it as the client's chain having diverged from the canonical chain, rather than leading with the status code; the 409 is mentioned only as the mechanism that surfaces it.
|
|
||
| If the client tries to resume at `fromBlock = N+2` with `parentBlockHash = hash(N+1)` (orphaned), the portal sees that `N+1` is no longer the parent of `N+2'` on the canonical chain → it answers 409. | ||
|
|
||
| ### 409 response body |
There was a problem hiding this comment.
Instead of duplicating documentation here, I would just reference to the 409 reponse body for the particular API. Otherwise those two may diverge.
There was a problem hiding this comment.
Good call on the divergence risk. The /stream 409 now has a documented response body in this PR (a ConflictResponse schema with previousBlocks), so the prose "Conflict response body" section can reference that instead of duplicating the shape — worth trimming so the two can't drift.
|
|
||
| ### Recommended client behaviour | ||
|
|
||
| When you receive a 409: |
There was a problem hiding this comment.
Similarly - when the conflict occurs, or when the reorg occures. In terminology of the domain language, not implementation.
There was a problem hiding this comment.
The forks section now leads with the concept ("What a conflict means") and uses domain terms — conflict / reorg — consistently. Let me know if a particular spot still reads implementation-first.
| Always pass `parentBlockHash` when resuming a stream. A client that does **not** pass it will silently process blocks from a forked chain after a reorg, with no signal that anything is wrong. | ||
|
|
||
|
|
||
| ## Self-hosting |
There was a problem hiding this comment.
I would suggest to move it out into a separate doc.
There was a problem hiding this comment.
Done — the single intro was split into docs/openapi/{01-introduction,02-blockchain-forks,03-getting-started}.md, and the architecture + self-hosting sections were moved out of the API reference entirely (the README is the self-hosting entry point). Hope that covers what you had in mind here.
|
|
||
| ### Real-time only | ||
|
|
||
| Stream the chain tip from your own RPC. No SQD tokens needed. Best for fresh-data use cases on small chains. |
There was a problem hiding this comment.
I don't know what is meant by a chain tip. Maybe clarify that? Or is it a common term?
There was a problem hiding this comment.
The rewrite introduces it as "the most recent blocks … the tip of the chain", so it's glossed on first use. Happy to add an explicit one-line definition of "chain tip" if it still reads as jargon.
| **1. Hotblocks service** (per RPC), **2. HotblocksDB** with `Api` retention, **3. Hotblocks-retain** to coordinate retention with the network, **4.** portal key, **5.** portal config: | ||
|
|
||
| ```yaml | ||
| # mainnet.config.yml | ||
| hostname: http://0.0.0.0:8080 | ||
| hotblocksDB: http://hotblocks-db:8081 | ||
| sqd_network: | ||
| datasets: https://cdn.subsquid.io/sqd-network/datasets.yml | ||
| metadata: https://cdn.subsquid.io/sqd-network/mainnet/metadata.yml | ||
| serve: "manual" | ||
| datasets: | ||
| ethereum-mainnet: | ||
| real_time: | ||
| kind: evm | ||
| ``` | ||
|
|
||
| **6.** Save `mainnet.env` as `.env`. **7.** Run the portal (same command as in **Archival only**). **8.** Verify: `curl localhost:8080/datasets/ethereum-mainnet` reports `real_time: true` and `start_block: 0`. |
There was a problem hiding this comment.
This renders poorly, we need to have each ** on a separate line.
There was a problem hiding this comment.
I believe this was in the self-hosting "Suitable when" lists — that whole section has been removed from the reference, so the rendering issue should be moot now. Let me know if it was elsewhere.
| /// #[utoipa::path( | ||
| /// ..., | ||
| /// extensions(("x-internal" = json!(true))), | ||
| /// )] |
There was a problem hiding this comment.
Unless I read the comment on the left of the diff it wasn't clear what we are talking about here. I suggest we rephrase to something like: "In order to mark the api as internal.. "
There was a problem hiding this comment.
Done — the marker is now documented in src/openapi.rs on INTERNAL_EXT_KEY, rephrased to: "To mark an API operation as internal — so it is pruned from the served OpenAPI spec unless show_internal is true — add this extension on its handler."
| const INTERNAL_MARKER: &str = "[INTERNAL]"; | ||
| const INTERNAL_EXT_KEY: &str = "x-internal"; | ||
|
|
||
| fn build_openapi_spec(show_internal: bool) -> utoipa::openapi::OpenApi { |
There was a problem hiding this comment.
I suggest we move this and some other openapi specific functions to a separate module.
There was a problem hiding this comment.
Done — build_openapi_spec and serve_openapi_spec (along with the x-internal filtering and the ApiDoc definition) now live in src/openapi.rs.
Serve the OpenAPI reference with Scalar (utoipa v5 + utoipa-scalar) at /docs, replacing Swagger UI, and ship a consumer-first Introduction. - Renderer: Scalar at /docs via a custom HTML/CSS template (docs/openapi/scalar_template.html); /api-docs/openapi.json served by serve_openapi_spec; utoipa-swagger-ui removed. - Introduction (docs/openapi/, concatenated into info.description): 01-introduction (orientation + finalized vs. real-time data model), 02-blockchain-forks (parentBlockHash / 409 protocol), 03-getting-started (first query + README pointer). Architecture/self-hosting content is kept out of the reference. - Internal-endpoint filtering via the x-internal OpenAPI extension; build_openapi_spec(show_internal) drops internal ops and empty tags while preserving doc-only tags. Toggle with SHOW_INTERNAL_DOCS. Datasets/Streaming shown by default; Monitoring/Debug hidden. - /stream 409 documents a ConflictResponse body (previousBlocks). - Move graceful_shutdown.md under docs/decisions/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| 3. If `previousBlocks` does not contain any block you recognise (the divergence is deeper than the array provided), * | ||
| *re-request earlier blocks** — open a new stream that includes a `fromBlock` further in the past — and repeat the |
Summary
/docs(viautoipa-scalar+ a custom HTML/CSS template), serving an OpenAPI spec built with utoipa v5 (preserve_path_order, so the sidebar order matchespaths(...)).info.descriptionto ship a consumer-first Introduction: where you landed → finalized vs. real-time data model → blockchain forks /parentBlockHash→ first query. Operator material (architecture internals, self-hosting topologies) is intentionally kept out of the API reference.x-internal) and hidden unlessSHOW_INTERNAL_DOCS=true.What changed
Renderer
utoipa-scalar 0.2mounted at/docswithScalar::custom_html(...); template indocs/openapi/scalar_template.html(themefastify;hiddenClients,hideModels,defaultOpenAllTags; developer tools disabled).utoipa-swagger-uiremoved;/api-docs/openapi.jsonserved by a smallserve_openapi_spechandler (the spec is injected as anaxum::Extension).API reference Introduction (
docs/openapi/)Consumer-first, three sections concatenated into
info.descriptionviainclude_str!insrc/openapi.rs:01-introduction.md— orientation (a portal is the HTTP gateway you query) + the finalized vs. real-time data model, with a simplified merge diagram.02-blockchain-forks.md— theparentBlockHashprotocol, what a 409 means, a reorg diagram, the conflict response body, and the recommended client recovery loop.03-getting-started.md— a minimal first/streamquery (+ link to the full SQD query syntax) and a one-line pointer to the README for self-hosting.Rendered order: orientation → data model → forks → first query → self-host pointer. The 409 response on
/streamlinks into the forks section via#description/how-parentblockhash-works./stream409 responsepreviousBlocksguarantee, walk-back procedure).ConflictResponseschema (previousBlocks: [{ number, hash }]) with an example, so the rendered docs show the body instead of "No Body".Internal-endpoint filtering
#[utoipa::path]s carry the standard OpenAPIx-internal: trueextension (replacing the old[INTERNAL]doc-comment marker).build_openapi_spec(show_internal)drops marked operations and prunes now-empty tag groups, while preserving doc-only tags.SHOW_INTERNAL_DOCS=true/--show-internal-docs. Two unit tests insrc/openapi.rsassert the filter behaviour on the realApiDoc.Other
graceful_shutdown.mdmoved underdocs/decisions/.Test plan
cargo buildcargo test --lib openapi::— internal-filter tests pass (hide_internal_drops_marked_ops_and_empty_tags,show_internal_keeps_all_ops)cargo run --bin gen_openapiproduces the full unfiltered spec (withx-internalextensions visible)/docs:/stream409 shows thepreviousBlocksbody schema + example (not "No Body")/api-docs/openapi.jsonreturns the same filtered specSHOW_INTERNAL_DOCS=true, confirm Monitoring / Debug groups appear🤖 Generated with Claude Code