# Configurations

Alepha provides two primitives for configuration: `$env` for environment variables and `$atom` for runtime state.

## Environment Variables with $env

`$env` reads environment variables with schema validation, type coercion, and defaults. Import it from `"alepha"`.

```typescript
import { $env, z } from "alepha";

class App {
  env = $env(
    z.object({
      DATABASE_URL: z.text(),
      PORT: z.integer().default(3000),
      DEBUG: z.boolean().optional(),
    }),
  );

  connect() {
    console.log(this.env.DATABASE_URL); // string, guaranteed to exist
    console.log(this.env.PORT); // number, defaults to 3000
    console.log(this.env.DEBUG); // boolean | undefined
  }
}
```

The schema must be a `z.object(...)`. Each property maps to an environment variable name.
Alepha validates values at instantiation time and throws if required variables are missing.

### How env values are resolved

`Alepha.create()` merges `process.env` with the `env` key of the state object passed to `create()`:

```typescript
const alepha = Alepha.create({
  env: {
    DATABASE_URL: "postgres://localhost/mydb",
    PORT: "8080",
  },
});
```

Values passed to `Alepha.create({ env })` take precedence over `process.env`. Variables from `.env` files loaded before the process starts (e.g. via `alepha dev`) are available automatically through `process.env`.

A Cloudflare Worker receives its vars and secrets on the request's `env` binding, not on `process.env`. `alepha.loadEnv(env)` lifts them into `alepha.env` at runtime: only string values are taken (a D1, R2 or KV binding is skipped), a value already set wins, and the parsed `$env` cache is cleared so the next read sees them. The entry `alepha build` generates for Workers already does this.

### Variable interpolation

String values support `$VAR` interpolation using other variables from the same schema:

```typescript
class Config {
  env = $env(
    z.object({
      HOST: z.text({ default: "localhost" }),
      PORT: z.integer().default(5432),
      DB_NAME: z.text({ default: "mydb" }),
      DATABASE_URL: z.text({ default: "postgres://$HOST:$PORT/$DB_NAME" }),
    }),
  );
}
```

Substitution is transitive: a value may reference a key whose own value references another one. A value that must keep a literal `$KEY` escapes it as `$$KEY`: with `PORT` declared, a password set to `pa$$PORTss` reads `pa$PORTss` rather than `pa5432ss`. Only keys declared in the same schema are substituted; an unknown `$NAME` stays as written.

### Reading a variable under another name

`aliases` lets a variable be read from other names when it is not set itself.
Hosts hand values over under names of their choosing - a port they allocated
arrives as `PORT`, a database they provisioned as `POSTGRES_URL` - and this is
how an app accepts them without renaming its own configuration:

```typescript
import { $env, z } from "alepha";

class Config {
  env = $env(
    z.object({
      DATABASE_URL: z.text({ aliases: ["POSTGRES_URL"] }),
      SERVER_PORT: z
        .integer()
        .meta({ aliases: ["PORT"] })
        .default(3000),
    }),
  );
}
```

The declared key always wins. Aliases are tried in order and only when the key
itself is absent from the environment, and the value found is coerced and
validated as the key it stands in for - so `PORT=8080` yields the number `8080`
on `SERVER_PORT`, and `PORT=nonsense` fails validation the same way
`SERVER_PORT=nonsense` would. Defaults still apply when neither is set.

An alias is only ever read. It is not a key of its own: it stays out of the
parsed result, out of `alepha gen env`'s variable list (it is mentioned against
the key it feeds) and out of the deploy manifest, so the declared name remains
the single one the rest of the app - and the deploy target - refers to.

`alepha/server` uses this for `SERVER_PORT`, which is why an app deployed to a
host that injects `PORT` binds the right port with no configuration.

### Environment caching

Alepha caches parsed env results per schema. Multiple services using the same `z.object(...)` reference will share the same parsed output.

### Declassifying a variable that is not secret

**Every environment variable is treated as a secret.** That is already what
happens on deploy - every declared key is pushed to the target as an encrypted
binding - so there is nothing to do for a `DATABASE_URL` or an API key.

The annotation is the opt-out, for the handful of variables that genuinely are
not sensitive:

```typescript
import { $env, z } from "alepha";

class Payments {
  env = $env(
    z.object({
      STRIPE_SECRET_KEY: z.text(), // secret, like everything else
      PUBLIC_URL: z.text({ secret: false }), // declassified: safe in plaintext
    }),
  );
}
```

`secret: true` is accepted and is the default, so writing it documents intent
without changing behaviour. `.meta({ secret: false })` is equivalent to the
option - `z.text({ ... })` forwards unknown options to `.meta()`.

The default runs this way round on purpose. The annotation is easy to forget,
and forgetting it must never be what exposes a value: a missed `secret: false`
costs you an unnecessarily encrypted log level, while a missed `secret: true`
under the opposite default would leak a key.

Three things read it:

- **`alepha gen env`** labels the declassified variables in the generated
  template, so whoever fills it in can see at a glance which ones are safe to
  commit - everything unlabelled belongs in a secret store:

  ```txt
  # (public)
  #PUBLIC_URL=

  # Stripe API key
  #STRIPE_SECRET_KEY=
  ```

