Documentation menu

Protect private files before hosting

Before your first record and publish: what .nexapolicy controls, why labels are repository-specific encryption rather than ignore rules, how to protect .env and credential files with a secrets label, validate the policy, and confirm sealed files before anything is hosted.

You have a project and nothing recorded. Before the first nexa record, decide which of its files are private and protect them. This page does that end to end, with a secrets label, and ends with the first version recorded and its protection confirmed. If the project has nothing private, the short path is two commands.

Every command here names the project first — nexa my-project policy validate — which is the same as nexa policy validate -p my-project. Substitute your own project name.

Why this comes before hosting

Protection is decided when a version is recorded, and it is never applied to versions recorded earlier. nexa publish then uploads every version recorded so far. So a file recorded before its label existed is readable in that version, and hosting it puts that version on Rinko Hub. Now, before the first record, is the only moment protection costs nothing.

What .nexapolicy controls

~/Development/.nexapolicy — created by nexa root — says which paths of each project are sealed, and under which label. It is read when you record: the files it matches are encrypted under the label's key before they are written into the version, and the policy in force is pinned into that version, so a collaborator receives the rules with the work. Editing the file later never reinterprets a version already recorded.

A label is encryption, not an ignore rule. A labelled file is still recorded, synced and hosted — as ciphertext that only devices holding the label's key can open. A label never leaves anything out of a version; to keep build output out, use an ignore block (see Leave build output out). The policy file names labels and never holds a key, so it is safe to read, review and publish.

Labels are repository-specific

Each project under the root is its own repository, and a label belongs to one repository: nexa my-project label create secrets creates secrets in my-project alone, with its own identifier and its own key holders. Another project's secrets is a different label that happens to share the name.

So the root holds one physical file, but every label rule in it sits inside the block of the project whose label it names: project "my-project" { … }. A label rule outside every project block — in global — is refused by check, validate, explain and record, because each project would resolve it against its own, unrelated labels:

Refused: a label in global
policy 1

global {
  label "secrets" { paths = ["**/.env*"] }
}
Command
nexa my-project policy validate
Expected output
error: object error
  caused by: invalid policy on line 3: labels are repository-specific: `label "secrets"` in `global` would apply to every project in this Development Root and resolve against each repository's own labels; scope it under the project instead: `project "my-project" { label "secrets" { paths = [...] } }`

To protect the same kind of file in several projects, create the label in each and give each project its own block. Earlier releases accepted the global form; a file still using it is refused until its rules are moved into project blocks, and nothing it would have sealed is recorded in the meantime.

Labels are not retroactive

Important: A label protects the next version, never an earlier one

A version recorded before a file was labelled keeps that file readable, permanently. Labelling it later seals it in the versions recorded after; history is never rewritten, so the earlier version still holds the plaintext — locally, and on Rinko Hub once published.

If a credential has already been recorded or published unprotected, treat it as exposed. Rotate it — issue a new key, token or password and revoke the old one — and protect the file before recording the replacement. Sealing the file afterwards does not make the old value secret again.

Exact paths and patterns

You writeIt protectsIf it matches no file
app.jsonExactly that path, character for characterrecord refuses, and nothing is recorded
secrets/**That directory and everything beneath itA warning: it may be written ahead of its files
**/*.pemAny path of that shape, at any depthA warning, for the same reason

An exact path names one file. If it is not there, the rule protects nothing — usually because of a typo — so recording refuses rather than producing a version that only looks protected. A subtree or glob that matches nothing yet is reported and allowed, because it can be written before its files exist. Paths are relative to the project directory.

Protect secrets, step by step

The example project holds .env, app.json, config/private.json, secrets/deploy.key and src/main.ts. Your files will differ: the paths below are examples of what commonly needs protecting, not a list every project has.

Create the label

Command
nexa my-project label create secrets
Expected output
Created label secrets.
  identifier: d35270b3bcc3891c2c0532d708e06f2f
  name:       secrets

No paths were given, so nothing is declared yet. Add them in ~/Development/.nexapolicy:
    project "my-project" {
      label "secrets" { paths = ["<path>"] }
    }
then check them with `nexa my-project policy validate`.
The key was granted to this device. Share it with another device using `nexa label grant secrets --device <identity>`, where the identity comes from `nexa device identity` on that machine.

The label now exists in my-project's repository, and its key was granted to this device. Nothing is protected yet: no rule names it.

Confirm this device holds the key

Command
nexa my-project label grants secrets
Expected output
Label:      secrets
Identifier: d35270b3bcc3891c2c0532d708e06f2f

  epoch 0    7a24-4d6c-bfed-9fa9

1 grant(s). A device keeps every epoch it was given: rotation adds an epoch, it never takes one away.
Command
nexa my-project device identity
Expected output
Device:   7a24-4d6c-bfed-9fa9
Identity: 764d6f11…0d563bf6

Give the identity line to whoever holds a label you need; they run `nexa label grant <label> --device <identity>`.

The device listed under the grant is the Device: line of nexa device identity — this machine holds the key. Another device of yours, or a collaborator's, gets it with nexa label grant secrets --device <identity>, or from Rinko Hub; see Sharing a key. A device without the key cannot read what the label seals.

Add the rules

Open ~/Development/.nexapolicy and add a block for the project. A first draft often contains mistakes; this one has two:

~/Development/.nexapolicy — first draft
policy 1

project "my-project" {
  label "secrets" {
    paths = [
      ".env",
      ".app.json",
      "credentials.json",
      "config/private.json",
      "secrets/**",
      "**/*.pem",
    ]
  }
}

