plugins.network · a marketplace whose gate fails closed · money honest-off
A plugin marketplace built the other way round: the publish gate came first, and it refuses anything it cannot certify.
Most extension directories are a listing surface with a review policy bolted on afterwards, which means the thing standing between a bad bundle and an install is a queue and a person’s attention. plugins.network was built from the gate outwards. A submitted bundle is parsed with a real compiler front end, walked as a syntax tree, and measured against named rules; dynamic code execution, a hardcoded credential shape, and a bundle that will not parse are hard blocks. If the scanner itself throws, the publish is refused — an unverifiable bundle is treated exactly like a failing one, because the alternative is a directory that publishes whatever breaks its own tooling.
Said before anything else: browsing requires a signed-in account, no purchase is possible on this product (a database constraint pins the live-charge column false), no payout rail exists, and no student record is reachable from any table this marketplace owns. The listing, adopt, license and ledger routes are registered in the API today.
What runs today
Every item below was read at a named symbol in the repository before it was written here, and every one of them is code that exists and is wired into the running application. Where something is absent, it is in the next section under its own heading rather than softened into this one.
Nine marketplace routes, registered in the API app
Create a draft listing; publish or delist your own; browse the published catalog; list your own listings; fetch one listing (published to anyone signed in, draft or delisted to its owner only, and a 404 rather than a 403 to anyone else, so an unpublished draft’s existence is never disclosed); adopt a listing; read your own license grants as a buyer; read the revenue-share ledger for a listing you publish. The module is imported and registered in the API application, not written and left on a shelf. Registered
A fail-closed adoption gate with four named verdicts
Adoption runs through one pure function with an explicit order of checks. A listing that is not published is refused. A priced listing without payment is refused. A revenue-share listing with no configured rate is refused rather than split at a guess. Only when all three pass does it return the single allow verdict. Each refusal has its own name in the response, so a caller is told which wall it hit rather than getting a generic denial. Registered
An owned static scanner, no third-party engine in the path
The scanner is ours: a syntax-tree walk built on a compiler front end already present in the workspace, with no scanner-as-a-service dependency and no copyleft code anywhere in the shippable path. Findings carry a rule id and a source location, and the same bundle always yields the same findings. Registered
An offline-verifiable license envelope
A successful adoption mints an ES256-signed payload carrying the purchased item, the grantee reference, an issue time, an expiry, a nonce and the signing key id. It is verifiable without calling us back, which is what makes it useful in an air-gapped or intermittently-connected install. The marketplace route implements no crypto of its own — it calls the platform’s existing signing package. Registered
Two tables with row-level security forced on, owner-scoped
A shipped schema migration creates the listing and license-grant tables, enables AND forces row-level security on both, and writes policies keyed on the acting user id. Forcing matters: it applies the policy to the table owner too, so a privileged connection does not quietly bypass the rule. A publisher sees their own listings; a buyer sees their own grants; a publisher sees the ledger for their own listing and nobody else’s. Registered
An operator-side space in the web app
The web application renders a marketplace space for the operator’s own use. It is an internal surface for working the marketplace, and it is not, and is not described here as, a public storefront for shoppers. Registered
The publish gate, and why fail-closed is the whole product
A directory’s real promise is not its catalog, it is the thing it refuses. The gate here is a wrapper with a deliberately small decision surface, and its two most important branches are the unglamorous ones: a scan that throws is a block, and a scan that returns a structurally unusable result is the same block. Both record the reason as a scan error and the route answers 422 without disclosing internals. Reverting either branch to allow is what the gate’s own unit tests exist to turn red on — the failure mode being defended against is not a clever attacker, it is a Tuesday where the scanner crashes on a malformed input and a hundred bundles sail through.
Rules that BLOCK a publish
codescan/dynamic-exec
A call to eval, a Function constructor used as a compiler, or an import whose specifier is computed at run time or points at a remote host. A bundle that can assemble its own code at run time makes every other rule on this list advisory, so this one blocks rather than flags.
codescan/secret-leak
A PEM private-key block, a cloud access-key id, a recognised provider token prefix, or a literal bearer token embedded in the source. A separate, softer heuristic flags a non-trivial high-entropy literal assigned to a secret-named identifier — that one is a review signal, not a block, because the false-positive shape there is a placeholder and blocking on placeholders trains people to ignore the gate.
codescan/parse-error
The bundle does not parse. This is the honest case that most directories treat as a nuisance: if we cannot read it, we cannot certify it, so we refuse it. There is no branch here that shrugs and publishes.
Rules that FLAG for review
codescan/network-egress
A fetch, a socket, a request object, an event source, or a beacon send. Flagged rather than blocked, and the reason is stated in the finding itself: there is no host allowlist in the rule set, so we can see that a bundle talks to the network but not judge where. Blocking every network call would block most legitimate plugins; pretending we have judged the destination would be worse.
codescan/obfuscation
A source line past the minified-beyond-threshold length, or a long string literal whose Shannon entropy clears a set bits-per-character bar — the shape of a packed or encoded payload. Both are smells rather than proofs, which is exactly why they surface for a human instead of failing the publish.
That is the whole rule set in the tree, named, so you can judge the gap for yourself instead of taking a coverage claim on trust. It is a starter set. It does not detect a supply-chain compromise in a transitive dependency, it does not sandbox and execute anything, and it does not reason about intent. It reads syntax, and it says so.
What this refuses today
Each line below is a present-tense description of something the serving path does not do. None of them is a promise with a date attached, because a promise on a marketing page is a thing we would owe you and a description is a thing you can check.
There is no public storefront
Every marketplace route runs the same signed-in check, browse included. No anonymous catalog route is registered, so a visitor without a platform account has nothing to look at here. The persistence layer underneath already restricts an anonymous read to published listings only, which means opening this is a routing decision rather than a rewrite — but it is a decision that has not been taken, and until it is, this page is not going to describe a storefront that does not answer requests.
Nothing on this product takes money
The license-grant table carries a CHECK constraint pinning its live-charge column to false, so the database refuses to store a grant that records a real charge. The adoption request’s payment field is named for exactly what it is — a simulated honest-off payment — rather than a boolean that reads as real. There is no payment key wired into this route.
No money moves to a publisher
The revenue-share function computes a split and records a ledger entry. That is the entire behaviour: no payout rail, no connected-account plumbing, no transfer. A ledger row is a record of what a split would be, and calling it a payment would be a lie about a number that has never left a database.
There is no bundle upload or storage rail
A publish accepts an optional source string and scans it when one is supplied. There is no signed-bundle upload, no artifact store, and no delivery path, which means in practice most publishes today carry nothing to scan. The response says so out loud: the scan block comes back marked unscanned with the reason recorded as no bundle source supplied. A visible hole beats a green tick nobody can audit.
There is no developer submission or review pipeline
Published status is currently the only human-ish signal a listing carries, and the publisher sets it themselves on their own listing. A submission queue, a reviewer role, and an appeal path are all absent. The adoption gate’s first check — refuse anything not published — is standing in for a review pipeline, and that is a narrower thing than a review pipeline.
The license term is a placeholder, not a policy
Every envelope expires 365 days after issue, from one constant, for every pricing model. Whether a one-time purchase ought to be perpetual, and whether a subscription ought to carry a rolling renewal window, are licensing questions nobody has answered. The constant is a stand-in and this page will not dress it up as a term of sale.
There is no platform commission
No take-rate is written anywhere in the code. The split function requires the rate as an argument and defaults nothing; a revenue-share listing missing its rate is refused at adoption with its own verdict. That is a commercial decision that has not been made, and a marketing page is the wrong place to make it by implication.
There is no AI anywhere on this surface
The scanner is a deterministic tree walk against named rules. No model is called, no finding is a probability, and the same input always yields the same output. This is not a posture we are holding pending a launch; it is a description of a static analyser.
Who this is for, and what the first week actually looks like
Two people, with genuinely different first weeks. Neither of them is a school administrator: this is a standalone developer marketplace, and the district-facing products in this company live at other addresses.
A developer publishing an extension
Week one is a listing and a gate reading, not a launch. You create a draft against the listing route, give it a name, a category, a stock-keeping code validated against the platform’s own grammar, a pricing model and an exact integer price in cents. You then run a publish with a bundle source attached and read the scan block that comes back: either a clean result with any review-level findings listed for you, or a 422 carrying the exact findings that blocked it, each with a rule id and a source location. The useful outcome of that first week is knowing precisely which of your patterns this gate objects to — and, if you disagree with a flag, having a rule id specific enough to argue about. What you do not get in week one is a shopfront, an install count, or a payment.
An operator adopting an extension into their own stack
Week one is a catalog read and a license envelope. You browse the published catalog with your platform account, adopt a listing, and receive a signed license payload you can verify offline against the published key id — useful precisely because your verification does not depend on our uptime. Adoption of anything priced runs through the honest-off path, explicitly flagged as such in the request, and the grant that comes back is recorded with its live-charge column false because the database will not accept anything else. The concrete deliverable is a verifiable entitlement record; the thing you cannot do is pay for it.
If neither of those describes you, the honest answer is that this product has nothing for you this week, and we would rather write that sentence than pad the page.
The objections worth answering
These are the ones a developer or an operator actually raises. Each is stated in the form it gets said in, not softened into a question we would prefer.
“A marketplace with no public storefront and no payments is not a marketplace.”
That is a fair reading of the word, and we are not going to argue the definition. What exists is the mechanism a marketplace is judged on later and cannot retrofit cheaply: the gate, the entitlement format, the owner-scoped data model, and the refusal behaviour. The storefront is a routing change over a persistence layer that already scopes an anonymous read correctly. The payments rail is a real build. Both are absent and both are named as absent.
“Your scanner will miss things a serious attacker can walk straight past.”
Yes. It reads syntax; it does not execute, sandbox, or reason about intent, and it does not follow a transitive dependency. Every rule it has is named on this page precisely so you can size that gap yourself rather than infer coverage from an absence of detail. The claim being made is narrow and checkable: these specific patterns block, these specific patterns flag, and an unverifiable bundle is refused rather than admitted.
“Fail-closed sounds good until it blocks a legitimate publish and I am stuck.”
Correct, and there is no appeal path in the tree — that is one of the absences listed above rather than a gap we are talking around. What the design does do is make the failure legible: a block returns the findings that caused it, each with a rule id and a location, so the disagreement is about a specific line rather than about a verdict. A gate that refuses without telling you why is the version of this that deserves the complaint.
“Row-level security on paper does not mean my data is separated.”
Which is why the migration does two things rather than one. It ENABLES row-level security and it FORCES it, and the second is the one that matters: without forcing, the table owner is exempt and a privileged connection reads everything. The policies key on the acting user id, and the listing-read path deliberately returns a not-found rather than a permission error for another publisher’s draft, so a probing client cannot use the error shape to confirm that a listing exists.
“You are a K-12 company. Why should I trust a developer marketplace from you, and is my data going near a school?”
The second half has a structural answer: the marketplace tables have no school or studio column at all, and their security policies key on the acting user rather than on a tenant, so there is no join from anything here to a student record. On the first half, the honest answer is that you should judge this on the mechanism rather than on the parent company — the gate, the offline-verifiable envelope, and the forced row-level security are the things to check, and they are all named here specifically enough to check.
“What stops the take-rate from appearing later at a number I did not agree to?”
Nothing on this page, and pretending otherwise would be a promise we cannot keep. What we can say is what the code does now: no rate exists in it, the split function refuses to invent one, and a revenue-share listing with no configured rate is rejected at adoption rather than split at a default. When a rate is set it will be a stated commercial term, not a constant that quietly appeared in a function signature.
The data this touches — and the data it structurally cannot
Two tables. A listing carries an opaque publisher reference, a stock-keeping code, a name, a category, a status, a pricing model, an exact integer price in cents, and an optional revenue-share rate in basis points. A license grant carries an opaque buyer reference, the item, the sale price, the ledger split figures, the signed license payload, a status, and the live-charge column the database pins false. Identity in both is an opaque reference: no names, no email addresses, no free-text profile.
No minors’ data is reachable from this product, and the reason is a schema fact rather than an assurance. Row-level security on both tables keys on the acting user id and not on a school or studio id. There is no school column, no roster relation and no student relation in either table, so there is no join from a listing or a grant to a student record. The company’s school products gate minors’ data behind consent and never publish it; that discipline is theirs to enforce and this marketplace is simply outside its blast radius, because the axis those rules operate on does not exist here.
Money is stored as exact integer cents throughout, never as a float, and the only arithmetic performed is a basis-point split reused verbatim from the platform’s existing pricing module rather than reimplemented. The license envelope carries a grantee reference, an issue time, an expiry, a nonce and a key id — enough to verify the entitlement offline, and deliberately nothing about a person.
Pricing, in the present tense
No price is set for anything on this product, and nothing here can take a charge. There is no pricing table on this page and no checkout, because the license-grant table’s live-charge column is pinned false by a database constraint — a grant recording a real charge cannot be written, which is a stronger statement than a switch left in the off position.
A listing can carry a price in exact integer cents and a pricing model of flat, subscription, or revenue share, and the adoption gate enforces the obvious rule that a priced item is not granted unpaid. But the payment that would satisfy that check today is an explicitly-named honest-off simulation supplied by the caller, and it is auditable precisely because it is named that in the request body rather than defaulting quietly to paid.
The platform’s share of a revenue-share sale is not written anywhere in the code. The split function takes the rate as an argument and defaults nothing, and a revenue-share listing with no configured rate is refused at adoption with a specific verdict rather than split at a guessed number. A conversation works out what, if anything, applies.
Where this sits in the software-factory family
This is the commercial and infrastructural wing of the estate rather than its school-facing one, and the design language says so: a connector-green accent for “plugged in”, a monospaced maker’s mark carrying route paths and rule ids, and none of the institutional navy the district-facing products wear, because a developer marketplace is not a procurement document. The family thread is the factory floor and the toolwall — capability that is made in one place, boxed in another, and distributed here.
preset.network
The sibling marketplace on the other axis: presets, templates, styles and saved configurations rather than executable extensions. Same estate, deliberately different signal — a preset changes how something looks, a plugin changes what it can do, and only one of those needs a code-scan gate standing in front of it.
inabox.software
The factory floor of this family: where a product is assembled and boxed. plugins.network is the distribution end of the same idea — capability that is composable and shippable rather than welded into one application.
schoolapi.software
The integration and developer API layer. If you are extending the estate rather than distributing an extension, that is the surface to read first; this one is about the marketplace around it.
Security and trust
The estate-wide posture the forced row-level security and the signed-envelope format on this page sit inside.
Common questions
Can I browse the catalog without an account?
No. All nine marketplace routes require a signed-in platform account — the module runs the same auth check on every handler, including the browse route. An anonymous storefront is a separate route that is not registered, so there is nothing to sign in around. The persistence layer underneath already scopes an anonymous read to published listings only, which is why opening this up later is a routing change rather than a redesign.
Can I buy a plugin here?
No, and the refusal is in the database rather than in a setting. The license-grant table carries a CHECK constraint pinning its live-charge column to false, so a row recording a real charge cannot be written at all. The adopt request's payment field is named for what it is — a simulated honest-off payment — and a paid listing adopted without it is refused with a specific verdict rather than quietly granted.
What does the publish gate actually block?
The scanner parses a submitted bundle with a real compiler front end and walks the syntax tree. It BLOCKS on dynamic code execution (eval, the Function constructor, a dynamic or remote import specifier), on a hardcoded credential shape (a PEM private-key block, a cloud access-key id, a known provider token prefix, a literal bearer token), and on a bundle that does not parse at all. It FLAGS for human review on network egress calls and on obfuscation smells — a line past the minified threshold, or a long high-entropy string blob. Those are the rules in the tree; it is a starter set and we say so rather than implying it catches everything.
What happens if the scanner itself crashes?
The publish is refused. A scan that throws and a scan that returns a structurally unusable result both resolve to the same blocked decision with the reason recorded as a scan error, and the route answers 422 without disclosing internals. That branch is the point of the gate: an unverifiable bundle is treated exactly like a failing one. Reverting either branch to allow is what the gate’s own unit tests turn red on.
If I publish without supplying a bundle, does it just pass?
It publishes, and the response tells you plainly that nothing was scanned: the codescan block comes back with scanned set to false and the reason recorded as no bundle source supplied. There is no signed-bundle upload and storage rail in the tree, so today a publish generally carries no bundle to scan. That is a visible hole in the response body rather than a green tick you cannot audit.
Does anything here touch student data?
No, and that is structural rather than a promise. The marketplace tables enforce row-level security keyed on the acting user id, not on a school or studio id — there is no school axis in either table, so there is no join from a listing or a grant to a student record. This is a standalone developer marketplace that happens to be built by a company whose other products are for schools; it is not a school product wearing a different name.
What is the platform’s cut?
There is no platform-wide cut written anywhere in the code, and that is deliberate. The revenue-share function takes the rate as an argument and defaults nothing; a listing priced on revenue share with no rate configured is refused at adoption with its own verdict rather than being split at a guessed number. Setting a commercial rate is a business decision that has not been made, and inventing one on a marketing page would be the wrong place to make it.
How long does a license last?
Every issued envelope currently expires 365 days after it is minted, for every pricing model, from a single named constant. We are calling that a placeholder rather than a term, because it is one: whether a one-time purchase should be perpetual and a subscription should carry a rolling renewal window is a licensing decision nobody has taken. The envelope itself is real — an ES256-signed payload you can verify offline without calling us.
Is there any AI in the scanner?
None. The scanner is a deterministic walk over a syntax tree against named rules, and a given bundle produces the same findings every time. No model is called anywhere on this surface, and no finding here is a probability.
The next concrete step
There is one, and it is small enough to name precisely: get in touch and tell us what you would publish, or what you would adopt. If you are publishing, the useful thing to send is the shape of your bundle — what it does at run time, whether it talks to the network and where, and whether it needs to assemble code dynamically. That last one is a hard block here, so it is worth knowing before you build against the gate rather than after.
What happens next on our side is equally concrete, and it is a build rather than a switch: an unauthenticated catalog route so a visitor can read listings without an account, and a signed-bundle upload rail so a publish has something to scan every time instead of sometimes. Neither exists in the tree today. We are naming them because you should be able to hold us to a specific missing thing rather than a mood.
What this page is, and is not, claiming
plugins.network is a plugin and extension marketplace whose distinguishing mechanism is a publish gate that fails closed: an owned static scanner walks a submitted bundle’s syntax tree against named rules, blocks on dynamic code execution, a hardcoded credential shape, or a bundle that does not parse, and refuses the publish outright when the scanner itself throws or returns nothing usable. Nine marketplace routes are registered in the API application, adoption runs through a four-verdict fail-closed gate, a successful adoption mints an ES256-signed offline-verifiable license envelope, and two tables enforce forced, owner-scoped row-level security. Every one of those was read at a named symbol before it was written here.
The absences are equally real. There is no public storefront: every route requires a signed-in account. Nothing takes money: a database constraint pins the live-charge column false. No money moves to a publisher: the revenue share is a ledger entry with no payout rail. There is no bundle upload or storage, so a publish frequently has nothing to scan and the response says so. There is no submission or review pipeline and no appeal path. The license term is a single placeholder constant. No platform commission exists in the code. No model is called anywhere on this surface. And no student record is reachable from any table this product owns, because the security policies key on the acting user rather than on a school and no school column exists.
No catalog size, publisher count, install figure, scan-accuracy number or testimonial appears on this page, because none of those would be true. No competitor is named. This is a for-profit vendor.