Architecture

How the index is shaped, built and read, for contributors to the marketplace itself.


Table of contents

  1. The index document
    1. features
    2. spls
    3. collisions
  2. The build pipeline
  3. Local-first resolution
  4. Dependency kinds
  5. The guardian, check:contracts

The index document

index.json is one self-describing document ("schema": 1) with build metadata (generated_at, generator, sources) and three collections.

{
  "schema": 1,
  "generated_at": "2026-10-06T18:01:58+00:00",
  "generator": "splent_cli/1.26.1",
  "sources": {
    "orgs": ["splent-io"],
    "repos": [],
    "workspace": false,
    "marketplace": "https://marketplace.splent.io"
  },
  "features": [ ... ],
  "spls": [ ... ],
  "collisions": [ ... ]
}

sources records what the build read. orgs and repos are the forge sources, workspace is true for a --local build, and marketplace is where the SPL lines came from (null with --local).

features

One entry per feature, keyed by id (org/repo) with a short name. Each entry is the full contract at the latest released tag plus what only the indexer can compute.

Group Fields
Identity id, org, repo, short, version (latest tag), project_version, source ("github" for a contract read at a released tag on the forge, whatever the host, "workspace" with --local)
Presentation description, archetype, category, tags, env
Contract provides (routes, blueprints, models, services, hooks, signals, commands, commands_help, translations, docker), requires (features, features_manual and features_optional by name, feature_versions with the version specifier any of them carries, env_vars, signals), extensible, docker, refinement
Settings config, the variables the feature declares under [tool.splent.config] (see below)
Tooling cli_version, requires_python, dependencies
Computed used_by (reverse dependencies across the whole index), pypi (published, latest, has_current), github (the forge repository’s url, description, pushed_at, stars and private, under that key whatever the forge)

config lists the feature’s settings in the order its pyproject.toml declares them, one object per key.

"config": [
  {
    "key": "AUTH_NAV_LOGIN",
    "default": "true",
    "required": false,
    "help": "\"Log in\" in the public header. A site where staff reach the panel by typing the URL turns it off."
  },
  {
    "key": "AUTODEPLOY_SECRET",
    "default": "",
    "required": true,
    "help": "Shared by the web container and the agent; every API call carries it."
  }
]
Field Meaning
key The variable, as the feature’s config.py reads it.
default The declared value as text, the way product:env writes it into an env file. Booleans are true or false, numbers and strings are written plainly and a list is joined by commas.
required true when the declared value is <SET>, so only the deployment can supply it. Its default is then empty.
help The comment block right above the key, joined into one paragraph, or the comment after its value when there is no block. Empty when the key has neither.

A table or a date under [tool.splent.config] never reaches the environment, so it is left out. config is the list to show as a feature’s settings. requires.env_vars is something else, the variables the feature’s code reads, which a product need not set when the feature declares a default for them. A consumer reads a missing config as an empty list.

provides.commands_help maps each command to its usage and a one line help, as feature:release writes it into the contract.

spls

One entry per product line the build can resolve, with name, description, the uvl pointer (mirror, doi, concept_doi, version, file) and the parsed model, so consumers never need a UVL parser.

Field Holds
model.features Each feature with its presence (mandatory or optional), org, package and group membership
model.alternative_groups owner, kind and members of each group
model.constraints Requires-implications as [a, b] pairs (a => b)

collisions

Computed across all indexed features. Each entry has a kind (route, service or model), the colliding item and the features that provide it. Two colliding features can coexist in an SPL only behind an alternative group, and the feature sheet and the configurator both show it.

The build pipeline

What triggers an index build, what it reads and what it writes TRIGGERS A push to main Every 6 hours workflow_dispatch after a release or publish Build job splent marketplace:index --registry registry.toml Forgejo Actions in splent_index READS, ANONYMOUSLY registry.toml, the sources contracts at their tags lines from the marketplace models by DOI from UVLHub WRITES index.json committed back, served raw
Any of three triggers runs one command. It reads its sources without a token, and committing index.json is its only write.

The published index lives in splent-io/splent_index. It holds registry.toml (the curated source list), a Forgejo Actions workflow (.forgejo/workflows/build-index.yml) and the index.json that workflow commits back. Only the recipe is written by hand.

Step What happens
Cadence On a push to main (except the job’s own commit of index.json, so a build never retriggers itself), every 6 hours, and on demand through workflow_dispatch
Rebuild requests feature:release, release:resume and spl:publish send that workflow_dispatch as their last step, so a new version reaches the index within a couple of minutes. It is best effort. It needs SPLENT_FORGE_TOKEN with write access to splent_index, and when the token is missing or the forge refuses, the command says so and finishes, because the release is already done. The 6-hour schedule is the safety net
Build Installs splent_framework and splent_cli from git.splent.io, runs splent marketplace:index --registry registry.toml --output index.json and commits the result, which the repository serves raw
Auth The contracts are read anonymously. The job blanks SPLENT_FORGE_TOKEN and GITHUB_TOKEN, because the token Forgejo injects is scoped to splent_index alone and would hide every other repository. No marketplace login is involved
SPL lines Read from the marketplace’s public GET /api/v1/spls (the job sets SPLENT_MARKETPLACE_URL to https://marketplace.splent.io). Each line is indexed at the DOI it publishes, with its model read from .splent_cache/spls/ or downloaded from UVLHub. If the marketplace cannot be read, the build stops and the previous index stays. A model it cannot fetch is indexed without one and warned about

The full option set (orgs, explicit repos, local mode) is at marketplace:index.

Local-first resolution

Every consumer falls back when a source is missing, but they rank the sources differently. The CLI is local-first, and so is a configurator installed without the marketplace feature. The web catalog takes the newer index, and a configurator installed next to it does the same.

Consumer Order
CLI (feature:search / feature:info) Workspace cache (.splent_cache/marketplace/index.json) → SPLENT_INDEX_URL, fetched when there is no cache or with --refresh → live query of the forge
Marketplace web feature In-memory TTL cache (60 seconds, SPLENT_INDEX_TTL_SECONDS) → the newer of the workspace cache and SPLENT_INDEX_URL (the published index by default), by generated_at → last stale copy → empty index
Configurator feature MarketplaceService (soft dependency, so the web rule when the marketplace feature is installed) → workspace cache → SPLENT_INDEX_URL (caching what it fetches)

For the CLI, and for a configurator without the marketplace feature, the workspace cache wins whenever it exists. A freshly built --local index may describe features and SPLs that are not published yet. Production containers have no workspace cache, so they read the published index.

The web catalog serves whichever copy was generated later. Ties go to the published copy, and a copy with no generated_at loses to one that has it. A developer who just ran marketplace:index sees their own build because it is newer, and a server whose copy is older follows the published one, with nothing to configure.

Dependency kinds

The contract distinguishes three kinds of feature dependency, inferred from how the code is written.

Kind Contract field Inferred from Enforced?
Hard requires.features Imports of other features, bare service_proxy("XService") calls, and ForeignKey references to a table another feature owns, in the feature’s own code (files under tests/ are left out). Code that breaks without the provider Yes. feature:install resolves them recursively and feature:remove blocks removal
Soft requires.features_optional What the code uses only when the product has it. service_proxy(...) or importlib.import_module(...) wrapped in a try: that tolerates the absence, connect_signal on another feature’s signal, and register_template_hook into another feature’s slot. Entries written by hand are kept across contract regeneration No, shown by the marketplace as “works better with”
Manual requires.features_manual Declared by the author for real dependencies static analysis cannot see, such as a feature that stores data through another’s admin UI, or one that only gates its views with role_required and so needs auth. Kept across contract regeneration and merged into requires.features Yes, same as hard

Call service_proxy bare when the feature needs the service, and wrap it in try/except when the feature only improves with it. The contract generator turns that choice into the marketplace’s dependency graph. The marketplace’s Elasticsearch upgrade and the configurator’s use of MarketplaceService are both soft dependencies.

The guardian, check:contracts

A dependency lives in two places, the contract (what feature:install and the marketplace see) and the SPL’s UVL constraints (what configuration validity sees). check:contracts checks that both agree. It fails on a contract dependency with no UVL constraint and warns on a UVL implication no contract backs. Run it in CI next to product:validate.


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es