Skip to content

Bundles

github-actions[bot] edited this page Oct 20, 2026 · 6 revisions

Bundles

How nself bundle install reaches an app-ready stack, and how nself records which bundles are installed and why each plugin is present.

Bundle install: app-ready

nself bundle install <bundle> is done when the bundle is app-ready: every member is installed, the generated files are reconciled, the stack is started, every member that declares boot migrations has applied them, every table in the members' manifests is tracked in Hasura, and the services answer healthy. The command prints one line per step and ends with APP-READY, or with NOT APP-READY: step <step> failed for plugin <plugin>.

# Step What it checks
1 licence Entitlement for a paid bundle. A free bundle (the Task Bundle) makes no bundle entitlement call: the step reads ok with free bundle: no bundle entitlement call; plugin licences checked.
2 fetch The registry resolves every member version.
3 install Each member is installed in order; a failure rolls back what this run installed.
4 ledger The bundle and its installed members are recorded in the ledger.
5 reconcile Generated project files are regenerated for the new membership (skipped when nothing changed).
6 start nself start brings the stack up.
7 readiness Each member with migrations.apply: boot reports every migration applied in /health (waits up to NSELF_PLUGIN_READY_TIMEOUT). The plugin applies its own SQL; nself never does.
8 tracking Tracks every table named in a member's manifest that Hasura does not track yet, with one targeted bulk request of pg_track_table operations (never replace_metadata, never an untrack; already tracked tables send no request), then verifies them with export_metadata. x means public.x; s.x means schema s. A manifest table missing from Postgres fails the step naming the plugin and the table. Tracking is NSELF_V15=1 behaviour; nself plugin install does the same for the plugins it installs, including newly installed dependencies, and fails with E140 (exit 2). Only members that declare migrations.apply: boot are tracked (nself plugin install names a member with tables but no boot migrations in a note; nself bundle install does not track it either, and the tracking check then reports its untracked tables as E135, as before). Tables count as tracked only on the default postgres source. A metadata write by someone else between the export and the bulk request can give a false E135 or E140 that a rerun clears.
9 health The stack services are healthy and each member answers HTTP 200 on /health.

The first failing step stops the run; later steps show skipped. Outside an nSelf project the plugins are installed and recorded, the later steps are skipped, and the output says NOT VERIFIED; the exit code stays 0.

nself bundle install <bundle> --dry-run prints the nine steps as planned and changes nothing: no files, no ledger, no hooks.

A bundle that is not app-ready fails with E135 (exit 2) naming the bundle, the step and the plugin. Starting the stack and the readiness, tracking and health steps are NSELF_V15=1 behaviour. With NSELF_V15 off the command installs, records the ledger and reconciles (a failed reconcile is an error in both modes), prints those four steps as skipped / not verified (v1.5 behaviour, NSELF_V15=1), ends with NOT VERIFIED instead of APP-READY, and exits 0. A free bundle skips the ping_api bundle entitlement call; each plugin's licence is still checked. Rerun the command after fixing the cause; members that are already installed are not reinstalled.

The Task Bundle reaches app-ready (v1.5). With NSELF_V15=1, nself bundle install task --yes on a clean project on Linux with Docker (nself init && nself start) installs the free plugins served by plugins.nself.org and ends with APP-READY: all eight members (storage, notifications, jobs, search, feature-flags, audit-log, webhooks, tokens) apply their own migrations at boot, the tracking step tracks every table in their manifests (the behaviour in step 8), and each member answers /health. One prerequisite: set TOKENS_ENCRYPTION_KEY in the project .env before the install (an exported shell variable is not passed to the plugin) (a random 32-byte hex string, for example openssl rand -hex 32). The tokens plugin refuses to start without it and nothing in the CLI generates it yet, so without it the tokens container restarts in a loop and the readiness step ends with E118 for tokens after its bounded wait.

nself plugin install <name> of a plugin with boot migrations waits for them too when the stack is running (E118 or E119, exit 2; a warning with NSELF_V15 off). With the stack stopped it says readiness is checked at the next nself start or nself doctor.

