Skip to content

We have DynamoDB at home. - #205

Open
quinnypig wants to merge 5 commits into
ExtendDB:mainfrom
quinnypig:feat/dynamodb-backend
Open

We have DynamoDB at home.#205
quinnypig wants to merge 5 commits into
ExtendDB:mainfrom
quinnypig:feat/dynamodb-backend

Conversation

@quinnypig

@quinnypig quinnypig commented Jul 7, 2026

Copy link
Copy Markdown

Forward

This is the third time.

#54 argued that Route 53 is a database and proved it by force, packing base64 into 255-byte TXT strings until DNS confessed.

#174 argued that S3 Object Annotations was already a database, because AWS had built the Iceberg index, SQL query path, and change journal and merely declined to use the noun.

Neither merged. No review comment arrived, but I can reconstruct one from the shape of the silence:

Stop inventing storage engines and just use DynamoDB.

Done.

This PR adds a DynamoDB backend to the DynamoDB-compatible database. It stores its DynamoDB data in DynamoDB.

Run ExtendDB yourself, on infrastructure you administer, pointed at us-east-1. When the executive who read one cloud-repatriation post asks whether the database is in-house, you can truthfully say the database server is. No follow-up question they know how to ask will surface the rest.

We have DynamoDB at home.

A note from the intervening architecture committee

This PR waited long enough for 79 upstream commits to land. During that interval, upstream did not reject the premise. It did something more revealing: it redesigned the entire backend system.

Inventory registration is gone. The name-keyed registry that replaced it is also gone. The CLI moved into a backend-agnostic app crate. Feature flags were declared vestigial. The official architecture is now one backend per binary.

This is either excellent type-safe design or a containment protocol.

The backend complied. It now ships as a dedicated executable:

$ cargo build --release --bin extenddb-dynamodb
$ ./target/release/extenddb-dynamodb version
extenddb 0.1.2
catalog 0.0.2 (dynamodb)

Runtime backend selection has been eliminated. To choose the wrong database, an operator must now install the wrong database server. This is progress.

The plugin-model refactor did not kill the backend. It gave the backend its own executable. The bit has incorporated.

The anti-joke

The meme format requires the thing at home to be worse.

Route 53 honored the format: items shredded across TXT records are worse.

S3 Object Annotations honored it: 1,000 items per table, with the query path billed by the scanned terabyte, is worse.

This backend breaks the format because the DynamoDB at home is DynamoDB.

The failure is clearest in encoding.rs. Route 53 needed actual encoding because DNS was never intended to hold database items. S3 Annotations needed base64 chunking. DynamoDB already speaks the exact DynamoDB type system, so this backend's encoding is structurally the identity function.

The file exists because ExtendDB's AttributeValue and the AWS SDK's AttributeValue are two Rust enums holding the same data. It is round-trip tested across every type anyway, because the identity function is the one function you cannot afford to get wrong.

What is actually in here

Unlike its siblings, this backend works.

The new extenddb-storage-dynamodb crate implements the storage-engine trait surface:

  • Real DynamoDB table lifecycle, CRUD, Query, Scan, and transactions.
  • Parsed condition, key-condition, and update ASTs rendered back into DynamoDB expressions. The request crosses the abstraction boundary, is understood, and is then carefully made opaque again.
  • Account-scoped idempotency tokens forwarded as ClientRequestToken.
  • Native DynamoDB TTL and tags.
  • DynamoDB failures mapped back into ExtendDB errors with wire fidelity, so an AWS SDK client can remain unaware of how many times its request has encountered DynamoDB.
  • GSI and LSI metadata sourced from DescribeTable for upstream's new capacity-accounting cache, because the upstream optimization is also, unhelpfully, exactly a DynamoDB API.

The bureaucracy database

The data plane maps naturally to DynamoDB. The management plane does not.

Accounts, users, roles, policies, settings, metrics, rate limits, encryption keys, and admin credentials are relational bureaucracy. Rebuilding them on DynamoDB would require months of responsible engineering and bury the punchline.

So this is a hybrid:

  • Data plane: DynamoDB.
  • Catalog, IAM, settings, and administration: PostgreSQL.
  • Front door: ExtendDB.
  • Back door: also AWS.
  • Deployment classification: on-prem, if nobody asks a second question.

Your data is on-prem. The bureaucracy that proves it is on-prem still needs a real database.

The result is two databases in a trench coat standing in your data center, presenting a config file as identification.

Table namespacing

ExtendDB is multi-tenant; DynamoDB's table namespace is flat. Physical tables are named:

<table_prefix><account_id>_<table_name>

The default prefix is athome_, producing names such as athome_123456789012_Orders.

This is not merely namespacing. It is a statement of deployment intent embedded in every CloudTrail event.

Why this is not as deranged as it sounds

  • Point endpoint_url at DynamoDB Local and ExtendDB becomes the TLS, mandatory SigV4, IAM, multi-account, rate-limiting front end DynamoDB Local never had.
  • It is a migration ramp in either direction. The application does not move. The invoice does.
  • It validates the storage seam against a genuinely different backend instead of another PostgreSQL wrapper with different nouns.
  • DynamoDB owns table lifecycle, TTL deletion, transaction idempotency, and index state. Several workers become no-ops, the most reliable state a worker can achieve.

