@alienplatform/bindings
Direct TypeScript bindings for Alien storage, kv, queue, vault, linked-container
discovery, and Postgres over an
in-process napi-rs addon. The addon itself lives in the Rust
crate crates/alien-bindings-node; this package is the published JavaScript
wrapper that loads it.
Remote Storage
Use Bindings.forRemoteDeployment from a trusted backend to access a Storage
resource in an existing deployment. Never put the Alien API token in browser,
mobile, or other client-side code. The token must have write access to the
deployment; read-only tokens cannot resolve cloud credentials.
import { Bindings } from "@alienplatform/bindings"
const bindings = await Bindings.forRemoteDeployment({
deploymentId: process.env.ALIEN_DEPLOYMENT_ID!,
token: process.env.ALIEN_API_TOKEN!,
})
const archive = bindings.storage("archive")
const write = await archive.put("reports/latest.json", Buffer.from(JSON.stringify({ ready: true })), {
condition: "absent",
attributes: {
contentType: "application/json",
cacheControl: "private, max-age=60",
metadata: { schema: "report-v1" },
},
})
const head = await archive.head("reports/latest.json")
const object = await archive.get("reports/latest.json")
const report = JSON.parse(object.data.toString())
console.log(write.eTag, head.meta, head.attributes, report)
const reports = await archive.list("reports/")
await archive.delete("reports/latest.json")
Cloud storage providers persist these object attributes. The local filesystem
provider rejects attribute-bearing writes because it cannot represent them.
GCS also rejects contentEncoding: "gzip": its decompressive transcoding omits
the response length required for byte-exact reads. Other GCS encodings, such as
br, are preserved.
condition: "absent" makes put an atomic create-only write. If the object
already exists, the operation fails with STORAGE_OBJECT_ALREADY_EXISTS
instead of replacing it.
Remote Storage exposes get, put, head, list, and delete. It does not
expose copy or signed URLs. The same Bindings and Storage handles remain valid
while the native client refreshes short-lived cloud credentials and periodically
rediscovers the deployment's assigned manager. Rotating the Alien API token
requires constructing a new Bindings value. Pass apiBaseUrl only when
targeting a non-default Alien API endpoint; plain HTTP is accepted only on a
loopback address for local development.
The named resource must be a Running, Frozen S3, GCS, or Azure Blob Storage resource with remote access enabled. Enabling remote access adds concrete object read/write/list/delete access for that bucket or container to the deployment's dedicated Remote Bindings identity. Generate and apply updated customer setup when enabling it on an existing deployment. The endpoint returns a short-lived lease for that Remote Bindings identity only after it validates the named resource, so the Alien token and all returned provider credentials must be treated as backend secrets.
Remote KV
A trusted backend can read and write a customer's KV resource using the same short-lived credential leases as remote Storage:
const bindings = await Bindings.forRemoteCustomer({ project, externalId, token })
await bindings.kv("check-cache").setJson("status", { ready: true }, { ttl: 86400 })
Enable remoteAccess on the KV resource and apply the updated setup first.
The resource must be Running and Frozen. AWS grants entry operations on the
selected DynamoDB table; Azure grants entity operations on the selected table.
Firestore IAM cannot isolate collections in the shared default database, so its
remote identity can access documents throughout the project. Enable it only
when that scope is appropriate.
Remote KV uses the same read, write, conditional-write, scan, and logical TTL behavior as workload KV. Raw values are limited to 408,576 bytes on DynamoDB, 783,360 bytes on Firestore (base64 encoding plus document metadata needs room), and 24,576 bytes on Azure and local storage. DynamoDB reserves 1 KiB of its 400 KiB item limit for keys and metadata. Use object Storage for larger values.
Linked containers
The same factories are re-exported by @alienplatform/sdk for Worker apps.
Long-running Container and Daemon apps can import this package directly. A
linked container is read-only service discovery:
import { container } from "@alienplatform/bindings"
const database = container("database")
const internalUrl = await database.getInternalUrl()
const publicUrl = await database.getPublicUrl() // string | null
Use the internal URL for calls between resources in the same deployment. Use the public URL only when the caller is outside that private network.
Postgres
Postgres is connection-only: every backend speaks the same wire protocol, so the binding hands back connection details and your app connects with its own driver. Resolving a managed cloud backend (Aurora, Cloud SQL, Flexible Server) reads the password from that cloud's secret store using the workload's own identity — the binding itself only ever carries a locator for it.
import { Client } from "pg"
import { postgres } from "@alienplatform/bindings"
const conn = await postgres("my-db").connection()
// Field style: node-postgres parses a URL's sslmode and would override `ssl`, so
// pass the fields when you need `conn.ssl` to take effect.
const client = new Client({
host: conn.host,
port: conn.port,
database: conn.database,
user: conn.username,
password: conn.password,
ssl: conn.ssl,
})
await client.connect()
conn.connectionString is the same details as a postgres:// URL, with the
credentials percent-encoded, for drivers that take one.
External (BYO) Postgres bindings use verified TLS by default. Their sslMode
policy is deliberately limited to "verify-full" and "disable" because
node-postgres cannot represent libpq's opportunistic prefer fallback without
owning the connection attempt. Use "disable" only as an explicit compatibility
opt-out for a plaintext-only server; missing legacy configuration defaults to
"verify-full".
Native addon resolution
Importing the package never loads the addon, so the package remains
sideEffects: false. Environment-backed factories load it on the first binding
operation. Bindings.forRemoteDeployment loads it immediately because manager
discovery is part of that async constructor. src/loader.ts resolves it in
order:
ALIEN_BINDINGS_ADDON_PATH— an explicit path to a.nodefile. A dev/test escape hatch only; never set in a published install.- The per-platform prebuild package
@alienplatform/bindings-<triple>, pulled in as anoptionalDependency. This is how end users get the addon:npm/buninstalls only the package matching the hostos/cpu/libc. TheoptionalDependenciesblock exists only in the published manifest — it is injected at publish time by the release pipeline, so a workspace checkout carries none. - Dev fallback: a locally-built addon at
crates/alien-bindings-node/alien-bindings-node.<triple>.node, found by walking up from the installed package. Loaded only if itsversion()matches this package's version (a stale build is warned about and rejected).
Local development
Build the addon for your host once, then run anything that imports the package:
bun run build:addon # or: pnpm -C packages/bindings run build:addon
build:addon runs napi build --platform --release against
crates/alien-bindings-node and drops the .node next to the crate, where the
loader's dev fallback (step 3 above) finds it. Rebuild after changing any Rust
in alien-bindings or alien-bindings-node. The built .node is gitignored.
build:addon only builds for the host triple; on a Mac, cross-building the
other mac triple (e.g. darwin-x64 from an arm64 host) needs an explicit
napi build --release --target x86_64-apple-darwin --cwd ../../crates/alien-bindings-node.
To point the loader at an addon somewhere else, set ALIEN_BINDINGS_ADDON_PATH
to its path (step 1).
Prebuild packages (npm/)
npm/<triple>/ holds the skeleton for each published per-platform package
(darwin-arm64, darwin-x64, linux-x64-gnu, linux-arm64-gnu). Each carries
a package.json (name/os/cpu/libc/main/files) and a README; the
.node is staged in at build time and is never committed. There is no musl
target: the deployment base images are glibc (chainguard/wolfi-base), not Alpine.
The release pipeline builds each addon on its native runner, stages it into the
matching npm/<triple>/ dir, rewrites the placeholder 0.0.0 versions to the
release version, injects the exact-version optionalDependencies into this
package's published manifest, and publishes the platform packages before the
wrapper. Pinning the exact version is what guarantees a published wrapper only
ever loads the matching-version platform addon.
Queue batches
queue("events").sendBatch([{ order: 1 }, { order: 2 }]) sends JSON messages;
sendBatchText(["first", "second"]) sends text. Python exposes send_batch and
send_batch_text on the queue handle. Rust exposes Queue::send_batch and
BoundQueue::send_batch.
Each returns one outcome per input in input order: sent means the provider
confirmed acceptance, rejected includes a code and message for a definite
rejection, and unknown includes a code and message when delivery could not be
established. Retrying unknown outcomes can produce duplicates. There are no
automatic retries or transaction guarantees across a batch. An exception before
outcomes are returned means no sends were attempted.
Messages must contain 1–65,536 UTF-8 bytes after serialization. Requests are chunked using SQS SendMessageBatch (up to ten entries and 256KiB), Pub/Sub publish, and Service Bus HTTP batch send. Service Bus also counts JSON envelope/escaping against the 256KiB batch limit, so heavily escaped text may be rejected. Empty input returns an empty result. Existing scoped publisher permissions cover batching.
Remote queues
Publish a Frozen queue with remoteAccess: true, apply its updated setup, and
resolve it from a backend:
const bindings = await Bindings.forRemoteCustomer({ project, externalId, token })
const events = bindings.queue("events")
await events.send({ order: 1 })
const results = await events.sendBatch([{ order: 2 }, { order: 3 }])
Python uses await Bindings.for_remote_customer(...), then
bindings.queue("events").send(...) or .send_batch(...). Both languages use the
Rust provider implementations. TypeScript and Python remote queue handles expose only sending;
cloud permissions also deny receiving, acknowledging, and purging. Credentials
refresh before operations as their leases expire. SQS, Pub/Sub, and Service Bus
use resource-scoped publisher grants. Batch results have the same partial-failure
semantics as local queue handles.
Sandbox capabilities for a new session come from await sandbox.capabilities().
After reconnecting, use await sandbox.capabilities(sandboxId) to query that
session. Azure sessions retain their original native or agent transport across
image upgrades and rollbacks, so jobs support can differ from a new session.