Skip to content

Design 16: Sub-Store as a native subscription platform - #5

Draft
lr00rl wants to merge 4 commits into
integrationfrom
docs/principal-design16-substore-native
Draft

Design 16: Sub-Store as a native subscription platform#5
lr00rl wants to merge 4 commits into
integrationfrom
docs/principal-design16-substore-native

Conversation

@lr00rl

@lr00rl lr00rl commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Specifies sub-project 1 of 6: the subscription store and the public distribution path.

Why this exists

The operator's goal moved from "integrate with a standalone Sub-Store" to "shut it down". That reverses design-substore-embed.md §7 open question 2, which had scoped v1 to conversion only — so the reversal is written down rather than left implicit.

Acceptance is deliberately not feature parity. It is: the operator can stop the standalone instance and every client that subscribed to it keeps working. That ordering puts distribution and migration ahead of operator breadth.

The load-bearing decision

The core keeps the entire public surface — route, token lookup, slug comparison, rate limit, audit, Content-Type, Subscription-Userinfo, output cache. The plugin answers exactly one question: given a subscription id, a format and a client UA, produce content.

A general http:serve capability was considered and rejected: it hands token checking, rate limiting, audit and response headers to plugin code, which is the shape the plugin-boundary review already turned down.

Two rules that exist because their opposite is the tempting default

  • A subscription response is never an empty body with HTTP 200. A proxy client that receives one deletes every node it had. Every internal failure returns non-2xx so the client keeps its previous configuration.
  • The last successful remote snapshot is durable, not cached. When a provider goes down or rotates its URL, that snapshot is the only thing still serving clients — upstream's ignore-failed-remote-sub behaviour.

Constraints were verified, not remembered

§3 records each one with its file:line. Two of them turned out to be pre-existing defects, recorded in §10 with their own tasks; nothing in this design depends on either being fixed first:

  1. Broker.KVPut enforces capability and key shape but no value size limit — any signed plugin with kv:write can grow state.json without bound, and that file is rewritten in full and fsynced on every state write.
  2. kv and static are absent from the bolt hot-store exclusion list, so both still ride the full-rewrite path despite having bolt buckets.

Also verified and worth noting: the single-segment /sub/<token> form can be removed outright because the deployment has zero proxy users, profiles and inbounds — no live subscription breaks.

Draft until the operator reviews the spec.

lr00rl added 4 commits August 5, 2026 02:29
The operator's goal changed: shut the standalone Sub-Store down rather than
integrate with it. That reverses design-substore-embed's §7 answer, which scoped
v1 to conversion only, so the reversal is recorded rather than left implicit.

Twenty-one upstream route modules are several subsystems, so this splits into
six sub-projects and specifies only the first: the subscription store and the
public distribution path. It goes first because it is the foundation and the
only part touching the core server, and because a feature-complete converter
nobody can subscribe to does not let anyone switch the old instance off.

The load-bearing decision is that the core keeps the entire public surface --
route, token, rate limit, audit, headers, cache -- and the plugin answers one
question: given a subscription id, a format and a client UA, produce content. A
general http:serve capability was rejected for handing token checking and rate
limiting to plugin code, which is the shape the plugin-boundary review already
turned down.

Two rules exist because their opposite is the tempting default: a subscription
response is never an empty body with HTTP 200, because a client that gets one
deletes every node it had; and the last successful remote snapshot is durable
rather than cached, because it is the only thing that keeps clients served when
a provider goes down.

Verifying the constraints turned up two pre-existing defects, recorded in §10
with their own tasks and depended on by nothing here: Broker.KVPut accepts
values of unbounded size, and kv/static still ride the full-rewrite state.json
path despite having bolt buckets.
The snapshot is produced by plugin-side fetching, so bolt was never reachable for it. Each plugin does get a writable confined working directory, which is where it belongs. Recorded before any code is written against the wrong home, and sub-project 2 owes a test that content there survives a re-arm.
A plugin has no durable storage that is not the state file. Bolt is unreachable from a plugin, and the working directory is deleted by SystemRunner.Stop by design -- its own comment says it removes the runtime dir. So the core owns the snapshot as an opaque blob and the plugin stays stateless: it fetches on request, hands the bytes back, and is given them again on the next render. Both earlier answers stay in the row rather than being quietly replaced, because the reason each failed is the useful part.
The first implementation leaked five ways whether a token was valid, the sharpest being a 400 for a bad format after the token had already been resolved. Every rejection now returns one fingerprint-free response, format is validated before resolution so ordering cannot leak, and the rate limiter refuses in the same voice instead of announcing a specially-limited path. Truth moves to the audit log rather than disappearing. Two things are recorded as deliberately not done: the server does not proxy to a decoy, because that disguise belongs at the reverse proxy and would be imperfect here anyway; and timing remains distinguishable, because equalising it costs a worst-case delay on every rejection for an imperfect result.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant