Customer buckets for a hosted product
Customer buckets (bring your own bucket) keep a customer's files at rest in their own AWS, Google Cloud, or Azure account, while your backend stays hosted. This guide adds that storage setting to a product you host.
It's the hosted-product counterpart of Deployment UX: instead of deploying your software, the customer's admin connects one bucket.
If you already deploy into the customer's cloud, see When you already deploy to the customer.
Architecture diagram. Customer cloud (Bucket, Remote bindings identity) is reached inbound from the Your backend (Your product, @alienplatform/bindings), via short-lived credentials.
- The customer's admin runs the initial setup once. It creates the bucket and a dedicated identity that can reach only that bucket.
- Your backend asks Alien for short-lived credentials for that bucket, then calls the cloud's storage API directly with
@alienplatform/bindings. - Object data goes from your backend to the bucket, not through Alien. Your backend never receives the admin's credentials.
Enable buckets
This happens once. In the dashboard, on the Buckets page, Connect your application creates a server API key, and Connect a bucket creates a setup link. From the CLI:
alien projects capabilities enable buckets
alien api-keys create --for remote-bindings --description productionSave the key as ALIEN_REMOTE_BINDINGS_KEY on your backend.
Creating setup links and reading connection status need your regular Alien API key (ALIEN_API_KEY), used by the client from Set up a client.
Add a Connect storage button
This runs on your server, in the route your Connect storage button calls. Create a setup link with the bucket item for the customer's external ID (your own stable ID for them), and send the customer to it.
The link opens the deployment portal, where their admin runs initial setup. The request is POST /v1/deployment-groups/setup-links:
import { alien, PROJECT } from "./alien"
export async function connectStorageLink(customer: { id: string; slug: string }) {
const link = await alien.setupLinks.create({
project: PROJECT,
externalId: customer.id,
name: customer.slug,
setupItems: [{ item: "bucket", required: true }],
})
return link.deploymentLink
}In the portal, the customer sees Data storage: "Choose where your data is stored." Their admin runs initial setup in their own account, which creates:
| Cloud | Created |
|---|---|
| AWS | An Amazon S3 bucket |
| Google Cloud | A Cloud Storage bucket |
| Azure | An Azure Blob Storage container |
Initial setup also creates a remote bindings identity with read, write, list, and delete access to that bucket only. It always creates a new bucket. It doesn't attach a bucket the customer already has.
Read and write
This runs on your server, wherever your product stores or reads a customer's file. Address the customer by external ID with Bindings.forRemoteCustomer. The bucket's resource name is storage:
import { Bindings } from "@alienplatform/bindings"
// One Bindings value per customer. Credentials refresh automatically.
export async function customerBucket(customerId: string) {
const bindings = await Bindings.forRemoteCustomer({
project: "docproc",
externalId: customerId,
token: process.env.ALIEN_REMOTE_BINDINGS_KEY!,
})
return bindings.storage("storage")
}
export async function saveUpload(customerId: string, docId: string, file: Uint8Array) {
const bucket = await customerBucket(customerId)
await bucket.put(`uploads/${docId}.pdf`, file, {
attributes: { contentType: "application/pdf" },
})
const { data } = await bucket.get(`uploads/${docId}.pdf`)
return data.byteLength
}The handle supports get, put, head, list(prefix), and delete. put with condition: "absent" creates an object only if it doesn't exist yet. Copy and signed URLs aren't available, so your backend proxies uploads and downloads.
Keep one Bindings value per customer and reuse it: it refreshes credentials as they expire. Create a new one after you rotate the API key.
@alienplatform/bindings loads a native module. It runs on Linux (x64 and arm64, glibc) and macOS.
Show the connection
This runs on your server, when your settings page loads. Read capabilities.buckets.state from the capability overview, GET /v1/projects/{idOrName}/project-capabilities/overview, with the capability helper from Customer-managed keys:
import { capability } from "./capability"
export async function storageStatus(customerId: string) {
return (await capability(customerId, "buckets"))?.state ?? "not-connected"
}The states are not-connected, setting-up, connected, needs-attention, and revoked.
If the customer deletes the bucket or the identity, your calls fail. Treat that as the customer's decision: show "Storage disconnected" and stop writing.
When you already deploy to the customer
If you already deploy your own stack to the customer's cloud, declare the bucket in alien.ts with remoteAccess: true and address it by deployment with Bindings.forRemoteDeployment({ deploymentId, token }). See Remote bindings and the examples.
Test it
Try it as your own test customer before you ship:
alien onboard "Test customer" --external-id org_test --setup-items storageOpen the printed link and run initial setup in a cloud account you control. Then, for each cloud you offer:
- Call
saveUploadwithorg_testand confirm the object appears in the bucket in the cloud console. - Read it back, list the prefix, and delete it.
- Delete the setup stack or resources in the cloud account, and confirm your calls fail and your UI shows "Storage disconnected".
Next
- Remote Sandboxes: run code next to that data
- Customer-managed keys: put the customer's key on your own records
- Remote bindings