The Baseworks runtime binding surface. createEnv() resolves logical names —
capability services (foldbase, org, iam), driver bindings (db, storage,
kv), and plain config — to typed clients or connection descriptors, without the
app hard-coding a single URL.
Resolution is lazy: an unresolved capability only throws when it is actually
used (or eagerly if you list it in require). The mechanism is pluggable — today
it reads process.env; a flect-manifest or Consul/DNS source drops in later
without touching call sites.
zod + fetch only. Driver packages (e.g. ioredis) are optional peers,
never hard dependencies.
pnpm add @baseworks/sdkCapability clients (@baseworks/foldbase, @baseworks/org, @baseworks/iam)
come as dependencies. KV drivers are peers — install the one you bind:
pnpm add ioredis # only if you bind a redis:// KVimport { createEnv } from '@baseworks/sdk'
const env = createEnv({
// optional: fail-fast at boot if these aren't configured
require: ['org', 'iam'],
})
// capability services → typed clients (from the registry)
const org = env.service('org') // OrgClient
const iam = env.service('iam') // IamClient
const fb = env.service('foldbase') // FoldBase
await org.listOrgs()
// driver bindings → descriptors; you construct the driver (layer discipline)
const main = env.db('main') // { kind:'db', url, token?, extra }
// KV → the raw driver client, handed back untouched
const redis = await env.kv<import('ioredis').Redis>('cache')
await redis.set('k', 'v')
// plain config
const port = env.var('PORT')For a name org, env.service('org') resolves in order:
- explicit override —
createEnv({ services: { org: 'http://…' } }) - per-binding env var —
ORG_SERVICE_URL, thenORG_URL - gateway convention —
BASEWORKS_GATEWAY+/org
Driver bindings (db/storage/kv) skip the gateway and try a kind-prefixed
var first, then the bare form:
| Kind | Tries | then |
|---|---|---|
db('main') |
DB_MAIN_URL |
MAIN_URL |
storage('files') |
STORAGE_FILES_URL |
FILES_URL |
kv('cache') |
KV_CACHE_URL |
CACHE_URL |
So one logical name can be reused across kinds without colliding, while the short
<NAME>_URL form keeps working.
Tokens / tenant ride along the binding: <NAME>_TOKEN (or the default
BASEWORKS_TOKEN, or createEnv({ token })) and <NAME>_TENANT. foldbase
defaults tenant to platform when none is given.
| Member | Returns | Notes |
|---|---|---|
service(name) |
typed client | cached per name; throws BindingError if unresolved |
db(name) / storage(name) |
ResolvedBinding |
descriptor only — you build the driver |
kv<T>(name) |
Promise<T> |
raw driver client; driver dynamically imported; cached per name |
var(key) |
string | undefined |
plain config passthrough |
has(name) |
boolean |
is a capability resolvable? (no build, no throw) |
require(names) |
void |
throws BindingError listing any unresolved |
createEnv(opts):
interface CreateEnvOptions {
services?: Record<string, string> // explicit per-service URL overrides (highest precedence)
token?: string // default bearer for outbound calls
require?: CapabilityName[] // fail-fast at construction
source?: EnvSource // inject a resolver (tests / flect / Consul)
env?: Record<string, string | undefined> // inject process.env (tests)
}The SDK writes no get/set wrapper — it picks a driver by URL scheme and hands back the driver's own client instance. Supported today:
| Scheme | Driver (optional peer) |
|---|---|
redis:// · rediss:// |
ioredis |
Absent binding → you simply never call kv. Absent driver → a clear
pnpm add ioredis error at first use, not a bare module-not-found. Unsupported
scheme → an explicit error. The client is cached per name (one connection).
Register the name → typed-client factory in src/registry.ts:
export interface CapabilityClients {
foldbase: FoldBase
org: OrgClient
iam: IamClient
billing: BillingClient // ← new
}
export const REGISTRY = {
// …
billing: (b) => new BillingClient({ baseUrl: b.url, token: b.token }),
}env.service('billing') is now typed and resolvable via BILLING_SERVICE_URL /
BASEWORKS_GATEWAY/billing.
nvm use 22 # vitest 4 needs node:util.styleText (Node ≥ 20.12)
pnpm test # vitest run
pnpm typecheck- Layer discipline —
db/storagereturn descriptors, not live drivers, so the SDK stays free of DB/driver dependencies. The app constructs the driver. - Lazy + cached — nothing connects at
createEnv; the firstservice/kvcall builds and caches. A failedkvbuild is not cached, so a later call retries. - Pluggable source — swap
EnvVarSourcefor a flect-manifest or Consul source viacreateEnv({ source }); call sites are unchanged.
UNLICENSED — internal Baseworks package.