Authoring registry items

Package a reusable capability — schema, functions, bindings and env vars — as a registry item others install with one command.

Last updated:

A registry item is a reusable slice of a Lunora app: some files, the npm dependencies they need, the wrangler.jsonc bindings they require, and the environment variables to prompt for. lunora registry add <name> applies all of that to a project in one step. Lunora's own auth, mail, and storage features ship this way.

Reach for a registry item when the thing you want to share is project code — a schema extension, functions, wiring. When it is a library with no project footprint, publish an npm package instead.

Using items

lunora registry list             # what is available
lunora registry view auth        # inspect an item before applying it
lunora registry add auth         # apply it
lunora registry add auth --dry-run   # print the plan, write nothing
lunora registry add auth --diff      # preview file-level changes

lunora add auth and lunora add email are friendlier front doors over the same machinery.

The manifest

An item is a directory containing a registry.json plus the files it scaffolds:

registry/ratelimit/registry.json
{
    "$schema": "../schema/registry-item.schema.json",
    "name": "ratelimit",
    "title": "Rate limiting",
    "description": "Token-bucket rate limiting wired into your procedures.",
    "requires": [],
    "deps": { "@lunora/ratelimit": "workspace:*" },
    "envVars": [
        {
            "name": "RATELIMIT_SALT",
            "description": "Salt for hashing rate-limit keys.",
            "secret": true
        }
    ],
    "files": [{ "from": "index.ts", "to": "lunora/ratelimit/index.ts", "merge": "create-or-skip" }]
}

Fields

FieldPurpose
nameThe install name. Letters, digits, -, _ only
title / descriptionA short label and a longer explanation
docsPost-install guidance, printed after the item is applied
filesRequired. The files to scaffold
deps / devDependenciesnpm packages added to the project's package.json
requiresOther items this one needs — resolved transitively, dependencies first
bindingswrangler.jsonc additions, as a key path plus a value
envVarsVariables scaffolded into .dev.vars
entrypointReexportsRe-exports to inject into the worker entry

Files and merge strategies

Each file names a source inside the item directory, a destination relative to the project root, and how to apply it:

StrategyBehaviour
create-or-skipWrite the whole file; leave it alone if it already exists
schema-extensionAST-merge into the project's lunora/schema.ts

create-or-skip never overwrites, so re-adding an item cannot clobber a user's edits. Use schema-extension when your item contributes tables — it splices an .extend(...) into the existing schema rather than replacing it.

Environment variables

Non-secret variables are written into .dev.vars with their value. Secrets get a placeholder and a reminder to set the real value in production; a variable with no value defaults to secret: true.

{
    "name": "BETTER_AUTH_URL",
    "description": "Public base URL of your app, used for callbacks and cookies.",
    "value": "http://localhost:8787",
    "secret": false
}

Bindings

bindings entries are structural edits to wrangler.jsonc — a key path and the value to set there:

{
    "bindings": [
        {
            "path": ["d1_databases"],
            "value": [{ "binding": "DB", "database_name": "replace-me-db", "database_id": "<replace-with-d1-create-id>" }]
        }
    ]
}

Use obvious placeholders for values only the user can supply. lunora doctor specifically checks for D1 ids and email destinations left at their placeholder, so a half-configured item surfaces rather than failing mysteriously at deploy.

Worker-entry re-exports

If your item ships a Durable Object, container, or workflow class, wrangler requires it to be exported from the worker entry. Declare that and the CLI injects it:

{
    "entrypointReexports": [{ "module": "_generated/workflows", "comment": "Lunora workflows" }]
}

Validation

Manifests are validated on parse, and the rules are security rules rather than style rules: from and to paths cannot traverse outside the project, environment-variable names must be valid keys, values cannot contain newlines (which would inject extra lines into .dev.vars), and name must be a single safe identifier segment — it becomes a path segment and, for schema extensions, an import specifier.

A manifest that violates any of these is rejected rather than partially applied.

Building the catalog

lunora registry build regenerates the local catalog index.json that list and view read:

lunora registry build
lunora registry build --check    # verify the index is current — a CI gate

Run --check in CI so an item added without rebuilding the index fails the build.

Distributing

Items resolve from the published registry by default. Two overrides:

  • --from <dir> — a local registry root, expecting per-item subdirectories each with a registry.json. This is how you test an item before publishing.
  • --source <ref> — another remote source. Values outside gh:, github:, and https:// need --allow-unsafe-source, because applying an item writes code into the user's project.

See also