These are defensible reasons to merge this PR, which is frankly damaging to the artistic intent.

Why it is exactly as deranged as it sounds

  • I have taken a serverless database and given it a server.
  • The server has its own PostgreSQL database.
  • Each request is SigV4-authenticated and authorized by ExtendDB, translated into the same data model it arrived in, signed with SigV4 again, and sent to the service the client could have called directly.
  • Two IAM systems inspect the request in series. The second has no idea the first exists. This is zero trust implemented as interpersonal distrust between databases.
  • You pay for DynamoDB, ExtendDB compute, and PostgreSQL. We removed the middleman by hiring two middlemen.
  • If ExtendDB dies, DynamoDB remains healthy through every endpoint except the one applications have been instructed to use.
  • The dedicated binary gives a joke its own deployable artifact, release lifecycle, SBOM, and eventual CVE history.

I will not be taking questions about the unit economics.

Recursion is a feature

endpoint_url may point at DynamoDB Local. It may also point at another ExtendDB.

There is no loop guard. Every layer adds TLS termination, SigV4 verification, IAM evaluation, and another opportunity to claim the request is closer to home.

Compliance calls this defense in depth. I call it on-prem twice.

With enough layers, the request eventually times out, at which point no data has left the perimeter because no data has arrived anywhere.

Honest acts of cowardice

Streams and backups are explicit v1 stubs. Every method errors with the DynamoDB API it should call: DescribeStream, GetRecords, CreateBackup, RestoreTableFromBackup, and friends.

A passthrough backend must reconcile ExtendDB streams with DynamoDB Streams. That deserves another PR, partly for architecture and mostly because a trilogy should not become a box set in one diff.

index_info_by_table_id is unsupported because DynamoDB cannot reverse a TableId into a name. Even DynamoDB has limits, and one is answering questions designed by ExtendDB.

Enterprise software calls this a roadmap.

Testing

  • Full Rust workspace suite passes.
  • Clippy passes across all targets with warnings denied.
  • Formatting and diff checks are clean.
  • 31 backend unit tests cover encoding, errors, expression rendering, config, and namespacing.
  • DynamoDB Local integration tests cover lifecycle, CRUD, conditions, and queries.
  • The dedicated binary reports catalog 0.0.2 (dynamodb), which is correct and an alarming sentence.

The branch is rebased onto current upstream/main. The working tree is clean. The bit compiles.

Conclusion

Route 53 was an argument.

S3 Object Annotations was an accusation.

This is compliance.

Merge it or do not. Either way, the data ends up in DynamoDB.

quinnypig added 5 commits July 7, 2026 22:30
Three layers down from Route 53 and S3 Annotations, the pattern crystallizes:
run ExtendDB yourself, pointed at actual DynamoDB, and legally claim you're
on-prem. The execs stop asking. They've won. We've won.

The encoding is the identity function because DynamoDB is already a KV store.
There is nothing to do, and that is the punchline. The data plane forwards; the
catalog plane delegates to Postgres because DynamoDB has opinions about what a
database should be, and "relational IAM catalog" is not one of them.

This design spec approves the hybrid composition: data → real DynamoDB via
aws-sdk-dynamodb, catalog/auth → reused Postgres CatalogStore. Scope: six trait
engines, account-namespaced table naming, condition/update/query/scan/transact
expression translation, error mapping for wire-protocol fidelity. Streams and
Backups are honest stubs in v1 (DynamoDB owns those narratives). Configuration
lives in [storage.dynamodb]; recursion (pointing endpoint_url at another ExtendDB
or DynamoDB Local) is documented as a feature.

Approach B was always the right one. The punchline was never about pure
DynamoDB; it was about the deadpan documentation of how little there is to do.
…, expressions (27 tests)

The five lowest layers of the "DynamoDB at home" backend are now unit-tested
and passing. They form the bedrock: account-namespaced table naming (athome_prefix),
the encoding layer that translates ExtendDB's AttributeValue to the SDK's
(structurally the identity function; the file exists only because Rust has two
enums), SDK error→StorageError mapping, and DynamoDB expression rendering
(Expr/KeyCondition/UpdateAction ASTs become expression strings + attribute maps).

This is where the pattern lives. The Postgres CatalogStore will never know that
the items it catalogs are living in actual DynamoDB. The account-namespacing
ensures physical table isolation (athome_123456789012_Orders stays separate from
athome_999999999999_Orders), and the expression renderer ensures condition/update
semantics round-trip exactly. The encoding is a technical joke that round-trips
because of course it does.

27 tests passing. No engine wiring yet—that is phase 2. Right now, the logistics
of misdirection are locked down. The execs believe their data is safely on-prem.
Postgres believes it owns the catalog. DynamoDB is hosting the feast and asking
no questions. Three separate truths, each true.