Validate

Command
nexa my-project policy validate
Expected output
~/Development/.nexapolicy is valid (policy version 1).
  my-project                   6 rule(s) over 1 label(s)

Syntax:    valid
Labels:    every key resolves; no two rules of equal specificity disagree
Matching:  project `my-project`, against the files on disk now
  exact paths  2 of 4 match a file
      credentials.json             secrets        0 files -- protects nothing
      .app.json                    secrets        0 files -- protects nothing
  patterns     1 of 2 match a file
      **/*.pem                     secrets        0 files yet

A rule matching no file protects nothing. Check what a path resolves to
with `nexa policy explain <path>`.
error: cannot do that here: the policy is well formed, but an exact path in it matches no file; `nexa record` will refuse until it is corrected

Two exact paths match nothing. credentials.json is simply not in this project — an example that does not apply here, so remove it. .app.json is a typo for app.json, and nexa policy explain shows what each spelling does:

Command
nexa my-project policy explain app.json
Expected output
Matched:        no rule
Effective label: none -- stored in the clear
Source file:    .nexapolicy (in the Development Root)
Command
nexa my-project policy explain .app.json
Expected output
Matched:
  rank 3/9/0   .app.json                    secrets              project my-project
Effective label: secrets (d35270b3bcc3891c2c0532d708e06f2f)
Source file:    .nexapolicy (in the Development Root)
Key:            held by this device

The real file would have been stored in the clear while the rule protected a file that does not exist. Correct the draft:

~/Development/.nexapolicy — corrected
policy 1

project "my-project" {
  label "secrets" {
    paths = [
      ".env",
      "app.json",
      "config/private.json",
      "secrets/**",
      "**/*.pem",
    ]
  }
}
Command
nexa my-project policy validate
Expected output
~/Development/.nexapolicy is valid (policy version 1).
  my-project                   5 rule(s) over 1 label(s)

Syntax:    valid
Labels:    every key resolves; no two rules of equal specificity disagree
Matching:  project `my-project`, against the files on disk now
  exact paths  3 of 3 match a file
  patterns     1 of 2 match a file
      **/*.pem                     secrets        0 files yet

A rule matching no file protects nothing. Check what a path resolves to
with `nexa policy explain <path>`.
Command
nexa my-project policy explain app.json
Expected output
Matched:
  rank 3/8/0   app.json                     secrets              project my-project
Effective label: secrets (d35270b3bcc3891c2c0532d708e06f2f)
Source file:    .nexapolicy (in the Development Root)
Key:            held by this device

Every exact path now names a file. **/*.pem matches nothing yet, which is allowed: it will protect a certificate the moment one is added.

Record, and confirm what was sealed

Command
nexa my-project record -m "Initial version"
Expected output
warning: 1 policy rule(s) match no file in this workspace, so they protect nothing here at present: `**/*.pem` (secrets)
Created version 74f1bb1d5f74
  workspace: main
  files:     5
  sealed:    4

Selected on branch main.

sealed: 4 is the check that matters: .env, app.json, config/private.json and secrets/deploy.key. Compare it with the files you meant to protect before going further. If it is lower than you expected, a file you meant to protect was recorded readable — fix the rule, rotate whatever that file holds, and record again.

Command
nexa my-project policy show
Expected output
Declared in ~/Development/.nexapolicy
  for project `my-project`

  config/private.json          readable   secrets        1 file
  app.json                     readable   secrets        1 file
  .env                         readable   secrets        1 file
  secrets/**                   readable   secrets        1 file
  **/*.pem                     readable   secrets        0 files yet

A rule matching 0 files protects nothing. Check a path with
`nexa policy explain <path>`.

Leave build output out

record captures every file in the project, so compiled output and dependency folders — a Rust target/, a node_modules/ — would be recorded too, often gigabytes of them. Before the first record, ignore them in the same file:

~/Development/.nexapolicy
policy 1

global {
  ignore { paths = ["**/target", "**/node_modules"] }
}

global applies to every project under the root; an ignore block inside a project block applies to that project alone. Ignored paths are never recorded, never shown by status, and left alone on disk. Ignoring is not protecting — an ignored file is just absent from the version — so secrets still need a label. Requires Nexa 0.1.7 or later; run nexa update first.

A project with no secrets

Not every project has private files, and none of the above is required. A project with no label and no block validates and records normally, and nothing stops it from being hosted:

Command
nexa docs-site policy validate
Expected output
~/Development/.nexapolicy is valid (policy version 1).
  my-project                   5 rule(s) over 1 label(s)  (another repository's labels: validate from that project)

Syntax:    valid
Labels:    every key resolves; no two rules of equal specificity disagree
Matching:  no rule applies to project `docs-site`
Command
nexa docs-site record -m "Initial version"
Expected output
Created version 4d999e16e6f0
  workspace: main
  files:     1

Selected on branch main.

There is no sealed: line because nothing is protected. Every file in this version will be readable by whoever can read the repository on Rinko Hub — which is what you want when nothing in it is secret. my-project's block is listed but not resolved here: its labels are my-project's.

Before you continue

  • Every private file in the project is named by a rule in its own project block.
  • Build output and dependency folders are covered by an ignore block.
  • nexa <project> policy validate succeeds.
  • nexa <project> policy explain <path> reports each private file as protected.
  • The first record's sealed: count matches the files you meant to protect.
  • Any credential that was ever recorded unprotected has been rotated.

Then host it on Rinko Hub. For the full grammar, see Labels, workspaces and .nexapolicy; for what a device without the key sees, Privacy and file control.