# Secrets

`context.secrets` is a tiny, runtime-agnostic accessor for configuration secrets and bindings. Core never reads `process.env` directly, so the same `context.secrets.require("STRIPE_KEY")` call resolves the same way everywhere: a VPS (Node/Bun `process.env`), Cloudflare Workers (the per-request `env` binding), or Deno. The environment difference lives in the adapter, not your procedures.

It reads secrets Outer doesn't already own. It is **not** a store: Outer never writes, encrypts, rotates, or persists secrets. `.auth()`'s own `secret` is still passed explicitly.

## The accessor

```ts
type OuterSecrets<T = Record<string, string | undefined>> = {
  get<K extends keyof T & string>(key: K): T[K];
  require<K extends keyof T & string>(key: K): NonNullable<T[K]>;
  readonly all: T;
};
```

`require` throws `Missing required secret "X".` when a value is unset or empty — the deterministic replacement for `process.env.X!`, which silently yields `undefined` at runtime on platforms without a `process` global rather than failing loudly.

Reads are **synchronous** by design: secrets come from an already-materialized source, never a fetch. For an async backend (Vault, AWS Secrets Manager), resolve its values at startup and pass the resulting object to `fromRecord`.

## Pass secrets on the constructor

Pick the adapter that matches how your runtime hands you the environment, then read it in any handler via `context.secrets`:

```ts
import { Outer } from "@outerjs/server";
import { fromEnv } from "@outerjs/server/secrets";
import { pglite } from "@outerjs/server/pglite";

const outer = new Outer({ db: pglite(), secrets: fromEnv() })
  .schema(v1_0)
  .auth({ secret: process.env.AUTH_SECRET! })
  .procedure("billing.charge", (base) =>
    base.handler(async ({ context }) => {
      const key = context.secrets.require("STRIPE_KEY"); // throws early if unset
      // …
    }),
  )
  .build();
```

## Adapters

Four adapters ship with the package.

| Adapter                   | Reads from                                                           | Types                                           |
| ------------------------- | -------------------------------------------------------------------- | ----------------------------------------------- |
| `fromSchema(schema, env)` | Any value, validated against a Zod / Standard Schema                 | Inferred from the schema (defaults, transforms) |
| `fromEnv()`               | `process.env` (Node / Bun)                                           | `string \| undefined`                           |
| `fromRecord(record)`      | A plain object — the Workers `env` binding, `Deno.env.toObject()`, … | Inferred from the record                        |
| `memorySecrets(values?)`  | A fixed map                                                          | Inferred — for tests                            |

The rule of thumb: **typed platform env → `fromRecord`, untyped `process.env` → `fromSchema`.**

### `fromSchema` — validate untyped env

Node's `process.env` is `string | undefined` with no types, so validating it up front earns its keep. `fromSchema` accepts any [Standard Schema](https://standardschema.dev) (Zod, Valibot, ArkType) and returns a **fully-typed** accessor — the integrated replacement for a standalone `schema.parse(env)`. Defaults and transforms flow through to `get`, `require`, and `all`; invalid input throws an error listing every failing path.

```ts
import { Outer } from "@outerjs/server";
import { fromSchema } from "@outerjs/server/secrets";
import { z } from "zod";

const secrets = fromSchema(
  z.object({
    PORT: z.coerce.number().default(3000),
    AUTH_SECRET: z.string().default("dev-only-secret"),
    // comma-separated string → string[]
    CORS_ORIGINS: z
      .string()
      .default("https://hub.outer.now")
      .transform((s) =>
        s
          .split(",")
          .map((o) => o.trim())
          .filter(Boolean),
      ),
    ADMIN_EMAIL: z.email().optional(),
  }),
  process.env,
);

secrets.all.CORS_ORIGINS; // string[] — validated once, typed everywhere
secrets.get("PORT"); // number

new Outer({ db: pglite(), cors: { origins: secrets.get("CORS_ORIGINS") }, secrets })
  .schema(v1_0)
  .auth({ secret: secrets.get("AUTH_SECRET") })
  .build();
```

A schema whose validation is asynchronous (async refinements) throws — resolve those at startup and pass the resolved object to `fromRecord`.

### `fromRecord` — a typed env, straight through

On Cloudflare Workers the `Env` interface generated by `wrangler types` already types the bindings and secrets, so there's nothing to re-declare. `fromRecord` takes the env directly and infers `OuterSecrets<Env>`:

```ts
import { Outer } from "@outerjs/server";
import { fromRecord } from "@outerjs/server/secrets";

export class OuterDO extends DurableObject<Env> {
  readonly outer;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    const secrets = fromRecord(env);

    this.outer = new Outer({
      db: { dialect: new DurableObjectSqliteDialect(ctx.storage.sql as any), kind: "sqlite" },
      storage: fromR2(secrets.get("OUTER_FILES")), // typed as R2Bucket
      secrets,
    })
      .schema(v1_0)
      .auth({ secret: secrets.get("AUTH_SECRET") ?? "dev-only-secret" })
      .build();
  }
}
```

`fromRecord` does no validation or transforms — apply defaults (`?? "…"`) and any string splitting at the use site. Values not present in the generated `Env` (config supplied via `.dev.vars`) can be added with a small type hint: `fromRecord(env as Env & { BASE_URL?: string })`.

## Typing note

`context.secrets` is typed to the default string-bag shape (`get` returns `string | undefined`), not your schema's inferred types. For the precise per-key types, read the accessor you constructed — in a single-file server or Worker it's in scope on the procedure closures. If you define procedures in separate modules and want the narrow types there, annotate the read: `(context.secrets as OuterSecrets<Env>).require("OUTER_FILES")`.

## Next steps

- [Deployment](/guide/deployment) — set the env per host, and match adapters to runtimes
- [Auth](/guide/auth) — where the `secret` your app validates first gets used