- **`alepha build`** records them under `variables` in `dist/manifest.json`,
  and every other declared key under `secrets`. The two lists are disjoint:
  each key lands in exactly one, with the description its schema declared.

- **`alepha deploy`** pushes a declassified key to Cloudflare as a
  `plain_text` binding instead of an encrypted `secret_text` one. That makes it
  readable in the dashboard and - the actual point - **editable** there, which a
  write-only secret is not. A key the app never declassified is still encrypted,
  so this only ever loosens what an author asked to loosen.

  Only keys the artifact itself vouched for are eligible: a key injected by an
  orchestrator through `.env.<env>.local`, or listed in `infra().secrets.keys` in
  `alepha.config.ts`, is not on `variables` and stays a secret.

## State Management with $atom

`$atom` defines a named, typed, validated piece of global state. Use it for application-level configuration and shared data.

### Defining an atom

```typescript
import { $atom, z } from "alepha";

const appConfig = $atom({
  name: "app.config",
  schema: z.object({
    theme: z.enum(["light", "dark"]),
    language: z.text({ default: "en" }),
  }),
  default: { theme: "light", language: "en" },
});
```

The `name` uniquely identifies the atom in the state store. The `schema` defines the shape and validation. The `default` provides the initial value.

Recommended naming convention for `name` is dot-separated, e.g. `"app.config"`, `"user.settings"`, etc.

If the schema itself is optional (wrapped with `.optional()`, e.g. `z.object({...}).optional()`), the `default` is optional too. Otherwise - even when every field inside the object is optional - `default` is required.

Beyond `name` / `schema` / `default`, an atom takes a few options that decide where its value may travel. On the server, what the current request wrote to the store is serialized into the HTML so the browser starts from the same state (SSR hydration), and a value set at boot never reaches the browser; these options shape that.

| Option        | Effect                                                                                                                                                                                                    |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `description` | Shown in the devtools and generated docs                                                                                                                                                                  |
| `persist`     | `"cookie"`, `"localStorage"` or `"sessionStorage"`: the browser keeps the value across reloads                                                                                                            |
| `serverOnly`  | The value never goes into the SSR payload, and the devtools show the atom without its value                                                                                                               |
| `personal`    | The value belongs to the visitor: it goes into a per-request `<script id="__ssr_private">`, never the shared payload, so a cached page cannot hand it to the next visitor. Default for cookie-bound atoms |
| `serialize`   | `(value) => value`: shapes the copy written into the HTML, to drop fields the browser has no business seeing. The server keeps the full value                                                             |

Four things to know before reaching for them:

- `serverOnly` and `persist` cannot be combined - `$atom()` throws at call time.
- `serialize` cannot remove a field the schema requires: the browser validates
  the result against `schema`.
- Cookie persistence is unsigned and unencrypted, and anything client-persisted is
  attacker-writable. Never persist roles, permissions, or entitlements in an atom; treat a
  persisted value as user input.
- `serverOnly` only covers the SSR payload and the devtools: an `$action` that returns the
  value still returns it.

### Reading and writing atoms

Use `alepha.store.get()` and `alepha.store.set()` (or `alepha.set()` as shorthand):

```typescript
const alepha = Alepha.create();

// Read
const config = alepha.store.get(appConfig);
console.log(config.theme); // "light"

// Write
alepha.store.set(appConfig, { theme: "dark", language: "fr" });

// Shorthand write on the container
alepha.set(appConfig, { theme: "dark", language: "fr" });

// Update from the current value
alepha.store.mut(appConfig, (current) => ({ ...current, theme: "light" }));
```

### Reading atoms with $store

`$store` creates a reactive getter that always returns the current atom value:

```typescript
import { $atom, $store, z } from "alepha";

const count = $atom({
  name: "count",
  schema: z.object({ value: z.number() }),
  default: { value: 0 },
});

class Counter {
  count = $store(count);

  current() {
    return this.count.value; // always reads current state
  }
}
```

Under the hood, `$store` registers the atom and replaces the property with a getter that reads from the state store. When the state changes, the next property access returns the updated value:

```typescript
const alepha = Alepha.create();
const counter = alepha.inject(Counter);

console.log(counter.count.value); // 0

alepha.store.set(count, { value: 42 });
console.log(counter.count.value); // 42
```

### State mutation events

`alepha.store.watch(atom, (value, prevValue) => ...)` observes one atom outside React, and returns an unsubscribe function. Underneath, every `store.set()` call emits a `"state:mutated"` event:

```typescript
alepha.events.on("state:mutated", ({ key, value, prevValue }) => {
  console.log(`State "${key}" changed from`, prevValue, "to", value);
});
```

> Atoms are not only about configuration.
> They power SSR hydration, React integration, and devtools.

For a value derived from one or more atoms, `$computed({ name, deps, get })` declares a read-only value recomputed on every read (`alepha.store.get(computed)`), never stored or hydrated. See [State Management](https://latest.alepha.dev/docs/guides-frontend-state-management.md).