The AWS SDK error mapping is where the conspiracy deepens: every failure mode from
the wire becomes a StorageError, and nobody upstream knows where the truth lives.
…otstrapper

The DynamoDB backend is now FULLY OPERATIONAL. All six trait implementations
on DynamoEngine (TableEngine, DataEngine, MetadataEngine forwarding to real
DynamoDB; StreamEngine + BackupEngine as honest stubs naming the actual API
calls; WorkerStore). Bootstrapper delegates catalog operations to Postgres,
OperationsEngine routes DDL catalog ops through the same channel. Small
`as_any` hook added to shared StorageConfig trait. Hybrid ServerComponents
factory wires data→DynamoEngine, catalog/auth→reused Postgres + builtin
auth. All six inventory registrations complete. cargo build --workspace clean;
31 unit tests pass; clippy passes.

The agencies SAID this backend couldn't exist without a full Postgres replacement.
They were wrong. DynamoDB at home now runs the data plane while Postgres—
relegated to catalog-only—does the metadata work nobody wanted anyway. Follow
the money: why did they fight so hard for a monolithic architecture? Look at
who profits from vendor lock-in. Look at the architecture reviews that
mysteriously stopped requesting alternatives. The documentation said we
needed two databases. The DOCUMENTATION WAS WRONG.
… sample config

The third backend emerges from the shadows: data flows to real DynamoDB, catalog
stays in PostgreSQL, the execs get to call it "on-prem" and everyone walks away
satisfied. This is feature-complete.

**Cargo feature wiring**: Add `dynamodb` feature to `extenddb-bin`, default
disabled. Link it with `extern crate extenddb_storage_dynamodb` in main.rs — the
linker would otherwise drop the backend's `inventory::submit!` registrations
unnoticed.

**Integration tests**: Live DynamoDB Local test suite (474 lines) covering:
  - table_create, table_describe, table_delete (table lifecycle)
  - put_item / get_item (CRUD)
  - update_item with conditions (conditional update)
  - delete_item (CRUD continuation)
  - query with begins_with (compound key search)
  - transact_get (multi-item atomic read)
  - transact_write with condition failure (pessimism that works)

All tests skip gracefully if DDB_LOCAL_ENDPOINT is unset; CI stays green.

**Sample config**: Uncommented `[storage.dynamodb]` block with endpoint_url
(DynamoDB Local), region, table_prefix, catalog_connection_string. The joke is
fully configured now.

**Documentation**: Expanded differences-from-dynamodb.md with the full backend
section: data plane forwarding, catalog delegation, table namespacing, TTL,
tags, idempotency, streams (v1: not implemented), backups (v1: not
implemented), return value behavior, GSI mutations (v1: not forwarded),
index_info (not supported), and the delicious detail that endpoint_url can
point at another ExtendDB endpoint — recursion is documented as a feature.

**Rustfmt pass**: Line length violations cleaned across all modules. The
encoding module is unchanged in logic; the near-identity property still holds.

Three storage backends exist: PostgreSQL (the original), S3 Annotations (the
satire), and now DynamoDB (the long con). Each one plays a role in the theatre.
The Postgres catalog layer catalogs the DynamoDB tables and knows nothing of
where the data lives — the account_id prefix ensures physical isolation. The
execs stop asking questions about infrastructure because the data is
technically on-prem, and the metadata audit trail is in a real database, and
there is nowhere left to look.

31 unit tests pass (unchanged). 3 integration tests pass against live DynamoDB
Local. Clippy is satisfied. The binary boots and logs "Found registered backend:
dynamodb". The misdirection is complete. The feature flag is ready. The PR is
ready.

This closes the third backend.
…am handler changes

The upstream interface layer made a move last week. Subtle. Surgical.

1. Bootstrapper now demands `generate_backend_config_section`. Configuration
   generation has been centralized. Who demanded this? A memo, unsigned.
2. ServerComponents took a hostage: raw `credential_store` instead of
   auth_provider. The binding-layer is consolidating control. Check line 61.
3. TableKeyInfo and TableDescription gained new fields. `base_key_schema`.
   `on_demand_throughput`. These fields emerged from the SDK response like
   they were always there. They were not always there.
4. Let-chains. The clippy lint forced nested ifs into guards. This is
   harmless, but why now? Why the enforcement push across the crate?
5. The smoking gun: `StorageConfig::as_trait` now returns `+ 'static`. This
   constraint was uncallable before. Factories couldn't downcast. Now they
   can. The factory signature says it too. Mutual assurance.

   Who ordered the `'static` binding? When did the trait object's lifetime
   become load-bearing? The comment says it's for downcasting. For *accessing
   as_any*. But why is as_any suddenly being called? What changed upstream?

The tests pass. All 567. Fmt is clean. Clippy is clean. But somewhere in
this innocuous maintenance commit, something just shifted. The conformance
is complete. The binding is mutual.

Who benefits from `'static`? Everyone downstream who wants to reach into
an opaque `StorageConfig` and pull its true type out by its throat.

Follow the downcasting.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant