Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
982 changes: 922 additions & 60 deletions Cargo.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"crates/engine",
"crates/storage",
"crates/storage-postgres",
"crates/storage-dynamodb",
"crates/auth",
"crates/server",
"crates/bin",
Expand All @@ -26,6 +27,7 @@ extenddb-cache = { path = "crates/cache" }
extenddb-engine = { path = "crates/engine" }
extenddb-storage = { path = "crates/storage" }
extenddb-storage-postgres = { path = "crates/storage-postgres" }
extenddb-storage-dynamodb = { path = "crates/storage-dynamodb" }
extenddb-auth = { path = "crates/auth" }
extenddb-server = { path = "crates/server" }

Expand Down Expand Up @@ -87,6 +89,11 @@ tracing-subscriber = { version = "0.3", features = ["json", "env-filter"] }
syslog-tracing = "0.3"
metrics = "0.24"

# AWS SDK (DynamoDB-at-home backend)
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-dynamodb = "1"
aws-smithy-runtime-api = "1"

# Config
clap = { version = "4", features = ["derive"] }
config = "0.14"
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ A DynamoDB-compatible API adapter, ExtendDB speaks the DynamoDB wire protocol
- JSON metrics endpoint with DynamoDB CloudWatch-style metric names and dimensions
- Daemon mode with syslog logging, plus `--foreground` for container and supervisor environments
- PostgreSQL storage — use standard backup, replication, and HA tools
- Optional `dynamodb` storage backend — store your data in actual DynamoDB while running ExtendDB yourself (data plane → DynamoDB, catalog → PostgreSQL); see [Differences from DynamoDB](docs/differences-from-dynamodb.md#the-dynamodb-storage-backend-dynamodb-at-home)

## Quick Start

Expand Down Expand Up @@ -179,6 +180,7 @@ crates/
engine/ — operation handlers
storage/ — storage trait definitions
storage-postgres/ — PostgreSQL backend
storage-dynamodb/ — "DynamoDB at home" backend (data → real DynamoDB, catalog → PostgreSQL)
auth/ — SigV4 verification, IAM policy engine
server/ — HTTP server, management API, web console
bin/ — CLI, config, daemon lifecycle
Expand Down
2 changes: 2 additions & 0 deletions crates/bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ path = "src/main.rs"
[features]
default = ["postgres"]
postgres = ["extenddb-storage-postgres"]
dynamodb = ["extenddb-storage-dynamodb"]

[dependencies]
extenddb-auth = { workspace = true }
Expand All @@ -22,6 +23,7 @@ extenddb-core = { workspace = true }
extenddb-engine = { workspace = true }
extenddb-storage = { workspace = true }
extenddb-storage-postgres = { workspace = true, optional = true }
extenddb-storage-dynamodb = { workspace = true, optional = true }
extenddb-server = { workspace = true }
tokio = { workspace = true }
anyhow = { workspace = true }
Expand Down
2 changes: 1 addition & 1 deletion crates/bin/src/cmd_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::init_helpers::{generate_config, generate_tls_cert_if_needed};
#[derive(Args)]
#[allow(clippy::doc_markdown)] // Clap help text, not rustdoc
pub struct InitArgs {
/// Storage backend (postgres) (default: postgres)
/// Storage backend (postgres, dynamodb) (default: postgres)
#[arg(long, default_value = "postgres")]
backend: Option<String>,

Expand Down
4 changes: 3 additions & 1 deletion crates/bin/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,9 @@ impl StorageConfig {
}

/// Get a reference to the underlying trait object for factory calls.
pub fn as_trait(&self) -> &dyn extenddb_storage::config::StorageConfig {
/// The `+ 'static` (true of the owned `Box` contents) lets factories
/// downcast via `StorageConfig::as_any`.
pub fn as_trait(&self) -> &(dyn extenddb_storage::config::StorageConfig + 'static) {
&*self.config
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ mod serve_helpers;
mod util;
mod workers;

// Force-link optional storage backend crates so their `inventory::submit!`
// registrations are included by the linker. Without an explicit reference, the
// linker drops the otherwise-unused crate and the backend would not register.
// (The postgres backend is linked transitively via a direct symbol reference in
// `config.rs`, so it needs no entry here.)
#[cfg(feature = "dynamodb")]
extern crate extenddb_storage_dynamodb;

use clap::{Parser, Subcommand};

#[derive(Parser)]
Expand Down
29 changes: 29 additions & 0 deletions crates/storage-dynamodb/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2026 ExtendDB contributors
# SPDX-License-Identifier: Apache-2.0
[package]
name = "extenddb-storage-dynamodb"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true

[dependencies]
extenddb-core = { workspace = true }
extenddb-storage = { workspace = true }
extenddb-storage-postgres = { workspace = true }
extenddb-auth = { workspace = true }
aws-config = { workspace = true }
aws-sdk-dynamodb = { workspace = true }
aws-smithy-runtime-api = { workspace = true }
async-trait = { workspace = true }
inventory = { workspace = true }
futures = { workspace = true }
sqlx = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
bigdecimal = { workspace = true }

[dev-dependencies]
tokio = { workspace = true }
141 changes: 141 additions & 0 deletions crates/storage-dynamodb/src/backup_engine.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright 2026 ExtendDB contributors
// SPDX-License-Identifier: Apache-2.0

//! `BackupEngine` implementation for the DynamoDB-at-home backend — honest stubs.
//!
//! DynamoDB has a native backup and PITR API. This v1 backend does not implement
//! it. Each method names the DynamoDB API call it would map to.

use futures::future::BoxFuture;

use extenddb_core::types::{
BackupDescription, BackupDetails, BackupSummary, ContinuousBackupsDescription, TableDescription,
};
use extenddb_storage::BackupEngine;
use extenddb_storage::error::StorageError;

use crate::DynamoEngine;

impl BackupEngine for DynamoEngine {
fn create_backup(
&self,
_account_id: &str,
_table_name: &str,
_backup_name: &str,
) -> BoxFuture<'_, Result<BackupDetails, StorageError>> {
// Maps to DynamoDB CreateBackup.
Box::pin(async {
Err(StorageError::Internal(
"Backups are not implemented in the dynamodb backend (v1). \
Maps to DynamoDB CreateBackup."
.into(),
))
})
}

fn describe_backup(
&self,
_backup_arn: &str,
) -> BoxFuture<'_, Result<BackupDescription, StorageError>> {
// Maps to DynamoDB DescribeBackup.
Box::pin(async {
Err(StorageError::Internal(
"Backups are not implemented in the dynamodb backend (v1). \
Maps to DynamoDB DescribeBackup."
.into(),
))
})
}

fn list_backups(
&self,
_account_id: &str,
_table_name: Option<&str>,
) -> BoxFuture<'_, Result<Vec<BackupSummary>, StorageError>> {
// Maps to DynamoDB ListBackups.
Box::pin(async {
Err(StorageError::Internal(
"Backups are not implemented in the dynamodb backend (v1). \
Maps to DynamoDB ListBackups."
.into(),
))
})
}

fn delete_backup(
&self,
_backup_arn: &str,
) -> BoxFuture<'_, Result<BackupDescription, StorageError>> {
// Maps to DynamoDB DeleteBackup.
Box::pin(async {
Err(StorageError::Internal(
"Backups are not implemented in the dynamodb backend (v1). \
Maps to DynamoDB DeleteBackup."
.into(),
))
})
}

fn restore_table_from_backup(
&self,
_account_id: &str,
_target_table_name: &str,
_backup_arn: &str,
) -> BoxFuture<'_, Result<TableDescription, StorageError>> {
// Maps to DynamoDB RestoreTableFromBackup.
Box::pin(async {
Err(StorageError::Internal(
"Backups are not implemented in the dynamodb backend (v1). \
Maps to DynamoDB RestoreTableFromBackup."
.into(),
))
})
}

