# facet.json

> Every field of the project config — what it controls, what the default is, and why.

Source: https://docs.astra-void.xyz/facet/reference/facet-json/

Written by `facet init` at the project root, committed with the project, and read by every command
except `list`.

```json title="facet.json — the defaults"
{
  "$schema": "https://facet.astra-void.xyz/schema.json",
  "style": "default",
  "theme": { "base": "zinc", "mode": "dark" },
  "aliases": {
    "ui": { "dir": "src/shared/ui" },
    "lib": { "dir": "src/shared/lib" },
    "hooks": { "dir": "src/shared/hooks" }
  },
  "velaConfig": "vela.config.ts"
}
```

The file is parsed loosely and normalized against those defaults, so a hand-edited config missing a
key picks the default up rather than crashing mid-copy.

## `$schema`

The JSON Schema the file advertises, for editor completion and validation. It is **generated** from
the CLI's `FacetConfig` type by the same deploy that publishes the registry, so it can never be a
version behind the CLI that writes files pointing at it.

## `style`

`"default"`, and there is no second value.

shadcn shipped two styles and has since deprecated `default`; the field outlived the second style.
That is most of the argument, and it is worse here: nothing about a Roblox component is verifiable
by reading it, so a second style doubles the set of components that have to be opened in Studio and
looked at — which is already the expensive, manual step.

The field stays because `facet.json` is a file you commit, and removing a key from it is a breaking
change for the benefit of deleting one line. It is also the natural place for a fork to say what it
serves. Nothing in the CLI branches on it, and it is typed as the literal `"default"` rather than
`string`, so any code that starts branching has to change the type first.

## `theme`

```json
"theme": { "base": "zinc", "mode": "dark" }
```

| Field | Values | |
| --- | --- | --- |
| `base` | `"zinc"` · `"slate"` · `"stone"` · `"neutral"` | Neutral ramp the semantic tokens derive from |
| `mode` | `"dark"` · `"light"` | Which side of the ramp they resolve to |

These are what `init` writes into the generated `vela.config.ts`, and what a later `doctor` uses to
print the correct `facetTheme({ … })` snippet if the config is unwired.

> **Editing this does not retheme anything on its own**
>
> The theme lives in `vela.config.ts`. Changing `facet.json` after `init` changes what the CLI would
> *suggest*, not what your build resolves — `init` never rewrites an existing Vela config. Change both,
> or change the Vela config alone. See [Theming](https://docs.astra-void.xyz/facet/guides/theming.md).

## `aliases`

Where each class of file lands, and how other copied files import it.

```json
"aliases": {
  "ui":    { "dir": "src/shared/ui",    "import": "shared/ui" },
  "lib":   { "dir": "src/shared/lib",   "import": "shared/lib" },
  "hooks": { "dir": "src/shared/hooks", "import": "shared/hooks" }
}
```

| Field | |
| --- | --- |
| `dir` | Directory from the project root. A **real path**, because roblox-ts projects are laid out by Rojo, not by module resolution. |
| `import` | The specifier other copied files use to reach this directory. **Omit for relative imports.** |

Which alias a file lands under is decided by its registry item type:

| Item type | Alias |
| --- | --- |
| `registry:ui` | `ui` |
| `registry:lib` | `lib` |
| `registry:hook` | `hooks` |
| `registry:block` | `ui` |

**Relative is the default**, because it needs no tsconfig `paths` and therefore works in a roblox-ts
project nobody configured for this. With `import` set, the rewrite is textual —
`~/lib/utils` → `shared/lib/utils`. Without it, the CLI computes a relative path from the importing
file's destination, so `~/lib/utils` inside `src/shared/ui/button.tsx` becomes `../lib/utils`.

If you set `import`, your tsconfig needs a matching `paths` entry. `facet doctor` **fails** on this
rather than warning, because every copied component imports through it. It accepts the specifier
itself or any wildcard that would cover it — `"shared/ui"`, `"shared/ui/*"` and `"shared/*"` all
count.

> **Changing an alias does not move files already copied**
>
> It only affects what the next `add` writes. To re-resolve an existing component, re-add it with
> `--overwrite` — and commit first, because that replaces the file wholesale.

## `velaConfig`

```json
"velaConfig": "vela.config.ts"
```

Path to the project's Vela config, from the project root. `init` creates it if it does not exist;
nothing ever rewrites it. `add` and `doctor` read it to check whether Facet's tokens are supplied.

That check is textual — it looks for `@facet-ui/theme` in the source. A config that spells the
tokens out by hand instead of spreading `facetTheme` is *unwired* but not necessarily broken, and
`doctor` then looks for each required token by name as a colour key. That is the most that can be
checked without evaluating a file that is arbitrary TypeScript.

## `registry`

```json
"registry": "https://ui.example.com/r"
```

Optional. A URL or a filesystem path. Omit for the published registry at
`https://facet.astra-void.xyz/r`.

This is the right place to point at a fork or a private registry for a team — everyone who clones the
project gets the same source without remembering a `--registry` flag. It is overridden by
`--registry` on the command line and by `FACET_REGISTRY_DIR` in the environment. See
[Using another registry](https://docs.astra-void.xyz/facet/guides/custom-registry.md).

### Pinning a revision

`https://facet.astra-void.xyz/r` **moves** — every push to Facet's `main` republishes it. The same
push also writes an immutable copy under the commit that produced it, and this field is how you pin
one:

```json
{ "registry": "https://facet.astra-void.xyz/r/a1b2c3d" }
```

Every command reads it — `add` copies from that revision, `diff` compares against it, `list` shows
what it holds, `doctor` checks it — and each prints which registry it used.
[`revisions.json`](https://facet.astra-void.xyz/revisions.json) lists what exists.

> **No new field, no new flag**
>
> This is deliberate, and it is the whole reason the versioning scheme is shaped this way. `registry`
> has been in `facet.json` since `init` first wrote one, for forks and private registries — so a CLI
> released long before revisions existed can pin one today. Nothing about the format changed either;
> a pinned revision is an ordinary registry base that happens never to move.

Pinning trades currency for stability, and the trade is real in both directions: a pinned project
gets a registry that cannot shift under it, and stops receiving fixes until someone changes this
line. See [Updating copied components](https://docs.astra-void.xyz/facet/guides/updating-copied-components.md#pinning-a-registry-revision)
for when that is worth doing.
