> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stackra.figentra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 0453 — The Stackra registry: one npm-compatible store on Cloudflare for packages, skills, agents, MCP servers and plugins, with our own licence engine

> Accepted, amended by 0461 · 2026-10-27

> [!NOTE]
>
> Decision 10 amended 2026-10-27 (STA-1355) by
> [0461](https://docs.stackra.figentra.com/adr/0461-the-pro-line-for-0-1-0-runtime-free-behind-entitlements-paid-studios-and-advanced-drivers.md)
> decision 3: inside the `@stackra-pro` scope a per-name `internal` row covers `@stackra-pro/ui`, so
> only Figentra's own internal tokens read it and a customer grant, `names: "*"` included, never
> covers it. The two scope rows and the rest of decision 10 stand.

> [!NOTE]
>
> Accepted 2026-10-27 (STA-1333, batch stackra-181): the owner decided the registry in the
> questionnaire of 2026-10-27 (Stackra finish plan, decision D5). Supersedes decisions 1 to 3 of
> [0177](https://docs.stackra.figentra.com/adr/0177-package-distribution-npmjs-and-keygen.md) and the registry decisions of its
> amendment of 2026-10-21. Amends [0025](https://docs.stackra.figentra.com/adr/0025-packages-publish-to-private-registry.md) (decisions
> 1 and 2), [0077](https://docs.stackra.figentra.com/adr/0077-open-core-free-and-pro-lists.md) (decision 2),
> [0178](https://docs.stackra.figentra.com/adr/0178-licensing-keygen-licences-and-the-stackra-licensing-verifier.md) (decisions 1 to 3,
> for registry access), [0175](https://docs.stackra.figentra.com/adr/0175-edge-domains-and-tls.md) (decision 3),
> [0290](https://docs.stackra.figentra.com/adr/0290-dev-environment-and-cloudflare-services.md) (decisions 16 and 17),
> [0316](https://docs.stackra.figentra.com/adr/0316-open-core-distribution.md) (decisions 1 and 2, the channel only),
> [0168](https://docs.stackra.figentra.com/adr/0168-runway-public-reusable-workflows-build-order-and-runway-yaml.md) (decision 6) and
> [0349](https://docs.stackra.figentra.com/adr/0349-product-scopes-package-names-and-the-module-key-registry-across-products.md)
> (decision 4, the channel only).

## Context

[0177](https://docs.stackra.figentra.com/adr/0177-package-distribution-npmjs-and-keygen.md) sent `@stackra/*` to npmjs and `@stackra-pro/*` to Keygen
Cloud. Neither is available or wanted now: the npmjs `@stackra` organisation was deleted and its name is locked until
about 2026-11-26, and the owner decided on 2026-10-27 to use no paid service and no Keygen. Stackra also ships more than
packages: skills, agents, MCP servers and plugins, which today have no versioned home. The analysis of 2026-10-20
(`notes/pk2-registry-analysis.md`) scored a Worker with R2 best on cost and licence fit, worst on the code we must own,
and left one question open since [0074](https://docs.stackra.figentra.com/adr/0074-open-core-licensing.md): whether a registry can hold per-customer,
revocable, expiring tokens. The owner chose that option: our own npm-compatible registry on Cloudflare from day 1, one
store for every kind of item, and our own licence engine; the registry is on the 0.1.0 finish line.

Alternatives weighed: npmjs for free and a managed registry (Cloudsmith) for Pro, rejected as a paid service and two
stores; Verdaccio on a VM, rejected for its single writer and the ops burden; GitHub Packages, rejected because every
customer would need a GitHub account; waiting for the npmjs scope to unlock, rejected because the finish line does not
wait and our registry does not need npmjs.

## Decision

### Host and member

1. **Host: `npm.stackra.figentra.com`.** The name the docs already use (0284, `workers/static/README.md`) is kept, and
   [0290](https://docs.stackra.figentra.com/adr/0290-dev-environment-and-cloudflare-services.md) decision 17 (delete the name) and
   [0175](https://docs.stackra.figentra.com/adr/0175-edge-domains-and-tls.md) decision 3 (point it at Keygen) are reversed. One host serves the npm protocol
   and the catalogue API; there is no second host for Pro, for licences or for the catalogue. The Worker takes the name
   as a Worker Custom Domain, as `static.` does (0284 decision 9).
2. **Member: `workers/registry`**, package `@figentra/worker-registry`, Worker script `stackra-registry`, under every
   rule of 0284 decision 1 (`tier:app`, UNLICENSED, tests in `__tests__/`). Its `wrangler.jsonc` is hand-written under
   the carve-out of 0284 decision 8 until gen writes Workers config. It deploys only from CI
   (`.github/workflows/deploy-registry.yml`, GitHub environment `registry`), with the Cloudflare token read through
   Infisical OIDC as 0284 decision 7 says; nobody runs `wrangler deploy` from a workstation. The R2 bucket, the D1
   database, the KV namespace and the custom domain are Pulumi resources in `@stackra/infra-cloudflare`.

### Storage

3. **R2 holds bytes, D1 holds truth, KV holds caches.**

   | Store | Name | Holds | Rule |
   | - | - | - | - |
   | R2 | `stackra-registry` | tarballs at `tarballs/<scope>/<name>/<version>.tgz`; provenance bundles at `attestations/<scope>/<name>/<version>.sigstore.json` | private bucket, no public URL; written once per version, never overwritten or deleted |
   | D1 | `stackra-registry` | `scopes`, `publishers`, `packages`, `versions` (manifest, `kind`, integrity, shasum, signatures, size, publisher run, `published_at`, `deprecated`), `dist_tags`, `licences`, `licence_grants`, `licence_tokens` (hash only), `issuer_tokens` (hash only), `publish_tokens` (hash only), `audit_events`, `downloads_daily` | the only source of truth; schema changes are numbered D1 migrations in the member |
   | KV | `stackra-registry-cache` | rendered packuments (full and abbreviated) per name; the GitHub OIDC key set (one hour) | written on publish, dist-tag change and miss; losing every key is harmless |

   The licence-token check is cached in the Workers Cache API of each data centre for `registry.tokenCacheSeconds`
   (default 60), not in KV, so the cache costs no KV writes and a revocation is effective everywhere within that time.
   Backups are D1 Time Travel; tarballs are immutable and rebuildable from the tagged release.

### npm protocol surface

4. **The registry speaks the subset of the npm registry protocol that npm, pnpm and Yarn need to install and publish
   scoped packages, and nothing else.**

   | Method and path | Purpose | Auth |
   | - | - | - |
   | `GET /-/ping` | health | none |
   | `GET /-/whoami` | the holder of the token (licence holder, or `ci:<repository>` for a publish token) | any token |
   | `GET /@scope/name` (and `@scope%2fname`) | packument; `Accept: application/vnd.npm.install-v1+json` gets the abbreviated form | scope read rule |
   | `GET /@scope/name/<version-or-tag>` | one version manifest | scope read rule |
   | `GET /@scope/name/-/name-<version>.tgz` | tarball, streamed from R2 | scope read rule |
   | `PUT /@scope%2fname` | publish one version (npm publish body with `_attachments`), or deprecate (manifest change of `deprecated` only) | publish token |
   | `GET`, `PUT`, `DELETE /-/package/@scope%2fname/dist-tags[/<tag>]` | dist-tags | read rule to read; publish token to change |
   | `POST /-/npm/v1/oidc/token/exchange/package/@scope%2fname` | GitHub ID token for a short-lived publish token (decision 6) | GitHub OIDC |
   | `GET /-/npm/v1/keys` | the registry's public signing keys, in npm's shape (decision 9) | none |
   | `GET /-/npm/v1/attestations/@scope%2fname@<version>` | the provenance bundle of a version | scope read rule |
   | `/-/stackra/v1/*` | the catalogue and licence APIs (decisions 12 and 14) | per endpoint |

   Everything else is refused: `npm login` and `adduser` (`PUT /-/user/*`) answer 403 with a pointer to the licence
   docs, unpublish (`DELETE` of a package or version) answers 405, search and audit endpoints answer 404 (a consumer maps
   only the Stackra scopes to this host, so audits of third-party packages stay with npmjs). **The registry never
   proxies an upstream**: an unknown name is 404, never fetched from npmjs, so no one can inject a package by name.

5. **Caching headers.** Open tarballs: `Cache-Control: public, max-age=31536000, immutable`. Licensed tarballs and
   packuments: `private, no-store`. Open packuments: `public, max-age=60`.

### Publishing

6. **Only the release workflows publish, through GitHub OIDC.** The `publishers` table names, per scope, the trusted
   publisher: `@stackra` is `figentra/stackra`, `@stackra-pro` is `figentra/stackra-pro`, each with workflow
   `.github/workflows/release.yml`, ref `refs/heads/main` and environment `release`, matched on the numeric
   `repository_id` and `repository_owner_id` claims as well as the names, so a renamed or recreated repository does not
   inherit the right. The job requests a GitHub ID token with audience `npm:npm.stackra.figentra.com` and exchanges it
   at the endpoint of decision 4 for a publish token bound to that one package name, valid `registry.publishTokenSeconds`
   (default 900), stored hashed. This is the exchange shape npm's trusted publishing uses; where the client does not run
   it against a host other than npmjs, the release workflow performs the exchange in one step and passes the token as
   `NODE_AUTH_TOKEN` for that job. There is no long-lived publish token, no human publish and no break-glass account; a
   fix is a new run of the workflow. [0289](https://docs.stackra.figentra.com/adr/0289-runway-and-environments-workers-one-encore-app-previews-and-publishing.md)
   decision 11 (a 7-day npm token for the first publish) does not apply here, because a trusted publisher is a scope row,
   not a per-package npmjs setting.
7. **What a publish must pass.** The Worker refuses a version that already exists (versions are immutable, also after a
   deprecation, [0013](https://docs.stackra.figentra.com/adr/0013-versioning-and-release.md)); a tarball over `registry.maxTarballBytes` (default 50 MiB);
   a tarball whose `package.json` name or version differs from the request; a `stackra.kind` outside decision 11; a
   scope whose licence rule fails (`@stackra-pro/*` must name the commercial licence and carry its `LICENSE`, and must
   contain no `src` and no source maps, [0316](https://docs.stackra.figentra.com/adr/0316-open-core-distribution.md) decision 2; `@stackra/*` must not name
   the commercial licence, and is MIT or, for a vendored third-party item, its upstream open-source licence with a
   `NOTICE`, [0024](https://docs.stackra.figentra.com/adr/0024-mit-license-with-exclusions.md) decision 5); a missing provenance bundle; and a client
   integrity or shasum that differs from the one the Worker computes. Dist-tag changes and deprecations need the same
   publish token. Every publish, tag change and deprecation is an `audit_events` row naming the workflow run.

### Integrity and signing

8. **Integrity.** The Worker computes the SHA-512 `dist.integrity` (SRI form), the SHA-1 `dist.shasum` and the SHA-256
   digest of each tarball at publish and stores them; installs verify integrity as npm clients always do.
9. **Registry signatures and provenance.** Each version carries `dist.signatures: [{ keyid, sig }]`, an ECDSA P-256
   signature over `<name>@<version>:<integrity>`, the format npm's registry signatures use, so `npm audit signatures`
   can verify our packages against the keys at `/-/npm/v1/keys` (`keytype: ecdsa-sha2-nistp256`, `keyid` =
   `SHA256:<base64 of the SHA-256 of the public key's SPKI DER>`, `expires`). The private key is the Worker secret
   `REGISTRY_SIGNING_KEY`, sourced from Infisical `prod` `/registry` ([0173](https://docs.stackra.figentra.com/adr/0173-secrets-infisical-is-the-source-of-production-secrets.md));
   it rotates yearly, and a rotated key stays listed with its `expires` so old signatures still verify. Provenance: the
   release workflow attests each packed tarball with GitHub artifact attestations (Sigstore,
   `actions/attest-build-provenance`) and sends the bundle with the publish. The Worker checks that the bundle's subject
   digest equals the tarball's SHA-256 and that its certificate names the trusted publisher's repository and workflow,
   stores it in R2 and serves it (decision 4). Full Sigstore chain verification is done by consumers
   (`gh attestation verify`) and by the release smoke job, not by the Worker.

### Scopes and kinds

10. **Read rules are scope rows, not code.** The `scopes` table ships with two rows:

    | Scope | Read rule | Who may read |
    | - | - | - |
    | `@stackra` | `open` | anyone, no token; a token sent with the request is ignored, so a revoked or wrong token never breaks a free install |
    | `@stackra-pro` | `licensed` | a request whose `Authorization: Bearer` licence token has a grant covering the name (decision 13); no token is 401, a token without that grant is 403 |

    A third rule, `internal`, exists for the restricted `@figentra/*` channel of
    [0349](https://docs.stackra.figentra.com/adr/0349-product-scopes-package-names-and-the-module-key-registry-across-products.md) decision 4: its row is
    added by the first OS batch that publishes, with the OS release workflow as its trusted publisher; it is not part of
    0.1.0. No other scope is added without an ADR. A consumer's `.npmrc` maps the scopes to the host and sends the
    licence token for it:

    ```ini theme={"system"}
    @stackra:registry=https://npm.stackra.figentra.com/
    @stackra-pro:registry=https://npm.stackra.figentra.com/
    //npm.stackra.figentra.com/:_authToken=${STACKRA_LICENCE_TOKEN}
    ```

11. **`stackra.kind` and the catalogue.** Every item is an npm tarball whose `package.json` may carry
    `"stackra": { "kind": <kind>, ... }`. The kinds are exactly `package`, `skill`, `agent`, `mcp` and `plugin`; a
    missing `stackra.kind` means `package`. A non-package item is named `@<scope>/<kind>-<name>` (for example
    `@stackra/skill-archify`), so items and packages never collide. `stackra add <kind> <name>` (stackra-184) resolves
    `@stackra/<kind>-<name>`, then `@stackra-pro/<kind>-<name>`, and installs:

    | Kind | Tarball holds | Installs into |
    | - | - | - |
    | `package` | a built package ([0025](https://docs.stackra.figentra.com/adr/0025-packages-publish-to-private-registry.md) decision 3) | `package.json` through pnpm |
    | `skill` | `SKILL.md` at the root, with its references and scripts | `.agents/skills/<name>/` |
    | `agent` | one agent definition, `agent.md` | `.agents/agents/<name>.md` |
    | `mcp` | the server, with `stackra.mcp` holding its `.mcp.yaml` server entry | an entry in `.mcp.yaml`, run as the pinned package |
    | `plugin` | a plugin folder with its manifest | `.agents/plugins/<name>/` |

    Non-package installs are pinned by name, version and integrity in `stackra-items.lock.yaml` at the workspace root
    (its schema is stackra-184's). The `stackra` field's schema per kind is a contract of `@stackra/skills`
    (`./contracts`: `StackraKind`, `StackraManifest`, `CatalogueItem`, `CataloguePage`), which the Worker and the CLI
    both import. The `package` kind's `tier`, `category`, `runtimes` and `summary` were added by the amendment of
    2026-10-27 at the end of this page.

12. **The catalogue API** is read-only and lists every item of the `open` and `licensed` scopes to anyone, metadata only
    (no tarball, no manifest file contents beyond `stackra`):

    * `GET /-/stackra/v1/catalogue?kind=&scope=&q=&cursor=&limit=` returns `CataloguePage`:
      `{ items: CatalogueItem[], next: string | null }`, newest publish first, `limit` at most 100.
    * `GET /-/stackra/v1/catalogue/@scope/name` returns one `CatalogueItem` with `versions`.
    * `CatalogueItem` is an object of `name`, `scope`, `kind`, `description`, `latest`, `distTags`,
      `access` (`"open"` or `"licensed"`), `licence`, `keywords`, `repository`, `homepage`, `publishedAt`,
      `deprecated` and `stackra`.
    * `GET /-/stackra/v1/entitlements` with a licence token returns what it may read:
      `{ holder, grants, expiresAt, status }`; the CLI shows it as `stackra whoami`.

### Licences

13. **Our own licence engine, in D1.** A licence is `{ id, holderRef, grants, notBefore, expiresAt, status }`, where
    `holderRef` is the opaque reference of the customer account in Figentra OS, `status` is `active`, `suspended` or
    `revoked`, and each grant is `{ scope: "@stackra-pro", names: "*" | string[] }` (an item of any kind is a name).
    A licence has one or more tokens (one per CI system or machine, each with a label), up to
    `registry.licence.maxTokens` (default 20). A token is `stkl_` followed by 43 base64url characters (32 random
    bytes), shown once at issue and stored only as its SHA-256 hash; a fast hash is enough because the token is random,
    and the prefix lets gitleaks find a leaked one. Each token and each licence can be revoked on its own. Reads of a
    `licensed` scope are refused once the licence is `suspended` or `revoked`, and `registry.licence.graceDays`
    (default 14) after `expiresAt`; a suspension or an expiry never touches anything a customer already installed
    ([0178](https://docs.stackra.figentra.com/adr/0178-licensing-keygen-licences-and-the-stackra-licensing-verifier.md) decision 4). Downloads count per
    licence and day in `downloads_daily`; no IP address is stored.
14. **The licence API and the OS billing hook.** The licence API is `/-/stackra/v1/licences`: `POST` issues a licence
    (with its first token), `PATCH /:id` renews (`expiresAt`) or sets the grants, `POST /:id/suspend`, `POST /:id/resume`,
    `POST /:id/revoke`, `POST /:id/tokens` and `DELETE /:id/tokens/:tokenId`. Its caller is the Figentra OS billing and
    licensing service, authenticated by an issuer token (role `issuer`, stored hashed in `issuer_tokens`, its value in
    Infisical `prod` `/registry`, rotated by the owner). Every call carries `Idempotency-Key`, the OS event id, so a
    retried billing webhook is applied once. The contract is the command and result types in `@stackra/licensing`
    `./contracts` (`RegistryLicenceCommand`, `RegistryLicence`, `RegistryLicenceGrant`): OS maps a subscription start
    to issue, a renewal to renew, a failed payment to suspend, a cancellation or refund to revoke, and a plan change
    (plans from `@stackra/entitlements`) to set the grants. The registry holds no prices and no plans.
15. **The `LicenceIssuer` port gets an own-issuer driver, the default.** `@stackra/licensing` adds the `registry`
    driver of `LicenceIssuer` over the licence API (stackra-183) and makes it the default; the `keygen` driver stays,
    optional, for an on-prem customer who runs Keygen. A signed offline licence file for on-prem (`checkOut`) is signed
    with its own Ed25519 key, `LICENCE_SIGNING_KEY`, never with the registry signing key.

### npmjs and npm.figentra.com

16. **The npmjs mirror is optional and off.** When the `@stackra` name unlocks on npmjs (about 2026-11-26), the owner
    re-registers the organisation anyway, so no one else can claim it and confuse a consumer whose `.npmrc` lacks the
    scope mapping. If the owner switches the mirror on (repository variable `STACKRA_NPM_MIRROR=on`), the release
    workflow publishes the same `@stackra/*` tarballs, byte for byte, to npmjs with npm trusted publishing and
    provenance, after they are on our registry. Our registry stays the source of truth: same versions, no npmjs-only
    version, never `@stackra-pro/*`, never a non-package kind unless the owner says so. A failed mirror publish is
    retried with the same tarball, never rebuilt.
17. **`npm.figentra.com` is retired.** The npflared Worker `npm-registry` gives up the `npm.stackra.figentra.com` name
    before `stackra-registry` takes it, and keeps serving only `npm.figentra.com`. Its remaining consumers
    (`@figentra/cli` and Academorix) move to this registry: a package that still has a consumer is republished from
    its source through a release workflow (its tarball is not copied, because only CI publishes), under the
    `internal` row of decision 10. When the last consumer has moved, `npm.figentra.com` answers 410 Gone for 30 days;
    then the owner deletes the `npm-registry` Worker, its R2 bucket and its DNS record (through Pulumi once the zone is
    imported, [0290](https://docs.stackra.figentra.com/adr/0290-dev-environment-and-cloudflare-services.md) decision 15). The `NPM_FIGENTRA_TOKEN` secret
    is deleted from Infisical the same day.

### Versions and settings

18. **One shared version.** `@stackra/*` and `@stackra-pro/*` release at one exact semver, first `0.1.0` (amending
    [0177](https://docs.stackra.figentra.com/adr/0177-package-distribution-npmjs-and-keygen.md) decision 5, which let Pro follow within a week). Decisions
    4 and 6 to 8 of 0177 stand: release checks, deprecation, canaries (dist-tag `canary`) and the `@stackra/source`
    condition.
19. **Every limit is a setting.** `registry.maxTarballBytes`, `registry.publishTokenSeconds`,
    `registry.tokenCacheSeconds`, `registry.licence.graceDays` and `registry.licence.maxTokens` are Worker vars with the
    defaults above; scopes and publishers are D1 rows seeded by a migration. No limit or scope is a constant in code.

## Consequences

* Free installs need no account and no token; Pro installs need one licence token in `.npmrc`; publishing needs no
  secret at all.
* We own an npm registry. Its protocol subset, its tests and its incidents are ours (stackra-182), and a client change
  in npm or pnpm can break it; the release smoke test installs from it with npm, pnpm and Yarn.
* Supply chain: no upstream proxy, immutable versions, registry signatures that `npm audit signatures` checks, and a
  provenance bundle per version. npm does not show our provenance on a package page; that needs the mirror.
* Cost: the Workers Free plan covers launch (100,000 requests a day, D1 5 million reads a day, 10 GB of R2); the
  owner moves to Workers Paid (5 USD a month) when daily requests pass about 80,000.
* [0178](https://docs.stackra.figentra.com/adr/0178-licensing-keygen-licences-and-the-stackra-licensing-verifier.md) no longer needs Keygen for registry
  access; Keygen is an optional on-prem driver.
* The open question of [0074](https://docs.stackra.figentra.com/adr/0074-open-core-licensing.md) and [0077](https://docs.stackra.figentra.com/adr/0077-open-core-free-and-pro-lists.md)
  (per-customer, revocable tokens) is closed by decision 13.
* Chores: the owner creates the `registry` GitHub environment, the Infisical `/registry` path with
  `REGISTRY_SIGNING_KEY` and the issuer token, re-registers the npmjs organisation when it unlocks, and switches off
  `npm.figentra.com` (decision 17).

## Enforcement

planned — every rule of decisions 4 to 14 is a check in `workers/registry` with its tests in `__tests__/` beside the
code (stackra-182, stackra-183): the refused routes, OIDC claim matching, the publish checks of decision 7, signatures
against `/-/npm/v1/keys`, scope read rules and licence status. The release workflows (stackra-185) publish only
through the exchange of decision 6; `zizmor` checks their `id-token: write` scope. `tools/scripts/validate-repository.mjs`
keeps refusing a member here that depends on `@stackra-pro/*`. A gitleaks rule for the `stkl_` prefix is planned.
Nothing checks the deletions of decision 17; they are owner steps logged in Linear.

## Decided, with triggers

1. **Third-party publishers.** Only Figentra's release workflows publish. Opening the catalogue to outside authors
   needs its own ADR, when the first outside item is requested.
2. **Perpetual fallback.** An expired licence does not keep access to versions released before its expiry. Revisit at
   the first customer contract that asks for it.
3. **Mirror.** Decision 16 stays off until the owner switches it on.

## Amendment 2026-10-27 (owner decision): the package fields of `stackra` and the member metadata

**Context.** Every member's `package.json` carried a description, licence and exports, but no keywords, homepage, bugs,
author or engines, and nothing an agent could read to place a member without opening its code: its tier, its
category, where it runs and when to reach for it. The owner decided on 2026-10-27 that every member of this repository
carries both, in one checked shape, and that decision 11's `stackra` field is where the agent-readable part lives.

**Decision.** This amends decision 11; the kinds and the other kinds' fields are unchanged.

1. **The `package` kind carries four more keys.** `"stackra": { "kind": "package", "tier", "category", "runtimes",
   "summary" }`. `tier` is `lib`, `app`, `service` or `config` (the turbo tier tag without `tier:`); `category` is the
   `packages/<category>` folder (`tooling` included) and is absent for apps, services and Workers; `runtimes` is a
   non-empty set from the file contract's `Runtime:` vocabulary (`isomorphic`, `node`, `browser`, `worker`,
   `react-native`), read from the headers of the files its export entries name; `summary` is one sentence for agents on
   when to reach for the member, never a copy of `description`. `STACKRA_MANIFEST_SCHEMA` (`@stackra/skills/contracts`,
   with the vocabularies `StackraTier`, `StackraCategory` and `StackraRuntime`) checks them and keeps them optional, so
   a `package` item published from elsewhere still parses; `exports` and `description` are never repeated there.
2. **Every member of this repository writes `"kind": "package"` and all four keys**, and the npm fields `keywords`
   (`"stackra"`, the category, then 3 to 8 domain terms; no tier or runtime words), `repository` with its `directory`,
   `homepage` (`https://github.com/figentra/stackra/tree/main/<dir>#readme`), `bugs`
   (`https://github.com/figentra/stackra/issues`), `author` (`Figentra (https://figentra.com)`) and `engines`
   (`{ "node": ">=24.0.0" }`). Versions, `private` and `publishConfig` are not part of this amendment.
3. **Turbo tags follow the field.** A member's `turbo.jsonc` `tags` are exactly `tier:<tier>`, then `category:<category>`
   (packages only), then `runtime:<r>` for each runtime in `runtimes` order. Exactly one tier tag stays the rule, and the
   boundary rules of the root `turbo.jsonc` still name tier tags only.
4. **`pnpm validate` enforces it** (`checkMemberMetadata` of `@stackra/repo-checks`, run in the Stackra repository): it
   refuses a missing field, a `stackra` field the built `STACKRA_MANIFEST_SCHEMA` refuses (CI builds before it
   validates), a tier or category that disagrees with the folder, and turbo tags that differ from the field. `pnpm new`
   and `stackra new` write the fields, so a new member passes.

**Consequences.** Agents and the catalogue read a member's tier, category, runtimes and purpose from `package.json`
alone, and `turbo run --filter` can select by category or runtime tag. Each new member needs a `--summary` sentence.

## Amendment 2026-10-31 (owner decision): the `@figentra-oss` scope

Figentra Flow is MIT, so it cannot publish to `@figentra`, which is `internal` with a `commercial` licence rule. The owner
added an open scope for Figentra's open products (migration `0005_figentra_oss.sql`); this amendment is the ADR decision
10 asks for before another scope is added. The scope rows are now:

| Scope | Read rule | Licence rule | Trusted publishers (environment) |
| - | - | - | - |
| `@stackra` | `open` | `open-source` | `figentra/stackra` (`release`) |
| `@stackra-pro` | `licensed` | `commercial` | `figentra/stackra-pro` (`release`) |
| `@figentra` | `internal` | `commercial` | `figentra/brand` (`brand-registry`), `figentra/commerce` (`release`), `figentra/os` (`release`) |
| `@figentra-oss` | `open` | `open-source` | `figentra/flow` (`release`) |

Every publisher uses `.github/workflows/release.yml` on `refs/heads/main` and is matched on its numeric repository and
owner ids (decision 6).

## Amendment 2026-11-01: contracts, the `contract` kind and branch prerelease

Written with `0511` (`adr/0511-cross-repo-service-contracts-and-cross-app-events`), which amends decisions 6, 10 and 11 as follows.

* **Decision 6 (publishers).** A publisher row names one producer repository, its workflow `.github/workflows/release.yml`
  and environment `release`, so a scope may hold one row per repository, as the `@figentra` rows of the 2026-10-31 table do.
  A contract name's row admits a prerelease version from any ref of that workflow, and a stable version only from
  `refs/heads/main`.
* **Decision 10 (read rules).** The `@figentra` row of the 2026-10-31 table is the `internal` read rule that contracts are
  read through. Its publishers are the producer repositories of `0511` decision 5.
* **Decision 11 (kinds).** The kinds are exactly `package`, `skill`, `agent`, `mcp`, `plugin` and `contract`. A contract is
  named `@figentra/<service>-contract` (`0511` decision 1), not `@<scope>/contract-<name>`, and `stackra add` does not install
  it. Its fields in the `stackra` object are set by the schema change that adds the kind.

The other decisions stand.

## Amendment 2026-11-01 (owner decision): sources in an internal scope

Decision 7's ban on `src` and source maps comes from [0316](https://docs.stackra.figentra.com/adr/0316-open-core-distribution.md) decision 2, which keeps
sources out of what customers install. It now applies only to a commercial scope whose read rule is `licensed`
(`@stackra-pro`). An `internal` scope (`@figentra`) may publish `src/` and `.map` files, because only a Figentra
internal licence reads it. The template lines of [0481](https://docs.stackra.figentra.com/adr/0481-template-lines-release-as-registry-packages-and-runway-checks-them-out.md)
are their sources: `@figentra/os-mobile` ships the Expo project EAS builds from. Every other commercial check still
applies there: the scope's licence string and a LICENSE file. The read rule is the scope row's own, so a per-name
`internal` rule inside `@stackra-pro` (`@stackra-pro/ui`) keeps the ban. The OS release workflow publishes those lines
as a trusted publisher of `@figentra` (migration `0006_figentra_os.sql`, decision 10). Linear STA-1828.

## Supersedes and sources

Supersedes: [0177](https://docs.stackra.figentra.com/adr/0177-package-distribution-npmjs-and-keygen.md) decisions 1 to 3 and decisions 1 to 3 of its
amendment of 2026-10-21 (trusted publishing to npmjs); its other decisions and amendments stand, decision 5 as amended
by decision 18. Amends: [0025](https://docs.stackra.figentra.com/adr/0025-packages-publish-to-private-registry.md) decisions 1 and 2,
[0077](https://docs.stackra.figentra.com/adr/0077-open-core-free-and-pro-lists.md) decision 2, [0178](https://docs.stackra.figentra.com/adr/0178-licensing-keygen-licences-and-the-stackra-licensing-verifier.md)
decisions 1 to 3, [0175](https://docs.stackra.figentra.com/adr/0175-edge-domains-and-tls.md) decision 3, [0290](https://docs.stackra.figentra.com/adr/0290-dev-environment-and-cloudflare-services.md)
decisions 16 and 17, [0316](https://docs.stackra.figentra.com/adr/0316-open-core-distribution.md) decisions 1 and 2 (the channel),
[0168](https://docs.stackra.figentra.com/adr/0168-runway-public-reusable-workflows-build-order-and-runway-yaml.md) decision 6 and
[0349](https://docs.stackra.figentra.com/adr/0349-product-scopes-package-names-and-the-module-key-registry-across-products.md) decision 4 (the channel).
Related: [0024](https://docs.stackra.figentra.com/adr/0024-mit-license-with-exclusions.md), [0074](https://docs.stackra.figentra.com/adr/0074-open-core-licensing.md),
[0113](https://docs.stackra.figentra.com/adr/0113-heroui-pro-distribution.md), [0173](https://docs.stackra.figentra.com/adr/0173-secrets-infisical-is-the-source-of-production-secrets.md),
[0284](https://docs.stackra.figentra.com/adr/0284-hosted-static-files-one-cloudflare-static-assets-worker.md),
[0289](https://docs.stackra.figentra.com/adr/0289-runway-and-environments-workers-one-encore-app-previews-and-publishing.md). Sources: owner decisions of
2026-10-27 (questionnaire; Stackra finish plan decision D5); Linear STA-1333; `notes/pk2-registry-analysis.md`
(2026-10-20); npm registry documentation for packuments, dist-tags, registry signatures and trusted publishing.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.