fn describe_continuous_backups(
&self,
_account_id: &str,
_table_name: &str,
) -> BoxFuture<'_, Result<ContinuousBackupsDescription, StorageError>> {
// Maps to DynamoDB DescribeContinuousBackups.
Box::pin(async {
Err(StorageError::Internal(
"Backups are not implemented in the dynamodb backend (v1). \
Maps to DynamoDB DescribeContinuousBackups."
.into(),
))
})
}

fn update_continuous_backups(
&self,
_account_id: &str,
_table_name: &str,
_pitr_enabled: bool,
) -> BoxFuture<'_, Result<ContinuousBackupsDescription, StorageError>> {
// Maps to DynamoDB UpdateContinuousBackups.
Box::pin(async {
Err(StorageError::Internal(
"Backups are not implemented in the dynamodb backend (v1). \
Maps to DynamoDB UpdateContinuousBackups."
.into(),
))
})
}

fn restore_table_to_point_in_time(
&self,
_account_id: &str,
_source_table_name: &str,
_target_table_name: &str,
) -> BoxFuture<'_, Result<TableDescription, StorageError>> {
// Maps to DynamoDB RestoreTableToPointInTime.
Box::pin(async {
Err(StorageError::Internal(
"Backups are not implemented in the dynamodb backend (v1). \
Maps to DynamoDB RestoreTableToPointInTime."
.into(),
))
})
}
}
Loading