nself doctor --deep reports E136 for each plugin the ledger lists whose plugin.json is gone from the plugin directory, and nothing when the ledger matches the disk. The fix is to reinstall the bundle (nself bundle install <bundle>) to restore the missing plugin; do not delete the ledger. A deliberate nself plugin remove of a bundle member drops that plugin's record from the ledger, so it is not reported.

The bundle ledger

The plugin directory ($NSELF_PLUGIN_DIR, default ~/.nself/plugins) keeps one machine-owned file, .nself-state/bundles.json. It is the install-state ledger, contract cli.bundle-ledger v1. Its JSON Schema is schemas/bundle-ledger.v1.schema.json.

The file starts with "_generated": "GENERATED BY nself — DO NOT HAND EDIT". nself writes it; do not edit it. A hand-edited or damaged ledger is reported as an error with the file path, never silently replaced.

{
  "_generated": "GENERATED BY nself — DO NOT HAND EDIT",
  "schema_version": 1,
  "bundles": {
    "chat": { "installed_at": "2026-10-10T12:00:00Z" }
  },
  "plugins": {
    "bots": {
      "installed_by": ["chat"],
      "explicit": false,
      "tier": "licensed",
      "version": "1.2.0",
      "checksum": ""
    }
  }
}
Field Meaning
bundles.<slug>.installed_at When the bundle was recorded as installed (RFC 3339, UTC).
plugins.<slug>.installed_by The installed bundles that reference the plugin, sorted.
plugins.<slug>.explicit True when the plugin was installed on its own, so removing a bundle must keep it.
plugins.<slug>.tier free or licensed.
plugins.<slug>.version The installed version.
plugins.<slug>.checksum Lowercase hex sha256 of the installed artifact, empty when none was recorded.

A plugin record exists only while installed_by is not empty or explicit is true.

How it is written

  • Writers take an OS lock on bundles.json.lock (next to the ledger), write a temp file in the same directory, fsync it, and rename it over the ledger. A reader sees the old file or the new one, never a partial file.
  • A crash between the temp write and the rename leaves the previous ledger intact.
  • The file is mode 0644 (it holds no secret) in a 0700 directory, <plugin dir>/.nself-state. That directory has no plugin.json, so plugin scans skip it. Before P7-PLUG-19 the ledger lived at <project>/.nself/state/bundles.json; it now follows the plugins, so every project sharing a plugin directory shares one ledger.
  • Windows is supported through WSL2, where the lock applies.

Remove rule

A bundle remove deletes a plugin only when the bundle was the last one in its installed_by and explicit is false. A plugin that another installed bundle still references, or that was installed on its own, stays. The ledger package implements the rule (RemoveBundle); nself bundle remove (and nself remove <bundle>) apply it.

Bundle remove

nself bundle remove <bundle> plans from the ledger, prints the plan, then removes:

  • Refcounts. Only plugins whose installed_by becomes empty and that are not explicit are removed, in reverse bundle order. Every other member stays and the plan says why (kept: py (installed_by: beta)). With bundles A={x,y} and B={y,z}, removing A removes x; y stays with installed_by: [B].
  • Shared plugin directory. The ledger lives in the plugin directory ($NSELF_PLUGIN_DIR or ~/.nself/plugins), so every project that uses that directory shares one record per bundle. bundle remove A in one project removes A's plugins for every project sharing the directory, as plugin remove always has. Use a per-project NSELF_PLUGIN_DIR to isolate projects.
  • Free bundles (the Task Bundle) remove like any other bundle.
  • Data is kept (schema and tables) unless you pass --delete-data. --delete-data asks for confirmation on a terminal; without a terminal it needs --yes, otherwise it exits 4 with E403 and changes nothing. A dry run needs neither.
  • Not recorded. The ledger at <plugin dir>/.nself-state/bundles.json records a bundle only when all its plugins are installed. For a bundle it does not record, bundle remove fails with E134 (exit 1): nothing it installed is removable as a unit. Remove the individual plugins with nself plugin remove <plugin>.
  • --dry-run prints the plan and changes nothing; it never creates the ledger. nself remove <plugin> --dry-run prints the plugin, the data rule, the referencing bundles and whether E133 would refuse, and changes nothing.
  • A failure stops the run. The ledger then keeps the bundle recorded and drops only the plugins already removed, so running the same command again continues from there.
  • --force keeps its meaning (overwrite hand-edited generated files). --keep-data is a hidden, deprecated alias; from v1.5 it does nothing and prints a warning. It is removed in v1.6.0.

