Skip to main content

AWS Provider

@rank-lang/plugin-aws is a first-party provider package for AWS-backed getters and mutations. It registers the aws namespace and ships ordinary Rank source modules alongside provider exports, so you can import typed helpers such as DynamoDB::createClient and DynamoDB::Table<...> directly from the provider package.

Like other providers, it is not built into the compiler. Register it in your project's manifest and Rank will materialize it through the same cache and lockfile flow used for registry-backed dependencies.

Start here if you want typed AWS reads and explicit write plans from Rank. For runnable setups, see Secrets-Backed Config, LocalStack Bootstrap, DynamoDB Report, and S3 Logs.

Install

Register the package in rank.toml:

manifestVersion = 1

[package]
name = "aws-example"
version = "0.1.0"
source = "src"

[providers]
aws = { registry = "npm", package = "@rank-lang/plugin-aws", version = "0.1.0" }

The package exposes three public modules:

use aws::{ DynamoDB, S3, SecretsManager }

For provider registration rules, registry configuration, and host security defaults, see the manifest reference.

Current Surface

@rank-lang/plugin-aws currently provides:

  • DynamoDB::Item and DynamoDB::Query getters
  • S3::Object<T> and SecretsManager::Secret<T> getters
  • DynamoDB::Put, DynamoDB::Update, and S3::Put mutation exports
  • provider-owned Rank modules for aws::DynamoDB, aws::S3, and aws::SecretsManager
  • typed helper aliases such as Table<T, PK>, UpdatePatch<Table>, and createClient(...)

The provider runtime memoizes live AWS SDK clients per provider session. The createClient(...) helpers themselves return plain serializable config objects, so they remain safe to use in pure preparation and serve-time setup flows.

Quick Start

Create typed client configs in ordinary Rank code:

use aws::{ DynamoDB, S3, SecretsManager }

dynamoClient: aws::DynamoDB::ClientConfig = DynamoDB::createClient({
region: `us-east-1`,
})

s3Client: aws::S3::ClientConfig = S3::createClient({
region: `us-east-1`,
})

secretsClient: aws::SecretsManager::ClientConfig = SecretsManager::createClient({
region: `us-east-1`,
})

These values are serializable configs, not live SDK clients. The live client instances exist only inside the admitted provider runtime.

Getter Example

use aws::{ DynamoDB, S3, SecretsManager }

User = Object {
userId: string,
email: string,
status: `active` | `inactive`,
}

UsersTable = DynamoDB::Table<User, `userId`>

dynamoClient: aws::DynamoDB::ClientConfig = DynamoDB::createClient({
region: `us-east-1`,
})

{ item, metadata } = DynamoDB::Item<UsersTable> {
client: dynamoClient,
tableName: `Users`,
key: {
userId: `123`,
},
}

Additional getter notes:

  • DynamoDB::Query<Resource> uses explicit target metadata and narrows indexName based on the selected table or index resource.
  • S3::Object<T> reads UTF-8 JSON objects.
  • SecretsManager::Secret<T> currently supports format: string and format: json.

Declaring Effects

use aws::DynamoDB::{ Table, UpdatePatch }
use std::Mutation

User = Object {
userId: string,
email: string,
status: `active` | `inactive`,
}

UsersTable = Table<User, `userId`>
UserPatch = UpdatePatch<UsersTable>

dynamoClient: aws::DynamoDB::ClientConfig = aws::DynamoDB::createClient({
region: `us-east-1`,
})

s3Client: aws::S3::ClientConfig = aws::S3::createClient({
region: `us-east-1`,
})

putUser: Mutation::Effect<User> = aws::DynamoDB::Put<UsersTable> {
client: dynamoClient,
tableName: `Users`,
}

activateUser: Mutation::Effect<UserPatch> = aws::DynamoDB::Update<UsersTable, UserPatch> {
client: dynamoClient,
tableName: `Users`,
key: {
userId: `123`,
},
}

uploadUser: Mutation::Effect<User> = aws::S3::Put<User> {
client: s3Client,
bucket: `profiles`,
key: `users/123.json`,
}

created = putUser({
userId: `123`,
email: `alice@example.com`,
status: `inactive`,
})

activated = activateUser({
status: `active`,
})

uploaded = uploadUser({
userId: created.userId,
email: created.email,
status: created.status,
})

Fulfillment returns the declared payload value rather than the provider receipt. Receipt metadata is still retained for commit summaries and Mutation::commit(...) projections.

Complete Project Example

The earlier snippet shows how AWS-backed effects are declared and fulfilled, but it still does not commit anything by itself. A complete runnable project also needs:

  • provider registration in rank.toml
  • host admission for network plus the exact mutation export names you call
  • a top-level pub main that returns Mutation::commit(...)

rank.toml:

manifestVersion = 1

[package]
name = "aws-mutation-example"
version = "0.1.0"
source = "src"

[providers]
aws = { registry = "npm", package = "@rank-lang/plugin-aws", version = "0.1.0" }

[security]
allow-provider-capabilities = ["network"]
allow-provider-mutation-exports = [
"aws::DynamoDB::Put",
]

src/main.rank:

use aws::DynamoDB::{ Table }
use std::Mutation

User = Object {
userId: string,
email: string,
status: `active` | `inactive`,
}

UsersTable = Table<User, `userId`>

