Skip to main content

Bindings & the runtime adapter

A binding connects a resource primitive (Database, Bucket, Secret, Queue) to a compute primitive (Function, Worker, CronJob, WebApp, API). At deploy time the SDK projects the binding's connection details into the running container. At runtime, an adapter uses those details to wire up a working client.

The net effect: this code is portable.

import { db, stripeKey } from '../infra.js';

export default async function handler() {
const key = await stripeKey.value();
const rows = await db.query('SELECT * FROM orders WHERE status = $1', ['paid']);
return { count: rows.length, apiKey: key };
}

stripeKey.value() calls Secrets Manager on AWS and reads a sealed-secret volume on the runtime — identical handler code.

Declaring bindings

Bindings go on the compute primitive's config. The keys (db, stripeKey below) are the names you'll import in your handler.

const web = new WebApp(app, 'web', {
handler: './src/server.ts',
bindings: {
db: ordersDb,
stripeKey: stripeKey,
receipts: receiptsBucket,
},
});

The SDK does three things at provision time:

  1. Permissions: on AWS, the compute primitive's IAM role gets the IAM statements it needs to use each bound resource (s3:GetObject for buckets, secretsmanager:GetSecretValue for secrets, etc.).
  2. Connection projection: a SAIL_BINDINGS environment variable is injected into the container, holding a JSON map of urn → { id, type, attributes, bindingName }. This is the wiring info.
  3. Filesystem projection (runtime target only): secret/database/bucket/queue credentials are mounted as files under /var/run/sprintsail/<kind>/<bindingName>/. The runtime adapter reads them on first use.

You don't see any of this. You just call db.query(...).

The runtime adapter

The adapter is the in-container counterpart to the provider. It implements the runtime methods every resource primitive exposes:

interface RuntimeAdapter {
getSecret(urn): Promise<string>;
queryDatabase(urn, sql, params): Promise<any[]>;
putToBucket(urn, key, body): Promise<void>;
getFromBucket(urn, key): Promise<Buffer | null>;
publishToQueue(urn, message): Promise<void>;
}

Two adapters ship today:

  • @sprintsail/runtime-aws — uses the AWS SDK (@aws-sdk/client-s3, pg against RDS, @aws-sdk/client-secrets-manager, @aws-sdk/client-sqs).
  • @sprintsail/runtime-sprintsail-runtime — uses @aws-sdk/client-s3 against MinIO, pg against CloudNativePG, projected secret files, amqplib against RabbitMQ.

The bundle the SDK builds for your handler imports the right adapter as a side effect before your code runs. You don't import either one yourself.

Cross-target compatibility

Because the adapter contract is fixed, the same handler runs on either target:

  • Bucket.put(key, body) writes to S3 on AWS, MinIO on the runtime. SDK is @aws-sdk/client-s3 in both cases (MinIO speaks S3).
  • Database.query(sql, params) runs against Postgres in both cases (pg client).
  • Secret.value() reads from Secrets Manager on AWS, a sealed-secret volume on the runtime.
  • Queue.publish(msg) sends to SQS on AWS, an AMQP queue on the runtime.

Worker triggers vs. bindings

Workers have an additional concept: triggers.

const processOrder = new Worker(app, 'process-order', {
handler: './src/handlers/process-order.ts',
triggers: [ordersQueue], // <- queues that *invoke* this worker
bindings: { db: ordersDb }, // <- resources the handler uses
});

A trigger is a queue that invokes the worker. A binding is something the handler uses. The same Queue can be both (a worker can publish to its own input queue), but they're semantically different — and the SDK wires them differently.

See also