nself remove <plugin> (and nself plugin remove) refuses a plugin an installed bundle still references: E133, exit 1, naming the bundle. Remove the bundle, or pass --force to remove only that plugin; the ledger then drops the plugin's record.

v1.4 mode (default today) v1.5 mode (NSELF_V15=1)
Paid bundle, no data flag drops plugin data, with one warning naming --delete-data and --keep-data keeps plugin data
Free bundle, no data flag keeps plugin data keeps plugin data
--delete-data needs --yes or a terminal confirmation (E403, exit 4) same
remove <plugin> of a bundle member removes it, with a warning refuses with E133 unless --force
Unrecorded bundle warns, removes nothing, exit 0 E134, exit 1
--keep-data keeps data deprecated no-op, with a warning

Plugin removal inside a bundle remove still bypasses the reverse-dependency check (force), as before.

First use (bootstrap)

When no ledger exists, nself derives one from the installed plugin directories and the bundle membership in bundles.json:

  • A bundle counts as installed when it can be installed as a unit (every bundle except the ɳSelf+ meta-bundle) and every member plugin is installed.
  • installed_by lists every installed bundle that contains the plugin (bundles may overlap).
  • A plugin in no installed bundle is explicit: true.

Bootstrap cannot tell a plugin you installed yourself from one a bundle installed, if a bundle later covers it. It treats that plugin as bundle-owned. Installs recorded after the ledger exists are exact.

A plugin or bundle whose name is not a valid slug cannot be recorded; bootstrap skips it and prints a warning. If the installed plugins cannot be read, there is no bootstrap: nself reports the error and writes nothing, rather than recording an empty ledger.

nself bundle list --json reads the ledger; the plain table never loads it. When the file does not exist it computes the bootstrap view in memory and writes nothing.

nself bundle list --json

Each row of the JSON array gains an additive ledger object, that bundle's slice of the ledger:

{
  "slug": "chat",
  "ledger": {
    "installed": true,
    "installed_at": "2026-10-10T12:00:00Z",
    "plugins": [
      { "slug": "bots", "installed_by": ["chat"], "explicit": false, "tier": "licensed", "version": "1.2.0", "checksum": "" }
    ]
  }
}

installed is false and installed_at is absent for a bundle that is not recorded as installed. plugins lists the bundle's member plugins that the ledger records, and is [] when there are none. All earlier fields are unchanged.

See also cmd-bundle.

Home


Getting Started


Commands


Features


Configuration


Plugins (87 + 10 monitoring)

Free (25)
Pro (62)
Planned (26)
  • plugin-audit
  • plugin-blog
  • plugin-checkout
  • plugin-commerce
  • plugin-drm
  • plugin-export
  • plugin-flow
  • plugin-import
  • plugin-ldap
  • plugin-mailgun
  • plugin-media
  • plugin-oauth-providers
  • plugin-pages
  • plugin-postmark
  • plugin-rate-limit
  • plugin-reports
  • plugin-saml
  • plugin-scheduler
  • plugin-sendgrid
  • plugin-sso
  • plugin-subscription
  • plugin-thumb
  • plugin-transcoder
  • plugin-twilio
  • plugin-waf
  • plugin-watermark

Guides


Architecture


Reference


Machine interface


Licensing


Security


Brand


Operations


Contributing


Admin


Changelog


All commands (21)

Clone this wiki locally