etcd as a Postgres table? Meet etcd_fdw
PostgreSQL's extensibility is one of its greatest strengths. Foreign Data Wrappers (FDWs) are a prime example: they let Postgres query external systems as if they were local tables. With the etcd_fdw extension, built in Rust with pgrx and supabase-wrappers, you can now point Postgres directly at an etcd cluster and run CRUD operations against it.

The building blocks: etcd, FDWs, and the wrapper
etcd is a distributed key-value store focused on consistency, commonly used as a configuration store in distributed systems like Kubernetes and OpenShift. A foreign data wrapper in Postgres is a pair of C functions—a handler and a validator—that Postgres calls to plan, read, and modify data stored outside the database. The handler registers all the low-level functions that interact with the foreign source; the validator checks connection options like server strings. Together, they allow you to create a SERVER and FOREIGN TABLE, making external data appear native.
The problem etcd_fdw solves is practical. Suppose two application instances share configuration that affects database logic. Full replication is heavy-handed—you'd be syncing all data just to share a few settings. etcd_fdw offers a lighter path: etcd remains the source of truth for configuration, while Postgres reads it through a foreign table.
Why Rust and supabase-wrappers?
While pgrx is a polished framework for building Postgres extensions in Rust, FDW support isn't built in directly. The supabase-wrappers crate fills that gap with macros that turn a simple struct into a working FDW. For example, implementing the handler for a basic hashmap-backed FDW requires only this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
[wrappers_fdw( version = "0.0.1", author = "Cybertec PostgreSQL International GmbH", error_type = "SimpleKvStoreError" )] pub(crate) struct SimpleKvStore { pub map: HashMap<String, String> } #[derive(Error, Debug)] pub enum SimpleKvStoreError {} impl ForeignDataWrapper<SimpleKvStore> for SimpleKvStore { fn new(server: ForeignServer) -> Result<SimpleKvStore, SimpleKvStoreError> {...} fn begin_scan( &mut self, _quals: &[Qual], columns: &[Column], _sorts: &[Sort], limit: &Option<Limit>, _options: &std::collections::HashMap<String, String>, ) -> Result<(), SimpleKvStoreError> {...} fn iter_scan(&mut self, row: &mut Row) -> Result<Option<()>, SimpleKvStoreError> {...} fn end_scan(&mut self) -> Result<(), SimpleKvStoreError> {...} fn begin_modify( &mut self, _options: &std::collections::HashMap<String, String>, ) -> Result<(),SimpleKvStoreError> {...} fn update(&mut self, rowid: &Cell, new_row: &Row) -> Result<(),SimpleKvStoreError> {...} fn delete(&mut self, rowid: &Cell) -> Result<(), SimpleKvStoreError> {...} fn end_modify(&mut self) -> Result<(), SimpleKvStoreError> {...} } |
Once the read and write logic is implemented, the foreign table behaves like any other—rows of key and value just work.
Setting up etcd_fdw
The setup follows the standard FDW pattern. Load the extension:
|
1 |
CREATE EXTENSION etcd_fdw; |
Then define the wrapper and create a server that points to your etcd endpoint. After that, creating a foreign table is a single statement:
|
1 |
CREATE FOREIGN DATA WRAPPER etcd_fdw handler etcd_fdw_handler validator etcd_fdw_validator; |
|
1 2 |
CREATE SERVER etcd_server foreign data wrapper etcd_fdw options (connstr '127.0.0.1:2379'); CREATE FOREIGN TABLE t_etcd_table (key text, value text) SERVER etcd_server OPTIONS (rowid_column ‘key’); |
Once configured, you query etcd as if it were a normal table. Inserts and selects map directly to etcd keys and values:
|
1 2 |
INSERT INTO t_etcd_table (key, value) VALUES ('foo', 'bar'), ('bar', 'baz'); SELECT * FROM t_etcd_table ; |
Which yields results like:
|
1 |
INSERT 0 2 |
And:
|
1 2 3 4 5 |
key | value -----+------- bar | baz foo | bar (2 rows) |
Changes made outside Postgres also show up immediately. When a key is added via etcd_ctl, the foreign table reflects it without any manual refresh:
|
1 |
etcdctl put 'alice' 'bob' |
|
1 2 3 4 5 6 7 |
SELECT * FROM t_etcd_table ; key | value -------+------- alice | bob bar | baz foo | bar (3 rows) |
What this means for extension developers
The combination of pgrx and supabase-wrappers lowers the barrier for writing FDWs in a memory-safe, expressive language. Instead of hand-writing C structs and callbacks, you deal with Rust types and macros. The etcd wrapper demonstrates the pattern works for a real distributed system, not just CSV files or local test data.



