Configuration
highx.config.ts, the lock file, and what each field is for.
The config declares what analysis cannot know. It does not list bindings — those are inferred from your code.
import { defineConfig } from "highx";
import { cloudflare } from "@highx/cloudflare/preset";
export default defineConfig({
adapter: cloudflare(),
name: "notes",
});That is a complete configuration. Bindings come from your code, resource IDs from the lock, and everything platform-specific from the adapter. What remains is what nothing else can tell you:
export default defineConfig({
adapter: cloudflare(),
name: "notes",
entry: "index.ts",
functions: "functions.ts",
output: "../.highx",
vars: { LOG_LEVEL: "debug" },
resources: { GLOBAL_DB: "notes-global", SESSION_KV: "notes-sessions" },
retention: { subscriptionHistoryLimit: 32, sagaRetentionDays: 30, idempotencyRetentionDays: 7 },
stages: { production: {} },
});Note what is not here: no compatibility date, no binding list, no resource kinds, no file format. Those are inferred, defaulted by the adapter, or the adapter's business entirely — which is what will let a second platform be added without this file changing shape.
Fields
adapter
The platform you deploy to, and the source of everything platform-specific: how to recognize its bindings, how to reach its secret store, how to verify credentials, and how to write its configuration file.
adapter: cloudflare(), // defaults are enough
adapter: cloudflare({ compatibilityDate: "2025-01-01" }), // unless you disagreecloudflare() accepts compatibilityDate, compatibilityFlags, and
observability. Without an adapter, bindings can be found but not classified,
and doctor fails with config:adapter rather than guessing.
name
The deployment's name, and the default prefix for resource names. The only field with no sensible default.
entry
The entry the compiler analyses. Defaults to index.ts.
functions
The module declaring your descriptors.
output
Where the generated API is written, relative to the config. Defaults to
.highx beside it; the scaffold sets ../.highx so a frontend imports it as
../.highx/api rather than reaching down through the server directory.
output: "../app/src/generated",The generated client module is string literals only, so importing it can never pull a handler into a browser bundle.
resources
Account-level names, keyed by binding name. The kind is not repeated, because your code already established it:
resources: { GLOBAL_DB: "notes-global" },Anything unnamed falls back to <name>-<binding>. An entry in the lock beats
both — a resource that exists is not silently repointed because a name changed.
retention
Bounds on operational storage. Declare it here and hand it to
createTenantObject: this field is what doctor reads, the tenant object is
what sweeps. → Retention
vars
Plain, committed configuration values. Never secrets — a vars entry is in
source control, visible in the dashboard, and shared by every environment.
stages
Named deployment targets, each with its own resources. --stage selects one;
without it, commands act on the base stage.
bindings
An escape hatch for the two cases inference cannot cover: a binding reached through indirection the compiler cannot follow, and a kind it guessed wrong.
bindings: [
{ name: "LEGACY_KV", kind: "kv" },
{ name: "ANALYTICS", kind: "queue", isOptional: true },
],An entry here wins over what was inferred. isOptional makes a missing binding
a warning rather than a failure.
classify
If you wrap a binding in a class of your own, name its constructor and anything passed to it is classified:
classify: { MyVectorStore: "r2", TelemetrySink: "queue" },statefulClasses
Only needed when you export more than one Durable Object class, since the pairing is otherwise unambiguous:
statefulClasses: { ACCOUNT_DO: "AccountDO", BILLING_DO: "BillingDO" },highx.lock.json
Records which real resource each binding resolved to:
{
"version": 1,
"resources": {
"GLOBAL_DB": { "kind": "d1", "name": "highx-global", "id": "8f1b3c2d-…" }
}
}Commit it. It exists because the platform configuration is generated, and a generated file cannot be where a real resource ID lives — the next generation would erase it, and a D1 database ID cannot be derived again.
A malformed lock is an error rather than being treated as empty, because an empty lock quietly means "provision everything again".