See NOTICE for important disclaimers.
This guide walks you through initializing a extenddb deployment, starting the server, and running your first DynamoDB commands against it.
- macOS (Homebrew) — covers Homebrew PostgreSQL, macOS syslog, and
--pg-user $(whoami) - Linux (Ubuntu/Debian, Amazon Linux, Fedora/RHEL) — covers system PostgreSQL,
journalctl, and--pg-user postgres
The fastest way to build extenddb from source is the platform installer script. It checks dependencies, builds the binary, sets up a Python venv, and generates PDF documentation:
# Linux
scripts/install-linux.sh
# macOS
scripts/install-macos.shThe scripts report missing dependencies and exit — they never install software on your behalf. After the script completes, continue from Step 2: Initialize the deployment below.
- PostgreSQL 14+ running locally (see
docs/local-postgres-setup.md) - Rust toolchain (1.85+)
- AWS CLI v2 (for testing)
- Python 3.10+ with virtual environment (see Python Environment Setup in the README)
cargo build --releaseThe binary is at target/release/extenddb.
Run extenddb init to create the catalog and data databases:
./target/release/extenddb initThis will:
- Create a
extenddbPostgreSQL user (if it doesn't exist) - Create the
extenddb_catalogdatabase (catalog metadata) - Create the
extenddbdatabase (user item data) - Run schema migrations
- Generate an AES-256-GCM encryption key (for future access key storage)
- Create a default account and print the account ID
- Create an
adminuser and print the credentials once - Generate a self-signed TLS certificate at
~/.extenddb/tls/ - Generate
extenddb.toml
Important: Save the admin credentials printed during init. They are shown once and cannot be retrieved later. These credentials are used to authenticate to the management API.
Re-initialization: extenddb init will abort if either the catalog or data database already exists. To re-initialize, first run extenddb destroy --config extenddb.toml --yes to drop the existing databases, then run extenddb init again.
To use a custom catalog database name:
./target/release/extenddb init --catalog-db my_catalogTo use a custom data database name:
./target/release/extenddb init --data-db my_data_dbFor remote PostgreSQL or Aurora, supply the admin password with --pg-pass:
# Pass the password inline:
./target/release/extenddb init \
--pg-host my-aurora-cluster.cluster-xxxx.us-east-1.rds.amazonaws.com \
--pg-user postgres --pg-pass <admin-password>When --pg-pass is omitted entirely, extenddb init connects without a password, relying on
PostgreSQL peer/ident authentication (works only on localhost via Unix socket).
To bind the server to a specific address (e.g., for remote access), pass --bind-addr during init. The address is included as a SAN in the self-signed certificate and written to the generated config file:
./target/release/extenddb init --bind-addr 10.0.1.5This generates a certificate with SANs: localhost, 127.0.0.1, and 10.0.1.5.
extenddb init auto-generates a self-signed TLS certificate at ~/.extenddb/tls/. If you need a certificate with different SANs (e.g., binding to 0.0.0.0 and connecting via a specific hostname), generate one manually with openssl:
# Create the TLS directory
mkdir -p ~/.extenddb/tls
# Generate a certificate with custom SANs
openssl req -x509 -newkey rsa:2048 -nodes \
-keyout ~/.extenddb/tls/key.pem \
-out ~/.extenddb/tls/cert.pem \
-days 3650 \
-subj "/CN=extenddb self-signed/O=extenddb" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1,DNS:myhost.example.com,IP:10.0.1.5"
# Restrict key file permissions
chmod 600 ~/.extenddb/tls/key.pemAdjust the -addext SANs to match the hostnames and IP addresses clients will use to connect. Every address that appears in endpoint_url must be listed as a SAN, or TLS verification will fail.
If the certificate already exists when extenddb init runs, it is preserved — init only generates a certificate when ~/.extenddb/tls/cert.pem and ~/.extenddb/tls/key.pem are both absent. To regenerate, delete the existing files first:
rm ~/.extenddb/tls/cert.pem ~/.extenddb/tls/key.pem
./target/release/extenddb init --bind-addr 10.0.1.5./target/release/extenddb verify --config extenddb.tomlYou should see all checks pass:
=== extenddb verify ===
--- Checking catalog connection...
OK: Connected to catalog.
--- Checking catalog version...
OK: Catalog version 0.0.2
--- Checking data database...
OK: Connected to data database 'extenddb_catalog_data'.
--- Enumerating tables...
Tables: 0
Indexes: 0
=== HEALTHY: All checks passed ===
extenddb runs as a daemon (background process) and logs to syslog. On startup it prints a banner to stdout confirming the version, catalog version, and bind address, then forks to background.
./target/release/extenddb serve --config extenddb.toml
# extenddb 0.0.2 (catalog 0.0.2) listening on 127.0.0.1:8000Check status (includes the daemon PID):
./target/release/extenddb status --config extenddb.toml
# extenddb is running on port 8000 (pid 12345)Read logs:
# Linux
journalctl -t extenddb -f # follow live
journalctl -t extenddb -n 50 # last 50 lines
journalctl -t extenddb --no-pager -o cat # plain output, no metadata
# macOS
log stream --predicate 'processImagePath ENDSWITH "extenddb"' --level info
log show --predicate 'processImagePath ENDSWITH "extenddb"' --last 5mChange the log level at runtime (takes effect within 30 seconds):
./target/release/extenddb settings --config extenddb.toml set log_level debugsqlx query traces are suppressed by default (level warn) so they don't flood the main log stream. The sqlx log level is independently configurable via the sqlx_log_level runtime setting:
# Enable sqlx debug logging for query troubleshooting
./target/release/extenddb settings --config extenddb.toml set sqlx_log_level debug
# Restore default (suppress most sqlx output)
./target/release/extenddb settings --config extenddb.toml set sqlx_log_level warnWhen sqlx logging is enabled, messages appear in the extenddb syslog with sqlx::query as the target. Filter them:
# Exclude sqlx messages
journalctl -t extenddb | grep -v sqlx
# Show only sqlx messages
journalctl -t extenddb | grep sqlxBy default, control plane operations (CreateTable, DeleteTable) emulate real DynamoDB's async behavior — tables transition through CREATING → ACTIVE and DELETING → removed states over a configurable delay (default: 5 seconds). Adjust with:
# Set to 0 for instant transitions (useful for fast test cycles)
./target/release/extenddb settings --config extenddb.toml set \
control_plane_delay_seconds 0
# Set to 10 seconds for more realistic behavior
./target/release/extenddb settings --config extenddb.toml set \
control_plane_delay_seconds 10Controls whether extenddb manage import-access-key is allowed (default: true).
# Disable credential import
./target/release/extenddb settings --config extenddb.toml set \
allow_credential_import false
# Re-enable
./target/release/extenddb settings --config extenddb.toml set \
allow_credential_import trueGSI updates are applied asynchronously with a configurable delay, simulating real DynamoDB's eventually consistent GSI behavior. The system-wide default is 10ms. Each GSI can override this with a per-index propagation_delay_ms stored in the catalog.
# Set system-wide default to 0 for synchronous GSI updates (fast tests)
./target/release/extenddb settings --config extenddb.toml set \
gsi_propagation_delay_ms 0
# Set to 50ms for more realistic eventual consistency
./target/release/extenddb settings --config extenddb.toml set \
gsi_propagation_delay_ms 50extenddb enforces provisioned throughput limits using a token bucket per table and partition, matching DynamoDB's burst and refill behavior. Throttling is enabled by default.
# Disable throttling (useful for bulk loading)
./target/release/extenddb settings --config extenddb.toml set \
throttling_enabled false
# Re-enable throttling
./target/release/extenddb settings --config extenddb.toml set \
throttling_enabled trueControls the target maximum time (in seconds) between an item's TTL expiry and its actual deletion. The TTL sweeper uses an indexed scan and runs every 60 seconds. Default: 300 seconds.
# Set to 60 seconds for faster TTL cleanup
./target/release/extenddb settings --config extenddb.toml set \
ttl_deletion_target_seconds 60
# Set to 600 seconds for less aggressive cleanup
./target/release/extenddb settings --config extenddb.toml set \
ttl_deletion_target_seconds 600Stop the server:
./target/release/extenddb stop --config extenddb.tomlIf extenddb stop is unavailable (e.g., older binary), fall back to manual process management:
- Find the process listening on the extenddb port (default 8000):
ss -tlnp | grep :8000 - Kill it:
kill <pid>
extenddb uses TLS with a self-signed certificate. To make AWS CLI and SDKs trust it, set AWS_CA_BUNDLE to the generated certificate:
export AWS_CA_BUNDLE=~/.extenddb/tls/cert.pemexport AWS_CA_BUNDLE=~/.extenddb/tls/cert.pem
export AWS_ENDPOINT_URL_DYNAMODB=https://127.0.0.1:8000
export AWS_ACCESS_KEY_ID=<access-key-from-create-access-key>
export AWS_SECRET_ACCESS_KEY=<secret-key-from-create-access-key>
export AWS_DEFAULT_REGION=us-east-1Add to ~/.aws/config:
[profile extenddb]
region = us-east-1
ca_bundle = ~/.extenddb/tls/cert.pem
services = extenddb-services
[services extenddb-services]
dynamodb =
endpoint_url = https://127.0.0.1:8000Add to ~/.aws/credentials:
[extenddb]
aws_access_key_id = <access-key-from-create-access-key>
aws_secret_access_key = <secret-key-from-create-access-key>Then: export AWS_PROFILE=extenddb
export AWS_CA_BUNDLE=~/.extenddb/tls/cert.pem
aws dynamodb list-tables --endpoint-url https://127.0.0.1:8000After extenddb init, create an IAM user and access key for SDK use:
# Create an account (use the account ID printed during init, or create a new one)
./target/release/extenddb manage --user admin --password <admin-pw> \
create-account --account-name dev-team
# Create an IAM user with a console password
./target/release/extenddb manage --user admin --password <admin-pw> \
create-user --account-id <account-id> \
--user-name alice --user-password secret
# Attach a policy granting DynamoDB access
./target/release/extenddb manage --user admin --password <admin-pw> \
put-user-policy --account-id <account-id> --user-name alice \
--policy-name FullAccess \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "*"
}]
}'
# Create an access key (shown once — save it)
./target/release/extenddb manage --user <account-id>/alice --password secret \
create-access-keyThen configure your SDK with the access key ID and secret access key returned by create-access-key.
aws dynamodb create-table \
--table-name MyTable \
--attribute-definitions AttributeName=pk,AttributeType=S \
--key-schema AttributeName=pk,KeyType=HASH \
--billing-mode PAY_PER_REQUESTaws dynamodb list-tablesaws dynamodb describe-table --table-name MyTableaws dynamodb delete-table --table-name MyTableaws dynamodb put-item \
--table-name MyTable \
--item '{"pk": {"S": "user-1"}, "name": {"S": "Alice"}, "age": {"N": "30"}}'aws dynamodb get-item \
--table-name MyTable \
--key '{"pk": {"S": "user-1"}}'aws dynamodb delete-item \
--table-name MyTable \
--key '{"pk": {"S": "user-1"}}' \
--return-values ALL_OLDaws dynamodb update-item \
--table-name MyTable \
--key '{"pk": {"S": "user-1"}}' \
--update-expression "SET age = :newage" \
--expression-attribute-values '{":newage": {"N": "31"}}' \
--return-values ALL_NEWaws dynamodb query \
--table-name MyTable \
--key-condition-expression "pk = :pk" \
--expression-attribute-values '{":pk": {"S": "user-1"}}'aws dynamodb scan --table-name MyTableaws dynamodb batch-write-item \
--request-items '{
"MyTable": [
{
"PutRequest": {
"Item": {
"pk": {"S": "user-2"},
"name": {"S": "Bob"}
}
}
},
{
"PutRequest": {
"Item": {
"pk": {"S": "user-3"},
"name": {"S": "Carol"}
}
}
}
]
}'aws dynamodb batch-get-item \
--request-items '{
"MyTable": {
"Keys": [
{"pk": {"S": "user-1"}},
{"pk": {"S": "user-2"}}
]
}
}'aws dynamodb transact-write-items \
--transact-items '[
{
"Put": {
"TableName": "MyTable",
"Item": {
"pk": {"S": "tx-1"},
"data": {"S": "hello"}
}
}
},
{
"ConditionCheck": {
"TableName": "MyTable",
"Key": {"pk": {"S": "user-1"}},
"ConditionExpression":
"attribute_exists(pk)"
}
}
]'aws dynamodb transact-get-items \
--transact-items '[
{
"Get": {
"TableName": "MyTable",
"Key": {"pk": {"S": "tx-1"}}
}
},
{
"Get": {
"TableName": "MyTable",
"Key": {"pk": {"S": "user-1"}}
}
}
]'extenddb supports DynamoDB Streams for change data capture. Enable streams when creating a table:
aws dynamodb create-table \
--table-name StreamTable \
--attribute-definitions AttributeName=pk,AttributeType=S \
--key-schema AttributeName=pk,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGESImportant: SDK users need a separate dynamodbstreams client. In every AWS SDK, DynamoDB and DynamoDB Streams are separate services. Both clients must point at the same extenddb endpoint URL:
import boto3
# Trust the self-signed certificate
import os
os.environ["AWS_CA_BUNDLE"] = os.path.expanduser("~/.extenddb/tls/cert.pem")
# DynamoDB client — for table/item operations
dynamodb = boto3.client("dynamodb", endpoint_url="https://127.0.0.1:8000")
# DynamoDB Streams client — for stream operations
streams = boto3.client("dynamodbstreams", endpoint_url="https://127.0.0.1:8000")List streams:
aws dynamodbstreams list-streams \
--endpoint-url https://127.0.0.1:8000Describe a stream (use the LatestStreamArn from DescribeTable):
aws dynamodbstreams describe-stream \
--endpoint-url https://127.0.0.1:8000 \
--stream-arn \
"arn:aws:dynamodb:us-east-1:<account-id>:table/StreamTable/stream/2026-04-08T07:00:00"Get a shard iterator and read records:
# Get iterator for a shard (use ShardId from DescribeStream)
aws dynamodbstreams get-shard-iterator \
--endpoint-url https://127.0.0.1:8000 \
--stream-arn \
"arn:aws:dynamodb:us-east-1:<account-id>:table/StreamTable/stream/2026-04-08T07:00:00" \
--shard-id "shard-0" \
--shard-iterator-type TRIM_HORIZON
# Read records using the iterator
aws dynamodbstreams get-records \
--endpoint-url https://127.0.0.1:8000 \
--shard-iterator "<iterator-from-above>"The standard pattern for consuming a DynamoDB stream is a polling loop:
import time
import boto3
streams = boto3.client("dynamodbstreams", endpoint_url="https://127.0.0.1:8000")
dynamodb = boto3.client("dynamodb", endpoint_url="https://127.0.0.1:8000")
# Get stream ARN from the table.
table = dynamodb.describe_table(TableName="StreamTable")
stream_arn = table["Table"]["LatestStreamArn"]
# Discover shards.
desc = streams.describe_stream(StreamArn=stream_arn)
shards = desc["StreamDescription"]["Shards"]
# Get iterators for each shard.
iterators = {}
for shard in shards:
resp = streams.get_shard_iterator(
StreamArn=stream_arn,
ShardId=shard["ShardId"],
ShardIteratorType="TRIM_HORIZON",
)
iterators[shard["ShardId"]] = resp["ShardIterator"]
# Poll loop.
while True:
for shard_id, iterator in list(iterators.items()):
if not iterator:
continue
resp = streams.get_records(ShardIterator=iterator, Limit=100)
for record in resp.get("Records", []):
print(f"{record['eventName']}: {record['dynamodb']['Keys']}")
iterators[shard_id] = resp.get("NextShardIterator")
time.sleep(1)See samples/stream_consumer.py for a complete working example with concurrent writer and poller threads.
Stream records are retained for 24 hours. A background worker cleans up expired records every hour.
curl --cacert ~/.extenddb/tls/cert.pem https://127.0.0.1:8000/health
# {"status":"healthy"}The management API is available at /management/* on the running extenddb server. The extenddb manage CLI subcommand is a thin client that calls these endpoints.
Admin commands require admin credentials (the username and password printed during extenddb init). IAM user self-service commands accept account_id/user_name as the --user value.
Note: The --password flag is visible in process listings (ps aux). For sensitive environments, use the EXTENDDB_ADMIN_PASSWORD environment variable instead.
./target/release/extenddb manage --user admin --password <pw> list-admins./target/release/extenddb manage --user admin --password <pw> \
create-admin --admin-name ops --admin-password secret123./target/release/extenddb manage --user admin --password <pw> \
change-admin-password --admin-name admin --new-password newpw./target/release/extenddb manage --user admin --password <pw> \
delete-admin --admin-name opsAccount IDs must be 12-digit numeric strings (matching AWS account ID format):
./target/release/extenddb manage --user admin --password <pw> \
create-account --account-id 123456789012 --account-name dev-team./target/release/extenddb manage --user admin --password <pw> list-accountsAccounts with existing tables cannot be deleted. Delete all tables first.
./target/release/extenddb manage --user admin --password <pw> \
delete-account --account-id 123456789012Create an IAM user with optional console password. If --user-password is provided, the user can authenticate to the management API for self-service operations.
./target/release/extenddb manage --user admin --password <pw> \
create-user --account-id 123456789012 \
--user-name alice --user-password secretA default self-service policy is automatically attached, allowing the user to manage their own access keys and change their own password.
./target/release/extenddb manage --user admin --password <pw> \
list-users --account-id 123456789012Deleting a user also removes their access keys, group memberships, tags, and policies (via CASCADE).
./target/release/extenddb manage --user admin --password <pw> \
delete-user --account-id 123456789012 --user-name aliceIAM users create their own access keys by authenticating with account_id/user_name:password. When authenticating as an IAM user, --account-id and --user-name are inferred automatically. The secret key is shown once and cannot be retrieved later.
Generated access keys are branded with extenddb-specific prefixes to distinguish them from real AWS credentials:
- Long-lived keys:
AKIAEXTENDDB+ 8 random chars (20 total) - Temporary credentials (AssumeRole):
ASIAEXTENDDB+ 8 random chars (20 total) - Secret keys:
extenddb+ 32 random chars (40 total)
# Self-service: account_id and user_name inferred from --user
./target/release/extenddb manage --user 123456789012/alice --password secret \
create-access-keyAdmins can also create access keys on behalf of any user:
./target/release/extenddb manage --user admin --password <pw> \
create-access-key --account-id 123456789012 --user-name alice./target/release/extenddb manage --user 123456789012/alice --password secret \
list-access-keys --account-id 123456789012 --user-name alice./target/release/extenddb manage --user 123456789012/alice --password secret \
delete-access-key --account-id 123456789012 \
--user-name alice --access-key-id AKIAEXTENDDB...Import real AWS credentials (or any AKIA* key) into extenddb. This enables the "just change the endpoint URL" workflow — use the same credentials against both extenddb and real DynamoDB.
Both --secret-access-key and --yes are required:
./target/release/extenddb manage --user admin --password <pw> \
import-access-key --account-id 123456789012 --user-name alice \
--access-key-id AKIAIOSFODNN7EXAMPLE \
--secret-access-key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
--yesSecurity note:
--secret-access-keyand--passwordare visible inpsoutput and shell history. For sensitive environments, use theEXTENDDB_ADMIN_PASSWORDenvironment variable for the password.
Credential import is gated by the allow_credential_import runtime setting (default: true). To disable:
./target/release/extenddb settings --config extenddb.toml set \
allow_credential_import falseEvery import is logged to syslog (access key ID, account, user — never the secret).
./target/release/extenddb manage --user 123456789012/alice --password secret \
change-user-password --account-id 123456789012 \
--user-name alice --new-password newsecret./target/release/extenddb manage --user admin --password <pw> \
create-group --account-id 123456789012 --group-name developers./target/release/extenddb manage --user admin --password <pw> \
add-group-member --account-id 123456789012 \
--group-name developers --user-name alice./target/release/extenddb manage --user admin --password <pw> \
remove-group-member --account-id 123456789012 \
--group-name developers --user-name alice./target/release/extenddb manage --user admin --password <pw> \
list-groups --account-id 123456789012./target/release/extenddb manage --user admin --password <pw> \
delete-group --account-id 123456789012 --group-name developersCreates or replaces a named policy on an IAM user. The policy document must be valid JSON with Version and Statement fields.
./target/release/extenddb manage --user admin --password <pw> \
put-user-policy --account-id 123456789012 \
--user-name alice \
--policy-name ReadOnly \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "dynamodb:GetItem",
"Resource": "*"
}]
}'./target/release/extenddb manage --user admin --password <pw> \
list-user-policies --account-id 123456789012 --user-name alice./target/release/extenddb manage --user admin --password <pw> \
delete-user-policy --account-id 123456789012 \
--user-name alice --policy-name ReadOnly./target/release/extenddb manage --user admin --password <pw> \
put-group-policy --account-id 123456789012 \
--group-name developers \
--policy-name FullAccess \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "*"
}]
}'./target/release/extenddb manage --user admin --password <pw> \
list-group-policies --account-id 123456789012 --group-name developers./target/release/extenddb manage --user admin --password <pw> \
delete-group-policy --account-id 123456789012 \
--group-name developers \
--policy-name FullAccess./target/release/extenddb manage --user admin --password <pw> \
tag-user --account-id 123456789012 --user-name alice \
--tags '[{"key":"Department","value":"Engineering"}]'./target/release/extenddb manage --user admin --password <pw> \
list-user-tags --account-id 123456789012 --user-name alice./target/release/extenddb manage --user admin --password <pw> \
untag-user --account-id 123456789012 --user-name alice --tag-keys Department./target/release/extenddb manage --user admin --password <pw> \
create-role --account-id 123456789012 --role-name data-reader \
--trust-policy '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::123456789012:user/alice"},
"Action": "sts:AssumeRole"
}]
}'./target/release/extenddb manage --user admin --password <pw> \
list-roles --account-id 123456789012./target/release/extenddb manage --user admin --password <pw> \
delete-role --account-id 123456789012 --role-name data-reader./target/release/extenddb manage --user admin --password <pw> \
put-role-policy --account-id 123456789012 \
--role-name data-reader \
--policy-name ReadOnly \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "dynamodb:GetItem",
"Resource": "*"
}]
}'./target/release/extenddb manage --user admin --password <pw> \
list-role-policies --account-id 123456789012 --role-name data-reader./target/release/extenddb manage --user admin --password <pw> \
delete-role-policy --account-id 123456789012 \
--role-name data-reader --policy-name ReadOnly./target/release/extenddb manage --user admin --password <pw> \
tag-role --account-id 123456789012 --role-name data-reader \
--tags '[{"key":"Team","value":"Backend"}]'./target/release/extenddb manage --user admin --password <pw> \
list-role-tags --account-id 123456789012 --role-name data-reader./target/release/extenddb manage --user admin --password <pw> \
untag-role --account-id 123456789012 --role-name data-reader --tag-keys TeamGenerates temporary ASIA* credentials for the specified role. The trust policy must allow the caller ARN.
./target/release/extenddb manage --user admin --password <pw> \
assume-role --account-id 123456789012 --role-name data-reader \
--caller-arn arn:aws:iam::123456789012:user/alice \
--session-name test-sessionWith optional session tags and session policy:
./target/release/extenddb manage --user admin --password <pw> \
assume-role --account-id 123456789012 --role-name data-reader \
--caller-arn arn:aws:iam::123456789012:user/alice \
--session-name test-session \
--session-tags '{"Project":"Alpha"}' \
--session-policy '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "dynamodb:GetItem",
"Resource": "*"
}]
}' \
--duration-seconds 1800./target/release/extenddb manage --user admin --password <pw> \
set-user-boundary --account-id 123456789012 \
--user-name alice \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "dynamodb:*",
"Resource": "*"
}]
}'./target/release/extenddb manage --user admin --password <pw> \
get-user-boundary --account-id 123456789012 --user-name alice./target/release/extenddb manage --user admin --password <pw> \
delete-user-boundary --account-id 123456789012 --user-name alice./target/release/extenddb manage --user admin --password <pw> \
set-role-boundary --account-id 123456789012 \
--role-name data-reader \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "dynamodb:GetItem",
"Resource": "*"
}]
}'./target/release/extenddb manage --user admin --password <pw> \
get-role-boundary --account-id 123456789012 --role-name data-reader./target/release/extenddb manage --user admin --password <pw> \
delete-role-boundary --account-id 123456789012 --role-name data-readerBy default, extenddb manage reads the server address from extenddb.toml. To target a different server:
./target/release/extenddb manage --user admin --password <pw> \
--endpoint 127.0.0.1:9000 list-adminsextenddb includes a built-in web console for managing accounts, users, groups, roles, and policies through a browser. The console is served at /console/ on the same port as the DynamoDB API.
Navigate to https://127.0.0.1:8000/console/ in your browser (adjust the host and port to match your extenddb.toml configuration). Accept the self-signed certificate warning on first visit.
- Admin users: Enter your admin username and password (the credentials printed during
extenddb init). - IAM users: Enter
account_id/user_nameas the username and your console password.
- Dashboard: Overview of accounts and admin users.
- Account management: Create, view, and delete accounts (admin only).
- User management: Create and delete IAM users, view access keys, policies, tags, and group memberships.
- Access key management: Create and delete access keys. The secret key is shown once at creation time.
- Group management: Create and delete groups, add and remove members.
- Role management: Create and delete roles, view trust policies.
- Policy management: Add and delete inline policies for users, groups, and roles. Includes a JSON editor with a default policy template.
Admin users have full access to all management operations. IAM users can view their own details and manage their own access keys.
Sessions expire after 8 hours of inactivity. Click "Logout" to end a session immediately.
All management and settings operations are logged to syslog at WARN level with structured targets for filtering:
- Management operations:
extenddb::audit::manage— covers admin CRUD, account CRUD, IAM user/group/role/policy CRUD, access key lifecycle, permissions boundaries, assume-role, and credential import. - Settings operations:
extenddb::audit::settings— coversextenddb settings setchanges.
Secrets (passwords, secret keys) are never included in audit log entries.
View audit entries:
journalctl -t extenddb | grep 'extenddb::audit'extenddb supports running external test suites (e.g., Java/JUnit, Python/pytest) against a running instance. Suites are registered in external-suites.toml at the project root and referenced by path — never copied into the repo.
# Start extenddb first
./target/release/extenddb serve --config extenddb.toml
# Set GSI propagation delay to 0 for external tests.
# External suites expect synchronous GSI behavior (matching real DynamoDB's
# typical sub-millisecond propagation). The async GSI path is tested
# separately by the extenddb-specific test_gsi_async.py suite.
./target/release/extenddb settings --config extenddb.toml set gsi_propagation_delay_ms 0
# Run all registered suites
python3 devtools/run-external-tests
# Dry run — show what would execute without running
python3 devtools/run-external-tests --dry-run
# Run a specific suite by name
python3 devtools/run-external-tests \
--suite "DynamoDB PostgreSQL Extension Functional Tests"
# Override the endpoint
python3 devtools/run-external-tests --endpoint http://localhost:9000
# Generate a JSON report
python3 devtools/run-external-tests --report results.json
# Show full test output
python3 devtools/run-external-tests --verboseAdd a [[suite]] entry to external-suites.toml:
[[suite]]
name = "My Test Suite"
path = "~/source/my-test-suite"
runner = "maven" # maven, gradle, pytest, or cargo
enabled = true
[suite.env]
DDB_ENDPOINT = "${EXTENDDB_ENDPOINT}"The ${EXTENDDB_ENDPOINT} placeholder is replaced with the --endpoint value at runtime.
Supported runners: maven (mvn test), gradle (gradle test), pytest (python3 -m pytest), cargo (cargo test).
Each runner requires its tools to be installed. The runner checks prerequisites before executing and skips suites with missing tools:
- maven:
java,mvn - gradle:
java,gradle - pytest:
python3,pytest - cargo:
cargo
./target/release/extenddb version
# extenddb 0.0.2
# catalog 0.0.2
# commit abc1234
# built 2026-04-17T12:00:00ZThe -V flag prints the same output:
./target/release/extenddb -VTo completely remove a deployment:
./target/release/extenddb destroy --config extenddb.toml --yesThe --yes flag is required to confirm destruction. Without it, the command exits with an error.
The storage.postgres.pool_size setting (default: 20, minimum: 10) controls the maximum number of concurrent PostgreSQL connections used for DynamoDB data operations. Each in-flight request that touches the database holds one connection for the duration of its transaction. Values below 10 are clamped at startup with a warning.
The storage.postgres.catalog_pool_size setting controls the maximum number of concurrent connections for the management/catalog pool (authorization queries, IAM operations, console). Defaults to pool_size if not set, minimum: 10. With auth enabled (provider = "builtin"), each DynamoDB request makes concurrent authorization queries against this pool — size it to match expected concurrency. Values below 10 are clamped at startup with a warning.
When to increase: If you see elevated latency under concurrent load, the pool may be saturated. Requests queue at the pool level when all connections are in use. Increase pool_size (and catalog_pool_size if auth is enabled) to allow more concurrent transactions.
Relationship to PostgreSQL max_connections: The total connection footprint is pool_size + catalog_pool_size + 1 (the extra 1 is for the log-level poller). PostgreSQL's default max_connections is 100. Ensure pool_size + catalog_pool_size + 1 does not exceed your PostgreSQL max_connections setting.
Example: To support 50 concurrent data operations with auth enabled, set both pools to 50 in extenddb.toml and ensure PostgreSQL allows at least 101 connections.
[storage.postgres]
pool_size = 50
catalog_pool_size = 50- Different items: Fully concurrent up to
pool_size. No contention. - Same item: Concurrent writes to the same item serialize on PostgreSQL's row lock (
SELECT ... FOR UPDATE). All updates succeed, but throughput for a single hot item is bounded by single-row transaction rate. - Reads: GetItem and Query do not acquire row locks and proceed concurrently with writes.
See docs/troubleshooting.md for common errors and fixes.
A complete Python sample application is included at samples/sample_app.py. It demonstrates the full extenddb lifecycle:
- Create tables — simple PK, PK+SK with GSI, and multi-part GSI keys (tournament pattern)
- Poll for ACTIVE — wait for all tables to reach ACTIVE status
- Load data — PutItem and BatchWriteItem
- Query — Query on base tables and GSIs (including multi-part GSI keys), Scan
- Update — UpdateItem with update expressions and conditions
- Batch read — BatchGetItem across multiple keys
- Transactions — TransactWriteItems and TransactGetItems across tables
- Delete — DeleteItem to remove individual items
- Drop tables — DeleteTable to tear down all tables
# Start extenddb
./target/release/extenddb serve --config extenddb.toml
# Create an IAM user with DynamoDB access (use account ID from init)
./target/release/extenddb manage --user admin --password '<admin-password>' \
create-user --account-id <account-id> \
--user-name sampleuser --user-password 'SamplePass1!'
./target/release/extenddb manage --user admin --password '<admin-password>' \
put-user-policy --account-id <account-id> \
--user-name sampleuser --policy-name FullAccess \
--policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"dynamodb:*","Resource":"*"}]}'
# Create an access key (self-service)
./target/release/extenddb manage --user <account-id>/sampleuser --password 'SamplePass1!' \
create-access-key
# Run the sample with the access key from create-access-key output
export AWS_CA_BUNDLE=~/.extenddb/tls/cert.pem
export EXTENDDB_ENDPOINT=https://127.0.0.1:8000
export AWS_ACCESS_KEY_ID=<access-key-id>
export AWS_SECRET_ACCESS_KEY=<secret-access-key>
python3 samples/sample_app.pyThe sample creates three tables (SampleUsers, SampleOrders, SampleTournamentMatches), exercises all major DynamoDB operations, and cleans up after itself.
Copyright 2026 ExtendDB contributors. Licensed under the Apache License, Version 2.0. See LICENSE for the full text.
This software is provided "as is" without warranty of any kind. ExtendDB is not affiliated with, endorsed by, or sponsored by Amazon Web Services. "DynamoDB" is a trademark of Amazon.com, Inc.