dynamoClient: aws::DynamoDB::ClientConfig = aws::DynamoDB::createClient({
region: `us-east-1`,
})

putUser: Mutation::Effect<User> = aws::DynamoDB::Put<UsersTable> {
client: dynamoClient,
tableName: `Users`,
}

plan = Mutation::plan({
created: putUser({
userId: `123`,
email: `alice@example.com`,
status: `active`,
}),
})

pub main = ||
Mutation::commit(
plan,
|report| {
status: report.status,
created: report.operations.created,
},
)

This example is structurally complete. To run it against real AWS or LocalStack, the target Users table must exist and the credentials or explicit endpoints passed to createClient(...) must be valid for that environment.

Running rank src/main.rank now evaluates the program, commits the plan, and prints the projected commit result as YAML by default. Pass --format json if you want JSON instead. rank check still only type-checks the graph and does not perform the write.

Commit Timing

Provider mutators are intentionally explicit about when I/O happens:

  • declaring putUser: Mutation::Effect<User> is static
  • calling putUser({...}) fulfills the payload and returns a commit-local User value
  • Mutation::plan(...) stays pure and only records required operations
  • the actual AWS write happens only when a host realizes the top-level Mutation::commit(...) descriptor

That explicit commit boundary is what lets Rank keep ordinary evaluation deterministic while still making writes typed, host-admitted, and projectable into ordinary output.

For the language-level mutation flow, see Mutations.

Emit And Serve

The same top-level Mutation::commit(...) descriptor can also be returned from HTTP::Response.body. That means AWS mutation outcomes can flow into CLI output, manifest emission, or serve responses without manually unpacking provider receipts.

use std::HTTP
use std::Mutation
use std::Runtime
use aws::DynamoDB::{ Table, createClient }

HealthRoute = HTTP::Route {
method: `GET`,
path: `/health`
}

Routes = HealthRoute

Entry = Object {
id: string,
active: bool,
}

UsersTable = Table<Entry, `id`>

dynamoClient: aws::DynamoDB::ClientConfig = createClient({
region: `us-east-1`,
})

putUser: Mutation::Effect<Entry> = aws::DynamoDB::Put<UsersTable> {
client: dynamoClient,
tableName: `Users`,
}

pub main = |req: Runtime::ExecutionContext<Routes>| {
status: 200,
body: Mutation::commit(
Mutation::plan({
createdUser: putUser({ id: `1`, active: true })
}),
|report| {
commitStatus: report.status,
createdUser: report.operations.createdUser,
}
)
}

For more on output shapes, see Output and manifests. For serve-specific runtime behavior, see Server.

Security And Operations

This provider is intentionally explicit about host admission:

  • getter and mutation exports require the network provider capability
  • mutation exports are separately default-denied and must be explicitly allowlisted by exact name
  • the host does not currently add AWS-specific table, bucket, or secret allowlists on top of IAM

Typical host security config:

[security]
allow-provider-capabilities = ["network"]
allow-provider-mutation-exports = [
"aws::DynamoDB::Put",
"aws::DynamoDB::Update",
"aws::S3::Put",
]

IAM remains the real resource boundary. Use least-privilege policies for the tables, buckets, and secrets your program touches.

If credentials enter through Env<T>, destructure them through @sensitive bindings so they stay redacted in diagnostics:

use std::Env

SysEnvironment = Object {
AWS_ACCESS_KEY_ID: string,
AWS_SECRET_ACCESS_KEY: string,
...: string,
}

@sensitive
{ AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = Env<SysEnvironment> {}

Typed Helpers

The provider-owned source modules expose additional schema helpers beyond the runtime exports.

DynamoDB

  • Table<T, PK, SK?> declares a base table schema
  • GSI<BaseTable, Name, IndexTable> and LSI<BaseTable, Name, IndexTable> declare query-only secondary-index schemas
  • KeyInput<Table> derives the exact key object from the table schema
  • UpdatePatch<Table> is shallow and excludes declared key fields
  • QueryInput<Resource> specializes table, GSI, and LSI query inputs

Receipts And Metadata

  • getter results return request metadata structures
  • DynamoDB::PutReceipt and DynamoDB::UpdateReceipt include requestId and optional consumed-capacity summaries
  • S3::PutReceipt includes requestId and optional eTag / versionId

LocalStack And Development

The package keeps a pinned LocalStack harness in packages/plugins/aws/test/localstack/docker-compose.yml for DynamoDB, S3, and Secrets Manager.

Typical workflow from the repo root:

npm run localstack:up -w @rank-lang/plugin-aws
npm run test:localstack -w @rank-lang/plugin-aws
npm run localstack:down -w @rank-lang/plugin-aws

General package development commands:

npm run build -w @rank-lang/plugin-aws
npm run typecheck -w @rank-lang/plugin-aws
npm run test -w @rank-lang/plugin-aws

Known v1 Limitations

  • S3::Object<T> and S3::Put<T> are JSON-only in v1
  • SecretsManager::Secret<T> supports SecretString only; SecretBinary is rejected
  • DynamoDB::Update<Table, Patch> currently lowers shallow SET updates only
  • DynamoDB::Query<Resource> uses native DynamoDB expression strings rather than a typed DSL
  • resource-scoped host allowlists for AWS tables, buckets, and secrets are still deferred

For provider packaging and publication workflows, see the provider authoring guide.