Documentation
Getting started
Point /v1 at https://api.stateway.dev with a
Bearer token. Dynamo and Cosmos are optional bridges over the same
partitions — use them only when an existing SDK already owns the client.
Access
Hosted Stateway is invite-only. There is no public signup. You should receive credentials from an operator:
-
Native
/v1+ DynamoDB:STATEWAY_AKID+STATEWAY_SECRET(use the secret as a Bearer token for/v1) -
Cosmos DB:
COSMOS_MASTER_KEY(base64), or both
export STATEWAY_ENDPOINT=https://api.stateway.dev
export STATEWAY_AKID=…
export STATEWAY_SECRET=…
export STATEWAY_TOKEN=$STATEWAY_SECRET # Bearer for /v1
export COSMOS_MASTER_KEY=… # if using Cosmos
Confirm with
curl -sS https://api.stateway.dev/health — expect
authentication: "required" and an
apis.native entry.
Clients
Zero-dependency helpers for invitees who do not have the private repo. Same API as the in-repo packages.
JavaScript (ESM, Node 18+ or browsers with
fetch):
import { Stateway } from "https://stateway.dev/client.js";
const sw = new Stateway({
endpoint: "https://api.stateway.dev",
token: process.env.STATEWAY_TOKEN,
});
await sw.createCollection({ name: "orders", hash: "pk", range: "sk" });
await sw.put("orders", { pk: "acme", sk: "o#1", total: 42.5 });
const { item, version } = await sw.get("orders", { pk: "acme", sk: "o#1" });
Python (stdlib only):
curl -fsS https://stateway.dev/client/stateway.py -o stateway.py
import os
from stateway import Stateway
sw = Stateway("https://api.stateway.dev", os.environ["STATEWAY_TOKEN"])
sw.create_collection("orders", hash="pk", range="sk")
sw.put("orders", {"pk": "acme", "sk": "o#1", "total": 42.5})
item = sw.get("orders", {"pk": "acme", "sk": "o#1"})["item"]
Helpers: waitForIndex /
wait_for_index, queryAll /
query_all, scanAll /
scan_all, and stream fan-out.
Native API (/v1)
Preferred surface: plain JSON, no AWS or Azure SDK required. Same
stored documents as the compatibility façades. Full endpoint table:
repo docs/native-api.md.
curl -sS -X POST "$STATEWAY_ENDPOINT/v1/collections" \
-H "Authorization: Bearer $STATEWAY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"orders","hash":"pk","range":"sk"}'
curl -sS -X PUT "$STATEWAY_ENDPOINT/v1/collections/orders/items" \
-H "Authorization: Bearer $STATEWAY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"pk":"acme","sk":"o#1","total":42.5}'
curl -sS -X POST "$STATEWAY_ENDPOINT/v1/collections/orders/get" \
-H "Authorization: Bearer $STATEWAY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"key":{"pk":"acme","sk":"o#1"}}'
curl -sS -X POST "$STATEWAY_ENDPOINT/v1/collections/orders/query" \
-H "Authorization: Bearer $STATEWAY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"key_condition":"pk = :pk AND begins_with(sk, :p)","values":{":pk":"acme",":p":"o"}}'
Also: TTL (attribute + default_ttl),
streams, indexes (CREATING→ACTIVE),
if_version, projection / return values, parallel scan,
batch, and single- or multi-collection transact.
Recipes
Optimistic concurrency. Get returns
version. Pass it back as if_version on put /
update / delete. Mismatch → 409 ConditionFailed.
const { item, version } = await sw.get("orders", { pk: "acme", sk: "o#1" });
await sw.update("orders", {
key: { pk: "acme", sk: "o#1" },
update: "SET total = :t",
values: { ":t": item.total + 1 },
if_version: version,
});
Index wait. Create returns
status: "CREATING". Wait for ACTIVE before
relying on a full backfill (queries during create can be incomplete).
await sw.createIndex("orders", { name: "byTag", hash: "tag", range: "sk" });
await sw.waitForIndex("orders", "byTag");
const page = await sw.queryIndex("orders", "byTag", {
key_condition: "tag = :t",
values: { ":t": "rush" },
});
Pagination. Query and scan return
next. Pass it as after on the next call, or
use queryAll / scanAll.
let after;
do {
const page = await sw.query("orders", {
key_condition: "pk = :pk",
values: { ":pk": "acme" },
limit: 50,
after,
});
// …page.items
after = page.next;
} while (after);
Expressions.
key_condition, update,
condition, and filter use DynamoDB
expression syntax with plain JSON values (not
AttributeValue maps). Alias attributes with names
({"#t":"total"}).
Streams. Enable with
setStream, list partition ids via
getStream, then poll
streamRecords per partition (after is
exclusive). Or use streamRecordsAll for one page across
every partition.
Errors
Failures are JSON
{"error":{"code":"…","message":"…"}}. Common codes:
-
ConditionFailed(409) —if_versionor condition expression rejected -
TransactionConflict(409) — retryable; a 2PC lock or concurrent write won -
NotFound(404) — missing collection, index, or item where required -
PartitionTooLarge(413) — write would exceed the partition stored-bytes ceiling Unauthorized(401) — missing or wrong Bearer token
Values on /v1 are plain JSON. Binary and typed Dynamo
sets do not round-trip through native the way AttributeValue maps do —
prefer strings, numbers, booleans, lists, and maps.
DynamoDB compatibility
Optional: use any AWS SDK against the same collections. Region in the
SigV4 scope can be us-east-1; data is not regional.
import boto3
import os
ddb = boto3.client(
"dynamodb",
endpoint_url=os.environ["STATEWAY_ENDPOINT"],
region_name="us-east-1",
aws_access_key_id=os.environ["STATEWAY_AKID"],
aws_secret_access_key=os.environ["STATEWAY_SECRET"],
)
ddb.create_table(
TableName="Orders",
AttributeDefinitions=[
{"AttributeName": "customerId", "AttributeType": "S"},
{"AttributeName": "orderId", "AttributeType": "S"},
],
KeySchema=[
{"AttributeName": "customerId", "KeyType": "HASH"},
{"AttributeName": "orderId", "KeyType": "RANGE"},
],
BillingMode="PAY_PER_REQUEST",
)
ddb.put_item(
TableName="Orders",
Item={
"customerId": {"S": "acme"},
"orderId": {"S": "o#1"},
"total": {"N": "42.5"},
},
)
Cosmos DB compatibility
import { CosmosClient } from "@azure/cosmos";
const client = new CosmosClient({
endpoint: process.env.STATEWAY_ENDPOINT,
key: process.env.COSMOS_MASTER_KEY, // base64 master key
});
const { database } = await client.databases.createIfNotExists({ id: "shop" });
const { container } = await database.containers.createIfNotExists({
id: "orders",
partitionKey: { paths: ["/customerId"] },
});
await container.items.create({
id: "ord-1",
customerId: "acme",
total: 42.5,
});
The same document is readable via Dynamo as table
shop/orders with keys customerId +
id.
Health and latency
curl -sS https://api.stateway.dev/health | jq .
Expect status: "ok". The JSON includes
latency_slo budgets (ms) for PutItem, GetItem, Query,
and cross-partition TransactWriteItems.
Consistency (said plainly)
- Strong inside a partition — conditional writes, atomic counters, local secondary indexes, single-partition transactions.
-
Eventually consistent across partitions — global
secondary indexes,
Scan, and the partition registryScanwalks. -
Atomic across partitions at a price —
TransactWriteItems/POST /v1/transactuse two-phase commit. Prefer single-partition transactions when you can.
What is not implemented
Use AWS DynamoDB and Azure Cosmos documentation as the API reference for the façades. These are deliberately absent:
-
Cosmos request units (
x-ms-request-charge) — no published formula to copy honestly - Cosmos spatial functions, stored procedures, triggers, UDFs
- Global tables, backup/restore, provisioned throughput controls
Item size follows Dynamo’s ~400 KB limit. A single partition has
a stored-bytes ceiling (default 8 GiB) so a Durable Object stays
under the platform limit — oversized writes fail with Dynamo
ItemCollectionSizeLimitExceededException / Cosmos 413.
Need access?
Hosted Stateway is invite-only — there is no public signup. If you already have keys, start from Connect or the native examples above. For self-hosting, fork the engine and run your own Worker; the credential shape is the same.
Product overview and consistency model: stateway.dev.