Durable Streams API https://s2.dev
  • Rust 99.3%
  • Shell 0.3%
  • Smarty 0.2%
  • Just 0.1%
Find a file
detail-app[bot] 0e99007688
fix(lite): resolve AWS region from profile chain for static credentials (#780)
**Detail bug report:** [View on
Detail](https://app.detail.dev/org_89d327b3-b883-4365-b6a3-46b6701342a9/bugs/bug_8cac9425-ce79-4db4-b569-bf59c78d8d81)

Closes #778

## Bug

In `lite/src/server.rs`, `s3_builder()` builds the `object_store` AWS S3
client backing SlateDB. It branches on whether static env credentials
(`AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY`) are present:

- The **static-credentials arm** set credentials but never resolved the
region — it relied solely on `AmazonS3Builder::from_env()`, which reads
only `AWS_*` env vars and silently falls back to `us-east-1` when
`AWS_REGION`/`AWS_DEFAULT_REGION` are unset.
- The **sibling `_ =>` arm** resolved region from the full AWS default
chain (env → profile → IMDS) via `aws_config::load_defaults` and called
`with_region`.

The two arms of the same `match` resolved the same field (`region`) by
different chains. A deployment that supplied credentials via env but
region via `~/.aws/config` hit the static arm, dropped the profile
region, and targeted `us-east-1`. Against real AWS S3 for a bucket
elsewhere this produces a wrong-region `301`; `object_store` does not
disable reqwest's redirect following, and `remove_sensitive_headers`
strips the `Authorization` header on the cross-host redirect, so the
followed request to the correct-region endpoint is unauthenticated and
AWS rejects it with a non-retryable `403`.

## Fix

The static arm now resolves region env-first (`AWS_REGION` then
`AWS_DEFAULT_REGION`), and only when both env knobs are absent falls
back to `aws_config::load_defaults(...).await.region()` — the same
standard chain the `_ =>` arm uses — then applies
`builder.with_region(region)`. This makes both arms resolve region by
the same chain tiers while preserving the env-first fast path for the
common static-creds + `AWS_REGION` workflow (`load_defaults` is only
reached when env region is absent, i.e. the trigger intersection). The
credential wiring (`StaticCredentialProvider` + `AWS_SESSION_TOKEN`) is
unchanged; the change is purely additive region resolution.

## Testing

Added three hermetic `#[tokio::test]` regression tests in
`lite/src/server.rs` (`mod tests`) covering the static arm's new
contract:
- profile region is applied when env region is absent (the regression —
confirmed to fail on the pre-fix code with `left: None, right:
Some("eu-west-1")` and pass after the fix)
- env region takes precedence over profile (preserves the documented
fast path)
- no bogus region is synthesized when no region is available anywhere

Routine checks all pass: `cargo check --locked -p s2-lite` (default and
`--all-features`), `cargo +nightly fmt --all --check`, `cargo clippy
--locked -p s2-lite --all-targets -- -D warnings --allow deprecated`,
and the full s2-lite nextest suite (331/331).

End-to-end smoke (not versioned — ran against a containerized
S3-compatible backend during development): built the release `server`
binary and ran it against `adobe/s3mock` over HTTP with static env creds
and the region supplied only via `AWS_CONFIG_FILE` (the bug-trigger
intersection, `AWS_REGION`/`AWS_DEFAULT_REGION` unset). The server
logged the resolved `region=us-east-1` from the profile tier (previously
silently dropped), SlateDB wrote manifest/WAL/compaction objects to S3,
and the full basin/stream/append/tail API lifecycle succeeded (HTTP
201/200), with a clean SIGTERM shutdown and no `403`/`301`/redirect
errors. (LocalStack's latest image is license-gated; S3Mock was used as
the equivalent HTTP S3-compatible backend.) This confirms the
custom-endpoint static-creds path still works and the profile region is
now honored; it does not reproduce the live-AWS redirect/auth-strip
chain because S3Mock does not issue wrong-region 301s or validate SigV4
region.

Not verified: live AWS S3 end-to-end (a real bucket in a non-`us-east-1`
region with static keys). The environment has no AWS credentials (`aws
sts get-caller-identity` returns `Unable to locate credentials`, no
`~/.aws`, no IMDS), so the live-AWS 301 → auth-stripped 403 path could
not be captured directly. The hermetic regression test covers the
trigger (region misresolution), and the S3Mock smoke covers
no-regression plus profile-region resolution; the live-AWS HTTP-chain
leg is the only uncovered item.

---
_Automatic Fixes PRs can be [configured
here](https://app.detail.dev/org_89d327b3-b883-4365-b6a3-46b6701342a9/settings/repos/repo_c4bd6a47-9b7d-4b62-9c18-8cf0ac18a8f9/bugs)._

---------

Co-authored-by: detail-app[bot] <180357370+detail-app[bot]@users.noreply.github.com>
History 2026-09-27 09:18:22 -07:00
.cargo ci: add Rust dependency cooldown gate (#713) 2026-08-24 21:23:18 +03:00
.claude/skills/release docs: add s2-storage and s2-resource-spec to release skill (#541) 2026-06-12 15:34:18 -04:00
.github feat(helm): persistent volumes and separate WAL storage (#768) 2026-09-22 21:20:21 +05:30
api chore: release (#783) 2026-09-26 04:47:13 +05:30
assets readme tweaks (#90) 2026-01-21 11:19:35 -05:00
charts/s2-lite-helm Bump s2-lite-helm chart to appVersion 0.43.0 2026-09-25 23:55:21 +00:00
cli chore: release (#783) 2026-09-26 04:47:13 +05:30
common chore: release (#783) 2026-09-26 04:47:13 +05:30
docs/adr docs: encryption key header (#416) 2026-04-21 21:36:32 -04:00
hooks fix: reject . and .. as access token and stream names (#318) 2026-03-15 11:27:54 +05:30
lite fix(lite): resolve AWS region from profile chain for static credentials (#780) 2026-09-27 09:18:22 -07:00
resource-spec chore: release (#783) 2026-09-26 04:47:13 +05:30
sdk chore: release (#783) 2026-09-26 04:47:13 +05:30
sim feat!: expose storage classes as strings and in location responses (#775) 2026-09-26 04:29:54 +05:30
storage chore: release (#783) 2026-09-26 04:47:13 +05:30
testcontainers chore: release (#783) 2026-09-26 04:47:13 +05:30
.gitignore feat(cli): detect install channel for version output and upgrade hints (#662) 2026-07-24 19:17:18 +05:30
.gitmodules chore: fix submodule sync workflow (#57) 2026-01-14 23:47:18 -05:00
.rustfmt.toml init 2025-11-07 12:09:00 -08:00
AGENTS.md ci: add Rust dependency cooldown gate (#713) 2026-08-24 21:23:18 +03:00
Cargo.lock chore: release (#783) 2026-09-26 04:47:13 +05:30
Cargo.toml chore: release (#783) 2026-09-26 04:47:13 +05:30
CLAUDE.md s2-lite backend (#11) 2026-01-04 23:37:38 -05:00
cliff.toml chore: rejig versioning and release workflow (#163) 2026-02-05 00:45:44 -07:00
Cross.toml feat(cli): detect install channel for version output and upgrade hints (#662) 2026-07-24 19:17:18 +05:30
deny.toml chore(deps): upgrade SlateDB to 0.16 and refresh dependencies (#755) 2026-09-17 22:09:54 -07:00
Dockerfile feat(cli): detect install channel for version output and upgrade hints (#662) 2026-07-24 19:17:18 +05:30
install.sh feat(cli): detect install channel for version output and upgrade hints (#662) 2026-07-24 19:17:18 +05:30
justfile ci: add Rust dependency cooldown gate (#713) 2026-08-24 21:23:18 +03:00
LICENSE update copyright line in license 2026-01-17 17:03:56 -05:00
README.md docs: correct lite flush interval default comment (#773) 2026-09-22 11:04:03 -07:00
release-plz.toml feat: add s2-testcontainers crate (#551) 2026-06-15 11:30:54 -04:00

S2, the durable streams API

s2.dev is a serverless datastore for real-time, streaming data.

This repository contains:

  • s2-cli - Command-line interface for S2
  • s2-lite - Open source, self-hostable server implementation of the S2 API
  • s2-sdk - Rust SDK for S2

Development

Use the nightly Cargo dependency commands so that the repository publication cooldown applies:

cargo +nightly add <crate>
cargo +nightly update
cargo +nightly update -p <crate>
cargo +nightly remove <crate>
cargo +nightly generate-lockfile

Use --locked with normal build, check, test, run, document, fetch, and metadata commands. The simulator is temporarily exempt until its separate lockfile is regenerated. The pull request dependency check verifies every proposed lock-file change before Rust build jobs start.

Install the repository Cargo tools from Homebrew bottles:

brew install cargo-deny cargo-nextest

Installation

Homebrew (macOS/Linux)

brew install s2-streamstore/s2/s2

Cargo

cargo install --locked s2-cli

Release Binaries (macOS/Linux)

curl -fsSL https://raw.githubusercontent.com/s2-streamstore/s2/main/install.sh | bash

Or specify a version with VERSION=x.y.z before the command. See all releases.

Docker

docker pull ghcr.io/s2-streamstore/s2

Authentication

Store an access token in the OS credential store:

s2 auth access-token set

Or pipe it in from a script or a secret manager:

op read 'op://S2/CLI/access-token' | s2 auth access-token set --stdin

For CI and other ephemeral environments, set S2_ACCESS_TOKEN in the environment. On a headless host with no credential store, opt into a private plaintext file instead (mode 0600 on Unix):

printf '%s' "$S2_ACCESS_TOKEN" |
  s2 auth access-token set --stdin --insecure-storage
unset S2_ACCESS_TOKEN

Plaintext access_token values in config.toml are deprecated. Migrate one with s2 auth access-token migrate, or re-store it with --insecure-storage on a headless host that has no credential store.

s2-lite

s2-lite is embedded as the s2 lite subcommand of the CLI. It's a self-hostable server implementation of the S2 API.

It uses SlateDB as its storage engine, which relies entirely on object storage for durability.

It is easy to run s2 lite against object stores like AWS S3 and Tigris. It is a single-node binary with no other external dependencies.

You can also simply not specify a --bucket, which makes it operate entirely in-memory (or use --local-root to persist to local disk instead).

Tip

When you point lite at a --bucket, data is always durable on object storage before being acknowledged or returned to readers — just like s2.dev.

The optional in-memory mode (no --bucket or --local-root specified) just makes it an effective S2 emulator for integration tests.

Quickstart

Here's how you can run in-memory without any external dependency:

# Using Docker
docker run -p 8080:80 ghcr.io/s2-streamstore/s2 lite

# Or directly with the CLI
s2 lite --port 8080
AWS S3 bucket example
docker run -p 8080:80 \
  -e AWS_PROFILE=${AWS_PROFILE} \
  -v ~/.aws:/home/nonroot/.aws:ro \
  ghcr.io/s2-streamstore/s2 lite \
  --bucket ${S3_BUCKET} \
  --path s2lite
Static credentials example (Tigris, R2 etc)
docker run -p 8080:80 \
  -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
  -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
  -e AWS_ENDPOINT_URL_S3=${AWS_ENDPOINT_URL_S3} \
  ghcr.io/s2-streamstore/s2 lite \
  --bucket ${S3_BUCKET} \
  --path s2lite

Note

Point the S2 CLI or SDKs at your lite instance like this:

export S2_ACCOUNT_ENDPOINT="http://localhost:8080"
export S2_BASIN_ENDPOINT="http://localhost:8080"
export S2_ACCESS_TOKEN="ignored"

Let's make sure the server is ready:

while ! curl -sf ${S2_ACCOUNT_ENDPOINT}/health -o /dev/null; do echo Waiting...; sleep 2; done && echo Up!

Install the CLI (see Installation above) or upgrade if s2 --version is older than 0.26

Let's create a basin with auto-creation of streams enabled:

s2 create-basin liteness --create-stream-on-append --create-stream-on-read

Test your performance:

s2 bench liteness --target-mibps 10 --duration 5s --catchup-delay 0s

S2 Benchmark

Now let's try streaming sessions. In one or more new terminals (make sure you re-export the env vars noted above),

s2 read s2://liteness/starwars 2> /dev/null

Now back from your original terminal, let's write to the stream:

nc starwars.s2.dev 23 | s2 append s2://liteness/starwars

S2 Star Wars Streaming

Kubernetes Deployment

Deploy s2-lite to Kubernetes using Helm. See the Helm chart documentation for installation instructions and configuration options.

Storage

Lite persists to an S3-compatible bucket or a local directory, or runs in-memory when neither is given. The write-ahead log (WAL) shares the main store by default; it can be placed in a separate bucket or directory to isolate WAL latency from flushes and compaction. --path applies to both stores.

Setting Main store WAL store
S3 bucket --bucket --wal-bucket / S2LITE_WAL_BUCKET
Local directory --local-root --wal-local-root / S2LITE_WAL_LOCAL_ROOT
S3 endpoint AWS_ENDPOINT_URL_S3 S2LITE_WAL_AWS_ENDPOINT_URL_S3
AWS region AWS_REGION S2LITE_WAL_AWS_REGION
Static credentials AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY S2LITE_WAL_AWS_ACCESS_KEY_ID + S2LITE_WAL_AWS_SECRET_ACCESS_KEY
Session token AWS_SESSION_TOKEN S2LITE_WAL_AWS_SESSION_TOKEN

Without static credentials, the standard AWS credential chain (profile, instance role, etc.) is used. The WAL bucket inherits the main store's S3 settings; each S2LITE_WAL_AWS_* variable overrides just that setting, except that a WAL key pair replaces the main credentials (and session token) as a set. A WAL store requires a persistent main store.

# LSM in S3, WAL on local disk
s2 lite --bucket my-bucket --wal-local-root /data/wal

# LSM and WAL in separate buckets
s2 lite --bucket my-bucket --wal-bucket my-wal-bucket

Monitoring

/health will return 200 on success for readiness and liveness checks

/metrics returns Prometheus text format

Internals

SlateDB settings

Settings reference

Use SL8_ prefixed environment variables, e.g.:

# Defaults to 50ms for S3, 5ms otherwise; follows the WAL store when set
SL8_FLUSH_INTERVAL=10ms

Design

Concepts

  • HTTP serving is implemented using axum
  • Each stream corresponds to a Tokio task called streamer that owns the current tail position, serializes appends, and broadcasts acknowledged records to followers
  • Appends are pipelined to improve performance against high-latency object storage
  • lite::backend::kv::Key documents the data modeling in SlateDB

Compatibility

API Coverage

Complete specs are available:

  • OpenAPI for the REST-ful core
  • Protobuf definitions
  • S2S, which is the streaming session protocol

Important

Unlike the cloud service where the basin is implicit as a subdomain, /streams/* requests must specify the basin using the S2-Basin header. The SDKs take care of this automatically.

Endpoint Support
/basins Supported
/streams Supported
/streams/{stream}/records Supported
/access-tokens Not supported https://github.com/s2-streamstore/s2/issues/28
/metrics Not supported